mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 21:31:46 +00:00
fc7d8b5d12
Brownfield strangler migration of the backend onto NestJS modules (auth, trips, days, places, assignments, packing, todo, budget, reservations, collab, files, photos, journey, share, settings, backup, oidc, oauth, admin, atlas, vacay, weather, airports, maps, categories, tags, notifications, system-notices) served through a per-prefix dispatcher, keeping the existing SQLite/better-sqlite3 DB and JWT httpOnly cookie auth, with behavioural parity for every route. Client: React 19 upgrade, "page = wiring container + data hook" pattern across all pages, per-domain Zustand stores bound to @trek/shared contracts, and decomposition of the large components (DayPlanSidebar, PackingListPanel, CollabNotes, FileManager, MemoriesPanel, PlacesSidebar, CollabChat, SystemNoticeModal, BudgetPanel, PlaceFormModal, ...) into focused render units backed by in-file hooks. Apply the shared global request pipeline (helmet/CSP, CORS, HSTS, forced HTTPS, the global MFA policy and request logging) to the NestJS instance as well, so a migrated route is protected identically to the legacy fallback rather than bypassing it.
2.5 KiB
2.5 KiB
Page pattern: wiring container + data hook
Every page under src/pages follows the same shape: the exported *Page
component is a thin wiring container and all of its state, effects, data
loading and event handlers live in a co-located use<Page>() hook.
src/pages/
DashboardPage.tsx ← container: reads the hook, renders JSX
dashboard/
useDashboard.ts ← state, effects, API calls, handlers
dashboardModel.ts ← (optional) pure types + helpers, no React
What goes where
The hook (use<Page>.ts) owns everything stateful:
useState/useReducer/useRefuseEffect/useLayoutEffectuseMemo/useCallback- store selectors, API calls, WebSocket listeners, handlers
- derived values
It returns a single object the page destructures.
The page (*Page.tsx) is presentation only:
const { ... } = use<Page>()useTranslation()fort/locale(a context hook, not state — allowed)- JSX, and
t-dependent display arrays like the tab list - presentational sub-components and pure helpers may live in the same file, before or after the default export
export default function DashboardPage() {
const { t } = useTranslation()
const { trips, isLoading, handleCreate } = useDashboard()
if (isLoading) return <Spinner />
return <Grid trips={trips} onCreate={handleCreate} />
}
Why
- Testable — page tests render JSX; hook logic is isolated and mockable.
- Readable — the container reads top-to-bottom as "what the page shows".
- Diffable — logic changes touch the hook, layout changes touch the page.
Notes
- A
<page>Model.tsis optional — use it for pure types and helpers shared between the hook and the page (no React imports). Seeatlas/atlasModel.tsfor a mutable-lookup-table example andadmin/adminModel.tsfor types only. - The post-guard derivations that depend on a now-narrowed value (e.g. after
if (!current) return) may stay in the page next to the JSX that uses them. - Keep the rendered JSX byte-identical when extracting — this is a refactor of where logic lives, not a redesign.
Enforcement
npm run lint:pages (scripts/check-page-pattern.mjs) scans each *Page.tsx
default-export body and fails if it calls useState, useReducer, useEffect,
useLayoutEffect, useMemo, useCallback or useRef directly. Move that logic
into the page's hook. Sub-components and helper hooks in the same file are not
flagged.