-
Notifications
You must be signed in to change notification settings - Fork 929
feat: DR-7293 Date picker #7554
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d7ced7c
13f55d6
66537ba
0f2b832
d149401
35ef558
38b7b80
87a576d
d371cab
733ab81
13baffc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ | |
| "chart", | ||
| "checkbox", | ||
| "codeblock", | ||
| "datepicker", | ||
| "dialog", | ||
| "dropdownmenu", | ||
| "empty", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| "use client"; | ||
|
|
||
| import { useState } from "react"; | ||
| import { | ||
| DatePickerSingle, | ||
| DatePickerRange, | ||
| createDateRangePresets, | ||
| } from "@prisma/eclipse"; | ||
| import type { DateRange } from "@prisma/eclipse"; | ||
|
|
||
| export function DatePickerSingleExample() { | ||
| const [date, setDate] = useState<Date>(); | ||
|
|
||
| return ( | ||
| <DatePickerSingle | ||
| date={date} | ||
| onDateChange={setDate} | ||
| placeholder="Select a date" | ||
| dateFormat="dd/MM/yyyy" | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| export function DatePickerRangeExample() { | ||
| const [dateRange, setDateRange] = useState<DateRange>(); | ||
|
|
||
| return ( | ||
| <DatePickerRange | ||
| dateRange={dateRange} | ||
| onDateRangeChange={setDateRange} | ||
| placeholder="Pick a date range" | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| export function DatePickerRangeWithPresetsExample() { | ||
| const [dateRange, setDateRange] = useState<DateRange>(); | ||
| const presets = createDateRangePresets(); | ||
|
|
||
| return ( | ||
| <DatePickerRange | ||
| dateRange={dateRange} | ||
| onDateRangeChange={setDateRange} | ||
| presets={presets} | ||
| placeholder="Pick a date range with presets" | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| export function DatePickerErrorExample() { | ||
| return ( | ||
| <div className="space-y-2"> | ||
| <DatePickerSingle placeholder="Error state example" isErrored={true} /> | ||
| <p className="text-sm text-foreground-error">Date is required</p> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export function DatePickerDisabledExample() { | ||
| return ( | ||
| <DatePickerSingle placeholder="Disabled state example" disabledBtn={true} /> | ||
| ); | ||
| } | ||
|
|
||
| export function DatePickerWithValidationExample() { | ||
| const [date, setDate] = useState<Date>(); | ||
| const [submitted, setSubmitted] = useState(false); | ||
|
|
||
| const hasError = submitted && !date; | ||
|
|
||
| const handleSubmit = (e: React.FormEvent) => { | ||
| e.preventDefault(); | ||
| setSubmitted(true); | ||
| if (date) { | ||
| alert(`Date selected: ${date.toLocaleDateString()}`); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <form onSubmit={handleSubmit} className="space-y-4"> | ||
| <div className="space-y-2"> | ||
| <label className="text-sm font-medium">Event Date *</label> | ||
| <DatePickerSingle | ||
| date={date} | ||
| onDateChange={(newDate) => { | ||
| setDate(newDate); | ||
| if (newDate) setSubmitted(false); | ||
| }} | ||
| placeholder="Select event date" | ||
| isErrored={hasError} | ||
| /> | ||
| {hasError && ( | ||
| <p className="text-sm text-foreground-error"> | ||
| Please select an event date | ||
| </p> | ||
| )} | ||
| </div> | ||
| <button | ||
| type="submit" | ||
| className="px-4 py-2 bg-background-ppg text-foreground-ppg rounded-md hover:bg-background-ppg/90" | ||
| > | ||
| Submit | ||
| </button> | ||
| </form> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,242 @@ | ||
| "use client"; | ||
|
|
||
| import * as React from "react"; | ||
| import { format } from "date-fns"; | ||
| import type { DateRange, Matcher } from "react-day-picker"; | ||
|
|
||
| import { cn } from "../lib/cn"; | ||
| import { Button } from "./button"; | ||
| import { Calendar } from "./ui/calendar"; | ||
| import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; | ||
|
|
||
| // Re-export types for consumers | ||
| export type { DateRange, Matcher } from "react-day-picker"; | ||
|
|
||
| export interface DatePickerProps { | ||
| /** | ||
| * The selected date (for single date picker) | ||
| */ | ||
| date?: Date; | ||
| /** | ||
| * Callback when date changes (for single date picker) | ||
| */ | ||
| onDateChange?: (date: Date | undefined) => void; | ||
| /** | ||
| * The selected date range (for range picker) | ||
| */ | ||
| dateRange?: DateRange; | ||
| /** | ||
| * Callback when date range changes (for range picker) | ||
| */ | ||
| onDateRangeChange?: (range: DateRange | undefined) => void; | ||
| /** | ||
| * Placeholder text when no date is selected | ||
| */ | ||
| placeholder?: string; | ||
| /** | ||
| * Mode: 'single' or 'range' | ||
| */ | ||
| mode?: "single" | "range"; | ||
| /** | ||
| * Preset date ranges (for range picker) | ||
| */ | ||
| presets?: Array<{ | ||
| label: string; | ||
| dateRange: DateRange; | ||
| }>; | ||
| /** | ||
| * Disabled dates | ||
| */ | ||
| disabled?: Matcher | Matcher[]; | ||
| /** | ||
| * Custom className for the trigger button | ||
| */ | ||
| className?: string; | ||
| /** | ||
| * Align popover content | ||
| */ | ||
| align?: "start" | "center" | "end"; | ||
| /** | ||
| * Whether the date picker is in an error state | ||
| */ | ||
| isErrored?: boolean; | ||
| /** | ||
| * Whether the trigger button is disabled | ||
| */ | ||
| disabledBtn?: boolean; | ||
| /** | ||
| * Date format string for displaying dates (date-fns format) | ||
| * @default "PPP" for single mode (e.g., "February 17th, 2026") | ||
| * @default "LLL dd, y" for range mode (e.g., "Feb 17, 2026") | ||
| * @example "dd/MM/yyyy" → "17/02/2026" | ||
| * @example "MM/dd/yyyy" → "02/17/2026" | ||
| * @example "yyyy-MM-dd" → "2026-02-17" | ||
| */ | ||
| dateFormat?: string; | ||
| } | ||
|
|
||
| export function DatePicker({ | ||
| date, | ||
| onDateChange, | ||
| dateRange, | ||
| onDateRangeChange, | ||
| placeholder, | ||
| mode = "single", | ||
| presets, | ||
| disabled, | ||
| className, | ||
| align = "start", | ||
| isErrored = false, | ||
| disabledBtn = false, | ||
| dateFormat, | ||
| }: DatePickerProps) { | ||
| const [open, setOpen] = React.useState(false); | ||
|
|
||
| // Single date picker | ||
| if (mode === "single") { | ||
| return ( | ||
| <Popover open={open} onOpenChange={setOpen}> | ||
| <PopoverTrigger asChild> | ||
| <Button | ||
| variant="default" | ||
| size="lg" | ||
| className={cn( | ||
| "w-full p-1.5 text-left font-normal bg-background-default border-stroke-neutral font-mono text-foreground-neutral", | ||
| !date && "text-foreground-neutral-weak", | ||
| isErrored && "border-stroke-error text-foreground-error", | ||
| disabledBtn && | ||
| "cursor-not-allowed text-foreground-neutral-weaker bg-background-neutral-weak", | ||
| className, | ||
| )} | ||
| type="button" | ||
| > | ||
|
Comment on lines
+100
to
+112
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On Line 107 and Line 154, the component looks disabled but neither trigger Suggested fix (apply to both single and range trigger buttons)- <Popover open={open} onOpenChange={setOpen}>
+ <Popover
+ open={open}
+ onOpenChange={(nextOpen) => {
+ if (!disabledBtn) setOpen(nextOpen);
+ }}
+ >
...
- <Button
+ <Button
+ disabled={disabledBtn}
variant="default"
size="lg"Also applies to: 147-159 🤖 Prompt for AI Agents |
||
| <i | ||
| className={cn( | ||
| "text-foreground-neutral-weak fa-duotone fa-calendar-range flex h-full items-center text-md before:inset-y-0 -mt-0.5", | ||
| (isErrored || disabledBtn) && "text-inherit", | ||
| )} | ||
| /> | ||
| {date ? ( | ||
| format(date, dateFormat || "P") | ||
| ) : ( | ||
| <span>{placeholder || "Pick a date"}</span> | ||
| )} | ||
| </Button> | ||
| </PopoverTrigger> | ||
| <PopoverContent className="w-auto p-0" align={align}> | ||
| <Calendar | ||
| mode="single" | ||
| selected={date} | ||
| onSelect={(newDate) => { | ||
| onDateChange?.(newDate); | ||
| setOpen(false); | ||
| }} | ||
| disabled={disabled} | ||
| initialFocus | ||
| /> | ||
|
carlagn marked this conversation as resolved.
|
||
| </PopoverContent> | ||
| </Popover> | ||
| ); | ||
| } | ||
|
|
||
| // Range date picker | ||
| return ( | ||
| <div className="flex flex-col gap-2"> | ||
| <Popover open={open} onOpenChange={setOpen}> | ||
| <PopoverTrigger asChild> | ||
| <Button | ||
| variant="default" | ||
| size="lg" | ||
| className={cn( | ||
| "w-full p-1.5 justify-start text-left font-normal bg-background-default border-stroke-neutral font-mono text-foreground-neutral", | ||
| !dateRange && "text-foreground-neutral-weak", | ||
| isErrored && "border-stroke-error text-foreground-error", | ||
| disabledBtn && | ||
| "cursor-not-allowed text-foreground-neutral-weaker bg-background-neutral-weak", | ||
| className, | ||
| )} | ||
| type="button" | ||
| > | ||
| <i | ||
| className={cn( | ||
| "text-foreground-neutral-weak fa-duotone fa-calendar-range flex h-full items-center before:inset-y-0 -mt-0.5 text-md", | ||
| (isErrored || disabledBtn) && "text-inherit", | ||
| )} | ||
| /> | ||
| {dateRange?.from ? ( | ||
| dateRange.to ? ( | ||
| <> | ||
| {format(dateRange.from, dateFormat || "dd/MM/yyyy")} -{" "} | ||
| {format(dateRange.to, dateFormat || "dd/MM/yyyy")} | ||
| </> | ||
| ) : ( | ||
| format(dateRange.from, dateFormat || "dd/MM/yyyy") | ||
| ) | ||
| ) : ( | ||
| <span>{placeholder || "Pick a date range"}</span> | ||
| )} | ||
| </Button> | ||
| </PopoverTrigger> | ||
| <PopoverContent className="w-auto p-0" align={align}> | ||
| <div className="flex"> | ||
| {presets && presets.length > 0 && ( | ||
| <div className="flex flex-col gap-1 border-r p-3"> | ||
| <div className="text-xs font-medium text-muted-foreground mb-1"> | ||
| Presets | ||
| </div> | ||
| {presets.map((preset, index) => ( | ||
| <Button | ||
| key={index} | ||
| variant="default-weaker" | ||
| size="lg" | ||
| className="justify-start" | ||
| type="button" | ||
| onClick={() => { | ||
| onDateRangeChange?.(preset.dateRange); | ||
| setOpen(false); | ||
| }} | ||
| > | ||
| {preset.label} | ||
| </Button> | ||
| ))} | ||
| </div> | ||
| )} | ||
| <Calendar | ||
| mode="range" | ||
| selected={dateRange} | ||
| onSelect={onDateRangeChange} | ||
| disabled={disabled} | ||
| numberOfMonths={2} | ||
| initialFocus | ||
| /> | ||
| </div> | ||
| </PopoverContent> | ||
| </Popover> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| // Convenience exports for specific use cases | ||
| export function DatePickerSingle( | ||
| props: Omit< | ||
| DatePickerProps, | ||
| "mode" | "dateRange" | "onDateRangeChange" | "presets" | ||
| >, | ||
| ) { | ||
| return <DatePicker {...props} mode="single" />; | ||
| } | ||
|
|
||
| export function DatePickerRange( | ||
| props: Omit<DatePickerProps, "mode" | "date" | "onDateChange">, | ||
| ) { | ||
| return <DatePicker {...props} mode="range" />; | ||
| } | ||
|
|
||
| // Re-export helper functions from lib | ||
| export { | ||
| createDateRangePresets, | ||
| createDateRangePreset, | ||
| getLastNDays, | ||
| getCurrentMonth, | ||
| getPreviousMonth, | ||
| } from "../lib/date-presets"; | ||
Uh oh!
There was an error while loading. Please reload this page.