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 <noreply@anthropic.com>
This commit is contained in:
82
client/src/components/home/RecentGames.tsx
Normal file
82
client/src/components/home/RecentGames.tsx
Normal file
@@ -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<Game[]>([]);
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<h2 className="text-lg font-bold mb-3">Letzte Spiele</h2>
|
||||
<div className="flex gap-4 flex-wrap">
|
||||
{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 (
|
||||
<Link
|
||||
key={game.id}
|
||||
href={`/archive/${game.id}`}
|
||||
className="card bg-base-200 shadow hover:shadow-lg transition-shadow overflow-hidden"
|
||||
style={{ width: BOARD_SIZE }}
|
||||
>
|
||||
<div className="pointer-events-none">
|
||||
<Chessboard
|
||||
boardWidth={BOARD_SIZE}
|
||||
position={fen}
|
||||
boardOrientation={myColor}
|
||||
isDraggablePiece={() => false}
|
||||
customDarkSquareStyle={{ backgroundColor: "#4b7399" }}
|
||||
customLightSquareStyle={{ backgroundColor: "#eae9d2" }}
|
||||
areArrowsAllowed={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-2 py-2 text-xs">
|
||||
<div className="font-semibold truncate">vs {opponent ?? "?"}</div>
|
||||
<div
|
||||
className={`mt-0.5 font-medium ${
|
||||
won ? "text-success" : drew ? "text-warning" : "text-error"
|
||||
}`}
|
||||
>
|
||||
{won ? "Gewonnen" : drew ? "Remis" : "Verloren"}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user