diff options
| author | joonhoekim <26rote@gmail.com> | 2025-03-25 15:55:45 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-03-25 15:55:45 +0900 |
| commit | 1a2241c40e10193c5ff7008a7b7b36cc1d855d96 (patch) | |
| tree | 8a5587f10ca55b162d7e3254cb088b323a34c41b /components/date-range-picker.tsx | |
initial commit
Diffstat (limited to 'components/date-range-picker.tsx')
| -rw-r--r-- | components/date-range-picker.tsx | 146 |
1 files changed, 146 insertions, 0 deletions
diff --git a/components/date-range-picker.tsx b/components/date-range-picker.tsx new file mode 100644 index 00000000..295160a5 --- /dev/null +++ b/components/date-range-picker.tsx @@ -0,0 +1,146 @@ +"use client" + +import * as React from "react" +import { format } from "date-fns" +import { CalendarIcon } from "lucide-react" +import { parseAsString, useQueryStates } from "nuqs" +import { type DateRange } from "react-day-picker" + +import { cn } from "@/lib/utils" +import { Button, type ButtonProps } from "@/components/ui/button" +import { Calendar } from "@/components/ui/calendar" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" + +interface DateRangePickerProps + extends React.ComponentPropsWithoutRef<typeof PopoverContent> { + /** + * The selected date range. + * @default undefined + * @type DateRange + * @example { from: new Date(), to: new Date() } + */ + defaultDateRange?: DateRange + + /** + * The placeholder text of the calendar trigger button. + * @default "Pick a date" + * @type string | undefined + */ + placeholder?: string + + /** + * The variant of the calendar trigger button. + * @default "outline" + * @type "default" | "outline" | "secondary" | "ghost" + */ + triggerVariant?: Exclude<ButtonProps["variant"], "destructive" | "link"> + + /** + * The size of the calendar trigger button. + * @default "default" + * @type "default" | "sm" | "lg" + */ + triggerSize?: Exclude<ButtonProps["size"], "icon"> + + /** + * The class name of the calendar trigger button. + * @default undefined + * @type string + */ + triggerClassName?: string + + /** + * Controls whether query states are updated client-side only (default: true). + * Setting to `false` triggers a network request to update the querystring. + * @default true + */ + shallow?: boolean +} + +export function DateRangePicker({ + defaultDateRange, + placeholder = "Pick a date", + triggerVariant = "outline", + triggerSize = "default", + triggerClassName, + shallow = true, + className, + ...props +}: DateRangePickerProps) { + const [dateParams, setDateParams] = useQueryStates( + { + from: parseAsString.withDefault( + defaultDateRange?.from?.toISOString() ?? "" + ), + to: parseAsString.withDefault(defaultDateRange?.to?.toISOString() ?? ""), + }, + { + clearOnDefault: true, + shallow, + } + ) + + const date = React.useMemo(() => { + function parseDate(dateString: string | null) { + if (!dateString) return undefined + const parsedDate = new Date(dateString) + return isNaN(parsedDate.getTime()) ? undefined : parsedDate + } + + return { + from: parseDate(dateParams.from) ?? defaultDateRange?.from, + to: parseDate(dateParams.to) ?? defaultDateRange?.to, + } + }, [dateParams, defaultDateRange]) + + return ( + <div className="grid gap-2"> + <Popover> + <PopoverTrigger asChild> + <Button + variant={triggerVariant} + size={triggerSize} + className={cn( + "w-full justify-start gap-2 truncate text-left font-normal", + !date && "text-muted-foreground", + triggerClassName + )} + > + <CalendarIcon className="size-4" /> + {date?.from ? ( + date.to ? ( + <> + {format(date.from, "LLL dd, y")} -{" "} + {format(date.to, "LLL dd, y")} + </> + ) : ( + format(date.from, "LLL dd, y") + ) + ) : ( + <span>{placeholder}</span> + )} + </Button> + </PopoverTrigger> + <PopoverContent className={cn("w-auto p-0", className)} {...props}> + <Calendar + initialFocus + mode="range" + defaultMonth={date?.from} + selected={date} + onSelect={(newDateRange) => { + void setDateParams({ + from: newDateRange?.from?.toISOString() ?? "", + to: newDateRange?.to?.toISOString() ?? "", + }) + }} + numberOfMonths={2} + /> + </PopoverContent> + </Popover> + </div> + ) +} |
