add date range select component

This commit is contained in:
sk1982 2024-03-19 00:54:42 -04:00
parent 4e5ade3e6b
commit 9f54d8bfb2
5 changed files with 117 additions and 5 deletions

24
package-lock.json generated
View File

@ -25,6 +25,7 @@
"next-client-cookies": "^1.1.0",
"next-themes": "^0.2.1",
"react": "^18",
"react-day-picker": "^8.10.0",
"react-dom": "^18",
"react-virtualized": "^9.22.5",
"sass": "^1.71.1",
@ -5106,6 +5107,16 @@
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
"dev": true
},
"node_modules/date-fns": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
"peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/db-migrate": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/db-migrate/-/db-migrate-0.11.14.tgz",
@ -9546,6 +9557,19 @@
"node": ">=0.10.0"
}
},
"node_modules/react-day-picker": {
"version": "8.10.0",
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.0.tgz",
"integrity": "sha512-mz+qeyrOM7++1NCb1ARXmkjMkzWVh2GL9YiPbRjKe0zHccvekk4HE+0MPOZOrosn8r8zTHIIeOUXTmXRqmkRmg==",
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/gpbl"
},
"peerDependencies": {
"date-fns": "^2.28.0 || ^3.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",

View File

@ -31,6 +31,7 @@
"next-client-cookies": "^1.1.0",
"next-themes": "^0.2.1",
"react": "^18",
"react-day-picker": "^8.10.0",
"react-dom": "^18",
"react-virtualized": "^9.22.5",
"sass": "^1.71.1",

View File

@ -18,6 +18,26 @@
container-type: size;
}
.rdp {
--rdp-accent-color: theme('colors.primary') !important;
--rdp-background-color: theme('colors.primary') !important;
}
.rdp-button.rdp-button {
@apply transition-colors;
&:hover:not(.rdp-day_selected) {
@apply text-white;
}
}
.rdp-day_today.rdp-day_today:not(.rdp-day_selected) {
@apply bg-secondary bg-opacity-25 font-semibold;
}
.dark .rdp :is(select, option) {
background-color: black;
}
@layer base {
:root {

View File

@ -0,0 +1,49 @@
import { Button, Input, InputProps, Modal, ModalContent, ModalHeader, Popover, PopoverContent, PopoverTrigger } from '@nextui-org/react';
import { useState } from 'react';
import { DayPicker, DateRange } from 'react-day-picker';
import 'react-day-picker/dist/style.css';
import { useBreakpoint } from '@/helpers/use-breakpoint';
import { ModalBody, ModalFooter } from '@nextui-org/modal';
export type DateSelectProps = {
range: DateRange | undefined,
onChange: (range: DateRange | undefined) => void
} & Omit<InputProps, 'isReadOnly' | 'value' | 'onChange'>;
export const DateSelect = ({ range, onChange, ...inputProps }: DateSelectProps) => {
const [open, setOpen] = useState(false);
const breakpoint = useBreakpoint();
const dayPicker = (<DayPicker mode="range"
toDate={new Date()}
fromDate={new Date(2015, 1, 1)}
selected={range}
onSelect={onChange}
captionLayout="dropdown-buttons"
showOutsideDays
className="!m-0 !sm:m-2"
/>);
return (<>
<Modal isOpen={!breakpoint && open} onOpenChange={setOpen}>
<ModalContent className="flex flex-col items-center overflow-hidden">{onClose => <>
<ModalHeader className="w-full">{ inputProps.placeholder ?? 'Select date range' }</ModalHeader>
<ModalBody>{ dayPicker }</ModalBody>
<ModalFooter className="w-full">
<Button color="primary" className="self-baseline">Close</Button>
</ModalFooter>
</>}</ModalContent>
</Modal>
<Popover isOpen={breakpoint !== undefined && open} onOpenChange={setOpen}>
<PopoverTrigger className="aria-expanded:scale-1 aria-expanded:opacity-100 select-none">
<div className="w-full">
<Input value={(range?.from || range?.to) ? `${range?.from?.toLocaleDateString() ?? ''}\u2013${range?.to?.toLocaleDateString() ?? ''}` : undefined}
type="text" {...inputProps} isReadOnly />
</div>
</PopoverTrigger>
<PopoverContent>
{ dayPicker }
</PopoverContent>
</Popover>
</>);
};

View File

@ -8,21 +8,24 @@ import { ArrowLongUpIcon } from '@heroicons/react/24/solid';
import { useDebounceCallback, useIsMounted } from 'usehooks-ts';
import { usePathname } from 'next/navigation';
import { SearchIcon } from '@nextui-org/shared-icons';
import { DateSelect } from '@/components/date-select';
type ValueType = {
slider: React.ComponentProps<typeof Slider>['value'],
select: React.ComponentProps<typeof Select>['selectedKeys'],
switch: React.ComponentProps<typeof Switch>['isSelected']
switch: React.ComponentProps<typeof Switch>['isSelected'],
dateSelect: React.ComponentProps<typeof DateSelect>['range']
};
type FilterTypes = {
select: typeof Select,
slider: typeof Slider,
switch: typeof Switch
switch: typeof Switch,
dateSelect: typeof DateSelect
};
type FilterField<D, T extends keyof FilterTypes, N extends string> = {
export type FilterField<D, T extends keyof FilterTypes, N extends string> = {
type: T,
name: N,
label: string,
@ -82,6 +85,7 @@ const FilterSorterComponent = <D, M extends string, N extends string, S extends
const [totalCount, setTotalCount] = useState(Array.isArray(data) ? data.length : -1);
const [currentPage, _setCurrentPage] = useState(defaultCurrentPage ?? 1);
const [selectedKeys, setSelectedKeys] = useState(new Set(['1']));
const [loadingRemoteData, setLoadingRemoteData] = useState(false);
const pathname = usePathname();
const prevNonce = useRef(1);
const resetPage = useRef(false);
@ -105,6 +109,8 @@ const FilterSorterComponent = <D, M extends string, N extends string, S extends
setCurrentPage(1);
resetPage.current = false;
page = 1;
if (dataRemote)
setTotalCount(-1);
}
const sort = sorters.find(s => sorter.has(s.name))!;
@ -128,13 +134,15 @@ const FilterSorterComponent = <D, M extends string, N extends string, S extends
const nonce = Math.random();
prevNonce.current = nonce;
setLoadingRemoteData(true);
Promise.resolve(data({ filters: filterState, sort: sort.name, pageSize: pageSizeNum, search: query, currentPage: page }))
.then(d => {
if (nonce === prevNonce.current) {
setProcessedData(d.data);
setTotalCount(d.total);
}
});
})
.finally(() => setLoadingRemoteData(false));
}, deps), 100, debounceOptions);
useEffect(() => {
@ -272,6 +280,16 @@ const FilterSorterComponent = <D, M extends string, N extends string, S extends
onValueChange={selected => setFilterState(f => ({ ...f, [filter.name]: selected }))}>
{filter.label}
</Switch>
else if (filter.type === 'dateSelect')
return <div className={`${filter.className ?? ''} flex w-full`} key={filter.name}>
<DateSelect range={filterState[filter.name] as any} label={filter.label} radius="none" className="rounded-l-lg overflow-hidden flex-grow"
onChange={v => setFilterState(f => ({ ...f, [filter.name]: v }))} size="sm"
{...filter.props as any} />
<Button isIconOnly={true} color="danger" className="rounded-l-none rounded-r-lg h-full" onClick={() =>
setFilterState(f => ({ ...f, [filter.name]: filter.value }))}>
<XMarkIcon className="h-full p-2" />
</Button>
</div>;
})}
<div className="flex mt-0.5 gap-2 flex-wrap sm:flex-nowrap flex-col-reverse sm:flex-row col-span-12">
<div className="flex gap-2 flex-grow">
@ -318,7 +336,7 @@ const FilterSorterComponent = <D, M extends string, N extends string, S extends
</Dropdown>
</div>}
{renderedData === null ? <Spinner className="m-auto" /> : renderedData}
{(renderedData === null || loadingRemoteData) ? <Spinner className="m-auto" /> : renderedData}
{totalCount !== -1 && !Number.isNaN(pageSizeNum) && <div className="mt-auto mb-4" >
<Pagination total={Math.ceil(totalCount / pageSizeNum)} showControls