feat: Enhance date picker usability

This commit is contained in:
gpt-engineer-app[bot]
2025-10-11 17:39:32 +00:00
parent 10950a4034
commit f083f8c25d
4 changed files with 183 additions and 1 deletions

View File

@@ -11,6 +11,9 @@ function Calendar({ className, classNames, showOutsideDays = true, ...props }: C
return ( return (
<DayPicker <DayPicker
showOutsideDays={showOutsideDays} showOutsideDays={showOutsideDays}
captionLayout="dropdown-buttons"
fromYear={props.fromYear || 1800}
toYear={props.toYear || new Date().getFullYear() + 10}
className={cn("p-3", className)} className={cn("p-3", className)}
classNames={{ classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0", months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",

View File

@@ -0,0 +1,79 @@
import * as React from "react";
import { format, parse, isValid } from "date-fns";
import { cn } from "@/lib/utils";
import { DatePicker, DatePickerProps } from "./date-picker";
interface DateInputWithFeedbackProps extends DatePickerProps {
showFeedback?: boolean;
}
export function DateInputWithFeedback({
showFeedback = true,
onSelect,
...props
}: DateInputWithFeedbackProps) {
const [parseStatus, setParseStatus] = React.useState<{
isValid: boolean;
message: string;
parsed?: Date;
}>({ isValid: true, message: "" });
const parseDate = (input: string): Date | null => {
if (!input) return null;
// Try ISO format: 2005-06-15
let parsed = parse(input, "yyyy-MM-dd", new Date());
if (isValid(parsed)) return parsed;
// Try US format: 06/15/2005
parsed = parse(input, "MM/dd/yyyy", new Date());
if (isValid(parsed)) return parsed;
// Try European format: 15/06/2005
parsed = parse(input, "dd/MM/yyyy", new Date());
if (isValid(parsed)) return parsed;
// Try short format: 6/15/05
parsed = parse(input, "M/d/yy", new Date());
if (isValid(parsed)) return parsed;
// Try short year format: 2005-6-15
parsed = parse(input, "yyyy-M-d", new Date());
if (isValid(parsed)) return parsed;
return null;
};
const handleSelect = (date: Date | undefined) => {
if (date) {
setParseStatus({
isValid: true,
message: `${format(date, "PPP")}`,
parsed: date
});
} else {
setParseStatus({ isValid: true, message: "" });
}
onSelect?.(date);
};
return (
<div className="space-y-1">
<DatePicker
allowTextEntry
onSelect={handleSelect}
{...props}
/>
{showFeedback && parseStatus.message && (
<p className={cn(
"text-xs",
parseStatus.isValid
? "text-green-600 dark:text-green-400"
: "text-amber-600 dark:text-amber-400"
)}>
{parseStatus.message}
</p>
)}
</div>
);
}

View File

@@ -1,9 +1,10 @@
import * as React from "react"; import * as React from "react";
import { format } from "date-fns"; import { format, parse, isValid } from "date-fns";
import { CalendarIcon } from "lucide-react"; import { CalendarIcon } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Calendar } from "@/components/ui/calendar"; import { Calendar } from "@/components/ui/calendar";
import { import {
Popover, Popover,
@@ -21,6 +22,8 @@ export interface DatePickerProps {
disablePast?: boolean; disablePast?: boolean;
fromYear?: number; fromYear?: number;
toYear?: number; toYear?: number;
allowTextEntry?: boolean;
dateFormat?: string;
} }
export function DatePicker({ export function DatePicker({
@@ -33,12 +36,65 @@ export function DatePicker({
disablePast = false, disablePast = false,
fromYear, fromYear,
toYear, toYear,
allowTextEntry = false,
dateFormat = "yyyy-MM-dd",
}: DatePickerProps) { }: DatePickerProps) {
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const [textInput, setTextInput] = React.useState("");
const [isTyping, setIsTyping] = React.useState(false);
const parseDate = (input: string): Date | null => {
if (!input) return null;
// Try ISO format: 2005-06-15
let parsed = parse(input, "yyyy-MM-dd", new Date());
if (isValid(parsed)) return parsed;
// Try US format: 06/15/2005
parsed = parse(input, "MM/dd/yyyy", new Date());
if (isValid(parsed)) return parsed;
// Try European format: 15/06/2005
parsed = parse(input, "dd/MM/yyyy", new Date());
if (isValid(parsed)) return parsed;
// Try short format: 6/15/05
parsed = parse(input, "M/d/yy", new Date());
if (isValid(parsed)) return parsed;
// Try short year format: 2005-6-15
parsed = parse(input, "yyyy-M-d", new Date());
if (isValid(parsed)) return parsed;
return null;
};
const handleTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setTextInput(value);
setIsTyping(true);
const parsed = parseDate(value);
if (parsed) {
onSelect?.(parsed);
}
};
const handleBlur = () => {
setIsTyping(false);
if (date) {
setTextInput(format(date, dateFormat));
} else {
setTextInput("");
}
};
const handleSelect = (selectedDate: Date | undefined) => { const handleSelect = (selectedDate: Date | undefined) => {
onSelect?.(selectedDate); onSelect?.(selectedDate);
setOpen(false); setOpen(false);
if (selectedDate) {
setTextInput(format(selectedDate, dateFormat));
}
}; };
const getDisabledDates = (date: Date) => { const getDisabledDates = (date: Date) => {
@@ -48,6 +104,48 @@ export function DatePicker({
return false; return false;
}; };
if (allowTextEntry) {
return (
<div className="relative flex gap-1">
<Input
type="text"
value={isTyping ? textInput : (date ? format(date, dateFormat) : "")}
onChange={handleTextChange}
onFocus={() => setIsTyping(true)}
onBlur={handleBlur}
placeholder={placeholder}
disabled={disabled}
className={cn("flex-1", className)}
/>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
size="icon"
disabled={disabled}
className="shrink-0"
type="button"
>
<CalendarIcon className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={date}
onSelect={handleSelect}
disabled={getDisabledDates}
initialFocus
className="p-3 pointer-events-auto"
fromYear={fromYear}
toYear={toYear}
/>
</PopoverContent>
</Popover>
</div>
);
}
return ( return (
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
@@ -59,6 +157,7 @@ export function DatePicker({
className className
)} )}
disabled={disabled} disabled={disabled}
type="button"
> >
<CalendarIcon className="mr-2 h-4 w-4" /> <CalendarIcon className="mr-2 h-4 w-4" />
<span className="truncate"> <span className="truncate">

View File

@@ -128,6 +128,7 @@ export function FlexibleDateInput({
disablePast={disablePast} disablePast={disablePast}
fromYear={fromYear} fromYear={fromYear}
toYear={toYear} toYear={toYear}
allowTextEntry={true}
/> />
)} )}