This repository has been archived on 2026-08-17. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
beenvoice-web/src/components/ui/date-picker.tsx
T
soconnorandCursor 3fb61ff4dd Polish onboarding, invoices, and time clock while promoting the first registrant to admin.
Refresh onboarding wizard and shell, tighten invoice edit/detail flows, align timer widgets with the redesigned clock panel, and assign admin role on first signup.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 03:40:25 -04:00

148 lines
3.8 KiB
TypeScript

"use client";
import * as React from "react";
import { parseDate } from "chrono-node";
import { CalendarIcon } from "lucide-react";
import { Button } from "~/components/ui/button";
import { Calendar } from "~/components/ui/calendar";
import { Input } from "~/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "~/components/ui/popover";
import { cn } from "~/lib/utils";
const DATE_FORMAT_OPTIONS: Intl.DateTimeFormatOptions = {
day: "2-digit",
month: "long",
year: "numeric",
};
function formatDate(date: Date | undefined) {
if (!date) {
return "";
}
return date.toLocaleDateString("en-US", DATE_FORMAT_OPTIONS);
}
// Longest month name in en-US long format (September 30, 2026).
const LONGEST_FORMATTED_DATE = formatDate(new Date(2026, 8, 30));
interface DatePickerProps {
date?: Date;
onDateChange: (date: Date | undefined) => void;
placeholder?: string;
className?: string;
disabled?: boolean;
id?: string;
size?: "sm" | "md" | "lg";
inputClassName?: string;
}
export function DatePicker({
date,
onDateChange,
placeholder = "Tomorrow or next week",
className,
inputClassName,
disabled = false,
id,
size = "md",
}: DatePickerProps) {
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState(formatDate(date));
const [month, setMonth] = React.useState<Date | undefined>(date);
const sizeClasses = {
sm: "h-9 text-xs",
md: "h-9 text-sm",
lg: "h-10 text-sm",
};
const wantsFullWidth = className?.includes("w-full");
React.useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- Keep text input and calendar month synchronized with the controlled date prop.
setValue(formatDate(date));
setMonth(date);
}, [date]);
return (
<div
className={cn(
"relative min-w-max",
wantsFullWidth ? "w-full" : "w-auto shrink-0",
className,
)}
>
<span
aria-hidden
className={cn(
"invisible block whitespace-nowrap px-3 pr-10",
sizeClasses[size],
inputClassName,
)}
>
{LONGEST_FORMATTED_DATE}
</span>
<Input
id={id}
value={value}
placeholder={placeholder}
disabled={disabled}
className={cn(
"bg-background absolute inset-0 w-full pr-10 tabular-nums",
sizeClasses[size],
inputClassName,
)}
onChange={(e) => {
setValue(e.target.value);
const parsedDate = parseDate(e.target.value);
if (parsedDate) {
onDateChange(parsedDate);
setMonth(parsedDate);
}
}}
onKeyDown={(e) => {
if (e.key === "ArrowDown") {
e.preventDefault();
setOpen(true);
}
}}
/>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="ghost"
disabled={disabled}
className="text-primary/80 hover:text-primary absolute top-1/2 right-2 z-20 size-6 -translate-y-1/2 p-0 transition-colors"
>
<CalendarIcon className="size-4" />
<span className="sr-only">Select date</span>
</Button>
</PopoverTrigger>
<PopoverContent
className="w-auto overflow-hidden rounded-xl p-0"
align="end"
>
<Calendar
mode="single"
selected={date}
captionLayout="dropdown"
month={month}
onMonthChange={setMonth}
onSelect={(selectedDate) => {
onDateChange(selectedDate);
setValue(formatDate(selectedDate));
setOpen(false);
}}
/>
</PopoverContent>
</Popover>
</div>
);
}