Public Access
150 lines
7.7 KiB
Markdown
150 lines
7.7 KiB
Markdown
# To Be Played – Frontend
|
||
|
||
Web app of **To Be Played**, a Steam backlog tracker: import your Steam library, decide what to play, and track what
|
||
you finished. The UI is in German and English, with light and dark mode.
|
||
|
||

|
||
|
||
| Game detail | Public profile | Mobile |
|
||
|---|---|---|
|
||
|  |  |  |
|
||
|
||
To Be Played is split into three repositories:
|
||
|
||
| Repository | Role |
|
||
|---|---|
|
||
| ToBePlayed-Auth | Users, login, settings, signs JWTs |
|
||
| ToBePlayed-Core | Steam library, backlog, wishlist, stats; verifies JWTs |
|
||
| **ToBePlayed-Frontend** (this one) | React single-page app |
|
||
|
||
## Features
|
||
|
||
- **Dashboard**: most played games, games in progress, "Pile of Shame" with backlog hours, its trend of the last 90
|
||
days ("this month: +N added, -N off the pile") and a random suggestion,
|
||
Steam avatar and a banner from the most played game of the last two weeks, and an achievement card: unlocked
|
||
total, perfect games, average completion and the 5 games closest to 100 %.
|
||
- **What to play next** (`/play-next`): three backlog picks for a time budget (up to 5 / 15 / 40 h or any) and a mood
|
||
(story, co-op, chill, action, scary from IGDB; speedrun from speedrun.com), high priority first, reroll without
|
||
repeats, and "Start playing" in one click.
|
||
- **Your stats** (`/stats`): totals and statuses; the pile in hours, in weeks at your pace of the last 90 days and in
|
||
years of full-time work, and its oldest unplayed game; most played and most loyal games; top genres and moods;
|
||
trips to the moon, the longest gaming day, games started but under one hour; finished games per year.
|
||
- **Library**: status filters, "short games" filter (up to 10 h), search, sorting by playtime, recently seen, newest
|
||
in library, rating or priority, paging. Each row shows the unlocked achievements.
|
||
- **Add a game by name** for games outside Steam (PlayStation, Switch, Xbox, physical): search IGDB, pick a hit,
|
||
a platform and optional hours. Works without a Steam ID.
|
||
- **Game detail**: status, priority, 1–5 star rating, notes, achievements, time to beat (story / extras / 100 %),
|
||
a link to the game's speedrun.com page when it has verified runs, a hint when Steam shows recent playtime for a
|
||
game still in the backlog, and one to set the status to 100 % when
|
||
every achievement is unlocked.
|
||
- **Finished?**: Core marks a game as finished by itself when the ending achievement is unlocked and the status was
|
||
never set by hand. Otherwise the game page asks: "You unlocked the ending, finished?" or "You played longer than
|
||
most players need, finished?", with "Mark as finished" and "Not yet".
|
||
- **Hide games**: a Steam game (e.g. a playtest or a tool) can be hidden on its detail page. It leaves the lists,
|
||
the dashboard, the stats and the public profile; the library's "Hidden" filter shows it again for unhiding.
|
||
- **Wishlist** with store price and discount.
|
||
- **Steam account** linked in the settings with "Sign in through Steam": the user signs in on Steam and Auth gets
|
||
the proved Steam ID, never a typed-in one. A Steam ID typed in before shows as "not verified" with a button to
|
||
verify it; unlinking is possible.
|
||
- **Public profile** at `/u/<display name>`, opt-in in the settings, with perfect games and average completion.
|
||
The user picks in the settings which stat groups of "Your stats" it shows (none by default).
|
||
- **Appearance**: accent color, corner style, wide layout, page background and banner from your own game art.
|
||
Saved in Auth, so it follows you to every device.
|
||
- **Broken pictures**: a Steam cover or achievement icon that does not load shows a placeholder and is reported
|
||
to Core once per page load (`POST /api/library/broken-images`, only with a session). Core repairs it at night;
|
||
a repaired cover comes back as `imageUrl`.
|
||
- German and English, dark mode, mobile layout, session-expired message instead of a silent logout.
|
||
- **Build version** in the settings: commit and build time of Frontend, Auth and Core, each hash links to its
|
||
commit. CI passes them as Docker build args; a local build shows `dev`.
|
||
- **Installable** as an app on phone and desktop (web app manifest, no service worker, so no offline mode).
|
||
- **Session**: the tokens are HttpOnly cookies that no script can read. `apiFetch` renews an expired access token
|
||
once and sends the request again; parallel requests and other tabs share one refresh.
|
||
|
||
## Tech stack
|
||
|
||
- React 19, React Router 7, Vite 8
|
||
- Tailwind CSS 4, Radix UI primitives (shadcn/ui style components), Lucide icons
|
||
- react-hook-form + zod for forms, i18next for translations (`public/i18n`)
|
||
- Vitest + React Testing Library + jsdom
|
||
- Production image: nginx (Alpine), SPA fallback, long-term caching for hashed assets, Content Security Policy
|
||
and security headers
|
||
- CI: Forgejo Actions (`npm ci`, `npm test`, `npm run build`, Docker image)
|
||
|
||
## Architecture
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
Browser["Browser"] --> Proxy["Traefik (TLS)"]
|
||
Proxy --> Frontend["ToBePlayed-Frontend<br/>nginx, static files"]
|
||
Proxy --> Auth["ToBePlayed-Auth"]
|
||
Proxy --> Core["ToBePlayed-Core"]
|
||
Core -->|"JWKS"| Auth
|
||
Auth --> Db[("PostgreSQL")]
|
||
Core --> Db
|
||
Core --> Steam["Steam Web API"]
|
||
Core --> Igdb["IGDB API"]
|
||
Core -->|"OAuth token"| Twitch["Twitch"]
|
||
Core --> Speedrun["speedrun.com API"]
|
||
Browser -.->|"game art"| Images["Steam and IGDB image hosts"]
|
||
Browser -.->|"Sign in through Steam"| SteamLogin["steamcommunity.com (OpenID)"]
|
||
Auth -->|"check_authentication"| SteamLogin
|
||
```
|
||
|
||
- nginx serves the built app. It has no backend of its own: the browser calls Auth and Core directly
|
||
(`VITE_API_URL`, `VITE_CORE_API_URL`). The Content Security Policy allows only these two hosts and the image hosts.
|
||
- Auth and Core each have their own database. Core verifies Auth's tokens with Auth's public keys (JWKS).
|
||
- Calls for the logged-in user go through `src/utils/api.js`. `clientFetch` sends the HttpOnly cookies and the
|
||
`X-TBP-Client` header. `apiFetch` answers a `401` with one `POST /api/auth/refresh` and sends the request again.
|
||
Parallel requests and other tabs share this refresh (`navigator.locks`). When the second answer is still a
|
||
`401`, the page calls `useSessionExpired`, which opens the login page. Public pages (landing page, public
|
||
profile) and the public `GET /api/version` in the settings use plain `fetch`.
|
||
- `src/utils/brokenImages.js` reports pictures that fail to load (`GameCover`, achievement icons on the game
|
||
detail page) to Core.
|
||
- `useLibrarySync` polls Core's `/api/library/sync-status` while a Steam sync runs, then loads the data again.
|
||
|
||
## Project structure
|
||
|
||
```text
|
||
src/
|
||
├── pages/ # One component per route (Dashboard, GameLibrary, GameDetail, ...)
|
||
├── components/ # Shared components; ui/ holds the base components
|
||
├── hooks/ # Data hooks (library sync, Steam profile, appearance, session expiry)
|
||
├── context/ # PreferencesContext: theme, language, appearance
|
||
├── utils/ # Validation schemas and helpers
|
||
└── test/ # Test setup (i18n mock)
|
||
```
|
||
|
||
Tests sit next to the code they test (`Component.test.jsx`).
|
||
|
||
## Run locally
|
||
|
||
Needs Node 24 and running Auth and Core services.
|
||
|
||
```bash
|
||
npm install
|
||
npm run dev
|
||
```
|
||
|
||
| Variable | Purpose |
|
||
|---|---|
|
||
| `VITE_API_URL` | Base URL of the Auth service |
|
||
| `VITE_CORE_API_URL` | Base URL of the Core service |
|
||
|
||
Values for development and production live in `.env.development` and `.env.production`. They end up in the
|
||
browser bundle, so they must never hold a secret.
|
||
|
||
## Tests and build
|
||
|
||
```bash
|
||
npm test
|
||
npm run build
|
||
```
|
||
|
||
## Roadmap
|
||
|
||
See the roadmap in the ToBePlayed-Core readme.
|
||
|
||
## License
|
||
|
||
All rights reserved. The source is public for reading only.
|