From ac5ac570e2da0276044167da5163809992f3cb8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Mon, 2 Mar 2026 21:52:16 +0100 Subject: [PATCH] add game detail component, route at /games/$gameId Co-Authored-By: Claude Opus 4.6 --- src/features/games/components/game-detail.tsx | 82 +++++++++++++++++++ src/routeTree.gen.ts | 21 +++++ src/routes/games/$gameId.tsx | 26 ++++++ 3 files changed, 129 insertions(+) create mode 100644 src/features/games/components/game-detail.tsx create mode 100644 src/routes/games/$gameId.tsx diff --git a/src/features/games/components/game-detail.tsx b/src/features/games/components/game-detail.tsx new file mode 100644 index 0000000..4a3f1d8 --- /dev/null +++ b/src/features/games/components/game-detail.tsx @@ -0,0 +1,82 @@ +import { Badge } from "@/shared/components/ui/badge" +import { useGame } from "@/shared/db/hooks" +import type { Game } from "@/shared/db/schema" +import { t } from "@/shared/i18n" +import { ExternalLink } from "lucide-react" +import { formatPlaytime } from "../schema" +import { FavoriteButton } from "./favorite-button" +import { GameStateSelect } from "./game-state-select" +import { StarRating } from "./star-rating" + +function getSteamHeaderImage(sourceId: string): string { + return `https://cdn.akamai.steamstatic.com/steam/apps/${sourceId}/header.jpg` +} + +interface GameDetailProps { + gameId: string +} + +export function GameDetail({ gameId }: GameDetailProps) { + const { game, loading, reload } = useGame(gameId) + + if (loading) return null + + if (!game) { + return

{t("game.notFound")}

+ } + + return +} + +function GameDetailContent({ game, onUpdate }: { game: Game; onUpdate: () => void }) { + const imageUrl = game.source === "steam" ? getSteamHeaderImage(game.source_id) : null + + return ( +
+ {imageUrl && ( + {game.title} + )} + +
+
+
+

{game.title}

+ + {game.source} + +
+
+ {game.playtime_hours > 0 && {formatPlaytime(game.playtime_hours)}} + {game.last_played && ( + + {t("game.lastPlayed")}: {game.last_played} + + )} +
+
+ +
+ +
+ + +
+ + {game.url && ( + + + {t("game.openStore")} + + )} +
+ ) +} diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 1923e6f..6699e63 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -16,6 +16,7 @@ import { Route as LibraryIndexRouteImport } from './routes/library/index' import { Route as DiscoverIndexRouteImport } from './routes/discover/index' import { Route as SettingsProviderRouteImport } from './routes/settings/$provider' import { Route as PlaylistsPlaylistIdRouteImport } from './routes/playlists/$playlistId' +import { Route as GamesGameIdRouteImport } from './routes/games/$gameId' const IndexRoute = IndexRouteImport.update({ id: '/', @@ -52,9 +53,15 @@ const PlaylistsPlaylistIdRoute = PlaylistsPlaylistIdRouteImport.update({ path: '/playlists/$playlistId', getParentRoute: () => rootRouteImport, } as any) +const GamesGameIdRoute = GamesGameIdRouteImport.update({ + id: '/games/$gameId', + path: '/games/$gameId', + getParentRoute: () => rootRouteImport, +} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute + '/games/$gameId': typeof GamesGameIdRoute '/playlists/$playlistId': typeof PlaylistsPlaylistIdRoute '/settings/$provider': typeof SettingsProviderRoute '/discover/': typeof DiscoverIndexRoute @@ -64,6 +71,7 @@ export interface FileRoutesByFullPath { } export interface FileRoutesByTo { '/': typeof IndexRoute + '/games/$gameId': typeof GamesGameIdRoute '/playlists/$playlistId': typeof PlaylistsPlaylistIdRoute '/settings/$provider': typeof SettingsProviderRoute '/discover': typeof DiscoverIndexRoute @@ -74,6 +82,7 @@ export interface FileRoutesByTo { export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute + '/games/$gameId': typeof GamesGameIdRoute '/playlists/$playlistId': typeof PlaylistsPlaylistIdRoute '/settings/$provider': typeof SettingsProviderRoute '/discover/': typeof DiscoverIndexRoute @@ -85,6 +94,7 @@ export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: | '/' + | '/games/$gameId' | '/playlists/$playlistId' | '/settings/$provider' | '/discover/' @@ -94,6 +104,7 @@ export interface FileRouteTypes { fileRoutesByTo: FileRoutesByTo to: | '/' + | '/games/$gameId' | '/playlists/$playlistId' | '/settings/$provider' | '/discover' @@ -103,6 +114,7 @@ export interface FileRouteTypes { id: | '__root__' | '/' + | '/games/$gameId' | '/playlists/$playlistId' | '/settings/$provider' | '/discover/' @@ -113,6 +125,7 @@ export interface FileRouteTypes { } export interface RootRouteChildren { IndexRoute: typeof IndexRoute + GamesGameIdRoute: typeof GamesGameIdRoute PlaylistsPlaylistIdRoute: typeof PlaylistsPlaylistIdRoute SettingsProviderRoute: typeof SettingsProviderRoute DiscoverIndexRoute: typeof DiscoverIndexRoute @@ -172,11 +185,19 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PlaylistsPlaylistIdRouteImport parentRoute: typeof rootRouteImport } + '/games/$gameId': { + id: '/games/$gameId' + path: '/games/$gameId' + fullPath: '/games/$gameId' + preLoaderRoute: typeof GamesGameIdRouteImport + parentRoute: typeof rootRouteImport + } } } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, + GamesGameIdRoute: GamesGameIdRoute, PlaylistsPlaylistIdRoute: PlaylistsPlaylistIdRoute, SettingsProviderRoute: SettingsProviderRoute, DiscoverIndexRoute: DiscoverIndexRoute, diff --git a/src/routes/games/$gameId.tsx b/src/routes/games/$gameId.tsx new file mode 100644 index 0000000..8e1ffda --- /dev/null +++ b/src/routes/games/$gameId.tsx @@ -0,0 +1,26 @@ +import { GameDetail } from "@/features/games/components/game-detail" +import { createFileRoute, useRouter } from "@tanstack/react-router" +import { ArrowLeft } from "lucide-react" + +export const Route = createFileRoute("/games/$gameId")({ + component: GameDetailPage, +}) + +function GameDetailPage() { + const { gameId } = Route.useParams() + const router = useRouter() + + return ( +
+ + +
+ ) +}