forked from sk1982/actaeon
add date range select component
This commit is contained in:
parent
4e5ade3e6b
commit
9f54d8bfb2
24
package-lock.json
generated
24
package-lock.json
generated
@ -25,6 +25,7 @@
|
|||||||
"next-client-cookies": "^1.1.0",
|
"next-client-cookies": "^1.1.0",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
|
"react-day-picker": "^8.10.0",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-virtualized": "^9.22.5",
|
"react-virtualized": "^9.22.5",
|
||||||
"sass": "^1.71.1",
|
"sass": "^1.71.1",
|
||||||
@ -5106,6 +5107,16 @@
|
|||||||
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
|
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/db-migrate": {
|
||||||
"version": "0.11.14",
|
"version": "0.11.14",
|
||||||
"resolved": "https://registry.npmjs.org/db-migrate/-/db-migrate-0.11.14.tgz",
|
"resolved": "https://registry.npmjs.org/db-migrate/-/db-migrate-0.11.14.tgz",
|
||||||
@ -9546,6 +9557,19 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/react-dom": {
|
||||||
"version": "18.2.0",
|
"version": "18.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
"next-client-cookies": "^1.1.0",
|
"next-client-cookies": "^1.1.0",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
|
"react-day-picker": "^8.10.0",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-virtualized": "^9.22.5",
|
"react-virtualized": "^9.22.5",
|
||||||
"sass": "^1.71.1",
|
"sass": "^1.71.1",
|
||||||
|
@ -18,6 +18,26 @@
|
|||||||
container-type: size;
|
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 {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
|
49
src/components/date-select.tsx
Normal file
49
src/components/date-select.tsx
Normal 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>
|
||||||
|
</>);
|
||||||
|
};
|
@ -8,21 +8,24 @@ import { ArrowLongUpIcon } from '@heroicons/react/24/solid';
|
|||||||
import { useDebounceCallback, useIsMounted } from 'usehooks-ts';
|
import { useDebounceCallback, useIsMounted } from 'usehooks-ts';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { SearchIcon } from '@nextui-org/shared-icons';
|
import { SearchIcon } from '@nextui-org/shared-icons';
|
||||||
|
import { DateSelect } from '@/components/date-select';
|
||||||
|
|
||||||
|
|
||||||
type ValueType = {
|
type ValueType = {
|
||||||
slider: React.ComponentProps<typeof Slider>['value'],
|
slider: React.ComponentProps<typeof Slider>['value'],
|
||||||
select: React.ComponentProps<typeof Select>['selectedKeys'],
|
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 = {
|
type FilterTypes = {
|
||||||
select: typeof Select,
|
select: typeof Select,
|
||||||
slider: typeof Slider,
|
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,
|
type: T,
|
||||||
name: N,
|
name: N,
|
||||||
label: string,
|
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 [totalCount, setTotalCount] = useState(Array.isArray(data) ? data.length : -1);
|
||||||
const [currentPage, _setCurrentPage] = useState(defaultCurrentPage ?? 1);
|
const [currentPage, _setCurrentPage] = useState(defaultCurrentPage ?? 1);
|
||||||
const [selectedKeys, setSelectedKeys] = useState(new Set(['1']));
|
const [selectedKeys, setSelectedKeys] = useState(new Set(['1']));
|
||||||
|
const [loadingRemoteData, setLoadingRemoteData] = useState(false);
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const prevNonce = useRef(1);
|
const prevNonce = useRef(1);
|
||||||
const resetPage = useRef(false);
|
const resetPage = useRef(false);
|
||||||
@ -105,6 +109,8 @@ const FilterSorterComponent = <D, M extends string, N extends string, S extends
|
|||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
resetPage.current = false;
|
resetPage.current = false;
|
||||||
page = 1;
|
page = 1;
|
||||||
|
if (dataRemote)
|
||||||
|
setTotalCount(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sort = sorters.find(s => sorter.has(s.name))!;
|
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();
|
const nonce = Math.random();
|
||||||
prevNonce.current = nonce;
|
prevNonce.current = nonce;
|
||||||
|
setLoadingRemoteData(true);
|
||||||
Promise.resolve(data({ filters: filterState, sort: sort.name, pageSize: pageSizeNum, search: query, currentPage: page }))
|
Promise.resolve(data({ filters: filterState, sort: sort.name, pageSize: pageSizeNum, search: query, currentPage: page }))
|
||||||
.then(d => {
|
.then(d => {
|
||||||
if (nonce === prevNonce.current) {
|
if (nonce === prevNonce.current) {
|
||||||
setProcessedData(d.data);
|
setProcessedData(d.data);
|
||||||
setTotalCount(d.total);
|
setTotalCount(d.total);
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
.finally(() => setLoadingRemoteData(false));
|
||||||
}, deps), 100, debounceOptions);
|
}, deps), 100, debounceOptions);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -272,6 +280,16 @@ const FilterSorterComponent = <D, M extends string, N extends string, S extends
|
|||||||
onValueChange={selected => setFilterState(f => ({ ...f, [filter.name]: selected }))}>
|
onValueChange={selected => setFilterState(f => ({ ...f, [filter.name]: selected }))}>
|
||||||
{filter.label}
|
{filter.label}
|
||||||
</Switch>
|
</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 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">
|
<div className="flex gap-2 flex-grow">
|
||||||
@ -318,7 +336,7 @@ const FilterSorterComponent = <D, M extends string, N extends string, S extends
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>}
|
</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" >
|
{totalCount !== -1 && !Number.isNaN(pageSizeNum) && <div className="mt-auto mb-4" >
|
||||||
<Pagination total={Math.ceil(totalCount / pageSizeNum)} showControls
|
<Pagination total={Math.ceil(totalCount / pageSizeNum)} showControls
|
||||||
|
Loading…
Reference in New Issue
Block a user