From d60899fdbf785b560b45d5808ac95ae481790160 Mon Sep 17 00:00:00 2001 From: Michess Date: Wed, 15 Apr 2026 15:22:38 +0200 Subject: [PATCH] feat(ui): simplified homepage, /play subpage, and fix bot-game PGN bug Homepage now shows only a "Spielen" card (links to /play), public games, last 3 games as mini-board thumbnails for logged-in users, and live games. New /play page contains all game-mode cards (create, vs AI, friends, correspondence, tournament). Bug fix: /ai page was constructing Chess instances from FEN on every move, discarding the full PGN history. Switching to loadPgn() preserves all moves so the saved game can be fully reviewed in the archive. Co-Authored-By: Claude Sonnet 4.6 --- client/src/app/ai/page.tsx | 10 ++- client/src/app/page.tsx | 87 ++++++---------------- client/src/app/play/page.tsx | 81 ++++++++++++++++++++ client/src/components/home/RecentGames.tsx | 82 ++++++++++++++++++++ 4 files changed, 193 insertions(+), 67 deletions(-) create mode 100644 client/src/app/play/page.tsx create mode 100644 client/src/components/home/RecentGames.tsx diff --git a/client/src/app/ai/page.tsx b/client/src/app/ai/page.tsx index c7677f7..5c318ac 100644 --- a/client/src/app/ai/page.tsx +++ b/client/src/app/ai/page.tsx @@ -66,7 +66,10 @@ export default function AiGamePage() { const { move } = await res.json(); if (move) { setGame((prev) => { - const newGame = new Chess(prev.fen()); + // Load from PGN (not FEN) to preserve the full move history + const newGame = new Chess(); + const prevPgn = prev.pgn(); + if (prevPgn) newGame.loadPgn(prevPgn); const result = newGame.move({ from: move.slice(0, 2), to: move.slice(2, 4), promotion: move[4] || "q" }); if (result) setLastMove({ from: result.from, to: result.to }); setStatus(getStatus(newGame)); @@ -86,7 +89,10 @@ export default function AiGamePage() { const currentTurn = game.turn() === "w" ? "white" : "black"; if (currentTurn !== playerColor) return false; - const newGame = new Chess(game.fen()); + // Load from PGN (not FEN) to preserve the full move history + const newGame = new Chess(); + const currentPgn = game.pgn(); + if (currentPgn) newGame.loadPgn(currentPgn); let move = null; try { move = newGame.move({ from: sourceSquare, to: targetSquare, promotion: "q" }); diff --git a/client/src/app/page.tsx b/client/src/app/page.tsx index d74b5c3..b3116db 100644 --- a/client/src/app/page.tsx +++ b/client/src/app/page.tsx @@ -1,9 +1,8 @@ "use client"; -import CreateGame from "@/components/home/CreateGame"; -import JoinGame from "@/components/home/JoinGame"; import PublicGames from "@/components/home/PublicGames/PublicGames"; import LiveGames from "@/components/home/LiveGames"; +import RecentGames from "@/components/home/RecentGames"; import Link from "next/link"; import { useSession } from "@/context/session"; @@ -14,77 +13,35 @@ export default function Home() { return (
- {/* Hauptaktionen */} -
- -
-
-

Spiel erstellen

-

Erstelle eine Partie und teile den Einladungslink mit Freunden

-
- -
+
+ {/* Spielen-Karte */} + +
+ +

Spielen

+

+ Partie erstellen, gegen KI, Turnier & mehr +

-
+ -
-
-

Gegen KI spielen

-

Fordere Stockfish auf verschiedenen Stufen heraus — von Anfänger bis Meister

-
- Jetzt spielen -
-
+ {/* Öffentliche Spiele */} +
+
- -
-
- {isLoggedIn ? ( - <> -

Freunde

-

Freunde hinzufügen, verwalten und zu einer Partie einladen

-
- Freunde verwalten -
- - ) : ( - <> -

Einladung annehmen

-

Einladungslink oder Code eingeben um beizutreten

- - - )} -
-
- -
-
-

Tagespartien

-

Spiele in deinem eigenen Tempo — Züge bis zu 3 Tage Zeit

-
- Tagespartien -
-
-
- -
-
-

Turnier

-

Spiele Round-Robin Turniere gegen mehrere Gegner

-
- Turniere -
-
-
-
+ {/* Letzte Spiele des eingeloggten Users */} + {isLoggedIn && user.id && typeof user.id === "number" && ( + + )} + {/* Laufende Partien – Live-Kacheln */} - {/* Öffentliche Spiele */} - -
); } diff --git a/client/src/app/play/page.tsx b/client/src/app/play/page.tsx new file mode 100644 index 0000000..e412fa5 --- /dev/null +++ b/client/src/app/play/page.tsx @@ -0,0 +1,81 @@ +"use client"; + +import CreateGame from "@/components/home/CreateGame"; +import JoinGame from "@/components/home/JoinGame"; +import Link from "next/link"; +import { useSession } from "@/context/session"; + +export default function PlayPage() { + const { user } = useSession(); + const isLoggedIn = user?.id && typeof user.id === "number"; + + return ( +
+

Spielen

+ +
+ +
+
+

Spiel erstellen

+

Erstelle eine Partie und teile den Einladungslink mit Freunden

+
+ +
+
+
+ +
+
+

Gegen KI spielen

+

Fordere Stockfish auf verschiedenen Stufen heraus — von Anfänger bis Meister

+
+ Jetzt spielen +
+
+
+ + {isLoggedIn ? ( +
+
+

Freunde

+

Freunde hinzufügen, verwalten und zu einer Partie einladen

+
+ Freunde verwalten +
+
+
+ ) : ( +
+
+

Einladung annehmen

+

Einladungslink oder Code eingeben um beizutreten

+ +
+
+ )} + +
+
+

Tagespartien

+

Spiele in deinem eigenen Tempo — Züge bis zu 3 Tage Zeit

+
+ Tagespartien +
+
+
+ +
+
+

Turnier

+

Spiele Round-Robin Turniere gegen mehrere Gegner

+
+ Turniere +
+
+
+ +
+
+ ); +} diff --git a/client/src/components/home/RecentGames.tsx b/client/src/components/home/RecentGames.tsx new file mode 100644 index 0000000..2b51ae6 --- /dev/null +++ b/client/src/components/home/RecentGames.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Chessboard } from "react-chessboard"; +import { Chess } from "chess.js"; +import Link from "next/link"; +import { API_URL } from "@/config"; +import type { Game } from "@michess/types"; + +function getFen(pgn?: string): string { + if (!pgn) return "start"; + try { + const c = new Chess(); + c.loadPgn(pgn); + return c.fen(); + } catch { + return "start"; + } +} + +const BOARD_SIZE = 160; + +export default function RecentGames({ userId }: { userId: number }) { + const [games, setGames] = useState([]); + + useEffect(() => { + fetch(`${API_URL}/v1/games?userid=${userId}`, { cache: "no-store" }) + .then((r) => (r.ok ? r.json() : [])) + .then((data: Game[] | Game) => { + if (Array.isArray(data)) setGames(data.slice(0, 3)); + }) + .catch(() => {}); + }, [userId]); + + if (!games.length) return null; + + return ( +
+

Letzte Spiele

+
+ {games.map((game) => { + const fen = getFen(game.pgn); + const myColor = game.white?.id === userId ? "white" : "black"; + const opponent = myColor === "white" ? game.black?.name : game.white?.name; + const won = game.winner === myColor; + const drew = game.winner === "draw"; + + return ( + +
+ false} + customDarkSquareStyle={{ backgroundColor: "#4b7399" }} + customLightSquareStyle={{ backgroundColor: "#eae9d2" }} + areArrowsAllowed={false} + /> +
+
+
vs {opponent ?? "?"}
+
+ {won ? "Gewonnen" : drew ? "Remis" : "Verloren"} +
+
+ + ); + })} +
+
+ ); +}