feat: custom calendar date picker replacing native input

Build fully custom CalendarPopover with date-fns + Radix Popover.
Month/year dropdown selectors, today button, clear button, past
dates dimmed. Rewrite DueDatePicker to use it instead of <input type=date>.
This commit is contained in:
Your Name
2026-02-15 22:04:31 +02:00
parent 03e0b132da
commit bc12b5569a
3 changed files with 786 additions and 52 deletions

View File

@@ -1,6 +1,7 @@
import { format, formatDistanceToNow, isPast, isToday } from "date-fns";
import { Button } from "@/components/ui/button";
import { X } from "lucide-react";
import { useBoardStore } from "@/stores/board-store";
import { CalendarPopover } from "@/components/card-detail/CalendarPopover";
interface DueDatePickerProps {
cardId: string;
@@ -13,70 +14,67 @@ export function DueDatePicker({ cardId, dueDate }: DueDatePickerProps) {
const dateObj = dueDate ? new Date(dueDate) : null;
const overdue = dateObj != null && isPast(dateObj) && !isToday(dateObj);
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const val = e.target.value;
updateCard(cardId, { dueDate: val || null });
function handleSelect(date: Date) {
updateCard(cardId, { dueDate: format(date, "yyyy-MM-dd") });
}
function handleClear() {
updateCard(cardId, { dueDate: null });
}
// Format the date value for the HTML date input (YYYY-MM-DD)
const inputValue = dateObj
? format(dateObj, "yyyy-MM-dd")
: "";
return (
<div className="flex flex-col gap-2">
{/* Header */}
<h4 className="font-mono text-xs uppercase tracking-widest text-pylon-text-secondary">
Due Date
</h4>
{/* Current date display */}
{dateObj && (
<div className="flex items-center gap-2">
<span
className={`text-sm font-medium ${
overdue ? "text-pylon-danger" : "text-pylon-text"
}`}
>
{format(dateObj, "MMM d, yyyy")}
</span>
<span
className={`text-xs ${
overdue ? "text-pylon-danger" : "text-pylon-text-secondary"
}`}
>
{overdue
? `overdue by ${formatDistanceToNow(dateObj)}`
: isToday(dateObj)
? "today"
: `in ${formatDistanceToNow(dateObj)}`}
</span>
</div>
)}
{/* Date input + clear */}
<div className="flex items-center gap-2">
<input
type="date"
value={inputValue}
onChange={handleChange}
className="h-7 rounded-md border border-pylon-text-secondary/20 bg-pylon-column px-2 text-xs text-pylon-text outline-none focus:border-pylon-accent focus:ring-1 focus:ring-pylon-accent"
/>
{/* Header with clear button */}
<div className="flex items-center justify-between">
<h4 className="font-mono text-xs uppercase tracking-widest text-pylon-text-secondary">
Due Date
</h4>
{dueDate && (
<Button
variant="ghost"
size="xs"
<button
onClick={handleClear}
className="text-pylon-text-secondary hover:text-pylon-danger"
className="rounded p-0.5 text-pylon-text-secondary transition-colors hover:bg-pylon-danger/10 hover:text-pylon-danger"
aria-label="Clear due date"
>
Clear
</Button>
<X className="size-3.5" />
</button>
)}
</div>
{/* Clickable date display -> opens calendar */}
<CalendarPopover
selectedDate={dateObj}
onSelect={handleSelect}
onClear={handleClear}
>
<button className="flex w-full items-center gap-2 rounded-md px-1 py-1 text-left transition-colors hover:bg-pylon-column/60">
{dateObj ? (
<>
<span
className={`text-sm font-medium ${
overdue ? "text-pylon-danger" : "text-pylon-text"
}`}
>
{format(dateObj, "MMM d, yyyy")}
</span>
<span
className={`text-xs ${
overdue ? "text-pylon-danger" : "text-pylon-text-secondary"
}`}
>
{overdue
? `overdue by ${formatDistanceToNow(dateObj)}`
: isToday(dateObj)
? "today"
: `in ${formatDistanceToNow(dateObj)}`}
</span>
</>
) : (
<span className="text-sm italic text-pylon-text-secondary/60">
Click to set date...
</span>
)}
</button>
</CalendarPopover>
</div>
);
}