Past dates visually unselectable and rejected client-side before API call
Why it matters
Clickable past dates send doomed POST requests to the booking API, wasting backend cycles, polluting logs with 4xx errors, and surfacing generic server-side failures that erode user trust. Customers who manage to submit a past date hit a confusing rejection screen mid-flow, abandon the booking, and never return. This is a preventable user-experience regression that also inflates telemetry noise and masks real availability failures in your error-rate dashboards.
Severity rationale
Low because server-side validation still blocks the booking, so the impact is UX friction and log pollution rather than data corruption or revenue loss.
Remediation
In your calendar slot component, compute isPast from new Date(slot.time) < new Date() once per render, then apply both a disabled attribute and a visually distinct class (opacity-50 cursor-not-allowed) when true. Short-circuit the click handler before any fetch call fires. See src/components/CalendarSlot.tsx:
const isPast = new Date(slot.time) < new Date()
if (isPast) { toast.error('This date has passed'); return }
Detection
-
ID:
past-date-protection -
Severity:
low -
What to look for: Count all date comparison checks in the calendar component. Look for client-side logic that compares slot dates to
new Date()orDate.now(). Verify both visual and interaction protection:- Visual state: Past dates must have distinct styling (grayed out,
opacity-50,cursor-not-allowed, or different color). - Interaction: Clicking a past date must either do nothing (
disabledattribute), show feedback ("This date has passed"), or prevent the booking form from opening. - API call prevention: The click handler must return early or be disabled before any
fetch/axioscall fires. - Code check: Look for
new Date(slot.time) < new Date(),isBefore(slotDate, now), or equivalent. Examine files matchingsrc/components/*Slot*,src/components/*Calendar*,src/components/*calendar*.
- Visual state: Past dates must have distinct styling (grayed out,
-
Pass criteria: At least 1 date comparison guard must exist in the calendar component preventing past-date selection. Past dates must have at least 2 visual distinctions (e.g.,
opacity-50+cursor-not-allowed, or gray color + disabled attribute). No API call may fire when a past date is clicked. Report: "Past-date guard found: [yes/no]. Visual distinctions: [list]." -
Fail criteria: Past dates look the same as available dates, or clicking them makes an API request, or no client-side date comparison exists.
-
Skip (N/A) when: Calendar does not display past dates at all, or does not allow date selection.
-
Detail on fail: Example:
"Past dates are not visually different. Clicking yesterday's date opens the booking form and sends an API request, which fails server-side with a generic error." -
Cross-reference: Check
visual-distinction— past is one of the 4 required distinct slot states. -
Cross-reference: Check
unavailable-feedback— past date clicks should produce feedback, not silent no-ops. -
Cross-reference: Check
same-day-cutoff— past-date protection is client-side; cutoff must also be validated server-side. -
Remediation: Disable and style past dates client-side:
export function CalendarSlot({ slot, onSelect }) { const now = new Date() const slotDate = new Date(slot.time) const isPast = slotDate < now const handleClick = () => { if (isPast) { toast.error('This date has passed') return } onSelect(slot) } return ( <button onClick={handleClick} disabled={isPast} className={` p-2 rounded border ${isPast ? 'bg-gray-100 text-gray-400 cursor-not-allowed opacity-50' : 'bg-green-100 text-green-900 cursor-pointer hover:bg-green-200' } `} aria-label={isPast ? 'Past date, unavailable' : `Available: ${slot.time}`} > {isPast && '◄'} {format(slotDate, 'MMM d')} </button> ) }
Taxons
History
- 2026-04-18·v1.0.0·Initial import from booking-calendar-availability·automated