diff --git a/libs/@guardian/react-crossword/.storybook/preview-head.html b/libs/@guardian/react-crossword/.storybook/preview-head.html index e9a2bc0d0..2405e8f68 100644 --- a/libs/@guardian/react-crossword/.storybook/preview-head.html +++ b/libs/@guardian/react-crossword/.storybook/preview-head.html @@ -1,6 +1,5 @@ diff --git a/libs/@guardian/react-crossword/src/@types/crossword.ts b/libs/@guardian/react-crossword/src/@types/crossword.ts index eb94aa79e..a5fd198af 100644 --- a/libs/@guardian/react-crossword/src/@types/crossword.ts +++ b/libs/@guardian/react-crossword/src/@types/crossword.ts @@ -51,6 +51,7 @@ export type Theme = { foreground: string; anagramHelperBackground: string; text: string; + provisionalText: string; errorText: string; gutter: number; highlight: string; diff --git a/libs/@guardian/react-crossword/src/components/AnagramHelper.stories.tsx b/libs/@guardian/react-crossword/src/components/AnagramHelper.stories.tsx index 606d7226d..90af71e4d 100644 --- a/libs/@guardian/react-crossword/src/components/AnagramHelper.stories.tsx +++ b/libs/@guardian/react-crossword/src/components/AnagramHelper.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react'; import { groupedClues as data } from '../../stories/formats/grouped-clues'; +import { progress12Across } from '../../stories/formats/grouped-clues.progress'; import { ContextProvider } from '../context/ContextProvider'; import { AnagramHelper } from './AnagramHelper'; @@ -8,7 +9,11 @@ const meta: Meta = { title: 'Components/Anagram Helper', decorators: [ (Story) => ( - + ), diff --git a/libs/@guardian/react-crossword/src/components/AnagramHelper.tsx b/libs/@guardian/react-crossword/src/components/AnagramHelper.tsx index e23746ffa..b3219595e 100644 --- a/libs/@guardian/react-crossword/src/components/AnagramHelper.tsx +++ b/libs/@guardian/react-crossword/src/components/AnagramHelper.tsx @@ -1,100 +1,58 @@ import { css } from '@emotion/react'; import { space } from '@guardian/source/foundations'; -import { SvgCross } from '@guardian/source/react-components'; -import { useCallback, useEffect, useState } from 'react'; +import { SvgCross, TextInput } from '@guardian/source/react-components'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCurrentClue } from '../context/CurrentClue'; import { useData } from '../context/Data'; import { useProgress } from '../context/Progress'; import { useTheme } from '../context/Theme'; import { useUIState } from '../context/UI'; -import { useUpdateCell } from '../hooks/useUpdateCell'; -import type { AnagramHelperProgress } from '../utils/getAnagramHelperProgressForGroup'; -import { getAnagramHelperProgressForGroup } from '../utils/getAnagramHelperProgressForGroup'; +import { biasedShuffle } from '../utils/biasedShuffle'; +import { getCellsWithProgressForGroup } from '../utils/getCellsWithProgressForGroup'; import { Button } from './Button'; import { Clue } from './Clue'; import { SolutionDisplay } from './SolutionDisplay'; -import { SolutionDisplayKey } from './SolutionDisplayKey'; import { WordWheel } from './WordWheel'; +const inputRegex = /[^A-Za-zÀ-ÿ0-9]/g; + export const AnagramHelper = () => { - const [shuffled, setShuffled] = useState(false); - const [candidateLetters, setCandidateLetters] = useState([]); - const [wordWheelLetters, setWordWheelLetters] = useState([]); - const [progressLetters, setProgressLetters] = useState< - AnagramHelperProgress[] - >([]); - const { entries } = useData(); - const { progress } = useProgress(); - const { updateCell } = useUpdateCell(); + const [letters, setLetters] = useState(''); + const [solving, setSolving] = useState(false); + const [shuffledLetters, setShuffledLetters] = useState([]); const theme = useTheme(); const { setShowAnagramHelper } = useUIState(); + const { entries, cells } = useData(); const { currentEntryId } = useCurrentClue(); - const entry = currentEntryId ? entries.get(currentEntryId) : undefined; + const { progress } = useProgress(); - const reset = useCallback(() => { - const progressLetters = getAnagramHelperProgressForGroup({ + const entry = useMemo(() => { + return currentEntryId ? entries.get(currentEntryId) : undefined; + }, [currentEntryId, entries]); + + const cellsWithProgress = useMemo(() => { + return getCellsWithProgressForGroup({ entry, + cells, entries, progress, }); - setProgressLetters( - getAnagramHelperProgressForGroup({ entry, entries, progress }), - ); - setCandidateLetters( - Array.from({ length: progressLetters.length }, () => ''), - ); - setShuffled(false); - }, [entries, entry, progress]); + }, [entry, cells, entries, progress]); - const save = useCallback(() => { - for (const progressLetter of progressLetters) { - if (!progressLetter.isSaved) { - updateCell({ - ...progressLetter.coords, - value: progressLetter.progress, - }); - } - } - }, [progressLetters, updateCell]); + const reset = useCallback(() => { + setShuffledLetters([]); + setSolving(false); + }, []); const shuffle = useCallback(() => { - setShuffled(true); - setCandidateLetters((prevState) => { - const shuffleLetters = [...prevState]; - const matchedLetters = Array.from( - { length: progressLetters.length }, - () => '', - ); - // remove letters that exist in progressLetters but only the number of times they exist - progressLetters.forEach((groupProgress, index) => { - const shuffleLetterIndex = shuffleLetters.indexOf( - groupProgress.progress, - ); - if (shuffleLetterIndex !== -1) { - matchedLetters[index] = - shuffleLetters.splice(shuffleLetterIndex, 1)[0] ?? ''; - } - }); + setShuffledLetters(biasedShuffle(letters.split(''))); + }, [letters]); - // shuffle the candidate letters and remove blanks - shuffleLetters - .sort(() => Math.random() - 0.5) - .filter((shuffleLetter) => shuffleLetter !== ''); + const start = useCallback(() => { + shuffle(); + setSolving(true); + }, [shuffle]); - return matchedLetters.map((letter) => { - if (letter === '') { - return shuffleLetters.pop() ?? ''; - } - return letter; - }); - }); - const newWordWheelLetters = [...candidateLetters] - .filter((letter) => !!letter) - .sort(() => Math.random() - 0.5); - setWordWheelLetters(newWordWheelLetters); - }, [candidateLetters, progressLetters]); - - //initialise the candidate letters and progress letters useEffect(() => { reset(); }, [reset]); @@ -102,11 +60,18 @@ export const AnagramHelper = () => { return (
{ flex-direction: column; `} > -
- -
-
* { - margin: 0 ${space[1]}px; - } - `} - > - - - -
+
+ { + const letters = event.target.value.replace(inputRegex, ''); + setLetters(letters.toUpperCase()); + }} + value={letters} + maxLength={cellsWithProgress.length} + /> +
+ + + {letters.length}/{cellsWithProgress.length} + +
+ )} + {solving && ( + <> + +
* { + margin: 0 ${space[1]}px; + } + `} + > + + +
+ + )}
- {entry && } -
+ /> + {entry && } -
); diff --git a/libs/@guardian/react-crossword/src/components/SolutionDisplay.stories.tsx b/libs/@guardian/react-crossword/src/components/SolutionDisplay.stories.tsx index 82a5e3ee1..230112792 100644 --- a/libs/@guardian/react-crossword/src/components/SolutionDisplay.stories.tsx +++ b/libs/@guardian/react-crossword/src/components/SolutionDisplay.stories.tsx @@ -22,24 +22,22 @@ type Story = StoryObj; export const Default: Story = { args: { - candidateLetters: ['T', 'E', '', ''], - progressLetters: [ + cellsWithProgress: [ { - coords: { x: 0, y: 0 }, + x: 0, + y: 0, progress: 'T', - isSaved: true, separator: ',', }, - { coords: { x: 1, y: 0 }, progress: 'E', isSaved: true }, + { x: 1, y: 0, progress: 'E' }, { - coords: { x: 2, y: 0 }, + x: 2, + y: 0, progress: 'S', - isSaved: true, separator: '-', }, - { coords: { x: 3, y: 0 }, progress: '', isSaved: true }, + { x: 3, y: 0, progress: '' }, ], - setCandidateLetters: () => {}, - setProgressLetters: () => {}, + shuffledLetters: ['T', 'E', 'S', 'T'], }, }; diff --git a/libs/@guardian/react-crossword/src/components/SolutionDisplay.tsx b/libs/@guardian/react-crossword/src/components/SolutionDisplay.tsx index 0e61a8f04..435e405d8 100644 --- a/libs/@guardian/react-crossword/src/components/SolutionDisplay.tsx +++ b/libs/@guardian/react-crossword/src/components/SolutionDisplay.tsx @@ -1,147 +1,132 @@ import { css } from '@emotion/react'; -import { isUndefined } from '@guardian/libs'; -import { space } from '@guardian/source/foundations'; -import { SvgPadlock } from '@guardian/source/react-components'; -import type { Dispatch, KeyboardEvent, SetStateAction } from 'react'; -import { useCallback } from 'react'; -import { useState } from 'react'; +import { textSans12 } from '@guardian/source/foundations'; import { useRef } from 'react'; -import { useEffect } from 'react'; import { useTheme } from '../context/Theme'; -import type { AnagramHelperProgress } from '../utils/getAnagramHelperProgressForGroup'; -import { keyDownRegex } from '../utils/keydownRegex'; -import { Button } from './Button'; -import { SolutionDisplayCell } from './SolutionDisplayCell'; +import type { + CellsWithProgress, + CellWithProgress, +} from '../utils/getCellsWithProgressForGroup'; type SolutionDisplayProps = { - shuffled: boolean; - setShuffled: Dispatch>; - setProgressLetters: Dispatch>; - progressLetters: AnagramHelperProgress[]; - setCandidateLetters: Dispatch>; - candidateLetters: string[]; + shuffledLetters: string[]; + cellsWithProgress: CellsWithProgress; }; -export const SolutionDisplay = ({ - shuffled, - setShuffled, - setProgressLetters, - progressLetters, - setCandidateLetters, - candidateLetters, -}: SolutionDisplayProps) => { - const [dragItemIndex, setDragItemIndex] = useState(); - const [dragOverItemIndex, setDragOverItemIndex] = useState(); - const inputRefs = useRef>([]); - const theme = useTheme(); - useEffect(() => { - inputRefs.current = inputRefs.current.slice(0, progressLetters.length); - }, [progressLetters]); +const getMatchedAndShuffledLetters = ({ + shuffledLetters, + cellsWithProgress, +}: SolutionDisplayProps): string[] => { + // Make copy of shuffled letters so we can mutate it without affecting the original + const shuffledLettersCopy = [...shuffledLetters]; - const updateCandidateLetter = (event: KeyboardEvent) => { - const index = Number(event.currentTarget.getAttribute('data-index')); - const allowedKeys = ['Backspace', 'ArrowLeft', 'ArrowRight']; - if ( - isNaN(index) || - (event.key.length !== 1 && !allowedKeys.includes(event.key)) - ) { - return; - } - if (event.key.length === 1 && !keyDownRegex.test(event.key)) { - return; - } - if (event.key === 'ArrowLeft') { - inputRefs.current[index - 1]?.focus(); - return; - } - if (event.key === 'ArrowRight') { - inputRefs.current[index + 1]?.focus(); - return; - } - if (keyDownRegex.test(event.key)) { - setCandidateLetters((prevState) => { - const newCandidateLetters = [...prevState]; - newCandidateLetters[index] = event.key.toUpperCase(); - return newCandidateLetters; - }); - inputRefs.current[index + 1]?.focus(); - } - if (event.key === 'Backspace') { - setCandidateLetters((prevState) => { - const newCandidateLetters = [...prevState]; - newCandidateLetters[index] = ''; - return newCandidateLetters; - }); - inputRefs.current[index - 1]?.focus(); - } - }; + const matchedLetters = Array.from( + { length: cellsWithProgress.length }, + () => '', + ); - const onDragEnd = useCallback(() => { - if ( - !isUndefined(dragItemIndex) && - !isUndefined(dragOverItemIndex) && - dragOverItemIndex !== dragItemIndex - ) { - setCandidateLetters((prev) => { - const newCandidateLetters = [...prev]; - const dragCandidate = newCandidateLetters[dragItemIndex]; - const dropCandidate = newCandidateLetters[dragOverItemIndex]; - if (!isUndefined(dropCandidate) && !isUndefined(dragCandidate)) { - newCandidateLetters[dragItemIndex] = dropCandidate; - newCandidateLetters[dragOverItemIndex] = dragCandidate; - } - return newCandidateLetters; - }); - setShuffled(true); + // Match the letters in the cells with the shuffled letters + for (const [index, cellWithProgress] of cellsWithProgress.entries()) { + const shuffleLetterIndex = shuffledLettersCopy.indexOf( + cellWithProgress.progress, + ); + if (shuffleLetterIndex !== -1) { + matchedLetters[index] = + shuffledLettersCopy.splice(shuffleLetterIndex, 1)[0] ?? ''; } - setDragOverItemIndex(undefined); - setDragItemIndex(undefined); - }, [dragItemIndex, dragOverItemIndex, setCandidateLetters, setShuffled]); + } - const updateProgressLetter = (index: number) => { - const newProgressLetters = [...progressLetters]; - const currentProgressLetter = progressLetters[index]; - if (isUndefined(currentProgressLetter)) { - return; - } - if (currentProgressLetter.progress !== candidateLetters[index]) { - currentProgressLetter.isSaved = false; - currentProgressLetter.progress = candidateLetters[index] ?? ''; + // Fill in the remaining cells with the shuffled letters + return matchedLetters.map((letter) => { + if (letter === '') { + return shuffledLettersCopy.pop() ?? ''; } + return letter; + }); +}; - //Any other letters with the same coords (crossing letters) need the same value - const coords = currentProgressLetter.coords; - for (const progressLetter of newProgressLetters) { - if ( - progressLetter.coords.x === coords.x && - progressLetter.coords.y === coords.y && - progressLetter.progress !== candidateLetters[index] - ) { - progressLetter.progress = candidateLetters[index] ?? ''; - progressLetter.isSaved = false; - } - } - setProgressLetters(newProgressLetters); - }; +export const SolutionDisplayCell = ({ + cellWithProgress, + shuffledLetter, +}: { + cellWithProgress: CellWithProgress; + shuffledLetter: string; +}) => { + const theme = useTheme(); + return ( +
+ {cellWithProgress.separator === '-' && ( +
+ )} + {cellWithProgress.number && ( +
+ {cellWithProgress.number} +
+ )} + {cellWithProgress.progress === '' + ? shuffledLetter + : cellWithProgress.progress} +
+ ); +}; +export const SolutionDisplay = ({ + cellsWithProgress, + shuffledLetters, +}: SolutionDisplayProps) => { + const containerRef = useRef(null); + const theme = useTheme(); + const solutionDisplayLetters = getMatchedAndShuffledLetters({ + shuffledLetters, + cellsWithProgress, + }); return (
event.preventDefault()} > - {progressLetters.map((progressLetter, index) => { - const progressValid = - progressLetter.progress === candidateLetters[index] || - progressLetter.progress === '' || - !shuffled; - const candidateLetter = candidateLetters[index]; + {cellsWithProgress.map((cellWithProgress, index) => { return (
- (inputRefs.current[index] = el)} - draggable={true} - value={candidateLetters[index]} - onDragStart={() => setDragItemIndex(index)} - onDragEnter={() => setDragOverItemIndex(index)} - onDragEnd={onDragEnd} - onChange={() => {}} // TODO: remove the need for this - onKeyDown={updateCandidateLetter} - maxLength={1} - tabIndex={index + 1} - data-index={index} - css={css` - box-sizing: border-box; - border: 1px solid ${theme.background}; - border-radius: 4px; - width: ${theme.cellSize - 1}px; - height: ${theme.cellSize - 1}px; - margin-left: 1px; - text-align: center; - align-content: center; - caret-color: transparent; - :active { - cursor: grabbing; - } - `} - /> - {progressLetter.progress !== candidateLetter && shuffled && ( - - )}
); diff --git a/libs/@guardian/react-crossword/src/components/SolutionDisplayCell.stories.tsx b/libs/@guardian/react-crossword/src/components/SolutionDisplayCell.stories.tsx deleted file mode 100644 index 886d3e57f..000000000 --- a/libs/@guardian/react-crossword/src/components/SolutionDisplayCell.stories.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import { groupedClues as data } from '../../stories/formats/grouped-clues'; -import { ContextProvider } from '../context/ContextProvider'; -import { SolutionDisplayCell } from './SolutionDisplayCell'; - -const meta: Meta = { - component: SolutionDisplayCell, - title: 'Components/SolutionDisplayCell', - - args: {}, - decorators: [ - (Story) => ( - - - - ), - ], -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = { - args: { - progressLetter: { - coords: { x: 0, y: 0 }, - progress: 'T', - isSaved: true, - }, - progressValid: true, - }, -}; - -export const Temporary: Story = { - args: { - progressLetter: { - coords: { x: 0, y: 0 }, - progress: 'T', - isSaved: false, - }, - progressValid: true, - }, -}; - -export const NotMatching: Story = { - args: { - progressLetter: { - coords: { x: 0, y: 0 }, - progress: 'T', - isSaved: true, - }, - progressValid: false, - }, -}; - -export const TemporaryNotMatching: Story = { - args: { - progressLetter: { - coords: { x: 0, y: 0 }, - progress: 'T', - isSaved: false, - }, - progressValid: false, - }, -}; diff --git a/libs/@guardian/react-crossword/src/components/SolutionDisplayCell.tsx b/libs/@guardian/react-crossword/src/components/SolutionDisplayCell.tsx deleted file mode 100644 index 815c92979..000000000 --- a/libs/@guardian/react-crossword/src/components/SolutionDisplayCell.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { css } from '@emotion/react'; -import { textSans12 } from '@guardian/source/foundations'; -import { useTheme } from '../context/Theme'; -import type { AnagramHelperProgress } from '../utils/getAnagramHelperProgressForGroup'; - -export const SolutionDisplayCell = ({ - progressLetter, - progressValid, -}: { - progressLetter: AnagramHelperProgress; - progressValid: boolean; -}) => { - const theme = useTheme(); - return ( -
- {progressLetter.separator === '-' && ( -
- )} - {progressLetter.number && ( -
- {progressLetter.number} -
- )} - {progressLetter.progress} -
- ); -}; diff --git a/libs/@guardian/react-crossword/src/components/SolutionDisplayKey.stories.tsx b/libs/@guardian/react-crossword/src/components/SolutionDisplayKey.stories.tsx deleted file mode 100644 index 959378fc5..000000000 --- a/libs/@guardian/react-crossword/src/components/SolutionDisplayKey.stories.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import { groupedClues as data } from '../../stories/formats/grouped-clues'; -import { ContextProvider } from '../context/ContextProvider'; -import { SolutionDisplayKey } from './SolutionDisplayKey'; - -const meta: Meta = { - component: SolutionDisplayKey, - title: 'Components/Solution Display Key', - - args: {}, - decorators: [ - (Story) => ( - - - - ), - ], -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = { - args: {}, -}; diff --git a/libs/@guardian/react-crossword/src/components/SolutionDisplayKey.tsx b/libs/@guardian/react-crossword/src/components/SolutionDisplayKey.tsx deleted file mode 100644 index 5d3bcee72..000000000 --- a/libs/@guardian/react-crossword/src/components/SolutionDisplayKey.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { css } from '@emotion/react'; -import { SolutionDisplayCell } from './SolutionDisplayCell'; - -export const SolutionDisplayKey = () => { - return ( -
-
Unsaved value
- -
Mismatched value
- -
- ); -}; diff --git a/libs/@guardian/react-crossword/src/components/WordWheel.tsx b/libs/@guardian/react-crossword/src/components/WordWheel.tsx index 27f2ad65f..9bd8325e5 100644 --- a/libs/@guardian/react-crossword/src/components/WordWheel.tsx +++ b/libs/@guardian/react-crossword/src/components/WordWheel.tsx @@ -52,7 +52,11 @@ const renderOuterLetters = ({ const WordWheelComponent = ({ letters }: { letters: string[] }) => { const theme = useTheme(); - const centerLetter = letters.length > 4 ? letters.shift() : undefined; + // Copy array to avoid mutating the original + const wordWheelLetters = [...letters]; + + const centerLetter = + letters.length > 4 ? wordWheelLetters.shift() : undefined; return ( @@ -70,7 +74,7 @@ const WordWheelComponent = ({ letters }: { letters: string[] }) => { {centerLetter} )} - {renderOuterLetters({ letters: letters, fill: theme.text })} + {renderOuterLetters({ letters: wordWheelLetters, fill: theme.text })} ); }; diff --git a/libs/@guardian/react-crossword/src/theme.ts b/libs/@guardian/react-crossword/src/theme.ts index b256a2f0a..1528be6cf 100644 --- a/libs/@guardian/react-crossword/src/theme.ts +++ b/libs/@guardian/react-crossword/src/theme.ts @@ -5,6 +5,7 @@ export const defaultTheme: Theme = { background: palette.neutral[7], text: palette.neutral[10], errorText: 'red', + provisionalText: palette.neutral[60], unsavedBackground: 'lightPink', foreground: palette.neutral[100], gutter: 1, @@ -15,7 +16,7 @@ export const defaultTheme: Theme = { buttonBackground: 'hotpink', buttonBackgroundHover: 'lightpink', border: 'orchid', - anagramHelperBackground: 'lightcoral', + anagramHelperBackground: 'floralwhite', clueMinWidth: 240, clueMaxWidth: 480, }; diff --git a/libs/@guardian/react-crossword/src/utils/biasedShuffle.ts b/libs/@guardian/react-crossword/src/utils/biasedShuffle.ts new file mode 100644 index 000000000..51545557f --- /dev/null +++ b/libs/@guardian/react-crossword/src/utils/biasedShuffle.ts @@ -0,0 +1,32 @@ +import { isUndefined } from '@guardian/libs'; + +// This uses the Fisher Yates shuffle algorithm, but with a bias factor. +// The Math.min(i + 1, n / 2) ensures that the random index j +// is biased toward indices farther from the current index i. +// This encourages items to move farther from their original positions. + +export const biasedShuffle = (array: T[]): T[] => { + const shuffled = [...array]; // Create a shallow copy of the array + const n = shuffled.length; + + for (let i = n - 1; i > 0; i--) { + // Use a bias factor but ensure the range includes all indices [0, i] + const biasFactor = Math.max(1, Math.min(i + 1, Math.floor(n / 2))); + const biasedJ = Math.max(0, i - Math.floor(Math.random() * biasFactor)); + + const unbiasedJ = Math.floor(Math.random() * (i + 1)); + + // Choose between full range and biased swap 50% chance to use bias + const finalJ = Math.random() > 0.5 ? unbiasedJ : biasedJ; + + if (!isUndefined(shuffled[i])) { + const temp = shuffled[i]; + if (!isUndefined(shuffled[finalJ]) && !isUndefined(temp)) { + shuffled[i] = shuffled[finalJ]; + shuffled[finalJ] = temp; + } + } + } + + return shuffled; +}; diff --git a/libs/@guardian/react-crossword/src/utils/getAnagramHelperProgressForGroup.ts b/libs/@guardian/react-crossword/src/utils/getCellsWithProgressForGroup.ts similarity index 57% rename from libs/@guardian/react-crossword/src/utils/getAnagramHelperProgressForGroup.ts rename to libs/@guardian/react-crossword/src/utils/getCellsWithProgressForGroup.ts index 4fdec34c6..74459a3f1 100644 --- a/libs/@guardian/react-crossword/src/utils/getAnagramHelperProgressForGroup.ts +++ b/libs/@guardian/react-crossword/src/utils/getCellsWithProgressForGroup.ts @@ -1,25 +1,25 @@ import { isUndefined } from '@guardian/libs'; import type { CAPIEntry } from '../@types/CAPI'; -import type { Coords, Entries, Progress } from '../@types/crossword'; +import type { Cell, Cells, Entries, Progress } from '../@types/crossword'; -export type AnagramHelperProgress = { +export type CellWithProgress = Cell & { progress: string; - coords: Coords; - isSaved: boolean; - number?: number; separator?: ',' | '-'; }; +export type CellsWithProgress = CellWithProgress[]; -export const getAnagramHelperProgressForGroup = ({ +export const getCellsWithProgressForGroup = ({ entry, entries, + cells, progress, }: { entry?: CAPIEntry; entries: Entries; + cells: Cells; progress: Progress; }) => { - const groupProgress: AnagramHelperProgress[] = []; + const groupProgress: CellsWithProgress = []; if (isUndefined(entry)) { return groupProgress; } @@ -27,33 +27,40 @@ export const getAnagramHelperProgressForGroup = ({ const entry = entries.get(entryId); if (!isUndefined(entry)) { groupProgress.push( - ...getAnagramHelperProgressForEntry({ entry, progress }), + ...getCellsWithProgressForEntry({ entry, cells, progress }), ); } } return groupProgress; }; -export const getAnagramHelperProgressForEntry = ({ +export const getCellsWithProgressForEntry = ({ entry, + cells, progress, }: { entry: CAPIEntry; + cells: Cells; progress: Progress; -}): AnagramHelperProgress[] => { - return Array.from({ length: entry.length }, (_, i) => { +}): CellsWithProgress => { + const cellsWithProgress: CellsWithProgress = []; + for (let i = 0; i < entry.length; i++) { const x = entry.direction === 'across' ? entry.position.x + i : entry.position.x; const y = entry.direction === 'across' ? entry.position.y : entry.position.y + i; - return { - coords: { x, y }, - number: i === 0 ? entry.number : undefined, - isSaved: true, - progress: progress.at(x)?.[y] ?? '', - separator: getSeparatorFromEntry(entry, i), - }; - }); + + const cell = cells.getByCoords({ x, y }); + if (cell) { + cellsWithProgress.push({ + ...cell, + progress: progress.at(x)?.[y] ?? '', + separator: getSeparatorFromEntry(entry, i), + }); + } + } + + return cellsWithProgress; }; const getSeparatorFromEntry = ( diff --git a/libs/@guardian/react-crossword/stories/formats/grouped-clues.progress.ts b/libs/@guardian/react-crossword/stories/formats/grouped-clues.progress.ts index d6da49007..b160f8970 100644 --- a/libs/@guardian/react-crossword/stories/formats/grouped-clues.progress.ts +++ b/libs/@guardian/react-crossword/stories/formats/grouped-clues.progress.ts @@ -15,3 +15,21 @@ export const progress = [ ['', '', '', '', '', '', '', '', '', '', '', '', '', '', ''], ['', '', '', '', '', '', '', '', '', '', '', '', '', '', ''], ]; + +export const progress12Across = [ + ['', '', '', '', '', 'F', '', '', '', '', '', '', '', '', ''], + ['', '', '', '', '', 'O', '', '', '', '', '', '', '', '', ''], + ['', '', '', '', '', 'O', '', '', '', '', '', '', '', '', ''], + ['', '', '', '', '', '', '', '', '', '', '', '', '', '', ''], + ['', '', '', '', '', '', '', '', '', '', '', '', '', '', ''], + ['', '', '', '', '', '', '', '', '', '', '', '', '', '', ''], + ['', '', '', '', '', '', '', '', '', '', '', '', '', '', ''], + ['', '', '', '', '', '', '', '', '', '', '', '', '', '', ''], + ['', '', '', '', '', '', '', '', '', '', '', '', '', '', ''], + ['', '', '', '', '', '', '', '', '', '', '', '', '', '', ''], + ['', '', '', '', '', '', '', '', '', '', '', '', '', '', ''], + ['', '', '', '', '', '', '', '', '', '', '', '', '', '', ''], + ['', '', '', '', '', '', '', '', '', '', '', '', '', '', ''], + ['', '', '', '', '', '', '', '', '', '', '', '', '', '', ''], + ['', '', '', '', '', '', '', '', '', '', '', '', '', '', ''], +];