mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 08:11:13 -05:00
feat: Enhance date picker usability
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
79
src/components/ui/date-input-with-feedback.tsx
Normal file
79
src/components/ui/date-input-with-feedback.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ export function FlexibleDateInput({
|
|||||||
disablePast={disablePast}
|
disablePast={disablePast}
|
||||||
fromYear={fromYear}
|
fromYear={fromYear}
|
||||||
toYear={toYear}
|
toYear={toYear}
|
||||||
|
allowTextEntry={true}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user