diff --git a/README.md b/README.md
index 3f9ccd0..2c22059 100644
--- a/README.md
+++ b/README.md
@@ -65,6 +65,10 @@ To Be Played is split into three repositories:
once (no "Hide duplicates" filter any more), and the numbers count it once. The editions of a set share one status, rating and notes (the page
reloads after a save to show it). "Not the same game" splits the game off its set for this user only, and it stays
split. "Same game as..." on the details page asks the auditors for a link.
+- **Detail per platform**: the details page of a game with several platforms (or a PlayStation title on a Steam game)
+ shows `PlatformsCard` above the editions: one row per platform with playtime, last played and the achievement or
+ trophy progress of that platform, a total row, and a link to the edition whose page lists the achievements. The hero
+ playtime and the library card show the set total (`totalPlaytimeMinutes`); sorting and the stats stay per edition.
- **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. Games already in ToBePlayed come first. Check several
hits to add them at once through the import review.
@@ -268,6 +272,13 @@ flowchart LR
Row -.->|"no platforms (hidden game)"| Badge["single platform badge"]
```
+```mermaid
+flowchart LR
+ DetailData["Core GET /api/library/games/{appid}: platforms[] + totalPlaytimeMinutes"] --> Platforms["PlatformsCard: row per platform, total"]
+ DetailData --> Hero["GameHeroCard: total playtime"]
+ Platforms -->|"row of another edition"| Other["/library/{appid} of that edition"]
+```
+
```mermaid
flowchart LR
Detail["GameDetail: EditionsCard"] -->|"Not the same game"| Split["Core DELETE /api/library/games/{appid}/set"]
diff --git a/public/i18n/de.json b/public/i18n/de.json
index a54e575..4f32846 100644
--- a/public/i18n/de.json
+++ b/public/i18n/de.json
@@ -796,6 +796,12 @@
"OTHER_OWNERS_other": "{{count}} andere Besitzer",
"SHOW_OWNERS": "Zeigen, wer es besitzt"
},
+ "PLATFORMS": {
+ "TITLE": "Spielzeit pro Plattform",
+ "TOTAL": "Gesamt",
+ "LAST_PLAYED": "Zuletzt gespielt",
+ "NEVER": "nie"
+ },
"LINKS": {
"ALSO_IN_LIBRARY": "Auch in deiner Bibliothek:",
"SAME_AS": "Dasselbe Spiel wie...",
diff --git a/public/i18n/en.json b/public/i18n/en.json
index 177de84..0366ae4 100644
--- a/public/i18n/en.json
+++ b/public/i18n/en.json
@@ -796,6 +796,12 @@
"OTHER_OWNERS_other": "{{count}} other owners",
"SHOW_OWNERS": "Show who owns it"
},
+ "PLATFORMS": {
+ "TITLE": "Playtime per platform",
+ "TOTAL": "Total",
+ "LAST_PLAYED": "Last played",
+ "NEVER": "never"
+ },
"LINKS": {
"ALSO_IN_LIBRARY": "Also in your library:",
"SAME_AS": "Same game as...",
diff --git a/src/components/GameHeroCard.jsx b/src/components/GameHeroCard.jsx
index fc6c1d4..e5a4838 100644
--- a/src/components/GameHeroCard.jsx
+++ b/src/components/GameHeroCard.jsx
@@ -76,7 +76,7 @@ export default function GameHeroCard({game, auditor, profileStyle, onCorrect, on
- {t('GAME_DETAIL.PLAYTIME', {time: formatPlaytime(game.playtimeMinutes)})}
+ {t('GAME_DETAIL.PLAYTIME', {time: formatPlaytime(game.totalPlaytimeMinutes ?? game.playtimeMinutes)})}
diff --git a/src/components/PlatformsCard.jsx b/src/components/PlatformsCard.jsx
new file mode 100644
index 0000000..587afe8
--- /dev/null
+++ b/src/components/PlatformsCard.jsx
@@ -0,0 +1,61 @@
+import {Link} from 'react-router-dom';
+import {useTranslation} from 'react-i18next';
+import {Trophy} from 'lucide-react';
+
+import {Card} from '@/components/ui/card';
+import {cn} from '@/lib/utils';
+import PlatformIcons from './PlatformIcons.jsx';
+import {formatPlaytime} from '../utils/games.js';
+import {formatDate} from '../utils/format.js';
+import {platformLabel} from '../utils/platforms.js';
+
+/**
+ * One row per platform of the game: playtime, last played and the progress of that platform. A row of another edition
+ * links to that edition, whose page lists its achievements.
+ */
+export default function PlatformsCard({game}) {
+ const {t, i18n} = useTranslation();
+ const lines = game.platforms;
+ if (!lines?.length) return null;
+
+ return (
+
+