# Past dates visually unselectable and rejected client-side before API call

- **Pattern:** `ab-000525` (`booking-calendar-availability.calendar-display.past-date-protection`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000525
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000525)

## 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`:

```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()` or `Date.now()`. Verify both visual and interaction protection:
  1. **Visual state:** Past dates must have distinct styling (grayed out, `opacity-50`, `cursor-not-allowed`, or different color).
  2. **Interaction:** Clicking a past date must either do nothing (`disabled` attribute), show feedback ("This date has passed"), or prevent the booking form from opening.
  3. **API call prevention:** The click handler must return early or be disabled before any `fetch`/`axios` call fires.
  4. **Code check:** Look for `new Date(slot.time) < new Date()`, `isBefore(slotDate, now)`, or equivalent. Examine files matching `src/components/*Slot*`, `src/components/*Calendar*`, `src/components/*calendar*`.
- **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:

  ```tsx
  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: user-experience

HTML version: https://auditbuffet.com/patterns/ab-000525
