OpenTrip Docs
Frontend

Frontend (Feature-Sliced Design)

Frontend (Feature-Sliced Design)

The browser frontend (apps/web) is a React + TypeScript + Vite PWA organized with Feature-Sliced Design v2.1. The dependency-free WeChat shell (apps/miniapp) authenticates natively and hosts that same responsive PWA in a WebView. Reference: ../reference/frontend-sources.md.

Layers

app -> pages -> widgets -> features -> entities -> shared. Imports go only downward. Details in layers.md.

Pages First

Prototype-specific logic stays in the page (pages/travel-planner, pages/trips) with page-private widgets. Only genuinely reusable code (UI primitives, API client, map wrapper, formatters, i18n) lives in shared.

Unauthenticated routing

Signed-out web visitors land on the marketing page (pages/landing) at the root; the auth form lives at /signin. Deep links (e.g. a shared trip) route straight to sign-in so the target path survives login, and embedded WeChat sessions skip the landing entirely. The gate lives in app/App.tsx. Landing copy is centralized in the landing i18n namespace (EN + 中文) and its screenshots reuse the README captures under pages/landing/assets.

Every landing section is centred in a max-w-6xl column except the closing call to action, which is the page's one full-bleed surface: an edge-to-edge band (navy in light, the raised card surface in dark) that butts straight into the footer, so the page ends on a single weighted block. LandingFooter therefore carries no top margin of its own. The ask sits left and the mobile schedule capture rises out of the band's bottom edge, cropped by it. Behind both, a travel photo is washed in at ~10% opacity (CTA_PHOTO_ID in pages/landing/lib/content) purely as texture — it is decorative, so it is aria-hidden with no alt text, and it removes itself if the CDN request fails rather than leaving a broken-image glyph.

Unsplash CDN urls are built with unsplashSrc / unsplashSrcSet (shared/lib/unsplash), shared by the landing CTA and the error surfaces. Photo ids are stable and referenced literally at the call site.

Error pages

pages/error is one variant-driven surface for every error state — 404, 500, 403, 503 and offline — so the whole family shares layout, motion and actions. ErrorPage takes a variant; copy lives in the error i18n namespace (EN + 中文) and imagery is pulled at random from a per-variant pool of Unsplash CDN photos (alt text only — nothing overlaid on the image). Every action maps to a real destination (home, signIn, retry) — no dead-end buttons. The gate in app/App.tsx renders the 404 surface for unrecognized paths (isKnownPath in app/router.tsx), and app/AppErrorBoundary.tsx catches render-time crashes and shows the 500 surface with a working "Try again".

Known routes

isKnownPath (in app/router.tsx) is the single allow-list the gate consults before rendering the 404 surface, so every new client route must be registered there (with a regression test in app/router.test.ts). Current routes:

  • /, /today, /journal — the authenticated home hub surfaces owned by pages/trips (Trips grid, Today, Travelogues). Grouped as HUB_PATHS.
  • /journal/:entryId — travelogue reader (matchJournalEntryId).
  • /trips/:id — the single-trip planner (matchTripId).
  • /invite/:token — invite accept surface (matchInviteToken).
  • /signin, /miniapp — auth form and the WeChat embedded entry.

In the WeChat shell the hub surfaces share one native page: switching between HUB_PATHS (or leaving a travelogue reader) stays in the current WebView via SPA history, and only returning to the hub from a deeper native page (e.g. a trip) resets the native stack. See miniapp.md.

Today and travelogues (hub)

Both surfaces live under pages/trips with the trips grid:

  • Today — place selection persisted per user in localStorage, weather via the shared /api/weather client, and shortcuts into upcoming or recent trips.
  • Travelogues — versioned device-local draft documents (local-journal preview). Do not document them as synced or shared until a backend adapter exists. Composer/reader details and mobile sheets are in mobile-pwa.md.

Traveler-facing guidance: ../user/today-and-weather.mdx and ../user/travelogues.mdx.

Path aliases

@/* maps to apps/web/src/* (see apps/web/tsconfig.json and apps/web/vite.config.ts). Import primitives as @/shared/ui/button, the map as @/shared/ui/map, etc.

Public API

Each slice exposes an index.ts. Import the slice, never its internals:

// good
import { Button } from "@/shared/ui/button";
// bad
import { Button } from "@/shared/ui/button/button";

Where does code go?

KindLocation
Providers, router, global stylesapp/
A route/screen compositionpages/<name>/
Reused composite blockwidgets/<name>/
Reusable user scenariofeatures/<name>/
Reusable domain data/rulesentities/<name>/
UI primitives, api client, map, i18n, utilsshared/
  • ui-system.md — cossUI tokens, primitives, polish.
  • miniapp.md — native WebView shell, auth bridge, and setup.
  • map.md — MapLibre wrapper.
  • i18n.md — internationalization.
  • data-caching.md — React Query write-echo vs Hyperdrive stale SELECTs (create-trip and other mutations).
  • mobile-pwa.md — mobile breakpoint contract, planner mobile shell, responsive dialogs, PWA install metadata and headers.

On this page