Skip to main content

πŸ‘¨β€πŸ’» Frontend

The frontend is a React 18 single-page app, written in TypeScript and built with Vite. Mantine provides the UI components, TanStack Router handles routing, and TanStack Query handles server state.

The full tech stack, the development commands (yarn start, yarn test, yarn build) and the code style conventions are documented in apps/frontend/README.md, which lives next to the code. Please read that first β€” this page only covers what it does not: debugging, and how the source tree is laid out.

✨ Code Standards​

We use ESLint and Prettier to keep our code tidy. Before you commit, run yarn lint:error to check your changes and yarn lint:error:fix to apply the automatic fixes β€” the ESLint config enforces Prettier through the prettier/prettier rule, so linting also flags unformatted code. The lint-frontend CI workflow runs the lint, test and build steps on every pull request that touches apps/frontend and fails on anything unclean, so nothing un-linted gets merged.

yarn build does not type-check, so run yarn typecheck as well. It runs tsc --noEmit over the app and vite.config.ts. The frontend has zero type errors and no baseline: any error fails it, and so does the typecheck-frontend CI workflow. Fix the type rather than working around it.

The repository ships a husky pre-commit hook (apps/frontend/.husky/pre-commit, which runs lint-staged), but it is currently inactive in the monorepo: yarn install runs the prepare script β€” husky β€” with the working directory set to apps/frontend, which has no .git of its own, so husky exits without registering the hook. Don't rely on it; lint before you commit.

πŸ§ͺ End-to-end tests​

apps/frontend/e2e holds a Playwright smoke suite: log in, the timeline shows photos, the lightbox opens, a favorite toggles and persists, log out. It has its own package.json, separate from the app's. The e2e workflow runs it nightly, on demand, and on pull requests labelled e2e, and uploads the Playwright report, traces and container logs when it fails.

Against the Docker e2e stack (what CI does). deploy/compose/docker-compose.e2e.yml builds the proxy, frontend and backend from your checkout, creates admin/admin and scans the eight sample photos in deploy/e2e/photos:

docker compose -f deploy/compose/docker-compose.e2e.yml up -d --build --wait
cd apps/frontend/e2e
yarn install
yarn playwright install chromium
yarn test # or `yarn test:ui` for the interactive runner
docker compose -f ../../../deploy/compose/docker-compose.e2e.yml down -v

Against a native stack (no Docker, see Native Windows Setup). Use a fresh data directory and an admin/admin account, copy deploy/e2e/photos to <DataDir>\data\e2e (it has to be inside the backend's data root), then point the suite at the Vite dev server and that folder:

$env:E2E_BASE_URL = "http://localhost:3000"
$env:E2E_SCAN_DIR = "C:\librephotos-devdata\data\e2e"
yarn test

The setup step sets the admin's scan directory and starts a scan when the account has no photos yet. A scan started this way first downloads the ML models (about 1.3 GB, once). The specs expect exactly the sample library, so run them against an account with no other photos. E2E_USERNAME and E2E_PASSWORD override the credentials.

πŸ› Debugging​

React Debug Tool​

React provides a debug tool, which you can download here.

WDYR​

WDYR explains to you why a component re-rendered, by logging the changed props and hooks to the browser console. It is wired up in src/wdyr.ts and only runs in development builds.

Set VITE_APP_WDYR to the literal string true β€” any other value (True, 1, unset) leaves it off. Where you set it depends on how you run the frontend:

  • Local dev server (yarn start) β€” add VITE_APP_WDYR=true to apps/frontend/.env.development.

  • Docker dev stack β€” add VITE_APP_WDYR=true to deploy/compose/.env, then recreate the container so it picks up the new environment:

    docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d frontend

Either way the dev server has to be restarted. WDYR needs its own JSX transform, which vite.config.ts only selects when the variable is set at startup, so flipping it mid-session has no effect.

REST API​

Our REST API is documented with Swagger and ReDoc. Once your development stack is up, both are served from the running instance β€” see Development Installation for the URLs.

πŸ™οΈ Structure​

Everything below is relative to apps/frontend/src.

  • routes holds the pages. Routing is file-based: the path of a file determines its URL. Routes under _protected require a logged-in user, routes under public do not, and __root.tsx is the shell they all render into. routeTree.gen.ts is generated from this folder by the TanStack Router plugin β€” never edit it by hand.
  • Pages should be split up into React components, which you can find in components, grouped by feature.
  • api_client is the web side of the API layer, and every request starts there. The requests themselves live in packages/api-client, which the mobile app uses too: one endpoint function per call in src/endpoints, each parsing the answer with its zod schema from src/schemas and returning the parsed type. api.ts sets up apiClient, the shared client with the web's policy around it: it attaches the access token, refreshes it on a 401, logs out when that fails, rejects a failed request with ApiError, and shows a "please report this" notification when an answer does not match its schema. Around it, one folder per domain (photos, albums, faces, …) holds the TanStack Query hooks the components actually call (useFetch…Query, use…Mutation): each wraps an endpoints.<name>(apiClient, …) call and adds the web's cache invalidation and notifications, with keys from the domain's key factory in packages/api-client/src/hooks/queryKeys.ts, which the mobile app's hooks share. Files under /media/ (thumbnails, originals, videos) get their URLs from mediaUrl and are fetched with fetchMedia, both in api_client/media/mediaUrl.ts. A component never calls apiClient or fetch itself: when it needs a request, add the endpoint and its schema (with a fixture test) to packages/api-client, then a hook to the domain's folder. Lint enforces this: no file in api_client calls apiClient directly or defines its own zod schema (#2347).
  • hooks and service contain cross-cutting hooks and helpers, such as authentication and notifications.
  • locales holds the translations, set up in i18n.ts. Translations are contributed through Weblate rather than edited directly: its librephotos-frontend component translates apps/frontend/src/locales/*/translation.json against the English file, apps/frontend/src/locales/en/translation.json. English ships in the main bundle, and i18n.ts loads every other directory under locales as its own chunk, so a language Weblate adds loads without a code change. To offer it in the Profile language picker, give it an entry in components/settings/languageOptions.ts and a dayjs locale in dayjsLocaleLoaders (i18n.ts); src/i18n.test.ts fails until both are there.
  • In util you can find miscellaneous functions.