feat(psn): show trophy sets on game detail and dashboard
Build, Test & Push Frontend / quality-check (push) Successful in 35s
Build, Test & Push Frontend / docker (push) Successful in 33s

Detail page groups achievements by set: Steam, then PSN trophies.
Tiles show the real count; a perfect game gets an amber trophy.
Dashboard card shows for PSN-only users; CSP allows PSN icons.
This commit is contained in:
lobo committed 2026-10-08 09:18:15 +02:00
1 parent 27c402fa34
commit e7b08cfb47
14 files changed
+349 -105

No files matched your search

+10 -3
View File
@@ -24,7 +24,8 @@ To Be Played is split into three repositories:
in Auth, so it follows the user to the phone; "Reset" goes back to the default. Tiles: 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 %.
total, perfect games, average completion and the 5 games closest to 100 %. The card counts PlayStation trophies too
(platinum = perfect) and also shows for a user with only a PlayStation link.
- **Coming games**: a game that is not released yet (and nobody played) shows a "Coming <date>" badge in the library, on
the details page, in the stat game lists and on the public profile. The pile counts only the games Core sends in
`pileGames`, so coming games are not on it; Core never picks them.
@@ -87,6 +88,10 @@ To Be Played is split into three repositories:
- **Platform everywhere**: each game shows its platform ("Steam" for Steam games). The favorite platform from the
settings is preselected when a game is on it; otherwise a game on one platform gets that one, and a game on
several waits for the user's pick.
- **PlayStation trophies** (slice 3): the game detail shows one card per set, Steam first, then PS5 and PS4
(`GameAchievements`, grouped by `achievementSets`). A trophy has its type (bronze to platinum), rarity and a DLC tag;
a hidden trophy shows "Hidden trophy" until it is earned; a set whose list is not stored yet says it loads with the
next sync. The sync banner and inbox text say "achievements or trophies". Library tiles show the best set of a game.
- **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
@@ -202,7 +207,8 @@ flowchart LR
Core --> Igdb["IGDB API"]
Core -->|"OAuth token"| Twitch["Twitch"]
Core --> Speedrun["speedrun.com API"]
Browser -.->|"game art"| Images["Steam and IGDB image hosts"]
Core -->|"NPSSO sign-in, played titles, trophies"| Psn["PlayStation API (unofficial)"]
Browser -.->|"game art, trophy icons"| Images["Steam, IGDB and PlayStation (psnobj) image hosts"]
Browser -.->|"Sign in through Steam"| SteamLogin["steamcommunity.com (OpenID)"]
Browser -.->|"bookmarklet, the user's login"| Stores["PlayStation, Xbox, Nintendo, Epic, GOG"]
Auth -->|"check_authentication"| SteamLogin
@@ -210,7 +216,8 @@ flowchart LR
- `useDragSort` (`src/hooks/useDragSort.js`) sorts a list with pointer events. Only the drag handle has `touch-action: none`, so the page still scrolls from a row.
- 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.
(`VITE_API_URL`, `VITE_CORE_API_URL`). The Content Security Policy allows only these two hosts and the image hosts (Steam, IGDB and, for trophy icons,
`psnobj.prod.dl.playstation.net`).
- 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.