diff --git a/package-lock.json b/package-lock.json index b79b1f5..322e76e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 913381d..388470b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/globals.scss b/src/app/globals.scss index 0409bf3..7fa4eb9 100644 --- a/src/app/globals.scss +++ b/src/app/globals.scss @@ -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 { diff --git a/src/components/date-select.tsx b/src/components/date-select.tsx new file mode 100644 index 0000000..d014d9a --- /dev/null +++ b/src/components/date-select.tsx @@ -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; + +export const DateSelect = ({ range, onChange, ...inputProps }: DateSelectProps) => { + const [open, setOpen] = useState(false); + const breakpoint = useBreakpoint(); + + const dayPicker = (); + + return (<> + + {onClose => <> + { inputProps.placeholder ?? 'Select date range' } + { dayPicker } + + + + } + + + +
+ +
+
+ + { dayPicker } + +
+ ); +}; diff --git a/src/components/filter-sorter.tsx b/src/components/filter-sorter.tsx index 7d5c120..e1157f8 100644 --- a/src/components/filter-sorter.tsx +++ b/src/components/filter-sorter.tsx @@ -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['value'], select: React.ComponentProps['selectedKeys'], - switch: React.ComponentProps['isSelected'] + switch: React.ComponentProps['isSelected'], + dateSelect: React.ComponentProps['range'] }; type FilterTypes = { select: typeof Select, slider: typeof Slider, - switch: typeof Switch + switch: typeof Switch, + dateSelect: typeof DateSelect }; -type FilterField = { +export type FilterField = { type: T, name: N, label: string, @@ -82,6 +85,7 @@ const FilterSorterComponent = sorter.has(s.name))!; @@ -128,13 +134,15 @@ const FilterSorterComponent = { if (nonce === prevNonce.current) { setProcessedData(d.data); setTotalCount(d.total); } - }); + }) + .finally(() => setLoadingRemoteData(false)); }, deps), 100, debounceOptions); useEffect(() => { @@ -272,6 +280,16 @@ const FilterSorterComponent = setFilterState(f => ({ ...f, [filter.name]: selected }))}> {filter.label} + else if (filter.type === 'dateSelect') + return
+ setFilterState(f => ({ ...f, [filter.name]: v }))} size="sm" + {...filter.props as any} /> + +
; })}
@@ -318,7 +336,7 @@ const FilterSorterComponent =
} - {renderedData === null ? : renderedData} + {(renderedData === null || loadingRemoteData) ? : renderedData} {totalCount !== -1 && !Number.isNaN(pageSizeNum) &&