plans
2026-05-11
2026 05 11 Unified Booking Flow

Unified Booking Flow Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Replace all booking entry point patterns with a single unified BookingModalContext.open(eventId) pattern. Remove ExperiencePicker modal and dead StickyCart component.

Architecture: All booking entry points call useBookingModal().open(eventId). The BookingModal is rendered once in the root layout via BookingModalByContext. No local isOpen/selectedEventId state in page components.

Tech Stack: React Context (BookingModalContext), Convex, Next.js App Router


File Inventory

Files to DELETE (dead code)

  • components/booking/experience-picker.tsx — unnecessary intermediate modal
  • components/booking/sticky-cart.tsx — dead code, not used
  • contexts/calendar-modal-context.tsx — calendar becomes display-only (no booking integration)
  • components/home/calendar-modal.tsx — replaced by UpcomingExperiencesSection

Files to MODIFY (entry points — standardize on context)

  • components/home/cta-section.tsx — replace ExperiencePicker with useBookingModal().open()
  • components/home/journey-section.tsx — replace ExperiencePicker with useBookingModal().open()
  • components/home/hero-bottom.tsx — replace ExperiencePicker with useBookingModal().open()
  • components/marketing/visit-cta-section.tsx — replace ExperiencePicker with useBookingModal().open()
  • components/schedule/schedule-list.tsx — replace local BookingModal with useBookingModal().open()
  • components/schedule/schedule-page.tsx — remove pickerOpen state
  • components/home/experience-schedule-section.tsx — replace local BookingModal with context
  • components/ui/upcoming-shows-section.tsx — replace local BookingModal with context
  • components/shows/experience-event-list.tsx — replace local BookingModal with context per row
  • components/shows/event-row.tsx — if exists, same pattern
  • components/audience/table-content.tsx — replace local BookingModal with context

Files to MODIFY (core infrastructure)

  • contexts/booking-modal-context.tsx — already correct, verify open(eventId) signature
  • components/ui/booking-modal.tsx — verify it reads eventId from context
  • app/layout.tsx — ensure BookingModalProvider wraps app (already correct)

Files to CREATE (if missing)

  • hooks/use-booking-modal.ts — typed hook wrapper for useContextSelector(BookingModalContext, ...)

Task 1: Verify Core Infrastructure

Goal: Ensure BookingModalContext and BookingModal work correctly with open(eventId) pattern.

Files:

  • Modify: contexts/booking-modal-context.tsx

  • Modify: components/ui/booking-modal.tsx

  • Step 1: Read current BookingModalContext

Read contexts/booking-modal-context.tsx to verify it has:

interface BookingModalActions {
  open: (eventId: string) => void;
  close: () => void;
  setEventId: (eventId: string | null) => void;
}
  • Step 2: Read BookingModal

Read components/ui/booking-modal.tsx to verify it reads eventId from context, not props.

  • Step 3: Create useBookingModal hook

Create hooks/use-booking-modal.ts:

import { useContextSelector } from "use-context-selector";
import { BookingModalContext } from "~/contexts/booking-modal-context";
 
export function useBookingModal() {
  const isOpen = useContextSelector(BookingModalContext, (s) => s.isOpen);
  const eventId = useContextSelector(BookingModalContext, (s) => s.eventId);
  const open = useContextSelector(BookingModalContext, (s) => s.open);
  const close = useContextSelector(BookingModalContext, (s) => s.close);
  const setEventId = useContextSelector(
    BookingModalContext,
    (s) => s.setEventId,
  );
 
  return { isOpen, eventId, open, close, setEventId };
}
  • Step 4: Commit
git add hooks/use-booking-modal.ts
git commit -m "feat(booking): add useBookingModal hook for unified entry point"

Task 2: Remove ExperiencePicker from CTA Sections

Goal: Replace all ExperiencePicker usages with useBookingModal().open(eventId).

Files:

  • Delete: components/booking/experience-picker.tsx
  • Modify: components/home/cta-section.tsx
  • Modify: components/home/journey-section.tsx
  • Modify: components/home/hero-bottom.tsx
  • Modify: components/marketing/visit-cta-section.tsx

cta-section.tsx

  • Step 1: Read current implementation

Read components/home/cta-section.tsx to find the ExperiencePicker usage pattern.

  • Step 2: Remove pickerOpen state and ExperiencePicker import

Replace:

const [pickerOpen, setPickerOpen] = useState(false);
// ...
<ExperiencePicker onClose={() => setPickerOpen(false)} />

With:

const { open } = useBookingModal();
// In button onClick:
open(eventId); // Use a hardcoded or derived eventId, or pass eventId prop
  • Step 3: Import useBookingModal

Add:

import { useBookingModal } from "~/hooks/use-booking-modal";
  • Step 4: Apply same pattern to journey-section.tsx

Read and modify components/home/journey-section.tsx — same pattern.

  • Step 5: Apply same pattern to hero-bottom.tsx

Read and modify components/home/hero-bottom.tsx — same pattern.

  • Step 6: Apply same pattern to visit-cta-section.tsx

Read and modify components/marketing/visit-cta-section.tsx — same pattern (may have multiple CTA variants).

  • Step 7: Delete ExperiencePicker

Delete components/booking/experience-picker.tsx.

  • Step 8: Commit
git add -A
git commit -m "refactor(booking): remove ExperiencePicker, use useBookingModal() everywhere"

Task 3: Remove Local BookingModal State from Schedule Page

Files:

  • Modify: components/schedule/schedule-list.tsx (or wherever the schedule list with booking is)

  • Modify: components/schedule/schedule-page.tsx

  • Step 1: Read schedule page

Read app/[locale]/(landing)/schedule/page.tsx to understand how it currently opens BookingModal.

  • Step 2: Remove pickerOpen state

