BeCoMe frontend¶
React frontend for BeCoMe (Best Compromise Mean), the system that aggregates expert opinions.
Contents¶
- Tech stack
- Development
- API proxy
- Environment variables
- Docker
- Internationalisation
- Accessibility
- Layout
- Assistant
- Testing
Tech stack¶
- React 19 + TypeScript
- Vite (build tool)
- Tailwind CSS + shadcn/ui
- Radix UI primitives, wrapped as the shadcn/ui components in
src/components/ui - React Query (data fetching)
- React Hook Form with Zod (forms and schema validation)
- Recharts (charts)
- Framer Motion (animation)
- React Router (routing)
- i18next with react-i18next (English and Czech)
- Sentry (
@sentry/react, browser error tracking) - react-markdown (assistant answers, rendered from an allowlist of elements)
- Cloudflare Turnstile (bot check on the sign-in, registration, password reset and resend-verification forms)
- Vitest with Testing Library (unit tests)
- Playwright with axe-core (end-to-end and accessibility tests)
Development¶
Run these from frontend/.
# Install dependencies
npm install
# Start dev server (port 8080)
npm run dev
# Build for production
npm run build
# Lint and type check
npm run lint
npm run typecheck
# Unit tests: watch mode, one run, one run with the coverage gate
npm test
npm run test:run
npm run test:coverage
# End-to-end tests (see Testing before a bare run)
npm run test:e2e
API proxy¶
The dev server proxies /api/v1/* requests to http://localhost:8000 (FastAPI backend).
Start the backend first:
cd .. && SECRET_KEY=dev-secret uv run uvicorn api.main:app --reload
Environment variables¶
VITE_API_URL: base URL of the API. Falls back to/api/v1when unset, which the dev server proxies. The Dockerfile also copies its origin into the Content-Security-Policy.VITE_SENTRY_DSN: DSN for browser error tracking. Tracking stays off when it is absent.VITE_APP_ENV: thedev,test, orprodtag on Sentry events.VITE_TURNSTILE_SITE_KEY: Cloudflare Turnstile site key. When it is empty, the forms draw no widget and submit without a token.VITE_ASSISTANT_ENABLED: build flag for the assistant panel. Only the exact stringtrueincludes it in the bundle; prod leaves it unset. See the Assistant section.
Declare every one of them as both an ARG and an ENV in Dockerfile, before
RUN npm run build. Railway passes service variables to the build as build args, but Docker
only exposes the ones the Dockerfile declares, and Vite inlines undefined for anything it
cannot see, with no error and no warning. That is how VITE_SENTRY_DSN stayed set on all
three Railway services while the bundler tree-shook Sentry.init out of every deployed bundle.
A new VITE_* variable means editing three places: Dockerfile, the Railway service, and the
ImportMetaEnv interface in src/vite-env.d.ts.
Docker¶
Build and run with Docker:
docker build -t become-frontend .
docker run -p 3000:8080 become-frontend
To reproduce a deployed build locally, pass the build args explicitly:
docker build --build-arg VITE_API_URL=https://api.becomify.app/api/v1 --build-arg VITE_APP_ENV=prod -t become-frontend .
Or use Docker Compose from the docker/ directory:
cd ../docker
SECRET_KEY=$(openssl rand -hex 32) docker compose up --build
Internationalisation¶
The interface is in English (en) and Czech (cs). The strings live in
src/i18n/locales/en/ and src/i18n/locales/cs/, one JSON file per namespace, with
common as the default. A new namespace has to be imported and registered in
src/i18n/index.ts.
Both locales must carry the same keys. tests/i18n/key-parity.test.ts compares the key set of
every namespace, and the set of namespaces itself.
A string that depends on a count uses i18next plural suffixes: key_one, key_other and so on,
called with t("key", { count }). English carries _one and _other, Czech _one, _few,
_many and _other, which are the categories Intl.PluralRules reports for each language. The
parity test folds the suffixes into one key before it compares the locales, and checks that every
group has exactly the categories of its language.
The language is taken from localStorage under the key become-language, then from the
browser's language list, and falls back to English. A choice made with the language switcher
is stored under the same key. Only en and cs are supported, and a regional tag such as
cs-CZ resolves to Czech. The lang attribute of <html> follows the interface language.
The three calls that send a localised email, register, resendVerification and
forgotPassword, pass the interface language in the Accept-Language header.
languageHeaders() in src/lib/api.ts builds it and get_email_language in
api/dependencies.py reads it.
Accessibility¶
- Skip link. It is defined in
index.html, outside the React root, so it exists before React mounts and is the first Tab stop..skip-to-contentinsrc/index.csskeeps it off screen until it has focus. Its text comes from thea11y.skipToContentkey and is updated when the language changes (src/i18n/index.ts). - Route changes. On a change of pathname,
RouteAnnouncerscrolls to the top, moves focus to#main-content, and announcesdocument.titlein a polite live region. It does nothing on the first load, so the skip link stays the first Tab stop. - Loading placeholder. While a page or the session check loads,
PageLoaderis the skip link's target and, if it held focus when the page replaces it, hands focus to that page'smain. - Page titles.
useDocumentTitlesets the title to<page> - BeCoMe. Every component insrc/pagescalls it. - Control names. The close buttons of dialogs and toasts and the toast region take their
names from the
a11y.closeanda11y.notificationskeys (src/components/ui/dialog.tsx,src/components/ui/toast.tsx). The language switcher's accessible name comes from theswitchToLanguagekey, for example "EN, switch to Čeština". - Audit.
e2e/wcag-audit.spec.tsruns axe-core with the WCAG 2.0 and 2.1 A and AA tags. It covers the landing, about, docs, FAQ, privacy, terms, case studies, case study, login, register and 404 pages, the landing and login pages in the dark theme, and the projects, onboarding and profile pages of a signed-in user.
Layout¶
The navigation bar shows its full row from the xl breakpoint (1280 px) and a menu button
below it, and a long signed-in name is truncated.
Assistant¶
The assistant panel is behind the build flag VITE_ASSISTANT_ENABLED. It is off by default,
and a default build carries no assistant code, strings, i18n keys or endpoint paths. CI checks this
with scripts/ci/check-assistant-bundle.sh on a build without the flag and a build with it. The
panel, its triggers, the privacy card on the profile page, the documentation section and the
onboarding tour step are lazy chunks that exist only when the flag is set.
Build with the flag to include it:
VITE_ASSISTANT_ENABLED=true npm run build
The feature lives in src/components/assistant/. Its backend half needs ASSISTANT_ENABLED
on the API and runs on a developer machine and on the dev, test and prod services (api/assistant/README.md).
e2e/assistant-panel.spec.ts needs no backend: it answers every API request with a route.
It is skipped unless the flag is in the environment when Playwright starts its dev server:
VITE_ASSISTANT_ENABLED=true npx playwright test e2e/assistant-panel.spec.ts --project=chromium
Testing¶
Unit tests live in tests/ and run under Vitest with happy-dom. The subdirectories are
components, contexts, data, hooks, i18n, lib and pages, plus factories and
mocks for shared test data and API mocks.
npm run test:coverage fails below these thresholds (vitest.config.ts): statements 98,
branches 95, functions 97, lines 98. ./scripts/ci/ci-local.sh fast runs npm run test:run
and does not check them, while CI runs npm run test:coverage. Run the coverage command
before pushing a frontend change.
Playwright specs live in e2e/. The dev server for them runs over HTTPS, because the session
cookies carry the __Host- prefix. The specs register real users, so they need the API and a
database. ./scripts/ci/e2e-local.sh starts PostgreSQL in Docker and the API, then runs them.
The projects in playwright.config.ts:
chromium,firefox,webkit: the functional specs.wcag-audit: the axe audit above.visual-regression: page screenshots of the landing hero, a case study and the project results charts, each in the light and dark theme.docs-screenshots: illustrations for the documentation site, not a check. It overwrites the tracked images indocs/user/img/, so name the project with--project=and do not rely on a barenpm run test:e2e.
The visual baselines live in e2e/visual-regression.spec.ts-snapshots/. The committed ones are
Linux renders (*-linux.png), and git ignores *-darwin.png (frontend/.gitignore). A render from a
Mac does not match what CI compares against, so do not regenerate them locally. After an
intended layout change, take the new images from the Playwright report of the failed CI run
instead. The header of .github/workflows/regen-snapshots.yml lists the steps.