Files
2026-05-10 20:29:42 +08:00

8.0 KiB

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.
  • Web production build uses Astro server output with the @astrojs/node adapter so client-owned dynamic shell routes such as /{locale}/history/:id can be refreshed directly.
  • 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, authenticated session recovery, 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.
  • AppShell.tsx is the single owner of authenticated app session recovery. Do not add another client wrapper that also refreshes the session around every authenticated route.
  • 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:

// lib/api.ts: transport-only business API
export function getPointsBalance(): Promise<PointsBalance> {
  return authFetch<PointsBalance>(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:

// 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.