Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions components/Book/ContentPage.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import Page, { PageProps } from "./page";
import Page, { PageProps } from "./Page";

export default function ContentPage(
props: PageProps,
) {
const className = `p-12 print:after:w-0 mb-2 bg-red-400 print:mb-0 ${
props.pageNumber !== 0 && (props.pageNumber % 2 === 0
const className = `p-12 print:after:w-0 mb-2 bg-red-400 print:mb-0 ${props.pageNumber !== 0 && (props.pageNumber % 2 === 0
? "page-even"
: "page-odd")
} ${
props.settings.pageSize === "A4" ? "after:max-2xl:w-0" : "after:max-xl:w-0"
}`;
} ${props.settings.pageSize === "A4" ? "after:max-2xl:w-0" : "after:max-xl:w-0"
}`;

return (
<Page {...props} className={`${props.className} ${className}`}>
Expand Down
2 changes: 1 addition & 1 deletion components/Book/CoverPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Page, { PageProps } from "./page";
import Page, { PageProps } from "./Page";

export default function CoverPage(
props: PageProps,
Expand Down
3 changes: 1 addition & 2 deletions components/Book/OneGamePage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Fragment } from "react";
import { Game } from "../../types";
import ChessBoard from "../Chess/ChessBoard";
import { type PageProps } from "./page";
import { type PageProps } from "./Page";
import GameTitle from "../GameTitle";
import ContentPage from "./ContentPage";
import MoveListTable from "../Chess/MoveListTable";
Expand Down
File renamed without changes.
43 changes: 31 additions & 12 deletions components/Book/SixGamePage.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
import { Game, Settings } from "../../types";
import ChessBoard from "../Chess/ChessBoard";
import { type PageProps } from "./page";
import { type PageProps } from "./Page";
import GameTitle from "../GameTitle";
import ContentPage from "./ContentPage";
import MoveListTable from "../Chess/MoveListTable";
import MoveListInline from "../Chess/MoveListInline";

function GameRow(props: { index: number; settings: Settings; game: Game }) {
function GameRow(props: { index: number; settings: Settings; game: Game, totalMoveRows: number, fullLength: Boolean }) {
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TypeScript primitive type boolean should be lowercase, not Boolean (which refers to the Boolean object wrapper).

Suggested change
function GameRow(props: { index: number; settings: Settings; game: Game, totalMoveRows: number, fullLength: Boolean }) {
function GameRow(props: { index: number; settings: Settings; game: Game, totalMoveRows: number, fullLength: boolean }) {

Copilot uses AI. Check for mistakes.
const moves = props.game.moves.split(" ");

// Find the best variant to use to get the most out the move data.
let size = "text-xs";
if (moves.length < 20 * 2) {
size = "text-xs";
} else if (moves.length < 33 * 2) {
size = "text-xs";
} else {
size = "text-xs";
}

return (
<div>
Expand All @@ -28,21 +22,46 @@ function GameRow(props: { index: number; settings: Settings; game: Game }) {
white={props.game.white}
/>
</div>
<MoveListTable moves={moves} highlightedPly={props.game.board.ply} size={size} columns="columns-2 gap-4" />
{props.totalMoveRows > 35 * (Number(props.fullLength) + 1) && moves.length > 40 * (Number(props.fullLength) + 1)
? (
<MoveListInline moves={moves} highlightedPly={props.game.board.ply} size={size} />
)
: (
<MoveListTable moves={moves} highlightedPly={props.game.board.ply} size={size} columns="columns-2 gap-4" />
)}
</div>
);
}

export default function SixGamePage(props: PageProps & { games: Game[] }) {
const sortedByMoveCountGames = [...props.games].sort((a, b) => {
return b.movesCount - a.movesCount;
});

const totalMoveRows = sortedByMoveCountGames[0].movesCount + (sortedByMoveCountGames[3]?.movesCount ?? 0) / 2;

let yGap = 'gap-y-4';
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The initial value of yGap is unused, since it is always overwritten.

Suggested change
let yGap = 'gap-y-4';
let yGap;

Copilot uses AI. Check for mistakes.
if (totalMoveRows < 80) {
yGap = 'gap-y-20';
} else if (totalMoveRows < 160) {
yGap = 'gap-y-12';
} else if (totalMoveRows < 220) {
yGap = 'gap-y-8';
} else {
yGap = 'gap-y-4';
}

return (
<ContentPage {...props}>
<div className="grid grid-cols-3 gap-4">
{props.games.map((game, i) => (
<div className={`grid grid-cols-3 gap-x-4 ${yGap} ${totalMoveRows}`}>
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class string includes ${totalMoveRows} which outputs a number directly into the className. This appears to be a debugging artifact and will create an invalid CSS class. Consider removing this debug value from the className.

Suggested change
<div className={`grid grid-cols-3 gap-x-4 ${yGap} ${totalMoveRows}`}>
<div className={`grid grid-cols-3 gap-x-4 ${yGap}`}>

Copilot uses AI. Check for mistakes.
{sortedByMoveCountGames.map((game, i) => (
<GameRow
index={i}
key={game.id}
settings={props.settings}
game={game}
totalMoveRows={totalMoveRows}
fullLength={props.games.length < 4}
/>
))}
</div>
Expand Down
2 changes: 1 addition & 1 deletion components/Book/ThreeGamePage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Fragment } from "react";
import { Game, Settings } from "../../types";
import ChessBoard from "../Chess/ChessBoard";
import { type PageProps } from "./page";
import { type PageProps } from "./Page";
import GameTitle from "../GameTitle";
import ContentPage from "./ContentPage";
import MoveListTable from "../Chess/MoveListTable";
Expand Down
106 changes: 4 additions & 102 deletions components/Book/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,104 +2,19 @@ import { ReactNode, useCallback, useId, useState } from "react";
import type { Game, Settings, User } from "../../types";
import OneGamePage from "./OneGamePage";
import ThreeGamePage from "./ThreeGamePage";
import Page from "./page";
import Page from "./Page";
import Head from "next/head";
import ContentPage from "./ContentPage";
import CoverPage from "./CoverPage";
import SixGamePage from "./SixGamePage";

function stringToNumber(input: string, min: number, max: number) {
let hash = 0;
for (let i = 0; i < input.length; i++) {
hash = (hash * 31 + input.charCodeAt(i)) >>> 0;
}
return (hash % max) + min;
}

type ChessPage = {
type: 'one',
games: [Game],
} | {
type: 'three',
games: [Game, Game, Game | undefined],
} | {
type: 'four',
games: [Game, Game, Game, Game],
} | {
type: 'six',
games: [Game, Game, Game, Game, Game | undefined, Game | undefined],
}

function convertGamesToPages(settings: Settings, games: Game[]): ChessPage[] {
const result: ChessPage[] = [];

function scoreGame(game: Game): number {
return game.black.rating * 100
+ game.white.rating * 100
- (Number(game.black.ratingProvisional) * 1)
- (Number(game.white.ratingProvisional) * 1);
}
const gamesSortedByStrength = games.sort((a, b) => {
const aScore = scoreGame(a);
const bScore = scoreGame(b);

if (aScore === bScore) {
return 0;
}

return aScore > bScore ? -1 : 1;
});

// Best Game gets a full page
if (gamesSortedByStrength[0]) {
result.push({
type: 'one',
games: [gamesSortedByStrength[0]]
});
}

const batch: Game[] = [];
for (let i = 1; i < gamesSortedByStrength.length; i++) {
batch.push(gamesSortedByStrength[i]);

// @todo: Highlight games without any blunders/mistakes.
// @todo: Maybe highlight games that are full of blunders.

if (batch.length < 4 && (i > gamesSortedByStrength.length * 0.33 || settings.pageSize === 'A5')) {
if (batch.length === 3) {
result.push({
type: 'three',
games: [...batch] as [Game, Game, Game],
});
batch.length = 0;
continue;
}
} else {
if (batch.length === 6) {
result.push({
type: 'six',
games: [...batch] as [Game, Game, Game, Game, Game, Game],
});
batch.length = 0;
continue;
}
}
}

// No more games to add to batch, what was left over in the batching?
if (batch.length === 1) result.push({ type: 'one', games: [...batch] as [Game] });
if (batch.length === 2 || batch.length === 3) result.push({ type: 'three', games: [...batch] as [Game, Game, Game | undefined] });
if (batch.length > 3) result.push({ type: 'six', games: [...batch] as any });
batch.length = 0;

return result;
}
import stringToSemiRandomNumber from "../../utils/stringToNumber";
import convertGamesToPages from "../../utils/convertGamesToPages";

export default function Book(
props: { data: { user: User; games: Game[]; settings: Settings } },
) {
const [activePage, setActivePage] = useState<number>(0);
const cover = stringToNumber(useId(), 1, 9);
const cover = stringToSemiRandomNumber(useId(), 1, 9);

const pageClickHandler = useCallback((pageNumber: number) => {
if (pageNumber < 0) {
Expand Down Expand Up @@ -188,7 +103,6 @@ export default function Book(
);

const gamePages = convertGamesToPages(props.data.settings, props.data.games);
console.log({ gamePages })
for (const gamePage of gamePages) {
if (gamePage.type === 'one') {
const game = gamePage.games[0];
Expand All @@ -213,18 +127,6 @@ export default function Book(
onClick={pageClickHandler}
/>
);
} else if (gamePage.type === 'four') {
// @todo: Four Game Page
// pages.push(
// <FourGamePage
// key={gamePage.games.map((game) => game.id).join("-")}
// pageNumber={pages.length}
// games={gamePage.games}
// settings={props.data.settings}
// className={activePage > pages.length ? "turned" : ""}
// onClick={pageClickHandler}
// />
// );
} else if (gamePage.type === 'six') {
pages.push(
<SixGamePage
Expand Down
28 changes: 5 additions & 23 deletions components/Chess/ChessBoard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Game } from "../../types";
import { Chess } from "chess.js";

const columns = [
"A",
Expand All @@ -13,23 +12,6 @@ const columns = [
] as const;

export default function ChessBoard(props: { game: Game, className?: string }) {
const chess = new Chess();
try {
chess.loadPgn(props.game.board.pgn);
while (chess.moveNumber() > 1 && (chess.moveNumber() - 1) * 2 + Number(chess.turn() === 'b') > props.game.board.ply) {
chess.undo();
}
} catch (e) {
console.log("Invalid game: " + props.game.id)
}

const board = [...chess.board()];

if (chess.turn() === 'b') {
board.reverse();
board.map(row => row.reverse());
}

return (
<div className="mx-4 relative chessboard">
<div className="w-full aspect-square grid grid-cols-8 border-gray-500 border-2 mb-8">
Expand All @@ -43,12 +25,12 @@ export default function ChessBoard(props: { game: Game, className?: string }) {
key={i}
className={`${isDark ? "" : "bg-white"} overflow-hidden`}
>
{board[row][col]?.type
{props.game.board.grid[row][col]?.type
? (
// eslint-disable-next-line
<img
src={`/pieces/${board[row][col]?.color === "w" ? "white" : "black"
}/${board[row][col]?.type.toUpperCase()}.svg`}
src={`/pieces/${props.game.board.grid[row][col]?.color === "w" ? "white" : "black"
}/${props.game.board.grid[row][col]?.type.toUpperCase()}.svg`}
className="block m-auto w-full h-full"
/>
)
Expand All @@ -59,13 +41,13 @@ export default function ChessBoard(props: { game: Game, className?: string }) {
</div>
<div className={`${props.className ?? ''} absolute top-full left-0 right-0 aspect-none grid grid-cols-8 col-span-8`}>
{(new Array(8).fill(true)).map((_, i) => {
return <div key={i} className="text-center h-0">{chess.turn() === 'b' ? columns[7 - i] : columns[i]}</div>;
return <div key={i} className="text-center h-0">{props.game.board.turn === 'b' ? columns[7 - i] : columns[i]}</div>;
})}
</div>
<div className={`${props.className ?? ''} absolute top-0 -left-4 bottom-0 aspect-none grid grid-cols-1 col-span-8`}>
{(new Array(8).fill(true)).map((_, i) => {
return <div key={i} className="flex items-center">{
chess.turn() === 'w'
props.game.board.turn === 'w'
? 8 - i
: i + 1
}</div>;
Expand Down
6 changes: 4 additions & 2 deletions components/Chess/MoveListInline.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Fragment } from "react";

const highlightedStyle = "outline-2 bg-black text-white outline-black";

export default function MoveListInline(props: { moves: string[], highlightedPly: number, size: string }) {
const pairs: [string, string?][] = [];

Expand All @@ -17,9 +19,9 @@ export default function MoveListInline(props: { moves: string[], highlightedPly:
<span className="whitespace-nowrap">
<span className="text-gray-400 font-anton">{i + 1}</span>
&nbsp;
{moves[0]}
{i * 2 === props.highlightedPly ? <span className={highlightedStyle}>{moves[0]}</span> : moves[0]}
&nbsp;
{moves[1]}.
{i * 2 + 1 === props.highlightedPly ? <span className={highlightedStyle}>{moves[1]}</span> : moves[1]}.
</span>
{" "}
</Fragment>
Expand Down
4 changes: 2 additions & 2 deletions components/Chess/MoveListTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ export default function MoveListTable(props: { moves: string[], highlightedPly:
<td className="text-gray-400 w-6 font-anton">
{i + 1}
</td>
<td><span className={i * 2 == props.highlightedPly - 1 ? highlightedStyle : ''}>{moves[0]}</span></td>
<td><span className={i * 2 + 1 === props.highlightedPly - 1 ? highlightedStyle : ''}>{moves[1]}</span></td>
<td><span className={i * 2 == props.highlightedPly ? highlightedStyle : ''}>{moves[0]}</span></td>
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent equality operator: line 24 uses == while line 25 uses ===. For consistency and to follow JavaScript best practices, use === on both lines.

Suggested change
<td><span className={i * 2 == props.highlightedPly ? highlightedStyle : ''}>{moves[0]}</span></td>
<td><span className={i * 2 === props.highlightedPly ? highlightedStyle : ''}>{moves[0]}</span></td>

Copilot uses AI. Check for mistakes.
<td><span className={i * 2 + 1 === props.highlightedPly ? highlightedStyle : ''}>{moves[1]}</span></td>
</tr>
))}
</tbody>
Expand Down
2 changes: 1 addition & 1 deletion components/Form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export default function Form(
}
try {
const data = JSON.parse(line) as LichessGame;
const result = await processGame(data, user.username, settings);
const result = processGame(data, user.username, settings);
if (result) {
// Game will only be added if it's interesting.
games.push(result);
Expand Down
Loading