114 lines
3.5 KiB
TypeScript
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>
|
|
);
|
|
}
|