Booking Page to Modal 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: Remove /booking page route and replace all /booking links with BookingModal overlay triggered from ExperienceCard onBookNow callbacks.
Architecture: Replace page-navigated booking with modal-overlay booking. The BookingModal component already exists and wraps the 4-step flow. Parent components manage modal open state and eventId, passing onBookNow to ExperienceCard which renders a <button> that triggers the modal instead of navigating to /booking.
Tech Stack: Next.js 16 (App Router), paraglide-js i18n, BookingModal, ExperienceCard, BookingModalContext
File Map
Components that need modal state management (these render ExperienceCard with onBookNow)
| File | Responsibility |
|---|---|
components/home/upcoming-experiences-section.tsx | Home page experience list — needs to manage BookingModal state |
components/ui/upcoming-shows-section.tsx | Dinner theater page — needs to manage BookingModal state |
components/shows/experience-event-list.tsx | Event row with "Book" button — needs BookingModal state |
components/home/experience-schedule-preview.tsx | Schedule calendar — needs BookingModal state |
CTA-only components (no specific event, use ExperiencePicker flow)
| File | Responsibility |
|---|---|
components/home/cta-section.tsx | Hero CTA — opens ExperiencePicker modal on click |
components/home/hero-bottom.tsx | Hero bottom CTA — opens ExperiencePicker modal |
components/home/journey-section.tsx | Journey CTA — opens ExperiencePicker modal |
components/marketing/visit-cta-section.tsx | Visit CTA — opens ExperiencePicker modal |
app/[locale]/(landing)/schedule/page.tsx | Schedule page hero CTA — opens ExperiencePicker modal |
Booking flow components (already exist, no changes needed)
| File | Responsibility |
|---|---|
components/ui/booking-modal.tsx | Already exists — wraps 4-step booking in modal |
components/booking/experience-picker.tsx | Allows event selection when no eventId provided |
components/ui/experience-card.tsx | Already supports onBookNow prop |
Pages to remove/redirect
| File | Action |
|---|---|
app/[locale]/(landing)/booking/page.tsx | Delete — page route no longer needed |
app/[locale]/(landing)/booking/layout.tsx | Delete — only existed for booking page |
Other pages needing updates
| File | Responsibility |
|---|---|
app/[locale]/audience/page.tsx | Uses /booking/${id}/tickets href — needs BookingModal |
Task 1: Create BookingModalContext
Why: Many parent components need to open the same BookingModal from different child interactions. Instead of prop-drilling, a context lets any child open the booking modal.
Files:
-
Create:
apps/frontend/contexts/booking-modal-context.tsx -
Step 1: Write the context file
// ipsoc checked: 2026-05-11
// SoC: Global booking modal state — avoids prop drilling for onBookNow callbacks
import {
createContext,
useContext,
useState,
useCallback,
type ReactNode,
} from "react";
type BookingModalState = {
isOpen: boolean;
eventId: string | null;
open: (eventId: string) => void;
close: () => void;
};
const BookingModalContext = createContext<BookingModalState | null>(null);
export function BookingModalProvider({ children }: { children: ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
const [eventId, setEventId] = useState<string | null>(null);
const open = useCallback((id: string) => {
setEventId(id);
setIsOpen(true);
}, []);
const close = useCallback(() => {
setIsOpen(false);
setEventId(null);
}, []);
return (
<BookingModalContext.Provider value={{ isOpen, eventId, open, close }}>
{children}
</BookingModalContext.Provider>
);
}
export function useBookingModal() {
const ctx = useContext(BookingModalContext);
if (!ctx) {
throw new Error("useBookingModal must be used inside BookingModalProvider");
}
return ctx;
}- Step 2: Add BookingModalProvider to locale layout
Files:
- Modify:
apps/frontend/app/[locale]/layout.tsx
Read the file first to find where to add the provider (after ConvexClientProvider).
import { BookingModalProvider } from "~/contexts/booking-modal-context";
import { BookingModal } from "~/components/ui/booking-modal";Add <BookingModalProvider> wrapping children, and <BookingModal> inside the providers:
{/* Before: children */}
{/* After: */}
<BookingModalProvider>
{children}
</BookingModalProvider>
<BookingModalByContext />Where BookingModalByContext reads from context:
"use client";
import { useBookingModal } from "~/contexts/booking-modal-context";
import { BookingModal } from "~/components/ui/booking-modal";
function BookingModalByContext() {
const { isOpen, eventId, close } = useBookingModal();
if (!eventId) return null;
return <BookingModal isOpen={isOpen} onClose={close} eventId={eventId} />;
}- Step 3: Commit
git add apps/frontend/contexts/booking-modal-context.tsx apps/frontend/app/\[locale\]/layout.tsx
git commit -m "feat: add BookingModalContext for global booking modal state"Task 2: Update UpcomingExperiencesSection to use modal
Files:
- Modify:
apps/frontend/components/home/upcoming-experiences-section.tsx
Changes:
- Import
useBookingModalfrom context - Remove
ExperienceSchedulePreviewfallback (it uses/bookinglinks) — replace with a simple "no events" message OR import and useBookingModaldirectly - Pass
onBookNow={open}to eachExperienceCard
- Step 1: Update the component
// ipsoc checked: 2026-05-06
// SoC: Pure UI component - data from useUpcomingShowsFrom hook
// FIXED: extracted useQuery to useUpcomingShowsFromShows hook (2026-05-06)
// FIXED: 2026-05-06 - uses unified ShowCard component
// IMPROVED: Use SectionWrapper for consistent spacing
"use client";
import { Heading } from "~/components/ui/typography";
import { SectionWrapper } from "~/components/ui/section-wrapper";
import { SectionContainer } from "~/components/ui/section-container";
import { ExperienceCard } from "~/components/ui/experience-card";
import { useBookingModal } from "~/contexts/booking-modal-context";
import * as m from "~/src/paraglide/messages";
export type UpcomingShowData = {
event: {
_id: string;
date: string;
time: string;
status: "SCHEDULED" | "CANCELLED" | "SOLD_OUT";
bookedCount: number;
actualCapacity: number;
};
show: {
_id: string;
title: string;
slug: string;
gallery: string[];
defaultDinnerPrice: number;
supportedTicketTypes: ("DINNER_THEATRE" | "SHOW_ONLY")[];
};
};
type UpcomingExperiencesSectionProps = {
shows?: UpcomingShowData[];
};
export function UpcomingExperiencesSection({
shows: propShows,
}: UpcomingExperiencesSectionProps) {
const { open } = useBookingModal();
const shows = propShows ?? [];
return (
<SectionWrapper id="upcoming">
<SectionContainer className="flex flex-col gap-12">
<Heading level="h2" align="center">
{m.home_upcomingExperiences_title()}
</Heading>
{shows.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-10">
{shows.map((showData: UpcomingShowData) => (
<ExperienceCard
key={showData.event._id}
experience={showData}
onBookNow={open}
/>
))}
</div>
) : (
<p className="text-center text-muted-foreground py-12">
{m.noDates()}
</p>
)}
</SectionContainer>
</SectionWrapper>
);
}- Step 2: Commit
git add apps/frontend/components/home/upcoming-experiences-section.tsx
git commit -m "feat: wire ExperienceCard onBookNow to BookingModal context"Task 3: Update UpcomingShowsSection to use modal
Files:
- Modify:
apps/frontend/components/ui/upcoming-shows-section.tsx
Changes: Add internal modal state (since this component doesn't receive onBookNow from parent) and pass to ExperienceCard.
- Step 1: Update the component
// ipsoc checked: 2026-05-11
// FIXED: 2026-05-11 - Use SectionContainer for consistent layout
// FIXED: 2026-05-11 - Internal modal state management (self-contained)Add useState for selectedEventId, pass to BookingModal inside the component:
// Inside component:
const [selectedEventId, setSelectedEventId] = useState<string | null>(null);
const handleBookNow = useCallback((eventId: string) => {
setSelectedEventId(eventId);
}, []);Pass onBookNow={handleBookNow} to ExperienceCard.
Render BookingModal at bottom of component:
{
selectedEventId && (
<BookingModal
isOpen={true}
onClose={() => setSelectedEventId(null)}
eventId={selectedEventId}
/>
);
}- Step 2: Commit
git add apps/frontend/components/ui/upcoming-shows-section.tsx
git commit -m "feat: add internal BookingModal state to UpcomingShowsSection"Task 4: Update ExperienceEventList to use modal
Files:
- Modify:
apps/frontend/components/shows/experience-event-list.tsx
Changes: Replace router.push with BookingModal state. Add modal state and render BookingModal inline.
- Step 1: Update EventRow component
// Inside the component file:
import { useState, useCallback } from "react";
import { BookingModal } from "~/components/ui/booking-modal";
function EventRow({ event }: { event: Doc<"experienceEvents"> }) {
const [modalOpen, setModalOpen] = useState(false);
// ... existing code ...
const handleBook = useCallback(() => {
setModalOpen(true);
}, []);
// ... replace router.push with setModalOpen(true)
return (
<>
{/* existing JSX */}
<button onClick={handleBook} ... />
<BookingModal
isOpen={modalOpen}
onClose={() => setModalOpen(false)}
eventId={event._id}
/>
</>
);
}- Step 2: Commit
git add apps/frontend/components/shows/experience-event-list.tsx
git commit -m "feat: replace router.push with BookingModal in ExperienceEventList"Task 5: Update ExperienceSchedulePreview to use modal
Files:
- Modify:
apps/frontend/components/home/experience-schedule-preview.tsx(around line 404)
Changes: Replace the LocaleLink to /booking with BookingModal state for each experience.
The component renders a list of ExperienceScheduleItem with a "Book Now" button per item. Add a selectedEventId state and render a BookingModal for the selected event.
- Step 1: Add modal state and replace LocaleLink
// Inside ExperienceSchedulePreview component function:
const [selectedEventId, setSelectedEventId] = useState<string | null>(null);Replace the LocaleLink around line 404:
<button
type="button"
onClick={() => setSelectedEventId(experience._id)}
className="inline-flex items-center justify-center px-5 py-2.5 bg-gold/20 hover:bg-gold text-gold hover:text-background text-sm font-semibold rounded-full border border-gold/30 hover:border-transparent transition-all duration-200 whitespace-nowrap"
>
{m.common_buttons_bookNow()}
</button>Add BookingModal at bottom of the return:
{
selectedEventId && (
<BookingModal
isOpen={true}
onClose={() => setSelectedEventId(null)}
eventId={selectedEventId}
/>
);
}- Step 2: Commit
git add apps/frontend/components/home/experience-schedule-preview.tsx
git commit -m "feat: replace /booking links with BookingModal in ExperienceSchedulePreview"Task 6: Update CTA sections to use ExperiencePicker
Files:
- Modify:
apps/frontend/components/home/cta-section.tsx - Modify:
apps/frontend/components/home/hero-bottom.tsx - Modify:
apps/frontend/components/home/journey-section.tsx - Modify:
apps/frontend/components/marketing/visit-cta-section.tsx
Why: These CTA sections don't have a specific event — clicking them should open the ExperiencePicker which lets users browse and select an event before booking.
Changes: Replace LocaleLink href="/booking" with a <button> that opens ExperiencePicker. The ExperiencePicker already exists in components/booking/experience-picker.tsx and handles event selection, then navigates.
Actually — looking at experience-picker.tsx line 73, it does router.push('/booking?...'). Since we're removing /booking, we need to modify ExperiencePicker to instead use BookingModal after event selection.
Task 6a: Modify ExperiencePicker to use modal
Files:
-
Modify:
apps/frontend/components/booking/experience-picker.tsx -
Step 1: Update ExperiencePicker to use modal instead of router
Replace router.push with BookingModal state:
import { useState } from "react";
import { BookingModal } from "~/components/ui/booking-modal";
// Inside component:
const [selectedEventId, setSelectedEventId] = useState<string | null>(null);Replace router.push(\/booking?${params.toString()}#tickets`)withsetSelectedEventId(eventId)`.
Add BookingModal render:
{
selectedEventId && (
<BookingModal
isOpen={true}
onClose={() => setSelectedEventId(null)}
eventId={selectedEventId}
/>
);
}- Step 2: Commit
git add apps/frontend/components/booking/experience-picker.tsx
git commit -m "feat: ExperiencePicker opens BookingModal instead of /booking page"Task 6b: Update CTA sections
Files:
- Modify:
apps/frontend/components/home/cta-section.tsx - Modify:
apps/frontend/components/home/hero-bottom.tsx - Modify:
apps/frontend/components/home/journey-section.tsx - Modify:
apps/frontend/components/marketing/visit-cta-section.tsx
Changes: Replace LocaleLink href="/booking" with a <button onClick={openPicker}> that sets picker state.
Add picker state to each section. Since these are separate components on the same page (home), they should share picker state — but they can't share state directly. The cleanest approach:
- Wrap the home page in a
BookingPickerProvider - OR: Make each CTA section open
ExperiencePickeras its own modal/dialog
The simplest approach that works: Each CTA section manages its own picker modal state.
// cta-section.tsx changes:
import { useState } from "react";
import { ExperiencePicker } from "~/components/booking/experience-picker";
// Inside component:
const [pickerOpen, setPickerOpen] = useState(false);
<button onClick={() => setPickerOpen(true)} ...>
{pickerOpen && (
<ExperiencePicker onClose={() => setPickerOpen(false)} />
)}Repeat for hero-bottom.tsx, journey-section.tsx, visit-cta-section.tsx.
- Step 3: Update each CTA section
Apply the same pattern to each file: add useState for picker, replace LocaleLink with <button>, render ExperiencePicker.
- Step 4: Commit
git add apps/frontend/components/home/cta-section.tsx
git add apps/frontend/components/home/hero-bottom.tsx
git add apps/frontend/components/home/journey-section.tsx
git add apps/frontend/components/marketing/visit-cta-section.tsx
git commit -m "feat: replace /booking links with ExperiencePicker in CTA sections"Task 7: Update Schedule page CTA
Files:
- Modify:
app/[locale]/(landing)/schedule/page.tsx
Changes:
- Replace
ctaHref="/booking"with a button that opensExperiencePicker - Replace each
href="/booking"in the show rows with picker state
- Step 1: Add picker state
Add useState and ExperiencePicker to the schedule page:
const [pickerOpen, setPickerOpen] = useState(false);
const [selectedEventId, setSelectedEventId] = useState<string | null>(null);Replace the PageHero CTA and show row buttons:
<button onClick={() => setPickerOpen(true)} className="inline-flex ...">
{m.schedule_bookTickets()}
</button>Each show row button:
<button
onClick={() => setSelectedEventId(show.eventId ?? "")}
className="inline-flex items-center gap-2 ..."
>
{m.schedule_bookNow()}
</button>Render both modals:
{
pickerOpen && <ExperiencePicker onClose={() => setPickerOpen(false)} />;
}
{
selectedEventId && (
<BookingModal
isOpen={true}
onClose={() => setSelectedEventId(null)}
eventId={selectedEventId}
/>
);
}- Step 2: Commit
git add "apps/frontend/app/[locale]/(landing)/schedule/page.tsx"
git commit -m "feat: replace /booking links with BookingModal on schedule page"Task 8: Update Audience page booking link
Files:
- Modify:
app/[locale]/audience/page.tsx(line 141)
Changes: Replace the /booking/${nextShow._id}/tickets href with modal state.
- Step 1: Add modal state and replace href
const [bookingEventId, setBookingEventId] = useState<string | null>(null);Replace:
<a href={localizeHref(`/booking/${nextShow._id}/tickets`)} ...>With:
<button onClick={() => setBookingEventId(nextShow._id)} ...>Add modal render at end of component's return:
{
bookingEventId && (
<BookingModal
isOpen={true}
onClose={() => setBookingEventId(null)}
eventId={bookingEventId}
/>
);
}- Step 2: Commit
git add "apps/frontend/app/[locale]/audience/page.tsx"
git commit -m "feat: replace /booking link with BookingModal on audience page"Task 9: Remove /booking page route
Files:
- Delete:
apps/frontend/app/[locale]/(landing)/booking/page.tsx - Delete:
apps/frontend/app/[locale]/(landing)/booking/layout.tsx
Why: The booking page no longer exists — all booking flows are now handled by BookingModal.
- Step 1: Delete the booking page files
rm apps/frontend/app/\[locale\]/\(landing\)/booking/page.tsx
rm apps/frontend/app/\[locale\]/\(landing\)/booking/layout.tsx
rmdir apps/frontend/app/\[locale\]/\(landing\)/booking- Step 2: Commit
git add -A
git commit -m "feat: remove /booking page route — booking now via modal only"Verification
After all tasks:
grep -rn "/booking" apps/frontend --include="*.tsx" | grep -v node_modules | grep -v ".next"— should return zero resultsgrep -rn "router.push.*booking" apps/frontend --include="*.tsx"— should return zero results- Build succeeds:
npm run buildinapps/frontend npm run deployto deploy to Cloudflare Pages
Self-Review Checklist
- All
/bookinglinks replaced with eitherBookingModal(for specific event) orExperiencePicker(for generic CTA) -
BookingModalis rendered in the locale layout (global singleton) -
ExperiencePickerno longer usesrouter.push('/booking') - Deleted
/bookingpage and layout files - No new
use clientdirectives added to server components - All
BookingModalrenders passisOpen,onClose, andeventIdprops - Build passes with
npm run build