Skip to content

Commit

Permalink
SSR improvements (#85)
Browse files Browse the repository at this point in the history
* updated useLocalStorage hook + supporting hooks

* ignore test files in build

* memoize moveToNextClue

* add aspect ratio to grid

* remove isLoading prop

* better GridError sizing

* improve storing of initial cells and clues

* useLocationHash hook and listen for hash change

---------

Co-authored-by: tblackwell-tm <t.blackwell@techmodal.com>
  • Loading branch information
t-blackwell and tblackwell-tm authored Feb 18, 2025
1 parent 0027cfc commit 6b2d8fd
Show file tree
Hide file tree
Showing 13 changed files with 582 additions and 194 deletions.
162 changes: 104 additions & 58 deletions lib/components/Crossword/Crossword.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from '~/utils/clue';
import { getBem } from '~/utils/bem';
import Controls from '~/components/Controls/Controls';
import useLocationHash from '~/hooks/useLocationHash/useLocationHash';
import './Crossword.css';

interface CrosswordProps {
Expand Down Expand Up @@ -56,26 +57,87 @@ export default function Crossword({
`crosswords.${id}`,
initialiseGuessGrid(data.dimensions.cols, data.dimensions.rows),
);
const [locationHash] = useLocationHash();

const clues = useCluesStore((store) => store.clues);
const cells = useCellsStore((store) => store.cells);
const storeClues = useCluesStore((store) => store.clues);
const storeCells = useCellsStore((store) => store.cells);
const selectCells = useCellsStore((store) => store.select);
const selectClue = useCluesStore((store) => store.select);
const setCells = useCellsStore((store) => store.setCells);
const setClues = useCluesStore((store) => store.setClues);

const parsedData = React.useMemo(() => {
try {
const initialisedCells = initialiseCells({
cols: data.dimensions.cols,
rows: data.dimensions.rows,
entries: data.entries,
guessGrid: loadGrid ?? guessGrid,
allowMissingSolutions,
});

const initialisedClues = initialiseClues(data.entries, initialisedCells);

return {
cells: initialisedCells,
clues: initialisedClues,
error: null,
};
} catch (err: unknown) {
const error =
err instanceof Error ? err.message : 'An unknown error occurred';
return { cells: null, clues: null, error };
}
}, [data, loadGrid, allowMissingSolutions]);

// coalesce store values with initial values
const cells = React.useMemo(
() =>
storeCells.length === 0 && parsedData.cells !== null
? parsedData.cells
: storeCells,
[storeCells, parsedData.cells],
);
const clues = React.useMemo(
() =>
storeClues.length === 0 && parsedData.clues !== null
? parsedData.clues
: storeClues,
[storeClues, parsedData.clues],
);

const selectedClue = clues.find((clue) => clue.selected);
const parentClue =
selectedClue?.group.length === 1
? selectedClue
: clues.find((clue) => clue.id === selectedClue?.group[0]);
const [gridErrorMessage, setGridErrorMessage] = React.useState<string>();
const [isAnagramHelperOpen, setIsAnagramHelperOpen] = React.useState(false);
const gridHeight =
data.dimensions.rows * CELL_SIZE + data.dimensions.rows + 1;
const gridWidth = data.dimensions.cols * CELL_SIZE + data.dimensions.cols + 1;
const inputRef = React.useRef<HTMLInputElement>(null);

// sync store values with parsed data
React.useEffect(() => {
if (parsedData.cells !== null) {
setCells(parsedData.cells);
}
}, [parsedData.cells]);

React.useEffect(() => {
if (parsedData.clues !== null) {
setClues(parsedData.clues);
}
}, [parsedData.clues]);

React.useEffect(() => {
const selectedClue = clues.find((clue) => clue.selected);

if (selectedClue === undefined) {
selectClue(locationHash.replace('#', ''));
}
}, [locationHash]);

// validate overriding guess grid if defined
if (
loadGrid !== undefined &&
Expand All @@ -88,46 +150,30 @@ export default function Crossword({
) {
return (
<div className={bem('Crossword')}>
<GridError message="Error loading grid" />
<GridError
message="Error loading grid"
style={{
height: gridHeight,
width: gridWidth,
aspectRatio: `${data.dimensions.cols} / ${data.dimensions.rows}`,
}}
/>
</div>
);
}

React.useEffect(() => {
try {
// initialise cells
const initCells = initialiseCells({
cols: data.dimensions.cols,
rows: data.dimensions.rows,
entries: data.entries,
guessGrid: loadGrid ?? guessGrid,
allowMissingSolutions,
});
setCells(initCells);

// initialise clues
const initClues = initialiseClues(
data.entries,
initCells,
window.location.hash.replace('#', ''),
);
setClues(initClues);

setGridErrorMessage(undefined);
} catch (error: unknown) {
if (error instanceof Error) {
setGridErrorMessage(error.message);
} else {
throw error;
}
}
}, [data]);

// something went wrong...
if (gridErrorMessage !== undefined) {
if (parsedData.error !== null) {
return (
<div className={bem('Crossword')}>
<GridError message={gridErrorMessage} />
<GridError
message={parsedData.error}
style={{
height: gridHeight,
width: gridWidth,
aspectRatio: `${data.dimensions.cols} / ${data.dimensions.rows}`,
}}
/>
</div>
);
}
Expand All @@ -141,31 +187,32 @@ export default function Crossword({
}
};

const moveToNextClue = (forwards: boolean) => {
// cycle through the clues
const index = clues.findIndex((clue) => clue.selected);
let nextIndex = 0;

// direction
if (forwards) {
nextIndex = index < clues.length - 1 ? index + 1 : 0;
} else {
nextIndex = index > 0 ? index - 1 : clues.length - 1;
}
const moveToNextClue = React.useCallback(
(forwards: boolean) => {
const index = clues.findIndex((clue) => clue.selected);
const nextIndex = forwards
? index < clues.length - 1
? index + 1
: 0
: index > 0
? index - 1
: clues.length - 1;

const nextClue = clues[nextIndex];
const nextCluePos = {
col: nextClue.position.x,
row: nextClue.position.y,
};
const nextClue = clues[nextIndex];
const nextCluePos = {
col: nextClue.position.x,
row: nextClue.position.y,
};

selectClue(nextClue.id);
selectCells(nextCluePos);
selectClue(nextClue.id);
selectCells(nextCluePos);

cellFocus(nextCluePos, nextClue.id);
cellFocus(nextCluePos, nextClue.id);

inputRef?.current?.focus({ preventScroll: true });
};
inputRef?.current?.focus({ preventScroll: true });
},
[clues, selectClue, selectCells, cellFocus],
);

return (
<div className={bem('Crossword')}>
Expand Down Expand Up @@ -214,7 +261,6 @@ export default function Crossword({
cols={data.dimensions.cols}
guessGrid={guessGrid}
inputRef={inputRef}
isLoading={cells.length === 0}
onCellChange={onCellChange}
onCellFocus={onCellFocus}
rawClues={data.entries}
Expand Down
7 changes: 1 addition & 6 deletions lib/components/Grid/Grid.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,8 @@
.MyCrossword .Grid:focus {
outline: none;
}
.MyCrossword .Grid--loading {
display: flex;
align-items: center;
justify-content: center;
background-color: var(--grid-background);
}
.MyCrossword .Grid__background {
background-color: var(--grid-background);
fill: var(--grid-background);
}
.MyCrossword .Grid__inputContainer {
Expand Down
20 changes: 0 additions & 20 deletions lib/components/Grid/Grid.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,26 +101,6 @@ describe.skip('all tests', () => {
expect(cells.length).toBe(23);
});

test('it renders with loading', () => {
initialiseStores(testData);

render(
<Grid
cellMatcher={cellMatcher}
cells={getCells()}
clues={getClues()}
cols={testData.dimensions.cols}
guessGrid={emptyGuessGrid}
isLoading
rawClues={testData.entries}
rows={testData.dimensions.rows}
setGuessGrid={jest.fn}
/>,
);

screen.getByRole('status');
});

test('arrows move between cells', async () => {
initialiseStores(testData);
const setGuessGrid = jest.fn();
Expand Down
Loading

0 comments on commit 6b2d8fd

Please sign in to comment.