import React, { useEffect, useState } from 'react'; import ReactDOM from 'react-dom/client'; import { BrowserRouter, Routes, Route, useLocation, useNavigate } from 'react-router-dom'; import type { Location } from 'react-router-dom'; import ConfigProvider from 'antd/es/config-provider'; import antdTheme from 'antd/es/theme'; import 'antd/dist/reset.css'; import Button from 'antd/es/button'; import ModalDialog from 'antd/es/modal'; import App from './App'; import ErrorBoundary from './components/ErrorBoundary'; import ErrorFallback from './components/ErrorFallback'; import DevThrow from './components/DevThrow'; import Admin from './admin/Admin'; import AuthRouteModal from './customer/AuthRouteModal'; import Welcome from './customer/Welcome'; import Account from './customer/Account'; import PrivacyPolicy from './customer/PrivacyPolicy'; import Submit from './intake/Submit'; import VerifyEmail from './customer/VerifyEmail'; import ForgotPassword from './customer/ForgotPassword'; import ResetPassword from './customer/ResetPassword'; import Cart from './cart/Cart'; import Orders from './customer/Orders'; import { CustomerAuthProvider } from './customer/CustomerAuthContext'; import { CartProvider } from './cart/CartContext'; import { FavoritesProvider } from './customer/FavoritesContext'; import { ThemeModeProvider, useThemeMode } from './theme/ThemeContext'; import './styles.css'; import { fetchConfig } from './api'; import BrevoTracking from './BrevoTracking'; // The brand accent is monochrome, so it inverts between themes rather than // switching to a different hue. const LIGHT_ACCENT = '#1a1a1a'; const DARK_ACCENT = '#f0f0f0'; const REDUCED_MOTION_QUERY = '(prefers-reduced-motion: reduce)'; // What /account renders over when it was entered directly — a bookmark, a link // in an email, or a redirect after signing in. There is no page behind in that // case, and a modal floating on nothing has nowhere to close back to. const STOREFRONT_BACKDROP: Partial = { pathname: '/', search: '', hash: '' }; // Routes presented as a modal over the page the customer was already on rather // than as a page of their own. Each stays a real, linkable URL — bookmarkable, // refreshable, and closed by the browser's Back button — while never being // somewhere with no way out. const MODAL_ROUTES = ['/account', '/login', '/register', '/forgot-password', '/reset-password', '/welcome']; // Respects the OS-level "reduce motion" accessibility setting by turning off // antd's transitions. Beyond the accessibility win, animated popups are a // standing source of flake in end-to-end tests, which drive the app with this // preference enabled. function usePrefersReducedMotion(): boolean { const [prefers, setPrefers] = useState( () => typeof window !== 'undefined' && window.matchMedia(REDUCED_MOTION_QUERY).matches ); useEffect(() => { const query = window.matchMedia(REDUCED_MOTION_QUERY); const update = () => setPrefers(query.matches); query.addEventListener('change', update); return () => query.removeEventListener('change', update); }, []); return prefers; } // /account is a route that renders as a modal over whatever the customer was // looking at, rather than a page of its own. It stays a real, linkable URL — // bookmarkable, refreshable, and closed by the browser's Back button — while // never being a place with no way out of it. // Both hoisted out of the components that declared them inline. // // S6478 flags a function-returning-JSX in a prop as "defining a component // during render". These are render props — ErrorBoundary's `fallback` is typed // `(error: Error) => React.ReactNode` and called as `this.props.fallback(...)` // — so React only ever sees the returned elements, never a new component type, // and the subtree destruction the rule warns about does not happen. The rule's // own message offers `allowAsProps` for exactly this shape, which cannot be set // from here. // // Hoisting rather than suppressing because it costs nothing: neither closes // over anything local, so at module level each is one stable function instead // of a new closure per render. See #181. function modalErrorFallback(error: Error) { return ( , and // antd would otherwise announce the dialog's accessible name and then the // identical heading right after it. footer={null} onCancel={() => { window.location.href = '/'; }} > { window.location.href = '/'; }} > Close } /> ); } function pageErrorFallback(error: Error) { return ( window.location.reload()}> Reload , ]} /> ); } function AppRoutes() { const location = useLocation(); const navigate = useNavigate(); const state = location.state as { background?: Location } | null; const modalPath = MODAL_ROUTES.includes(location.pathname) ? location.pathname : null; // In-app navigation names the page to render behind. Anything else — a // bookmark, an email link, a link in a password reset message — falls back to // the storefront, so closing always lands somewhere real. const background = state?.background; const backdrop = modalPath ? background ?? { ...location, ...STOREFRONT_BACKDROP } : location; // Where a Google sign-in should land the customer: the page behind the modal, // not the modal's own path. Built here because the backdrop is only known // here, and validated again on the server (#345). const returnTo = `${backdrop.pathname}${backdrop.search ?? ''}`; function closeModal() { // Back, when there is somewhere to go back to, so closing the modal and // pressing Back do the same thing and neither leaves a dead entry behind. if (background) navigate(-1); else navigate('/', { replace: true }); } // Steps within the auth flow replace rather than push, so the whole detour // stays a single history entry and closing returns to where it started // instead of walking back through every tab that was looked at. function goWithinAuth(path: string) { navigate(path, { state: background ? { background } : undefined, replace: true }); } return ( <> {/* Renders nothing. Mounted here because this is the innermost place that has both the router and the auth context, which is what it needs to report route changes for a consenting customer (#56). */} {import.meta.env.DEV && } } /> } /> } /> {/* A page rather than a modal route, deliberately: order history is a list you read, like the cart, not a dialog you dismiss. Adding it to MODAL_ROUTES would put it back in the 700px box it just left. */} } /> } /> {/* Public and unauthenticated: the token is the whole access control. */} } /> } /> {/* Rendered outside the Routes above, which are showing the backdrop. */} {/* Unconditional, so /?boom=modal fires this boundary with the storefront rendered behind it — no session needed. */} {import.meta.env.DEV && } {modalPath === '/account' && } {modalPath === '/login' && ( )} {modalPath === '/register' && ( )} {modalPath === '/forgot-password' && ( goWithinAuth('/login')} /> )} {/* One-time, right after a Google sign-up (#342). A route rather than a flag so it has an address and uses the same modal machinery as every other auth screen. */} {modalPath === '/welcome' && } {modalPath === '/reset-password' && ( goWithinAuth('/forgot-password')} onBackToSignIn={() => goWithinAuth('/login')} /> )} ); } function Root() { const { mode } = useThemeMode(); const prefersReducedMotion = usePrefersReducedMotion(); return ( ); } // Fired at entry purely for its side effect: it sets the origin uploaded // images are fetched from (#103). Not awaited, because nothing should wait on // it — anything that renders first gets a site-relative path, which the app's // own origin still serves. // // Rejections are swallowed on purpose. A config that cannot be fetched is a // broken deployment which every other request will report; failing here would // only replace the app with an error page before it has drawn anything. void fetchConfig().catch(() => {}); ReactDOM.createRoot(document.getElementById('root')!).render( );