alpha01 filetti: web app, crossword service and tor batch
This commit is contained in:
113
webapp/components/crossword-player.tsx
Normal file
113
webapp/components/crossword-player.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user