Architecture
This page describes the source layout, layer responsibilities, key design patterns, shared UI components, and the i18n system.
Source Layout
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
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:
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
Stepunion inuseBookingMachine.ts, add transition actions, then add the rendering branch inBookingWizard.tsx. - Adding a new API call: Add the typed function to
src/lib/api/booking.tsfollowing the existing pattern (apiGet/apiPost/apiDeletefromclient.ts). - Adding a new error code: Add the string to the
ApiErrorCodeunion inerrors.tsand updateKNOWN_BODY_CODESif the backend sends it as a body error. - Adding a language: Add a JSON file to
src/i18n/, import it inindex.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.