• English
  • Architecture

    This page describes the source layout, layer responsibilities, key design patterns, shared UI components, and the i18n system.


    Source Layout

    src/
    ├── main.tsx              # React bootstrap (BrowserRouter, i18n import)
    ├── App.tsx               # Root: ErrorBoundary → AppRoutes
    ├── routes.tsx            # Route table (3 routes)
    ├── index.css             # Tailwind base + custom globals
    
    ├── pages/                # Thin route containers — no business logic
    │   ├── HomePage.tsx
    │   ├── BookingPage.tsx
    │   └── ManageRoute.tsx
    
    ├── features/             # Business feature modules
    │   ├── booking/          # Full booking wizard + sub-components
    │   │   ├── BookingWizard.tsx          # Orchestrator; owns state machine
    │   │   ├── useBookingMachine.ts       # Step state machine (useState-based FSM)
    │   │   ├── BookingProfileProvider.tsx # Fetches salon profile; React context
    │   │   ├── StoreHeader.tsx            # Salon info panel (collapsible)
    │   │   ├── SlotPicker.tsx             # Reusable date/time slot picker
    │   │   └── steps/
    │   │       ├── PickService.tsx        # Step 1: choose services
    │   │       ├── VerifyContact.tsx      # Step 3: name + contact input + OTP request
    │   │       ├── EnterCode.tsx          # Step 4: OTP entry
    │   │       └── Done.tsx               # Step 6: confirmation
    │   └── manage/
    │       └── ManagePage.tsx             # View / cancel / reschedule flow
    
    ├── components/           # Shared, reusable UI components
    │   ├── ConfirmDialog.tsx
    │   ├── Countdown.tsx
    │   ├── EmptyState.tsx
    │   ├── ErrorBoundary.tsx
    │   ├── ErrorState.tsx
    │   ├── LanguageSwitch.tsx
    │   ├── LoadingState.tsx
    │   ├── useCountdown.ts
    │   └── ui/               # Low-level primitives
    │       ├── Badge.tsx, Button.tsx, Card.tsx, ChipBar.tsx
    │       ├── Drawer.tsx, Input.tsx, InputOTP.tsx
    │       ├── SelectableCard.tsx, Skeleton.tsx, Spinner.tsx
    
    ├── lib/                  # Pure logic — zero UI, zero React (except PowProvider)
    │   ├── api/
    │   │   ├── booking.ts    # All API call functions
    │   │   ├── client.ts     # fetch wrapper (apiGet/apiPost/apiDelete)
    │   │   └── errors.ts     # ApiError class + error code mapping
    │   ├── pow/
    │   │   ├── PowProvider.tsx        # Context; caches solved PoW tokens
    │   │   ├── solve.ts               # SHA-256 brute-force solver (main thread)
    │   │   ├── solveAuto.ts           # Dispatches to Web Worker if available
    │   │   ├── solver.worker.ts       # Worker entry point
    │   │   └── leadingZeroBits.ts     # Bit-counting utility
    │   ├── contact/
    │   │   └── validate.ts    # Email and NANP phone validation + formatting
    │   ├── services/
    │   │   └── group.ts       # Service list → grouped by category
    │   ├── time/
    │   │   ├── calendar.ts    # YYYY-MM-DD arithmetic, monthGrid
    │   │   ├── hours.ts       # Parse/format business hours JSON
    │   │   └── tz.ts          # Format UTC instants in salon's IANA timezone
    │   ├── config.ts          # apiUrl() — single URL construction point
    │   └── storage.ts         # localStorage: saved bookings list
    
    └── i18n/
        ├── index.ts           # i18next initialization
        ├── en.json            # English strings
        └── zh-TW.json         # Traditional Chinese strings

    Layer Responsibilities

    src/pages/ — Route Containers

    Page components are intentionally thin. They extract URL params and pass them down; they do not fetch data or hold business state. Example: BookingPage extracts slug and ?serviceId= from the URL, then renders the provider stack and wizard.

    src/features/ — Business Modules

    Each feature directory owns its domain end-to-end: state, API calls (via lib/api), and UI. Features are self-contained; they do not import from each other.

    src/components/ — Shared UI

    Stateless or lightly stateful UI elements used across multiple features. src/components/ui/ contains low-level primitives (Button, Card, Input, etc.).

    src/lib/ — Pure Logic

    All functions in src/lib/ (except PowProvider.tsx) are framework-agnostic utilities with no React imports. This layer is independently testable and is where all side-effectful operations (API calls, crypto, storage) are encapsulated.


    Key Design Patterns

    Flat State Machine in useBookingMachine

    The booking wizard step sequence is implemented as a plain useState-based finite state machine in src/features/booking/useBookingMachine.ts. There is no XState or external library. The machine returns action callbacks (pickServices, pickTime, holdCreated, toEnterCode, etc.), and BookingWizard passes them down as props to each step component.

    React Context for Profile Data

    BookingProfileProvider (src/features/booking/BookingProfileProvider.tsx) fetches the salon profile once per slug and exposes it via React context. Three consumers read from it directly: StoreHeader, PickService (currency), and BookingWizard (timezone, otpMethods, hoursJson).

    PoW Context (PowProvider)

    PowProvider (src/lib/pow/PowProvider.tsx) wraps the booking wizard and provides getFreshPow(). The provider caches solved tokens for 240 seconds and deduplicates concurrent callers with a single in-flight Promise. The solver runs in a Web Worker to avoid blocking the main thread.

    SlotPicker is Reused in Two Contexts

    SlotPicker (src/features/booking/SlotPicker.tsx) accepts a load prop ((date) => Promise<AvailabilityResponse>) that is injected by the parent. In BookingWizard the function is getAvailability(slug, serviceIds, date); in ManagePage it is getManageAvailability(token, date). This keeps the picker generic.

    Cancellation Pattern

    Both BookingProfileProvider and PickService use an active boolean flag in their fetch useEffects to prevent stale state updates after unmount or re-fetch. This is a React idiom used consistently throughout src/features/.

    loadRef for Stable Callbacks

    SlotPicker stores the load prop in a useRef so the fetchDate effect's dependency array is stable while still always calling the latest version of the function. This avoids infinite re-render loops when the caller creates a new function reference on each render.


    Shared UI Components

    ComponentPurpose
    ErrorBoundaryTop-level React error boundary (wraps all routes)
    ConfirmDialogModal confirmation (used for cancel and reschedule actions)
    CountdownDisplays a live countdown in mm:ss format; used for hold expiry and code resend timer
    LanguageSwitchDropdown to toggle enzh-TW
    EmptyStateStyled placeholder for empty lists
    ErrorStateStyled error display with optional retry button
    LoadingStateCentered spinner for full-page loading
    ui/ChipBarHorizontally scrollable chip filter bar (used for service categories and day navigation)
    ui/SelectableCardCard with checkbox indicator (used for service selection)
    ui/DrawerBottom sheet drawer (used for calendar in SlotPicker)
    ui/InputOTP6-slot OTP input with auto-advance
    ui/BadgeStatus badge (booking status display in ManagePage)

    i18n

    Two languages are supported: English (en) and Traditional Chinese (zh-TW). Both bundles are bundled statically (no lazy loading).

    Initialization (src/i18n/index.ts): i18next + react-i18next. Default language is en; fallback is en. The module is imported as a side effect in src/main.tsx.

    Key namespaces in the single translation namespace:

    Key prefixCovers
    commonShared labels: next, back, retry, loading, slowDown
    serviceService picker labels, duration format, selection limits
    timeDate/time picker: headings, time-of-day labels (morning/afternoon/evening)
    contactName/phone/email field labels and validation messages
    verifyOTP request step messages, channel switching
    codeOTP entry step messages, resend with countdown
    bookingPost-submit: pending title, hold expiry, error messages
    notBookable404 salon page message
    homeLanding page copy
    manageManage page: status labels, cancel/reschedule dialog copy, error messages
    storeSalon info panel: hours display, directions link

    Language switching is available at runtime via <LanguageSwitch>, which appears on HomePage and ManageRoute.


    Notes for Future Agents

    • Adding a new booking step: Add a value to the Step union in useBookingMachine.ts, add transition actions, then add the rendering branch in BookingWizard.tsx.
    • Adding a new API call: Add the typed function to src/lib/api/booking.ts following the existing pattern (apiGet/apiPost/apiDelete from client.ts).
    • Adding a new error code: Add the string to the ApiErrorCode union in errors.ts and update KNOWN_BODY_CODES if the backend sends it as a body error.
    • Adding a language: Add a JSON file to src/i18n/, import it in index.ts, and add the locale code to the resources map.
    • Changing the API base URL: Only src/lib/config.ts (apiUrl()) constructs URLs — do not hardcode paths elsewhere.