Architecture
Routing and localisation
Two locales, English (default) and Spanish, served as path prefixes: /… and /es/…. src/routes/index.tsx mounts the same pageRoutes array twice under a shared Layout; there is no separate Spanish component tree.
- Strings come from
react-i18nextnamespaces insrc/lib/i18n/locales/{en,es}/<namespace>.json(common, nav, footer, home, about, books, store, checkout, conferences, donate, resources, contact, news, gettingStarted, pureWarrior, pureWarriorAbout, account). - Locale detection: URL path (custom detector in
src/lib/i18n/detect.ts), then localStorage, then the browser.LocaleProviderkeepsi18n.languageand<html lang>in sync with the URL. LocalizedLink/LocalizedNavLinkprefix/esfor internal links and pass external, anchor and mail links through untouched.
Content sources
Most marketing copy was moved off Sanity into static JSON and i18n files (snapshot and rationale in docs/content-extraction/README.md). Sanity now holds only siteSettings, past conference recaps, newsArticle and contactPage. Resources moved to the backend; events, products and funds always came from it.
Sanity access goes through getClient(preview) in src/lib/sanity/client.ts (published CDN client or stega-enabled draft client) and the useSanityQuery<T> hook, which returns a {status, data, error} union. GROQ queries live in src/lib/sanity/queries/.
Backend access
Each src/lib/*.ts module builds ${VITE_EMAW_API_BASE_URL}/rest/... and uses fetch with the shared useAsync pattern. There is no global API client.
- Auth (
src/lib/auth.ts,src/lib/account/AuthProvider.tsx): access token in memory, refresh token inlocalStorage['emaw:refresh']and sent in the body (the backend's HttpOnly cookie is third-party here).authFetchrefreshes once on 401.optionalAuthHeaders()lets checkouts run signed in or as a guest. - Hand-off (
src/lib/app.ts,OpenAppPage):/appis the only entry point into the web app; see Authentication. - Stripe (
src/lib/stripe.ts): oneloadStripeinstance per locale;CHECKOUT_APPEARANCEandPAYMENT_ELEMENT_OPTIONS(card only; Apple Pay, Google Pay and Link off; billing fields from the site's ownAddressFields). All three checkouts use@stripe/react-stripe-js/checkoutElements with a backend-issuedclient_secret.
State
- Cart:
CartProviderinLayout, lines{slug, quantity}in localStorage,useCart(). - Checkout steps for events and donations are kept in
sessionStorageso a refresh resumes them (emaw:donation-checkout, 30 minutes). - Auth:
AuthProvidercontext,useAuth(),useAfterSignIn()for?next=routing.
Media
Resource media URLs come fully formed from the backend (media.url, an R2 public URL). ResourceVideoPlayer renders an HTML5 <video> or a youtube-nocookie.com iframe. Cover images are Azure Blob URLs. The site has no R2 configuration.