-
Notifications
You must be signed in to change notification settings - Fork 2
Data Table PRO MAX 🔥 #28
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: master
Are you sure you want to change the base?
Changes from all commits
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.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import type { Meta, StoryObj } from "@storybook/react-vite"; | ||
import * as React from "react"; | ||
import { DatePicker } from "./date-picker"; | ||
|
||
const meta = { | ||
title: "Component/DatePicker", | ||
} satisfies Meta; | ||
|
||
export default meta; | ||
|
||
type Story = StoryObj<typeof meta>; | ||
|
||
export const Demo = { | ||
render: () => { | ||
const [date, setDate] = React.useState<Date | undefined>(undefined); | ||
|
||
return ( | ||
<div> | ||
<DatePicker value={date} onDateChange={setDate}/> | ||
<span className="mt-4">{ date?.toString() }</span> | ||
</div> | ||
); | ||
}, | ||
} satisfies Story; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { useCallback, useId, useState } from "react" | ||
import { format } from "date-fns" | ||
import { CalendarIcon } from "lucide-react" | ||
import { Popover, PopoverContent, PopoverTrigger } from "../shadcn/components/ui/popover" | ||
import { Button } from "../shadcn/components/ui/button" | ||
import { Calendar } from "../shadcn/components/ui/calendar" | ||
import { cn } from "#shadcn/lib/utils.js" | ||
|
||
export type DatePickerProps = { | ||
value: Date | undefined | ||
onDateChange?: (value?: Date) => void | ||
formatDate?: (value: Date) => string | ||
} | ||
|
||
export function DatePicker(props: DatePickerProps) { | ||
const id = useId(); | ||
const [open, setOpen] = useState(false); | ||
|
||
const formatDate = useCallback((date: Date) => props.formatDate ? props.formatDate(date) : format(date, "MM/dd/yyyy"), []); | ||
|
||
const onSelect = useCallback((value: Date | undefined) => { | ||
setOpen(false); | ||
props.onDateChange && props.onDateChange(value); | ||
}, []); | ||
|
||
return ( | ||
<Popover open={open} onOpenChange={setOpen}> | ||
<PopoverTrigger asChild> | ||
<Button | ||
id={id} | ||
variant={"ghost"} | ||
className="group bg-background hover:bg-background w-full justify-start px-3 font-normal" | ||
> | ||
<CalendarIcon | ||
size={16} | ||
className="text-muted-foreground/80 group-hover:text-foreground shrink-0 transition-colors" | ||
aria-hidden="true" | ||
/> | ||
<span | ||
className={cn("truncate", !props.value && "text-muted-foreground")} | ||
> | ||
{props.value ? formatDate(props.value) : "Pick a date"} | ||
</span> | ||
</Button> | ||
</PopoverTrigger> | ||
<PopoverContent className="w-auto p-2" align="start"> | ||
<Calendar | ||
mode="single" | ||
selected={props.value} | ||
onSelect={onSelect} | ||
captionLayout="dropdown" | ||
/> | ||
</PopoverContent> | ||
</Popover> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import type { Meta, StoryObj } from "@storybook/react-vite"; | ||
import * as React from "react"; | ||
import { DateRangePicker } from "./date-range-picker"; | ||
|
||
const meta = { | ||
title: "Component/DateRangePicker", | ||
} satisfies Meta; | ||
|
||
export default meta; | ||
|
||
type Story = StoryObj<typeof meta>; | ||
|
||
export const Demo = { | ||
render: () => { | ||
const [dateRange, setDateRange] = React.useState<{ from?: Date; to?: Date } | undefined>(undefined); | ||
|
||
return ( | ||
<div className="space-y-4"> | ||
<DateRangePicker value={dateRange} onDateRangeChange={setDateRange}/> | ||
<div className="text-sm"> | ||
{dateRange ? ( | ||
<div className="space-y-1"> | ||
<div>From: {dateRange.from?.toString() || 'Not set'}</div> | ||
<div>To: {dateRange.to?.toString() || 'Not set'}</div> | ||
</div> | ||
) : ( | ||
<div>No date range selected</div> | ||
)} | ||
</div> | ||
</div> | ||
); | ||
}, | ||
} satisfies Story; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import { useCallback, useId, useState } from "react" | ||
import { format } from "date-fns" | ||
import { CalendarIcon } from "lucide-react" | ||
import type { DateRange } from "react-day-picker" | ||
import { Popover, PopoverContent, PopoverTrigger } from "#shadcn/components/ui/popover.js" | ||
import { Button } from "#shadcn/components/ui/button.js" | ||
import { Calendar } from "#shadcn/components/ui/calendar.js" | ||
import { cn } from "#shadcn/lib/utils.js" | ||
|
||
|
||
type DateRangePickerProps = { | ||
value?: { from?: Date; to?: Date } | undefined | ||
onDateRangeChange?: (value: { from?: Date; to?: Date } | undefined) => void | ||
fromPlaceholder?: string | undefined | ||
toPlaceholder?: string | undefined | ||
formatDate?: (value: Date) => string | ||
} | ||
|
||
export function DateRangePicker(props: DateRangePickerProps) { | ||
const id = useId(); | ||
const [open, setOpen] = useState(false); | ||
|
||
const formatDate = useCallback((date: Date) => props.formatDate ? props.formatDate(date) : format(date, "MM/dd/yyyy"), []); | ||
|
||
const onSelect = useCallback((range: DateRange | undefined) => { | ||
if (!range) { | ||
props.onDateRangeChange && props.onDateRangeChange(undefined) | ||
} else { | ||
const result: { from?: Date; to?: Date } = {} | ||
if (range.from) result.from = range.from | ||
if (range.to) result.to = range.to | ||
props.onDateRangeChange && props.onDateRangeChange(result) | ||
} | ||
}, []) | ||
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. Bug: Stale Closures in Date Picker CallbacksThe Additional Locations (2)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. |
||
|
||
// Convert to DateRange type for Calendar | ||
let selectedRange: DateRange | undefined = undefined | ||
if (props.value?.from) { | ||
selectedRange = { from: props.value.from } | ||
if (props.value.to) selectedRange.to = props.value.to | ||
} | ||
|
||
return ( | ||
<Popover open={open} onOpenChange={setOpen}> | ||
<PopoverTrigger asChild> | ||
<Button | ||
id={id} | ||
variant="ghost" | ||
className="group bg-background hover:bg-background w-full justify-start px-3 font-normal" | ||
> | ||
<CalendarIcon | ||
size={16} | ||
className="text-muted-foreground/80 group-hover:text-foreground shrink-0 transition-colors" | ||
aria-hidden="true" | ||
/> | ||
<span | ||
className={cn("truncate", !props.value?.from && "text-muted-foreground")} | ||
> | ||
{props.value?.from ? ( | ||
props.value.to ? ( | ||
<> | ||
{formatDate(props.value.from)} - {formatDate(props.value.to)} | ||
</> | ||
) : ( | ||
formatDate(props.value.from) | ||
) | ||
) : ( | ||
"Pick a date range" | ||
)} | ||
</span> | ||
</Button> | ||
</PopoverTrigger> | ||
<PopoverContent className="w-auto p-2" align="start"> | ||
<Calendar | ||
mode="range" | ||
selected={selectedRange} | ||
onSelect={onSelect} | ||
/> | ||
</PopoverContent> | ||
</Popover> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,3 @@ | ||
"use client"; | ||
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. Why? |
||
import { | ||
ChevronDownIcon, | ||
ChevronLeftIcon, | ||
|
@@ -12,6 +11,7 @@ import { | |
} from "react-day-picker"; | ||
import { Button, buttonVariants } from "#shadcn/components/ui/button"; | ||
import { cn } from "#shadcn/lib/utils"; | ||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select"; | ||
|
||
function Calendar({ | ||
className, | ||
|
@@ -157,6 +157,25 @@ function Calendar({ | |
<ChevronDownIcon className={cn("size-4", className)} {...props} /> | ||
); | ||
}, | ||
Dropdown: ({ className, ...props }) => { | ||
return ( | ||
<Select | ||
defaultValue={props.value?.toString() || ''} | ||
onValueChange={(val) => props.onChange?.({ target: { value: val } } as any)} | ||
> | ||
<SelectTrigger className="relative w-full" size={"regular"}> | ||
<SelectValue /> | ||
</SelectTrigger> | ||
<SelectContent className="max-h-60 overflow-auto" side="bottom" sideOffset={4}> | ||
{props.options?.map((opt) => ( | ||
<SelectItem key={opt.value} value={opt.value.toString()}> | ||
{opt.label} | ||
</SelectItem> | ||
))} | ||
</SelectContent> | ||
</Select> | ||
) | ||
}, | ||
Comment on lines
+160
to
+178
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. This is not related to data table. Move to separate commit. |
||
DayButton: CalendarDayButton, | ||
WeekNumber: ({ children, ...props }) => { | ||
return ( | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Stale Closure in
onSelect
CallbackThe
onSelect
useCallback
has an empty dependency array, but it referencesprops.onDateChange
. This can lead to stale closure issues, causing the callback to use an outdatedonDateChange
prop.