Skip to content

Commit

Permalink
onComplete prop (#86)
Browse files Browse the repository at this point in the history
* onComplete prop and checkComplete store method

* onComplete alert

* update readme

---------

Co-authored-by: tblackwell-tm <t.blackwell@techmodal.com>
  • Loading branch information
t-blackwell and tblackwell-tm authored Feb 18, 2025
1 parent 6b2d8fd commit b61e2f4
Show file tree
Hide file tree
Showing 8 changed files with 86 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export default function MyPage() {
| `loadGrid` | `GuessGrid \| undefined`<br />Optional object to override storage mechanism. Called when the component is initialized with the ID of the crossword. Should return an array-based representation of the crossword grid. See [guess grid](#guess-grid) below for more information. |
| `onCellChange` | `(cellChange: CellChange) => void \| undefined`<br />Optional function. Called after a grid cell has changed its guess. The object contains the properties `pos`, `guess` and `previousGuess`. |
| `onCellFocus` | `(cellFocus: CellFocus) => void \| undefined`<br />Optional function. Called after the focus switches to a new cell. The object returned contains the properties `pos` and `clueId`. |
| `onComplete` | `() => void \| undefined`<br />Optional function. Called once after the grid has been successfully filled. |
| `saveGrid` | `(value: GuessGrid \| ((val: GuessGrid) => GuessGrid)) => void \| undefined`<br />Optional function to override storage mechanism. Called after the grid has changed with the ID of the crossword and array-based representation of the grid. See [guess grid](#guess-grid) below for more information. |
| `stickyClue` | `'always' \| 'never' \| 'auto'` = 'auto'<br />Optional value to define when to show the sticky clue above the grid. Defaults to `'auto'` (shown on `xs` and `sm` breakpoints). |
| `theme` | `'red' \| 'pink' \| 'purple' \| 'deepPurple' \| 'indigo' \| 'blue' \| 'lightBlue' \| 'cyan' \| 'teal' \| 'green' \| 'deepOrange' \| 'blueGrey'` = 'blue'<br />Optional value to override the main colour applied to the highlighted cells and clues. Defaults to `'blue'`. |
Expand Down
21 changes: 21 additions & 0 deletions lib/components/Controls/Controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ interface ControlsProps {
gridRows: number;
onAnagramHelperClick: () => void;
onCellChange?: (cellChange: CellChange) => void;
onComplete?: () => void;
setGuessGrid: (value: GuessGrid | ((val: GuessGrid) => GuessGrid)) => void;
solutionsAvailable: boolean;
}
Expand All @@ -33,6 +34,7 @@ export default function Controls({
gridRows,
onAnagramHelperClick,
onCellChange,
onComplete,
setGuessGrid,
solutionsAvailable,
}: ControlsProps) {
Expand All @@ -46,6 +48,7 @@ export default function Controls({

const answerAllCells = useCellsStore((store) => store.answerAll);
const setCells = useCellsStore((store) => store.setCells);
const checkComplete = useCellsStore((store) => store.checkComplete);
const answerAllClues = useCluesStore((store) => store.answerAll);
const answerSomeClues = useCluesStore((store) => store.answerSome);

Expand Down Expand Up @@ -165,6 +168,12 @@ export default function Controls({

setCells(updatedCells);

if (onComplete !== undefined) {
if (checkComplete() === true) {
onComplete();
}
}

// if all cells are populated, mark clue as answered
selectedCell.clueIds.forEach((clueId) => {
const clue = clues.find((c) => c.id === clueId)!;
Expand Down Expand Up @@ -214,6 +223,12 @@ export default function Controls({

setCells(updatedCells);

if (onComplete !== undefined) {
if (checkComplete() === true) {
onComplete();
}
}

updateAnsweredForCrossingClues(selectedClue, updatedCells);

// update guesses in local storage
Expand Down Expand Up @@ -334,6 +349,12 @@ export default function Controls({
answerAllClues(true);
setShowRevealGridConfirm(false);

if (onComplete !== undefined) {
if (checkComplete() === true) {
onComplete();
}
}

// update guesses in local storage
const updatedCells = cells.map((cell) => ({
...cell,
Expand Down
6 changes: 6 additions & 0 deletions lib/components/Crossword/Crossword.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ interface CrosswordProps {
loadGrid?: GuessGrid;
onCellChange?: (cellChange: CellChange) => void;
onCellFocus?: (cellFocus: CellFocus) => void;
onComplete?: () => void;
saveGrid?: (value: GuessGrid | ((val: GuessGrid) => GuessGrid)) => void;
stickyClue: 'always' | 'never' | 'auto';
}
Expand All @@ -49,6 +50,7 @@ export default function Crossword({
loadGrid,
onCellChange,
onCellFocus,
onComplete,
saveGrid,
stickyClue,
}: CrosswordProps) {
Expand All @@ -65,6 +67,7 @@ export default function Crossword({
const selectClue = useCluesStore((store) => store.select);
const setCells = useCellsStore((store) => store.setCells);
const setClues = useCluesStore((store) => store.setClues);
const checkComplete = useCellsStore((store) => store.checkComplete);

const parsedData = React.useMemo(() => {
try {
Expand Down Expand Up @@ -121,6 +124,7 @@ export default function Crossword({
React.useEffect(() => {
if (parsedData.cells !== null) {
setCells(parsedData.cells);
checkComplete();
}
}, [parsedData.cells]);

Expand Down Expand Up @@ -263,6 +267,7 @@ export default function Crossword({
inputRef={inputRef}
onCellChange={onCellChange}
onCellFocus={onCellFocus}
onComplete={onComplete}
rawClues={data.entries}
rows={data.dimensions.rows}
setGuessGrid={saveGrid ?? setGuessGrid}
Expand All @@ -277,6 +282,7 @@ export default function Crossword({
gridRows={data.dimensions.rows}
onAnagramHelperClick={() => setIsAnagramHelperOpen((val) => !val)}
onCellChange={onCellChange}
onComplete={onComplete}
setGuessGrid={saveGrid ?? setGuessGrid}
solutionsAvailable={data.solutionAvailable}
/>
Expand Down
12 changes: 11 additions & 1 deletion lib/components/Grid/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ interface GridProps {
inputRef?: React.RefObject<HTMLInputElement>;
onCellChange?: (cellChange: CellChange) => void;
onCellFocus?: (cellFocus: CellFocus) => void;
onComplete?: () => void;
rawClues: GuardianClue[];
rows: number;
setGuessGrid: (value: GuessGrid | ((val: GuessGrid) => GuessGrid)) => void;
Expand All @@ -56,6 +57,7 @@ export default function Grid({
inputRef,
onCellChange,
onCellFocus,
onComplete,
rawClues,
rows,
setGuessGrid,
Expand All @@ -74,6 +76,7 @@ export default function Grid({
const setCells = useCellsStore((state) => state.setCells);
const selectClue = useCluesStore((state) => state.select);
const answerClue = useCluesStore((state) => state.answerSome);
const checkComplete = useCellsStore((state) => state.checkComplete);

const updateViewBoxScale = React.useCallback(() => {
if (svgRef.current !== null) {
Expand Down Expand Up @@ -396,7 +399,7 @@ export default function Grid({
// overwrite the cell's value
setCells(updatedCells);

// if all cells are populated, mark clue as answered
// if all clue's cells are populated, mark clue as answered
selectedCell.clueIds.forEach((clueId) => {
const clue = clues.find((c) => c.id === clueId)!;
const populated = isCluePopulated(clue, updatedCells);
Expand All @@ -408,6 +411,13 @@ export default function Grid({

moveNext();

// if all grid's cells are populated, mark crossword as complete
if (onComplete !== undefined) {
if (checkComplete() === true) {
onComplete();
}
}

updateGuesses(updatedCells);
} else {
// prevent keys scrolling page
Expand Down
3 changes: 3 additions & 0 deletions lib/components/MyCrossword/MyCrossword.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface MyCrosswordProps {
loadGrid?: GuessGrid;
onCellChange?: (cellChange: CellChange) => void;
onCellFocus?: (cellFocus: CellFocus) => void;
onComplete?: () => void;
saveGrid?: (value: GuessGrid | ((val: GuessGrid) => GuessGrid)) => void;
stickyClue?: 'always' | 'never' | 'auto';
theme?: Theme;
Expand All @@ -44,6 +45,7 @@ export default function MyCrossword({
loadGrid,
onCellChange,
onCellFocus,
onComplete,
saveGrid,
stickyClue = 'auto',
theme = 'blue',
Expand All @@ -66,6 +68,7 @@ export default function MyCrossword({
loadGrid={loadGrid}
onCellChange={onCellChange}
onCellFocus={onCellFocus}
onComplete={onComplete}
saveGrid={saveGrid}
stickyClue={stickyClue}
/>
Expand Down
14 changes: 13 additions & 1 deletion lib/stores/useCellsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,25 @@ import type { Cell, CellPosition } from '~/types';

type CellsStore = {
cells: Cell[];
complete: boolean;
checkComplete: () => boolean | null;
setCells: (cells: Cell[]) => void;
select: (pos: CellPosition) => void;
answerAll: (answered: boolean) => void;
};

export const useCellsStore = create<CellsStore>((set) => ({
export const useCellsStore = create<CellsStore>((set, get) => ({
cells: [],
complete: false,
checkComplete: () => {
if (get().complete) {
return null; // don't trigger multiple times
}

const isComplete = get().cells.every((cell) => cell.val === cell.guess);
set({ complete: isComplete });
return isComplete;
},
setCells: (cells) => {
set(() => ({ cells }));
},
Expand Down
21 changes: 21 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ h6 {
align-items: center;
margin-bottom: 16px;
gap: 16px;
min-height: 36px;
}
.Page__control {
display: flex;
Expand All @@ -39,3 +40,23 @@ h6 {
.Page__control label {
font-size: 0.9rem;
}
.Page__alert {
display: flex;
align-items: center;
gap: 8px;
background-color: #e5f9f5;
padding: 4px 12px;
font-size: 0.8rem;
border-radius: 6px;
}
.Page__alertIcon {
display: flex;
align-items: center;
justify-content: center;
background-color: #00cc99;
color: #fff;
border-radius: 50%;
width: 16px;
height: 16px;
line-height: 1.2;
}
10 changes: 10 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Theme = (typeof THEME_OPTIONS)[number];
function App() {
const [theme, setTheme] = useState<Theme>('blue');
const [showDefinitions, setShowDefinitions] = useState(false);
const [complete, setComplete] = useState(false);

return (
<div className="Page">
Expand Down Expand Up @@ -56,13 +57,22 @@ function App() {
type="checkbox"
/>
</div>
{complete ? (
<div className="Page__alert">
<div className="Page__alertIcon">
<span></span>
</div>
<span>Complete</span>
</div>
) : null}
</div>
<MyCrossword
allowedHtmlTags={
showDefinitions ? ['u', ...ALLOWED_HTML_TAGS] : ALLOWED_HTML_TAGS
}
id="example"
data={data}
onComplete={() => setComplete(true)}
theme={theme}
/>
</main>
Expand Down

0 comments on commit b61e2f4

Please sign in to comment.