Files
cruciverba_1/webapp/components/crossword-player.tsx

114 lines
3.5 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import type { CrosswordResponse } from "@/lib/types";
type CrosswordPlayerProps = {
crossword: CrosswordResponse;
labels: {
topic: string;
difficulty: string;
lexicon: string;
};
readOnly?: boolean;
revealSolution?: boolean;
};
type CellState = Record<string, string>;
function cellKey(row: number, col: number) {
return `${row}:${col}`;
}
export function CrosswordPlayer({
crossword,
labels,
readOnly = false,
revealSolution = false,
}: CrosswordPlayerProps) {
const [activeCell, setActiveCell] = useState<string | null>(null);
const [letters, setLetters] = useState<CellState>({});
useEffect(() => {
const firstCell = crossword.grid.cells.find((cell) => cell.kind === "letter");
if (firstCell) {
setActiveCell(cellKey(firstCell.row, firstCell.col));
}
}, [crossword]);
function writeLetter(row: number, col: number, value: string) {
const normalized = value.slice(-1).toUpperCase();
setLetters((current) => ({
...current,
[cellKey(row, col)]: normalized,
}));
}
return (
<div className="player-shell">
<div className="chip-row">
<span className="chip">{labels.topic}: {crossword.generator.topic.join(", ")}</span>
<span className="chip">{labels.difficulty}: {crossword.generator.difficulty}</span>
<span className="chip">{labels.lexicon}: {crossword.generator.runtime_lexicon}</span>
</div>
<div className="grid-board-wrap">
<div
className={`grid-board ${revealSolution ? "grid-board--solution" : ""}`}
style={{
gridTemplateColumns: `repeat(${crossword.grid.cols}, minmax(0, 1fr))`,
}}
>
{crossword.grid.cells.map((cell) => {
const key = cellKey(cell.row, cell.col);
if (cell.kind === "block") {
return <div className="grid-cell grid-cell--block" key={key} />;
}
const currentValue = revealSolution ? cell.solution ?? "" : letters[key] ?? "";
const isActive = activeCell === key;
const className = [
"grid-cell",
isActive && !readOnly ? "grid-cell--active" : "",
currentValue ? "grid-cell--filled" : "",
revealSolution ? "grid-cell--revealed" : "",
]
.filter(Boolean)
.join(" ");
if (readOnly) {
return (
<div className={className} key={key}>
{cell.number ? <span className="grid-cell__number">{cell.number}</span> : null}
{currentValue}
</div>
);
}
return (
<button
aria-label={`Cell ${cell.row + 1}, ${cell.col + 1}`}
className={className}
key={key}
onClick={() => setActiveCell(key)}
onKeyDown={(event) => {
if (/^[a-zA-Z]$/.test(event.key)) {
writeLetter(cell.row, cell.col, event.key);
}
if (event.key === "Backspace") {
writeLetter(cell.row, cell.col, "");
}
}}
type="button"
>
{cell.number ? <span className="grid-cell__number">{cell.number}</span> : null}
{currentValue}
</button>
);
})}
</div>
</div>
</div>
);
}