# Web Spec ## Scope This spec applies to `web/**`. Read this file before changing the Astro site, React app islands, authenticated app routes, API clients, i18n, or responsive layout. ## Current Stack - Astro 6 for static public pages and route files. - React 19 for interactive client UI. - React Router DOM for the authenticated business app shell. - Tailwind CSS 4 through `@tailwindcss/vite`. - TypeScript strict mode. - Local i18n from `web/src/i18n/utils.ts`. - Backend API base for production: `https://api.meeyao.com`. - Local development API access uses the Vite `/api` proxy in `web/astro.config.mjs`. Do not introduce a second frontend framework, a second router, or scattered API URL construction for web code. ## Route Architecture Public pages are Astro pages under `web/src/pages/{locale}/` and use `Marketing.astro`. Authenticated pages are Astro route shells that all render `DashboardAppPage.astro`. The actual logged-in application is a single React Router app: - `DashboardApp.tsx` owns React Router routes for dashboard, store, history, notifications, profile, settings, and divination pages. - `AppShell.tsx` owns the persistent sidebar, mobile drawer, route guard, and authenticated layout. - Business page components render only their page body. They must not wrap themselves in `AppShell`. - Sidebar navigation must use React Router navigation so the shell remains mounted and only the right-side content changes. - Direct browser refresh on each existing business route must still render the app shell through Astro. Login and public marketing/legal pages are not part of the authenticated app shell. ## Auth Rules - Login and registration are the same email-code flow. The backend auto-registers new email accounts. - Test credentials for local verification: `test@example.com` with code `123456`. - Auth state is stored by `web/src/lib/auth.ts` under one local storage key. - Every authenticated route must recover or refresh the session before showing business content. - Missing, expired, invalid, or refresh-failed tokens must clear local auth and redirect to `/{locale}/login`. - Do not add silent success paths for auth failures. ## API Rules - All API paths live in `web/src/lib/api-routes.ts`. - Shared request behavior lives in `web/src/lib/api-client.ts`. - Auth/session behavior lives in `web/src/lib/auth.ts`. - Business API functions live in `web/src/lib/api.ts`. - Shared authenticated read caching lives in `web/src/lib/data-client.ts` and `web/src/lib/resources.ts`. - Components must call typed API helper functions, not inline `fetch('/api/...')`. - Components that need profile, points, packages, history, notifications, or unread-count data should use the resource hooks/functions from `web/src/lib/resources.ts` instead of starting their own duplicate GET lifecycle. - Dashboard-visible user, points, notification, and history data must come from the backend. Do not hardcode those values. - Production API host is `https://api.meeyao.com`; local dev should use same-origin `/api` and the Vite proxy. ### Authenticated Data Resource Pattern Use this pattern for backend reads that are reused across authenticated pages: ```typescript // lib/api.ts: transport-only business API export function getPointsBalance(): Promise { return authFetch(API_ROUTES.points.balance); } // lib/resources.ts: cache policy + hook/function surface export function usePoints() { return useResource({ key: pointsBalanceKey, ttlMs: 60_000, fetcher: getPointsBalance, staleWhileRevalidate: true, }); } ``` Resource contracts: - `lib/api.ts` remains transport-only: no per-endpoint ad hoc memory cache there. - `lib/resources.ts` owns resource keys, TTLs, in-flight dedupe, stale-while-revalidate behavior, prefetch, and mutation invalidation. - `clearAuth()` must clear the shared data cache so authenticated data cannot leak across users. - Resource hooks must support disabled/optional keys for pages where an id may be absent; do not create a fetcher that intentionally rejects during normal render. - Active hooks must refetch after invalidation when they still need the resource. Invalidation matrix: - Profile, avatar, or settings write -> set the profile resource with the returned backend profile. - Divination run or follow-up completion -> invalidate points and the relevant history list/thread resources. - Notification mark-read -> patch the notification list and decrement unread count when the item changes from unread to read. - Mark-all-notifications-read -> patch the notification list and set unread count to zero. - Logout, expired refresh, or invalid auth -> clear auth and clear all resource data. Wrong vs correct: ```typescript // Wrong: every page starts an independent duplicate GET. useEffect(() => { getUserProfile().then(setProfile); }, []); // Correct: subscribe to the shared profile resource. const profileState = useProfile(); ``` ## Layout Rules - Build mobile-first, then add `sm:`, `md:`, `lg:`, and `xl:` refinements. - Business pages must not require horizontal scrolling at common phone widths such as `390x844`. - Use responsive stacks for fixed-width desktop columns: `flex-col lg:flex-row`, `w-full lg:w-[...]`. - Keep the authenticated shell as `h-screen` with the main content scrollable. - Mobile sidebar must be reachable through the menu button and must not hide the page content permanently. - Public header mobile navigation must expose feature, pricing, about, login, and language switching. ### Mobile Guided Overlays - Keep one dimming strategy per viewport. Do not combine a full-screen dark overlay with a spotlight element that also uses an oversized outer shadow on the same mobile viewport. - Mobile spotlight targets should fit inside the phone viewport. If a desktop tutorial highlights a tall panel, use a smaller mobile-only target such as the rows or controls that the step actually explains. - Tooltip placement and arrow direction must match: a tooltip above the target uses a bottom arrow pointing down; a tooltip below the target uses a top arrow pointing up. - When the app shell owns scrolling, compute mobile overlay coordinates relative to the page component host and visible scroll container, not the document body. ## i18n Rules - Supported locales: `zh`, `zh_Hant`, `en`. - Routes are prefixed by locale, including the default locale. - User-visible text should come from `web/src/i18n/utils.ts` or locale-specific content assets. - Do not add user-facing strings in only one locale. ## Design Source - Pencil design files under `web/design/` are the visual source for login and public page design. - If UI implementation diverges from Pencil, inspect the design first and keep the code aligned unless the user explicitly asks to change the design. - Assets in `web/public/images` and `web/public/legal` are symlinks to `web/design/assets`; do not duplicate them. ## Verification Before finishing meaningful web changes: - Run `npm run build` in `web/`. - Use Chrome DevTools on `http://localhost:4322/` for at least one desktop and one phone viewport when layout or routing changed. - For authenticated changes, verify the test account can log in and lands on `/{locale}/dashboard`. - Verify direct unauthenticated access to a business route redirects to login. - Verify sidebar navigation changes content without a full document reload. ## Forbidden - Do not hardcode API URLs or endpoint paths in components. - Do not add `.env` as a required file for normal local development. - Do not reintroduce page-refresh sidebar navigation inside the authenticated app. - Do not wrap business page components in nested cards or duplicate `AppShell`. - Do not add fallback/mock success behavior for failed API calls.