To ensure maintainability and support future data structure migrations, the application uses a versioned data contract for both localStorage and URL shareable payloads.
All persisted or shared data is wrapped in a versioned envelope to prevent direct coupling between the data format and the application logic.
// Generic envelope structure
export type Envelope<T, V extends number> = { version: V; data: T };The migrateToLatest utility acts as the single point of entry for de-serializing data. It automatically detects the version and upgrades legacy or older-versioned data to the latest supported version before it reaches the application state.
- Read/Parse: Data is retrieved and parsed from
localStorageor URL params. - Detection:
migrateToLatestidentifies if the data is a rawTrip[](Legacy v0) or anEnvelope(v1+). - Upgrade: If necessary, it chains migration functions (e.g.,
v0 -> v1 -> v2) untilCURRENT_VERSIONis reached. - Usage: The application always receives the latest format.
When the Trip data structure changes in a breaking way, follow these steps to perform a migration:
- Define the New Format:
- Update
src/types/trip.tswith the new structure.
- Update
- Update Migration Utility (
src/utils/migration.ts):- Increment
CURRENT_VERSIONtoN. - Define a new concrete envelope type for vN (e.g.,
type VersionedTripEnvelopeV2 = Envelope<TripV2[], 2>). - Create a migration function
migrateV<N-1>ToV<N>(data: Trip<N-1>[]): Trip<N>[]. - Update
migrateToLatestto handle the new version in the migration chain.
- Increment
- Update
isVersionedEnvelope:- Update the type guard to include support for the new version number.
- Update
useTrips.ts:- Ensure that any new properties or structural changes are handled during the hook's initialization or
addTrips/deleteTripmethods if needed.
- Ensure that any new properties or structural changes are handled during the hook's initialization or
- Test:
- Add test cases in
src/tests/migration.test.tsto verify the upgrade path fromv<N-1>tov<N>.
- Add test cases in
The application uses Llama 3.1 (8B) for itinerary generation, accessed via the Hugging Face Serverless API.
We enforce a JSON schema through strict system prompts. This allows us to reliably extract:
title: A clean summary of the trip.start/stop: Geocodable endpoints for map rendering and future filtering.content: The markdown-formatted itinerary.
To maintain user safety in a BYOK (Bring Your Own Key) environment, the application implements a strict Whitelist-Only sanitization policy for AI-generated content.
- Strict Whitelisting: We use
rehype-sanitizeto strip all potentially dangerous HTML tags (<script>,<object>,<embed>,<form>, etc.) and attributes (event handlers likeonclick). - Iframe Protection: Raw
<iframe>tags from the AI are blocked entirely. - Managed Map Rendering: Iframes are ONLY permitted when generated by our internal
TripViewerlogic, which converts verified Google Maps URLs into secure embedded views. - No Tracking: By blocking
<img>tags and external CSS, we prevent the AI from including tracking pixels or malicious styling.
The project follows a Smart Container & Dumb Presenter pattern (Clean Architecture) to ensure high testability and separation of concerns.
- Role: Data orchestration, service invocation (API calls), and state management.
- Location: Found in
src/app/(Pages) andsrc/hooks/. - Responsibility: They do not contain complex UI logic or styling. They pass data and event handlers down to presentational components.
- Role: Visual representation and user interaction.
- Location: Found in
src/components/. - Responsibility: They are "stateless" (logic-lite) and rely entirely on props. They must be easily unit-testable in isolation using Vitest and React Testing Library.
To maintain architectural clarity, every presentational component MUST have a co-located unit test file (e.g., src/components/MyComponent.test.tsx).
To maintain a "zero-backend" architecture using Next.js (SSR), we must ensure that the server-rendered HTML and client-rendered UI are identical during the initial mount.
- Initial State: All components using
localStorageor browser-native APIs must initialize with a default "empty" state (e.g.,[]for trips,''for API keys). - Server Rendering: The server renders the page using these empty defaults.
- Client Hydration: The client initially renders the same empty state, preventing a mismatch.
- Client Mounting: After the component mounts on the client, a
useEffecthook is triggered to safely read fromlocalStorageand update the state. - Re-render: React performs a controlled update, populating the UI with the retrieved data.
This pattern is mandatory for all components accessing persistent browser storage to avoid React hydration errors.