Remove local useState for pickerOpen.

  • Step 3: Import and use useBookingModal
import { useBookingModal } from "~/hooks/use-booking-modal";
// ...
const { open } = useBookingModal();
// In per-show "Book Now" handler:
open(occurrenceId);
  • Step 4: Commit
git add components/schedule/schedule-list.tsx components/schedule/schedule-page.tsx
git commit -m "refactor(schedule): use BookingModalContext for booking"

Task 4: Remove Local BookingModal State from Experience Sections

Files:

  • Modify: components/home/experience-schedule-section.tsx

  • Modify: components/ui/upcoming-shows-section.tsx

  • Step 1: Read experience-schedule-section.tsx

Find where it opens BookingModal with local selectedEventId state.

  • Step 2: Replace with useBookingModal
const { open } = useBookingModal();
// Per-row onClick:
open(occurrenceId);
  • Step 3: Read upcoming-shows-section.tsx

Same pattern — replace local state with context.

  • Step 4: Commit
git add components/home/experience-schedule-section.tsx components/ui/upcoming-shows-section.tsx
git commit -m "refactor: unify booking entry points in experience sections"

Task 5: Remove Local BookingModal State from Show Pages

Files:

  • Modify: components/shows/experience-event-list.tsx

  • Modify: components/audience/table-content.tsx

  • Step 1: Read experience-event-list.tsx

Find per-row modalOpen state and <BookingModal> usage.

  • Step 2: Replace with useBookingModal per row
// Instead of local state per row, use eventId in the click handler
const { open } = useBookingModal();
// onClick on "Book" button for row with id:
open(occurrenceId);
  • Step 3: Read audience/table-content.tsx

Same pattern — replace local bookingEventId state with context.

  • Step 4: Commit
git add components/shows/experience-event-list.tsx components/audience/table-content.tsx
git commit -m "refactor: use BookingModalContext in show and audience pages"

Task 6: Remove StickyCart (Dead Code)

Files:

  • Delete: components/booking/sticky-cart.tsx

  • Step 1: Verify no imports exist

grep -r "sticky-cart" apps/frontend/components --include="*.tsx"

Expected: Only the file itself or no results.

  • Step 2: Delete the file
rm components/booking/sticky-cart.tsx
  • Step 3: Commit
git rm components/booking/sticky-cart.tsx
git commit -m "chore: remove dead StickyCart component"

Task 7: Simplify CalendarModal (Display-Only)

Files:

  • Modify: components/home/calendar-modal.tsx
  • Modify: contexts/calendar-modal-context.tsx

The CalendarModal becomes purely a date display tool — it no longer has any booking integration. The selected day highlights shows on the page but does NOT open BookingModal.

  • Step 1: Read current calendar-modal-context.tsx

  • Step 2: Simplify context if needed

If CalendarModalContext has more than just navigation state (isOpen, currentMonth, selectedDay, prevMonth, nextMonth, setSelectedDay), trim it. It should NOT have open or any booking-related actions.

  • Step 3: Ensure CalendarModal doesn't call booking

Read components/home/calendar-modal.tsx. Verify clicking a day does NOT call useBookingModal().open(). If it does, remove that behavior — calendar is display-only.

  • Step 4: Commit
git add components/home/calendar-modal.tsx contexts/calendar-modal-context.tsx
git commit -m "refactor(calendar): simplify to display-only, no booking integration"

Task 8: Verify All Entry Points Converted

Goal: Ensure no local BookingModal instantiation remains.

  • Step 1: Search for remaining local BookingModal usage
grep -rn "<BookingModal" apps/frontend/components --include="*.tsx" | grep -v "booking-modal.tsx"

Expected: Only BookingModalByContext in layout, or zero results.

  • Step 2: Search for ExperiencePicker import
grep -rn "experience-picker" apps/frontend --include="*.tsx"

Expected: Zero results after deletion.

  • Step 3: Commit any remaining fixes

Final Architecture After Refactor

┌─────────────────────────────────────────────────────────────────────────────┐
│                    UNIFIED BOOKING FLOW (AFTER)                             │
└─────────────────────────────────────────────────────────────────────────────┘

  Any Page Component                              Root Layout
         │                                            │
         │  const { open } = useBookingModal()        │
         │                                            │
         │  open(eventId)                             │
         │         │                                   │
         └────────►│                                   │
                  │                                   ▼
                  │         ┌─────────────────────────────────────────┐
                  │         │         BookingModalContext              │
                  │         │  (isOpen, eventId, open, close, etc.)   │
                  │         └─────────────────────────────────────────┘
                  │                    │
                  ▼                    ▼
           ┌─────────────────────────────┐
           │      BookingModal           │
           │  (reads eventId from       │
           │   context, not props)       │
           └─────────────────────────────┘


           ┌─────────────────────────────┐
           │     BookingFlow             │
           │  (StepTickets → Addons →    │
           │   Checkout → Confirmation)  │
           └─────────────────────────────┘

Entry points are now just:

const { open } = useBookingModal();
open(eventId);

No more:

  • Local selectedEventId state
  • <BookingModal isOpen={...} eventId={...} />
  • ExperiencePicker intermediate modal
  • StickyCart dead code

Testing Checklist

  • Homepage CTA opens BookingModal with correct event
  • Journey section CTA opens BookingModal
  • Hero bottom CTA opens BookingModal
  • Schedule page "Book Now" per show opens BookingModal
  • Experience schedule section "Book" button opens BookingModal
  • Upcoming shows section opens BookingModal
  • Show detail page "Book" opens BookingModal
  • CalendarModal no longer triggers booking
  • No ExperiencePicker modal exists
  • No local BookingModal state in any page component