diff --git a/apps/web/src/app/[locale]/games/[slug]/page.tsx b/apps/web/src/app/[locale]/games/[slug]/page.tsx
new file mode 100644
index 0000000..fd4f388
--- /dev/null
+++ b/apps/web/src/app/[locale]/games/[slug]/page.tsx
@@ -0,0 +1,35 @@
+import { notFound } from "next/navigation";
+import { getGameBySlug } from "@/lib/api";
+import { GameHeader } from "@/components/game/GameHeader";
+import { GameInfo } from "@/components/game/GameInfo";
+import { ScreenshotGallery } from "@/components/game/ScreenshotGallery";
+
+export default async function GamePage({
+ params,
+}: {
+ params: Promise<{ locale: string; slug: string }>;
+}) {
+ const { locale, slug } = await params;
+
+ let game;
+ try {
+ const res = await getGameBySlug(slug, locale);
+ game = Array.isArray(res.data) ? res.data[0] : res.data;
+ } catch {
+ notFound();
+ }
+
+ if (!game) notFound();
+
+ return (
+ <>
+
+
+
+ {game.screenshots && (
+
+ )}
+
+ >
+ );
+}
diff --git a/apps/web/src/components/game/GameHeader.tsx b/apps/web/src/components/game/GameHeader.tsx
new file mode 100644
index 0000000..9f1202a
--- /dev/null
+++ b/apps/web/src/components/game/GameHeader.tsx
@@ -0,0 +1,29 @@
+import Image from "next/image";
+import type { Game } from "@afterlife/shared";
+
+interface GameHeaderProps {
+ game: Game;
+}
+
+export function GameHeader({ game }: GameHeaderProps) {
+ return (
+
+ {game.coverImage && (
+
+ )}
+
+
+
{game.title}
+
+ {game.developer} · {game.releaseYear}–{game.shutdownYear}
+
+
+
+ );
+}
diff --git a/apps/web/src/components/game/GameInfo.tsx b/apps/web/src/components/game/GameInfo.tsx
new file mode 100644
index 0000000..7239988
--- /dev/null
+++ b/apps/web/src/components/game/GameInfo.tsx
@@ -0,0 +1,79 @@
+import { useTranslations } from "next-intl";
+import Link from "next/link";
+import type { Game } from "@afterlife/shared";
+
+interface GameInfoProps {
+ game: Game;
+ locale: string;
+}
+
+export function GameInfo({ game, locale }: GameInfoProps) {
+ const t = useTranslations("game");
+
+ const statusColors = {
+ online: "text-green-400",
+ maintenance: "text-yellow-400",
+ coming_soon: "text-blue-400",
+ };
+
+ return (
+
+
+
+
+
+
+
- {t("developer")}
+ - {game.developer}
+
+ {game.publisher && (
+
+
- {t("publisher")}
+ - {game.publisher}
+
+ )}
+
+
- {t("released")}
+ - {game.releaseYear}
+
+
+
- {t("shutdown")}
+ - {game.shutdownYear}
+
+
+
- {t("server_status")}
+ -
+ {t(`status_${game.serverStatus}`)}
+
+
+
+
+ {game.serverLink && game.serverStatus === "online" && (
+
+ {t("play_now")}
+
+ )}
+ {game.documentary && (
+
+ {t("view_documentary")}
+
+ )}
+
+
+
+
+ );
+}
diff --git a/apps/web/src/components/game/ScreenshotGallery.tsx b/apps/web/src/components/game/ScreenshotGallery.tsx
new file mode 100644
index 0000000..d5b4a5a
--- /dev/null
+++ b/apps/web/src/components/game/ScreenshotGallery.tsx
@@ -0,0 +1,48 @@
+"use client";
+
+import Image from "next/image";
+import { useState } from "react";
+import type { StrapiMedia } from "@afterlife/shared";
+
+interface ScreenshotGalleryProps {
+ screenshots: StrapiMedia[];
+}
+
+export function ScreenshotGallery({ screenshots }: ScreenshotGalleryProps) {
+ const [selected, setSelected] = useState(0);
+
+ if (screenshots.length === 0) return null;
+
+ return (
+
+
+
+
+ {screenshots.length > 1 && (
+
+ {screenshots.map((ss, i) => (
+
+ ))}
+
+ )}
+
+ );
+}