An interactive, character-led storytelling experience that combines CMS-driven content, photo capture, and a 3D gallery. The app runs on the React Router framework stack with Vite, TanStack Query, styled-components, and i18next.
-
Install dependencies
npm install
-
Configure environment
No .env file is required for local development—API hosts and feature flags are hard-coded for dev vs. prod. If you ever need to host the app under a subpath, set VITE_BASE_PATH when running build commands (details below).
-
Run the dev server (Vite + React Router framework mode)
npm run dev
By default the app is served on http://localhost:5173 with hot module reloading.
| Command | Description |
|---|---|
npm run dev |
Starts the React Router dev server with Vite under the hood. |
npm run dev:host |
Same as dev, but binds to all interfaces for device testing. |
npm run build |
Framework build that emits the production bundle to build/client. |
npm run preview |
Serves the already-built bundle via vite preview. |
npm run lint |
Runs ESLint (React, Hooks, TanStack Query, React Three rules) against src. |
npm run format / format:write |
Checks or writes formatting using Prettier + import sorting. |
Husky + lint-staged run lint/prettier --write on staged files before every commit.
The app no longer ships with .env files—development/production hosts are hard-coded inside src/config/api.js. The only optional override is the build-time VITE_BASE_PATH, which Vite consumes when emitting assets under a subdirectory (e.g., when deploying to /chemisee/).
| Variable | Purpose | Default |
|---|---|---|
VITE_BASE_PATH |
Controls the public base path for the Vite build (useful when the app is hosted under a subdirectory). | / |
Export the variable inline when running build commands, for example:
VITE_BASE_PATH=/chemisee npm run buildLegacy REACT_APP_* flags are no longer read by the codebase and can be removed from your environment.
- React Router framework mode defines the entire route tree inside
src/routes.js. Each entry references a route module (e.g.,src/routes/Root.jsx,src/routes/CharacterLayout.jsx, or leaf screens undersrc/components/*). - Loaders before render: Every route exports a
clientLoaderthat prefetches CMS content via the sharedQueryClientfromsrc/queryClient.js. Components rely onuseLoaderData()so that CMS slices are ready on first paint. - Shared providers live in
src/root.jsx, wrapping the app withQueryClientProvider,LanguageProvider, global state, theming, and global styles. The entry point (src/entry.client.jsx) hydrates everything with<HydratedRouter />. - CMS helpers in
src/api/queries.jsandsrc/utils/*describe how to extract specific content sections (welcome, introduction, gallery, etc.) from the fetched tree. - i18n uses
i18nextwith locale files bundled fromsrc/i18n/locales/*.jsonand runtime detection configured insrc/i18n/index.js. - 3D + media experiences (photo capture, gallery) live under
src/components/*with scoped hooks insidesrc/hooks/*and shared UI building blocks insrc/components/UIorsrc/components/shared.
Character navigation flows are centralized in src/characterRoutesConfig.js. This file defines the ordered list of routes for each character (janitor, artist, future).
CHARACTER_FLOWS: An object mapping character slugs to arrays of route names.getNextRoute(characterSlug, currentStep): A helper function that returns the next route in the sequence. If the current step is the last one or not found, it defaults to"ending".
Example usage in a component:
import { getNextRoute } from "@/characterRoutesConfig"
// ... inside component
const nextRoute = getNextRoute(characterSlug, "photo-capture")
navigate(`/characters/${characterSlug}/${nextRoute}`)Typical loader implementation (simplified):
import { extractFromContentTree } from "@/api/hooks"
import { loadCharacterSection } from "@/utils/loaderHelpers"
export async function clientLoader({ params }) {
const {
section: introduction,
characterSlug,
character,
} = await loadCharacterSection(
params,
(content, characterIndex) =>
extractFromContentTree.getIntroduction(content, characterIndex),
{ missingMessage: "Introduction data missing from CMS" },
)
return { characterSlug, character, introduction }
}Key takeaways:
- Loaders validate params and throw typed responses for React Router to surface in error boundaries.
ensureQueryDatakeeps TanStack Query as the single cache source; components never re-fetch the same CMS tree.- Child routes inherit data from parents through
useRouteLoaderData("parentId"), so shared lookups (character metadata, modal state) live in one place.
The shared helpers in src/utils/loaderHelpers.js keep loaders tidy:
loadCmsContent({ locale? })– hydrates the entire CMS tree (used byRoot,Welcome, etc.).loadCharacterContext(params, { locale?, content? })– resolves the character slug from the URL and returns{ characterSlug, characterIndex, character, characters, content, locale }, throwing typed errors for invalid slugs or missing CMS nodes.loadCharacterSection(params, extractor, { missingMessage?, missingStatus?, locale?, content? })– builds onloadCharacterContext, runs your extractor (e.g.,getIntroduction,getGallery), and throws automatically if the section is missing. The helper returns all character context fields plussectionso loaders can stay concise.requireContentSection(section, message, status?)– lower-level guard exported in case a loader needs to validate extra CMS slices (e.g.,Endingfetching both the ending and the closing reflection).
Prefer loadCharacterSection whenever a loader needs both the character context and a guaranteed CMS slice—it cuts ~10 lines per loader and standardises error responses.
src/
├── api/ # Fetch helpers, TanStack Query definitions, upload utils
├── components/ # Feature modules (Welcome, PhotoCapture, Gallery, etc.)
├── hooks/ # Reusable hooks consumed by components & loaders
├── providers/ # Context providers (CapturedImagesProvider, LanguageProvider)
├── routes/ # Route modules (Root layout, CharacterLayout, ErrorPage)
├── theme/ # styled-components theme + ThemeProvider
├── utils/ # CMS tree helpers, language utilities, preload helpers
└── entry.client.jsx # Hydrates <HydratedRouter />
- ESLint (flat config) enforces React, Hooks, TanStack Query, and @react-three best practices.
- Prettier (with automatic import sorting) is required on save; use
npm run format:writeto fix formatting issues. - Husky + lint-staged run lint/format against staged files to keep commits clean.
- Run
npm run buildto emitbuild/client. Clean the folder before committing; generated assets should remain untracked. - If the site is hosted under a subpath (e.g.,
/chemisee/), setVITE_BASE_PATH=/chemisee/before building. - If your CMS lives somewhere other than
http://localhost:8080/api(dev) or/cms/api(prod), updatesrc/config/api.jsor introduce your own environment toggle before building.
With the router owning navigation and loaders seeding the cache, feature work should focus on crafting route modules, keeping data derivations inside loaders, and letting components stay pure/presentational.
The application connects to a backend API which is configured via a config.json file.
Development:
The application uses the config.json file located in the root of the repository.
{
"API_BASE_URL": "http://localhost:8080/api"
}Production / Deployment:
The application expects to find a config.json file in the same directory as the index.html. You should ensure your deployment process places this file in the public root.
The application will fetch ./config.json at runtime before bootstrapping the React app.
Note: The environment variable API_BASE_URL is no longer used.