@scrabble-solver/scrabble-solver 2.18.1 → 2.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +14 -14
- package/.next/cache/.previewinfo +1 -1
- package/.next/cache/.rscinfo +1 -1
- package/.next/cache/.tsbuildinfo +1 -1
- package/.next/cache/webpack/client-production/0.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack.old +0 -0
- package/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/.next/cache/webpack/edge-server-production/index.pack.old +0 -0
- package/.next/cache/webpack/server-production/0.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack.old +0 -0
- package/.next/diagnostics/framework.json +1 -1
- package/.next/dynamic-css-manifest.json +2 -2
- package/.next/prerender-manifest.json +4 -4
- package/.next/react-loadable-manifest.json +37 -37
- package/.next/routes-manifest.json +1 -1
- package/.next/server/chunks/309.js +1 -1
- package/.next/server/chunks/344.js +1 -1
- package/.next/server/chunks/371.js +1 -1
- package/.next/server/chunks/392.js +1 -1
- package/.next/server/chunks/465.js +1 -1
- package/.next/server/chunks/546.js +1 -1
- package/.next/server/chunks/641.js +1 -1
- package/.next/server/chunks/67.js +1 -0
- package/.next/server/chunks/712.js +1 -1
- package/.next/server/chunks/846.js +1 -1
- package/.next/server/chunks/886.js +1 -1
- package/.next/server/chunks/942.js +1 -0
- package/.next/server/chunks/953.js +1 -1
- package/.next/server/chunks/961.js +1 -1
- package/.next/server/dynamic-css-manifest.js +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/404.html +2 -2
- package/.next/server/pages/500.html +2 -2
- package/.next/server/pages/_app.js +1 -1
- package/.next/server/pages/_app.js.nft.json +1 -1
- package/.next/server/pages/_document.js +2 -2
- package/.next/server/pages/_document.js.nft.json +1 -1
- package/.next/server/pages/_error.js +1 -1
- package/.next/server/pages/_error.js.nft.json +1 -1
- package/.next/server/pages/api/dictionary/[locale]/[word].js +1 -1
- package/.next/server/pages/api/dictionary/[locale]/[word].js.nft.json +1 -1
- package/.next/server/pages/api/dictionary/[locale].js +1 -1
- package/.next/server/pages/api/dictionary/[locale].js.nft.json +1 -1
- package/.next/server/pages/api/solve.js +1 -1
- package/.next/server/pages/api/solve.js.nft.json +1 -1
- package/.next/server/pages/api/verify.js +1 -1
- package/.next/server/pages/api/verify.js.nft.json +1 -1
- package/.next/server/pages/index.html +2 -2
- package/.next/server/pages/index.js +4 -4
- package/.next/server/pages/index.js.nft.json +1 -1
- package/.next/server/pages/index.json +1 -1
- package/.next/server/pages/not-found.html +2 -2
- package/.next/server/pages/not-found.js.nft.json +1 -1
- package/.next/server/pages-manifest.json +1 -1
- package/.next/static/chunks/100.305bdf9c75b1758f.js +1 -0
- package/.next/static/chunks/256.66ff9fb6d7db9ec7.js +1 -0
- package/.next/static/chunks/{307.67f52f13e3d60d4e.js → 307.89e6100f58443679.js} +1 -1
- package/.next/static/chunks/{363.01eed3233f92b6a4.js → 363.cec65b1f9668a75e.js} +1 -1
- package/.next/static/chunks/{422.1559ae6756912ca5.js → 422.3e55b4c94e328701.js} +1 -1
- package/.next/static/chunks/{663.d2c9ca36b26695b8.js → 663.bd2c45413fbca518.js} +1 -1
- package/.next/static/chunks/693.677c88e0eb1340b5.js +1 -0
- package/.next/static/chunks/{820.97b82f751ca49579.js → 820.823e9e885ebb66af.js} +1 -1
- package/.next/static/chunks/827.c81b92b383a748c9.js +1 -0
- package/.next/static/chunks/{865.a9b563a45848a83c.js → 865.59baba8296c9b116.js} +1 -1
- package/.next/static/chunks/912.077603e4f0bc6cca.js +1 -0
- package/.next/static/chunks/{939.19b33a3d3c351d73.js → 939.2ad86b50405c7897.js} +1 -1
- package/.next/static/chunks/980.7ce17464fc56648a.js +1 -0
- package/.next/static/chunks/{main-b92e08f3fc96dd8d.js → main-7e52da35a4beea0b.js} +1 -1
- package/.next/static/chunks/pages/_app-f9ec81278ffa7b1a.js +1 -0
- package/.next/static/chunks/pages/index-b5bf018100015277.js +1 -0
- package/.next/static/chunks/webpack-9f5c9d275348829b.js +1 -0
- package/.next/static/css/65bcc9440e067113.css +1 -0
- package/.next/static/css/{4dbcacc1122ac6b9.css → 92f85ce358a0a468.css} +1 -1
- package/.next/static/css/a691348b788f8bc0.css +1 -0
- package/.next/static/css/cd4d7f2458487399.css +1 -0
- package/.next/static/{5KOWzYqfY1Z2rjrzptr8D → nvGZh1gPuX98L07YmxlYD}/_buildManifest.js +1 -1
- package/.next/trace +21 -21
- package/.next/trace-build +1 -1
- package/AGENTS.md +9 -0
- package/CLAUDE.md +1 -0
- package/package.json +14 -12
- package/src/components/Board/Board.tsx +3 -1
- package/src/components/Board/BoardPure.tsx +26 -20
- package/src/components/Board/components/Cell/Cell.tsx +4 -4
- package/src/components/Board/lib.test.ts +106 -0
- package/src/components/Board/lib.ts +34 -3
- package/src/components/Board/selectors.ts +17 -5
- package/src/components/Button/Button.module.scss +1 -1
- package/src/components/Modal/Modal.module.scss +0 -4
- package/src/components/Results/Header.tsx +30 -5
- package/src/components/Results/Result.tsx +45 -55
- package/src/components/Results/Results.module.scss +0 -126
- package/src/components/Results/Results.module.scss.d.ts +0 -10
- package/src/components/Results/Results.tsx +25 -5
- package/src/components/Solver/Solver.module.scss +4 -4
- package/src/components/Solver/Solver.tsx +5 -3
- package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.module.scss +5 -1
- package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.module.scss.d.ts +1 -0
- package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.tsx +5 -2
- package/src/components/Table/Cell.tsx +42 -0
- package/src/components/Table/Header.tsx +11 -0
- package/src/components/{Results → Table}/HeaderButton.tsx +22 -14
- package/src/components/Table/Row.tsx +59 -0
- package/src/components/{ResultsInput/ResultsInput.tsx → Table/Search.tsx} +9 -11
- package/src/components/Table/Table.module.scss +147 -0
- package/src/components/Table/Table.module.scss.d.ts +19 -0
- package/src/components/Table/index.ts +5 -0
- package/src/components/Tile/Tile.module.scss +2 -1
- package/src/hooks/useColumns.ts +7 -5
- package/src/hooks/useIsCompactLayout.ts +14 -0
- package/src/i18n/languages/english.json +6 -1
- package/src/i18n/languages/french.json +6 -1
- package/src/i18n/languages/german.json +6 -1
- package/src/i18n/languages/persian.json +6 -1
- package/src/i18n/languages/polish.json +6 -1
- package/src/i18n/languages/romanian.json +6 -1
- package/src/i18n/languages/spanish.json +6 -1
- package/src/i18n/languages/turkish.json +6 -1
- package/src/icons/QuestionSquare.svg +5 -0
- package/src/lib/createCoordinatesComparator.ts +23 -0
- package/src/lib/createKeyComparator.ts +1 -4
- package/src/lib/createSortComparator.ts +22 -0
- package/src/lib/getCoordinates.test.ts +21 -0
- package/src/lib/getCoordinates.ts +16 -0
- package/src/lib/getNextSort.ts +12 -0
- package/src/modals/RemainingTilesModal/RemainingTilesModal.tsx +3 -1
- package/src/modals/RemainingTilesModal/components/Character/Character.tsx +8 -9
- package/src/modals/ResultsModal/ResultsModal.tsx +14 -10
- package/src/modals/WordsModal/WordsModal.module.scss +16 -27
- package/src/modals/WordsModal/WordsModal.module.scss.d.ts +3 -6
- package/src/modals/WordsModal/WordsModal.tsx +56 -42
- package/src/modals/WordsModal/components/WordsTable/WordRow.tsx +92 -0
- package/src/modals/WordsModal/components/WordsTable/WordsTable.module.scss +69 -0
- package/src/modals/WordsModal/components/WordsTable/WordsTable.module.scss.d.ts +16 -0
- package/src/modals/WordsModal/components/WordsTable/WordsTable.tsx +137 -0
- package/src/modals/WordsModal/components/WordsTable/index.ts +1 -0
- package/src/modals/WordsModal/components/index.ts +1 -0
- package/src/pages/api/verify.ts +7 -6
- package/src/pages/index.tsx +28 -3
- package/src/parameters/index.ts +2 -0
- package/src/sdk/verify.ts +3 -2
- package/src/solver-worker/client.test.ts +10 -3
- package/src/solver-worker/index.worker.ts +7 -7
- package/src/solver-worker/messages.ts +3 -3
- package/src/state/hoveredWord/index.ts +4 -0
- package/src/state/hoveredWord/initialState.ts +5 -0
- package/src/state/hoveredWord/selectors.ts +5 -0
- package/src/state/hoveredWord/slice.ts +18 -0
- package/src/state/hoveredWord/types.ts +5 -0
- package/src/state/index.ts +1 -0
- package/src/state/localStorage.ts +13 -2
- package/src/state/results/lib.ts +9 -26
- package/src/state/results/selectors.ts +9 -1
- package/src/state/results/slice.ts +4 -16
- package/src/state/sagas.ts +26 -1
- package/src/state/selectors.ts +18 -1
- package/src/state/store.ts +2 -0
- package/src/state/types.ts +2 -0
- package/src/state/verify/initialState.ts +7 -0
- package/src/state/verify/lib.test.ts +55 -0
- package/src/state/verify/lib.ts +49 -0
- package/src/state/verify/selectors.ts +37 -0
- package/src/state/verify/slice.ts +13 -2
- package/src/state/verify/types.ts +6 -2
- package/src/styles/_tokens.scss +2 -0
- package/src/styles/mixins.scss +5 -0
- package/src/styles/variables.module.scss +1 -0
- package/src/styles/variables.module.scss.d.ts +1 -0
- package/src/styles/variables.scss +1 -1
- package/src/types/index.ts +24 -11
- package/tsconfig.tsbuildinfo +1 -1
- package/.next/server/chunks/316.js +0 -1
- package/.next/server/chunks/916.js +0 -1
- package/.next/static/chunks/106.e414e88694add27f.js +0 -1
- package/.next/static/chunks/127.125d896a19061d71.js +0 -1
- package/.next/static/chunks/18.8b91e137d239e15c.js +0 -1
- package/.next/static/chunks/583.7a46807be14c377e.js +0 -1
- package/.next/static/chunks/711.ba7b8f900dfd40d0.js +0 -1
- package/.next/static/chunks/912.ff8037ba9d4db5c8.js +0 -1
- package/.next/static/chunks/pages/_app-2e607dcd4135f340.js +0 -1
- package/.next/static/chunks/pages/index-e0087247c995328d.js +0 -1
- package/.next/static/chunks/webpack-cfb34d175e081981.js +0 -1
- package/.next/static/css/b74cd7de82dabcdd.css +0 -1
- package/.next/static/css/c403ce739a9c0d83.css +0 -1
- package/.next/static/css/fbfdce70731d0d2b.css +0 -1
- package/src/components/Results/Cell.tsx +0 -32
- package/src/components/ResultsInput/ResultsInput.module.scss +0 -12
- package/src/components/ResultsInput/ResultsInput.module.scss.d.ts +0 -7
- package/src/components/ResultsInput/index.ts +0 -1
- /package/.next/static/{5KOWzYqfY1Z2rjrzptr8D → nvGZh1gPuX98L07YmxlYD}/_ssgManifest.js +0 -0
package/src/pages/index.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import { execSync } from 'child_process';
|
|
|
6
6
|
import fs from 'fs';
|
|
7
7
|
import dynamic from 'next/dynamic';
|
|
8
8
|
import path from 'path';
|
|
9
|
-
import { type FunctionComponent, useCallback, useEffect, useState } from 'react';
|
|
9
|
+
import { type FunctionComponent, useCallback, useEffect, useRef, useState } from 'react';
|
|
10
10
|
import { useDispatch } from 'react-redux';
|
|
11
11
|
|
|
12
12
|
import { Logo } from '@/components/Logo';
|
|
@@ -14,13 +14,23 @@ import { NavButtons } from '@/components/NavButtons';
|
|
|
14
14
|
import { Solver } from '@/components/Solver';
|
|
15
15
|
import { useDirection } from '@/hooks/useDirection';
|
|
16
16
|
import { useEffectOnce } from '@/hooks/useEffectOnce';
|
|
17
|
+
import { useIsCompactLayout } from '@/hooks/useIsCompactLayout';
|
|
17
18
|
import { useLanguage } from '@/hooks/useLanguage';
|
|
18
19
|
import { useLocalStorage } from '@/hooks/useLocalStorage';
|
|
19
20
|
import { LOCALE_FEATURES } from '@/i18n/constants';
|
|
20
21
|
import { schedulePreloadModals } from '@/modals/preload';
|
|
21
22
|
import { CONFIG_PENDING_CLASS, GITHUB_PROJECT_URL, NPM_PACKAGE_URL, SITE_DESCRIPTION, SITE_URL } from '@/parameters';
|
|
22
23
|
import { registerServiceWorker } from '@/serviceWorkerManager';
|
|
23
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
hoveredWordSlice,
|
|
26
|
+
initialize,
|
|
27
|
+
reset,
|
|
28
|
+
resultsSlice,
|
|
29
|
+
selectConfig,
|
|
30
|
+
selectIsHydrated,
|
|
31
|
+
selectLocale,
|
|
32
|
+
useTypedSelector,
|
|
33
|
+
} from '@/state';
|
|
24
34
|
|
|
25
35
|
import styles from './index.module.scss';
|
|
26
36
|
|
|
@@ -84,6 +94,8 @@ const Index: FunctionComponent<Props> = ({ version }) => {
|
|
|
84
94
|
const config = useTypedSelector(selectConfig);
|
|
85
95
|
const locale = useTypedSelector(selectLocale);
|
|
86
96
|
const isHydrated = useTypedSelector(selectIsHydrated);
|
|
97
|
+
const isCompactLayout = useIsCompactLayout();
|
|
98
|
+
const previousIsCompactLayout = useRef(isCompactLayout);
|
|
87
99
|
const [modals, setModals] = useState<Record<Modal, boolean>>({
|
|
88
100
|
dictionary: false,
|
|
89
101
|
keyMap: false,
|
|
@@ -131,6 +143,7 @@ const Index: FunctionComponent<Props> = ({ version }) => {
|
|
|
131
143
|
const handleCloseResults = useCallback(() => patchModals({ results: false }), [patchModals]);
|
|
132
144
|
const handleCloseSettings = useCallback(() => patchModals({ settings: false }), [patchModals]);
|
|
133
145
|
const handleCloseWords = useCallback(() => patchModals({ words: false }), [patchModals]);
|
|
146
|
+
const handlePreviewWord = useCallback(() => patchModals({ menu: false, words: false }), [patchModals]);
|
|
134
147
|
|
|
135
148
|
useDirection(LOCALE_FEATURES[locale].direction);
|
|
136
149
|
useLanguage(locale);
|
|
@@ -159,6 +172,16 @@ const Index: FunctionComponent<Props> = ({ version }) => {
|
|
|
159
172
|
}
|
|
160
173
|
}, [isHydrated]);
|
|
161
174
|
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
if (previousIsCompactLayout.current === isCompactLayout) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
previousIsCompactLayout.current = isCompactLayout;
|
|
181
|
+
dispatch(resultsSlice.actions.changeResultCandidate(null));
|
|
182
|
+
dispatch(hoveredWordSlice.actions.clear());
|
|
183
|
+
}, [dispatch, isCompactLayout]);
|
|
184
|
+
|
|
162
185
|
useEffectOnce(() => {
|
|
163
186
|
if (process.env.NODE_ENV === 'production') {
|
|
164
187
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
@@ -212,7 +235,9 @@ const Index: FunctionComponent<Props> = ({ version }) => {
|
|
|
212
235
|
|
|
213
236
|
{mountedModals.keyMap && <KeyMapModal isOpen={modals.keyMap} onClose={handleCloseKeyMap} />}
|
|
214
237
|
|
|
215
|
-
{mountedModals.words &&
|
|
238
|
+
{mountedModals.words && (
|
|
239
|
+
<WordsModal isOpen={modals.words} onClose={handleCloseWords} onPreview={handlePreviewWord} />
|
|
240
|
+
)}
|
|
216
241
|
|
|
217
242
|
{config.supportsRemainingTiles && mountedModals.remainingTiles && (
|
|
218
243
|
<RemainingTilesModal isOpen={modals.remainingTiles} onClose={handleCloseRemainingTiles} />
|
package/src/parameters/index.ts
CHANGED
|
@@ -8,6 +8,8 @@ export const BREAKPOINTS = {
|
|
|
8
8
|
xl: Number(variables.breakpointXl),
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
export const BREAKPOINT_HEIGHT_L = Number(variables.breakpointHeightL);
|
|
12
|
+
|
|
11
13
|
export const GITHUB_PROJECT_URL = 'https://github.com/kamilmielnik/scrabble-solver';
|
|
12
14
|
|
|
13
15
|
export const NPM_PACKAGE_URL = 'https://www.npmjs.com/package/scrabble-solver';
|
package/src/sdk/verify.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type BoardJson, type Game, type Locale } from '@scrabble-solver/types';
|
|
2
2
|
|
|
3
3
|
import { verifyLocally } from '@/solver-worker';
|
|
4
|
+
import { type VerifiedWord } from '@/types';
|
|
4
5
|
|
|
5
6
|
import { fetchJson } from './fetchJson';
|
|
6
7
|
|
|
@@ -11,8 +12,8 @@ interface Payload {
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
interface Response {
|
|
14
|
-
invalidWords:
|
|
15
|
-
validWords:
|
|
15
|
+
invalidWords: VerifiedWord[];
|
|
16
|
+
validWords: VerifiedWord[];
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export const verify = async ({ board, game, locale }: Payload): Promise<Response> => {
|
|
@@ -2,7 +2,7 @@ import { type ResultJson } from '@scrabble-solver/types';
|
|
|
2
2
|
import { afterAll, beforeEach, describe, expect, it } from 'bun:test';
|
|
3
3
|
|
|
4
4
|
import type * as clientModule from './client';
|
|
5
|
-
import { type SolverWorkerRequest, type SolverWorkerResponse } from './messages';
|
|
5
|
+
import { type SolverWorkerRequest, type SolverWorkerResponse, type VerifyResult } from './messages';
|
|
6
6
|
|
|
7
7
|
type Listener = (event: { data: SolverWorkerResponse }) => void;
|
|
8
8
|
|
|
@@ -102,9 +102,9 @@ describe('solver worker client', () => {
|
|
|
102
102
|
|
|
103
103
|
it('resolves a verify with the worker words', async () => {
|
|
104
104
|
const verify = verifyLocally(verifyPayload);
|
|
105
|
-
respond({ data:
|
|
105
|
+
respond({ data: createVerifyResult(), id: lastRequestId(), outcome: 'answered' });
|
|
106
106
|
|
|
107
|
-
expect(await verify).toEqual(
|
|
107
|
+
expect(await verify).toEqual(createVerifyResult());
|
|
108
108
|
});
|
|
109
109
|
|
|
110
110
|
it('does not wait for a prefetch to answer', () => {
|
|
@@ -149,3 +149,10 @@ function lastRequestId(): number {
|
|
|
149
149
|
function createResults(word: string): ResultJson[] {
|
|
150
150
|
return [{ blankIndices: [], id: 0, isHorizontal: true, points: 1, tiles: word.split(''), x: 0, y: 0 }];
|
|
151
151
|
}
|
|
152
|
+
|
|
153
|
+
function createVerifyResult(): VerifyResult {
|
|
154
|
+
return {
|
|
155
|
+
invalidWords: [{ direction: 'horizontal', isValid: false, word: 'zz', x: 0, y: 0 }],
|
|
156
|
+
validWords: [{ direction: 'vertical', isValid: true, word: 'ab', x: 3, y: 5 }],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -5,7 +5,7 @@ import { BLANK } from '@scrabble-solver/constants';
|
|
|
5
5
|
import { solve } from '@scrabble-solver/solver';
|
|
6
6
|
import { Board, type Locale, Tile } from '@scrabble-solver/types';
|
|
7
7
|
|
|
8
|
-
import { type SolveRequestPayload, type VerifyRequestPayload } from '@/types';
|
|
8
|
+
import { type SolveRequestPayload, type VerifiedWord, type VerifyRequestPayload } from '@/types';
|
|
9
9
|
|
|
10
10
|
import { revalidateDictionary } from './dictionaries';
|
|
11
11
|
import { getGaddag } from './getGaddag';
|
|
@@ -64,15 +64,15 @@ async function handleVerify(
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
const board = Board.fromJson(boardJson);
|
|
67
|
-
const words = board.getWords().sort((a, b) => a.localeCompare(b, locale));
|
|
68
|
-
const invalidWords:
|
|
69
|
-
const validWords:
|
|
67
|
+
const words = board.getWords().sort((a, b) => a.word.localeCompare(b.word, locale));
|
|
68
|
+
const invalidWords: VerifiedWord[] = [];
|
|
69
|
+
const validWords: VerifiedWord[] = [];
|
|
70
70
|
|
|
71
71
|
for (const word of words) {
|
|
72
|
-
if (gaddag.has(word)) {
|
|
73
|
-
validWords.push(word);
|
|
72
|
+
if (gaddag.has(word.word)) {
|
|
73
|
+
validWords.push({ ...word, isValid: true });
|
|
74
74
|
} else {
|
|
75
|
-
invalidWords.push(word);
|
|
75
|
+
invalidWords.push({ ...word, isValid: false });
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { type Locale, type ResultJson } from '@scrabble-solver/types';
|
|
2
2
|
|
|
3
|
-
import { type SolveRequestPayload, type VerifyRequestPayload } from '@/types';
|
|
3
|
+
import { type SolveRequestPayload, type VerifiedWord, type VerifyRequestPayload } from '@/types';
|
|
4
4
|
|
|
5
5
|
export interface VerifyResult {
|
|
6
|
-
invalidWords:
|
|
7
|
-
validWords:
|
|
6
|
+
invalidWords: VerifiedWord[];
|
|
7
|
+
validWords: VerifiedWord[];
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export type SolverWorkerRequest =
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
|
2
|
+
|
|
3
|
+
import { type VerifiedWord } from '@/types';
|
|
4
|
+
|
|
5
|
+
import { hoveredWordInitialState } from './initialState';
|
|
6
|
+
|
|
7
|
+
export const hoveredWordSlice = createSlice({
|
|
8
|
+
initialState: hoveredWordInitialState,
|
|
9
|
+
name: 'hoveredWord',
|
|
10
|
+
reducers: {
|
|
11
|
+
set: (state, action: PayloadAction<VerifiedWord>) => {
|
|
12
|
+
state.word = action.payload;
|
|
13
|
+
},
|
|
14
|
+
clear: (state) => {
|
|
15
|
+
state.word = null;
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
});
|
package/src/state/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Board, type BoardJson, isObject, type Locale } from '@scrabble-solver/types';
|
|
2
2
|
import store2 from 'store2';
|
|
3
3
|
|
|
4
|
-
import
|
|
4
|
+
import { englishTranslations } from '@/i18n/i18n';
|
|
5
|
+
import type { Rack, TranslationKey, Translations } from '@/types';
|
|
5
6
|
|
|
6
7
|
import type { SettingsState } from './settings/types';
|
|
7
8
|
|
|
@@ -80,7 +81,13 @@ export const localStorage = {
|
|
|
80
81
|
return undefined;
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
if (
|
|
84
|
+
if (
|
|
85
|
+
!isObject(stored) ||
|
|
86
|
+
stored.locale !== locale ||
|
|
87
|
+
stored.version !== version ||
|
|
88
|
+
!isObject(stored.translations) ||
|
|
89
|
+
!hasEveryTranslation(stored.translations)
|
|
90
|
+
) {
|
|
84
91
|
store.remove(TRANSLATIONS);
|
|
85
92
|
return undefined;
|
|
86
93
|
}
|
|
@@ -93,6 +100,10 @@ export const localStorage = {
|
|
|
93
100
|
},
|
|
94
101
|
};
|
|
95
102
|
|
|
103
|
+
function hasEveryTranslation(translations: Translations): boolean {
|
|
104
|
+
return (Object.keys(englishTranslations) as TranslationKey[]).every((key) => typeof translations[key] === 'string');
|
|
105
|
+
}
|
|
106
|
+
|
|
96
107
|
/**
|
|
97
108
|
* Introduced in 2.15.26 on 2026/04/27.
|
|
98
109
|
* Life expectancy: 1y.
|
package/src/state/results/lib.ts
CHANGED
|
@@ -1,25 +1,15 @@
|
|
|
1
1
|
import { type Result, type ShowCoordinates } from '@scrabble-solver/types';
|
|
2
2
|
|
|
3
|
+
import { createCoordinatesComparator } from '@/lib/createCoordinatesComparator';
|
|
3
4
|
import { createKeyComparator } from '@/lib/createKeyComparator';
|
|
4
5
|
import { createRegExp } from '@/lib/createRegExp';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
type CellFilter,
|
|
10
|
-
type Comparator,
|
|
11
|
-
type GroupedResults,
|
|
12
|
-
ResultColumnId,
|
|
13
|
-
type Sort,
|
|
14
|
-
SortDirection,
|
|
15
|
-
} from '@/types';
|
|
6
|
+
import { createSortComparator } from '@/lib/createSortComparator';
|
|
7
|
+
import { getCoordinates as getPointCoordinates } from '@/lib/getCoordinates';
|
|
8
|
+
import { type CellFilter, type ComparatorFactory, type GroupedResults, ResultColumnId, type Sort } from '@/types';
|
|
16
9
|
|
|
17
10
|
export const getCoordinates = (result: Result, showCoordinates: ShowCoordinates): string => {
|
|
18
11
|
const firstCell = result.cells[0];
|
|
19
|
-
|
|
20
|
-
const y = getCoordinate(firstCell.y, showCoordinates === 'original' ? 'number' : 'letter');
|
|
21
|
-
|
|
22
|
-
return result.isHorizontal() ? `${y}${x}` : `${x}${y}`;
|
|
12
|
+
return getPointCoordinates({ isHorizontal: result.isHorizontal(), x: firstCell.x, y: firstCell.y }, showCoordinates);
|
|
23
13
|
};
|
|
24
14
|
|
|
25
15
|
export const groupResults = (
|
|
@@ -69,14 +59,11 @@ export const resultMatchesCellFilter = (result: Result, cellFilters: CellFilter[
|
|
|
69
59
|
return matchesExcludeFilters && matchesIncludeFilters;
|
|
70
60
|
};
|
|
71
61
|
|
|
72
|
-
const comparators: Record<ResultColumnId,
|
|
62
|
+
const comparators: Record<ResultColumnId, ComparatorFactory<Result>> = {
|
|
73
63
|
[ResultColumnId.BlanksCount]: (locale: string) => createKeyComparator('blanksCount', locale),
|
|
74
64
|
[ResultColumnId.ConsonantsCount]: (locale: string) => createKeyComparator('consonantsCount', locale),
|
|
75
|
-
[ResultColumnId.Coordinates]: (locale: string, showCoordinates: ShowCoordinates) =>
|
|
76
|
-
|
|
77
|
-
const aValue = getCoordinates(a, showCoordinates);
|
|
78
|
-
const bValue = getCoordinates(b, showCoordinates);
|
|
79
|
-
return stringComparator(aValue, bValue);
|
|
65
|
+
[ResultColumnId.Coordinates]: (locale: string, showCoordinates: ShowCoordinates) => {
|
|
66
|
+
return createCoordinatesComparator({ getItemCoordinates: getCoordinates, locale, showCoordinates });
|
|
80
67
|
},
|
|
81
68
|
[ResultColumnId.Points]: (locale: string) => createKeyComparator('points', locale),
|
|
82
69
|
[ResultColumnId.TilesCount]: (locale: string) => createKeyComparator('tilesCount', locale),
|
|
@@ -95,9 +82,5 @@ export const sortResults = (
|
|
|
95
82
|
return undefined;
|
|
96
83
|
}
|
|
97
84
|
|
|
98
|
-
|
|
99
|
-
const comparator = createComparator(locale, showCoordinates);
|
|
100
|
-
const finalComparator = sort.direction === SortDirection.Descending ? reverseComparator(comparator) : comparator;
|
|
101
|
-
const sortedResults = [...results].sort(finalComparator);
|
|
102
|
-
return sortedResults;
|
|
85
|
+
return [...results].sort(createSortComparator({ comparators, locale, showCoordinates, sort }));
|
|
103
86
|
};
|
|
@@ -16,7 +16,7 @@ export const selectResultCandidate = createSelector([selectResults], (results) =
|
|
|
16
16
|
|
|
17
17
|
export const selectResultsQuery = createSelector([selectResults], (results) => results.query);
|
|
18
18
|
|
|
19
|
-
const selectResultsResults = createSelector([selectResults], (results) => results.results);
|
|
19
|
+
export const selectResultsResults = createSelector([selectResults], (results) => results.results);
|
|
20
20
|
|
|
21
21
|
export const selectResultsSort = createSelector([selectResults], (results) => results.sort);
|
|
22
22
|
|
|
@@ -44,6 +44,14 @@ export const selectProcessedResults = createSelector([selectGroupedSortedResults
|
|
|
44
44
|
return results ? [...results.matching, ...results.other] : undefined;
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
+
export const selectResultCandidateIndex = createSelector(
|
|
48
|
+
[selectProcessedResults, selectResultCandidate],
|
|
49
|
+
(results, candidate) => {
|
|
50
|
+
const index = results && candidate ? results.findIndex((result) => result.id === candidate.id) : -1;
|
|
51
|
+
return index === -1 ? undefined : index;
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
|
|
47
55
|
export const selectIsResultMatching = createSelector(
|
|
48
56
|
[selectProcessedResults, selectResultsQuery, selectCellFilters, selectResultIndex],
|
|
49
57
|
(results, query, cellFilters, index) => {
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
|
2
2
|
import { type Result } from '@scrabble-solver/types';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { getNextSort } from '@/lib/getNextSort';
|
|
5
|
+
import { type ResultColumnId } from '@/types';
|
|
5
6
|
|
|
6
7
|
import { resultsInitialState } from './initialState';
|
|
7
8
|
|
|
8
|
-
const toggleDirection = (direction: SortDirection): SortDirection => {
|
|
9
|
-
return direction === SortDirection.Ascending ? SortDirection.Descending : SortDirection.Ascending;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
9
|
export const resultsSlice = createSlice({
|
|
13
10
|
initialState: resultsInitialState,
|
|
14
11
|
name: 'results',
|
|
@@ -24,7 +21,7 @@ export const resultsSlice = createSlice({
|
|
|
24
21
|
|
|
25
22
|
changeResultCandidate: (state, action: PayloadAction<Result | null>) => {
|
|
26
23
|
const candidate = action.payload;
|
|
27
|
-
return { ...state, candidate };
|
|
24
|
+
return candidate === state.candidate ? state : { ...state, candidate };
|
|
28
25
|
},
|
|
29
26
|
|
|
30
27
|
changeResults: (state, action: PayloadAction<Result[]>) => {
|
|
@@ -38,16 +35,7 @@ export const resultsSlice = createSlice({
|
|
|
38
35
|
},
|
|
39
36
|
|
|
40
37
|
sort: (state, action: PayloadAction<ResultColumnId>) => {
|
|
41
|
-
|
|
42
|
-
const { column, direction } = state.sort;
|
|
43
|
-
|
|
44
|
-
return {
|
|
45
|
-
...state,
|
|
46
|
-
sort: {
|
|
47
|
-
column: columndId,
|
|
48
|
-
direction: column === columndId ? toggleDirection(direction) : direction,
|
|
49
|
-
},
|
|
50
|
-
};
|
|
38
|
+
return { ...state, sort: getNextSort(state.sort, action.payload) };
|
|
51
39
|
},
|
|
52
40
|
|
|
53
41
|
reset: () => resultsInitialState,
|
package/src/state/sagas.ts
CHANGED
|
@@ -14,12 +14,14 @@ import { memoize } from '@/lib/memoize';
|
|
|
14
14
|
import { waitForFirstIntent, waitForIdleOrFirstIntent } from '@/lib/waitForIdleOrFirstIntent';
|
|
15
15
|
import { findWordDefinitions, solve, verify, visit } from '@/sdk';
|
|
16
16
|
import { prefetchDictionary } from '@/solver-worker';
|
|
17
|
+
import { type VerifiedWord } from '@/types';
|
|
17
18
|
|
|
18
19
|
import { initialize, reset } from './actions';
|
|
19
20
|
import { appSlice, selectVersion } from './app';
|
|
20
21
|
import { boardSlice, selectBoard } from './board';
|
|
21
22
|
import { cellFiltersSlice, selectCellFilter } from './cellFilters';
|
|
22
23
|
import { dictionarySlice, selectDictionary } from './dictionary';
|
|
24
|
+
import { hoveredWordSlice } from './hoveredWord';
|
|
23
25
|
import { i18nSlice, selectLoadedTranslations } from './i18n';
|
|
24
26
|
import { localStorage } from './localStorage';
|
|
25
27
|
import { rackSlice, selectCharacters, selectRack } from './rack';
|
|
@@ -51,6 +53,7 @@ export function* rootSaga(): AnyGenerator {
|
|
|
51
53
|
yield takeEvery([rackSlice.actions.changeCharacter.type, rackSlice.actions.changeCharacters.type], onRackValueChange);
|
|
52
54
|
yield takeEvery(resultsSlice.actions.applyResult.type, onApplyResult);
|
|
53
55
|
yield takeLatest(resultsSlice.actions.changeResultCandidate.type, onResultCandidateChange);
|
|
56
|
+
yield takeLatest([hoveredWordSlice.actions.set.type, hoveredWordSlice.actions.clear.type], onHoveredWordChange);
|
|
54
57
|
yield takeEvery(settingsSlice.actions.changeGame.type, onGameChange);
|
|
55
58
|
yield takeEvery(settingsSlice.actions.changeLocale.type, onLocaleChange);
|
|
56
59
|
yield takeLatest(dictionarySlice.actions.submit.type, onDictionarySubmit);
|
|
@@ -68,6 +71,7 @@ function* onCellValueChange({ payload }: PayloadAction<{ value: string; x: numbe
|
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
yield put(resultsSlice.actions.changeResultCandidate(null));
|
|
74
|
+
yield put(hoveredWordSlice.actions.clear());
|
|
71
75
|
yield put(verifySlice.actions.submit());
|
|
72
76
|
}
|
|
73
77
|
|
|
@@ -99,6 +103,7 @@ function* onGameChange(): AnyGenerator {
|
|
|
99
103
|
}
|
|
100
104
|
|
|
101
105
|
yield put(resultsSlice.actions.reset());
|
|
106
|
+
yield put(hoveredWordSlice.actions.clear());
|
|
102
107
|
yield* resetRack();
|
|
103
108
|
yield put(verifySlice.actions.submit());
|
|
104
109
|
}
|
|
@@ -250,6 +255,7 @@ function* onReset(): AnyGenerator {
|
|
|
250
255
|
yield put(boardSlice.actions.init(Board.create(config.boardWidth, config.boardHeight)));
|
|
251
256
|
yield put(cellFiltersSlice.actions.reset());
|
|
252
257
|
yield put(dictionarySlice.actions.reset());
|
|
258
|
+
yield put(hoveredWordSlice.actions.clear());
|
|
253
259
|
yield put(rackSlice.actions.reset());
|
|
254
260
|
yield put(resultsSlice.actions.reset());
|
|
255
261
|
yield put(solveSlice.actions.reset());
|
|
@@ -280,6 +286,7 @@ function* onLocaleChange({ payload: locale }: PayloadAction<Locale>): AnyGenerat
|
|
|
280
286
|
}
|
|
281
287
|
|
|
282
288
|
yield put(dictionarySlice.actions.reset());
|
|
289
|
+
yield put(hoveredWordSlice.actions.clear());
|
|
283
290
|
yield put(resultsSlice.actions.changeResultCandidate(null));
|
|
284
291
|
yield put(verifySlice.actions.submit());
|
|
285
292
|
}
|
|
@@ -289,8 +296,26 @@ function* onResultCandidateChange({ payload: result }: PayloadAction<Result | nu
|
|
|
289
296
|
return;
|
|
290
297
|
}
|
|
291
298
|
|
|
299
|
+
yield put(hoveredWordSlice.actions.clear());
|
|
300
|
+
yield* searchDictionary(result.words);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function* onHoveredWordChange({ payload: word }: PayloadAction<VerifiedWord | undefined>): AnyGenerator {
|
|
304
|
+
if (!word) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
yield put(resultsSlice.actions.changeResultCandidate(null));
|
|
309
|
+
|
|
310
|
+
const board: Board = yield select(selectBoard);
|
|
311
|
+
const collidingWords = board.getCollidingWords(word);
|
|
312
|
+
|
|
313
|
+
yield* searchDictionary([word.word, ...collidingWords.map((collidingWord) => collidingWord.word)]);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function* searchDictionary(words: string[]): AnyGenerator {
|
|
292
317
|
const locale: Locale = yield select(selectLocale);
|
|
293
|
-
const uniqueWords = Array.from(new Set(
|
|
318
|
+
const uniqueWords = Array.from(new Set(words));
|
|
294
319
|
const input = uniqueWords.join(LOCALE_FEATURES[locale].separator);
|
|
295
320
|
|
|
296
321
|
if (!memoizedFindWordDefinitions.hasCache(locale, input)) {
|
package/src/state/selectors.ts
CHANGED
|
@@ -3,10 +3,13 @@ import { createSelector } from '@reduxjs/toolkit';
|
|
|
3
3
|
import { unorderedArraysEqual } from '@/lib/unorderedArraysEqual';
|
|
4
4
|
|
|
5
5
|
import { selectBoard } from './board';
|
|
6
|
+
import { selectHoveredWord } from './hoveredWord';
|
|
6
7
|
import { getRemainingTiles } from './lib';
|
|
7
8
|
import { selectCharacters } from './rack';
|
|
9
|
+
import { selectResultsResults } from './results';
|
|
8
10
|
import { selectConfig, selectLocale } from './settings';
|
|
9
|
-
import { selectLastSolvedParameters } from './solve';
|
|
11
|
+
import { selectLastSolvedParameters, selectSolveError } from './solve';
|
|
12
|
+
import { selectLastVerifiedBoard } from './verify';
|
|
10
13
|
|
|
11
14
|
const selectHasBoardChanged = createSelector(
|
|
12
15
|
[selectLastSolvedParameters, selectBoard],
|
|
@@ -29,6 +32,20 @@ export const selectAreResultsOutdated = createSelector(
|
|
|
29
32
|
},
|
|
30
33
|
);
|
|
31
34
|
|
|
35
|
+
export const selectUpToDateResults = createSelector(
|
|
36
|
+
[selectResultsResults, selectAreResultsOutdated, selectSolveError],
|
|
37
|
+
(results, areResultsOutdated, solveError) => {
|
|
38
|
+
return results && !areResultsOutdated && !solveError ? results : null;
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
export const selectUpToDateHoveredWord = createSelector(
|
|
43
|
+
[selectHoveredWord, selectLastVerifiedBoard, selectBoard],
|
|
44
|
+
(hoveredWord, lastVerifiedBoard, board) => {
|
|
45
|
+
return hoveredWord && lastVerifiedBoard.equals(board) ? hoveredWord : null;
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
|
|
32
49
|
export const selectRemainingTiles = createSelector(
|
|
33
50
|
[selectConfig, selectBoard, selectCharacters, selectLocale],
|
|
34
51
|
getRemainingTiles,
|
package/src/state/store.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { boardSlice } from './board';
|
|
|
6
6
|
import { cellFiltersSlice } from './cellFilters';
|
|
7
7
|
import { dictionarySlice } from './dictionary';
|
|
8
8
|
import { hoveredTileSlice } from './hoveredTile';
|
|
9
|
+
import { hoveredWordSlice } from './hoveredWord';
|
|
9
10
|
import { i18nSlice } from './i18n';
|
|
10
11
|
import { rackSlice } from './rack';
|
|
11
12
|
import { resultsSlice } from './results';
|
|
@@ -23,6 +24,7 @@ export const store = configureStore({
|
|
|
23
24
|
cellFilters: cellFiltersSlice.reducer,
|
|
24
25
|
dictionary: dictionarySlice.reducer,
|
|
25
26
|
hoveredTile: hoveredTileSlice.reducer,
|
|
27
|
+
hoveredWord: hoveredWordSlice.reducer,
|
|
26
28
|
i18n: i18nSlice.reducer,
|
|
27
29
|
rack: rackSlice.reducer,
|
|
28
30
|
results: resultsSlice.reducer,
|
package/src/state/types.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { BoardState } from './board';
|
|
|
3
3
|
import type { CellFiltersState } from './cellFilters';
|
|
4
4
|
import type { DictionaryState } from './dictionary';
|
|
5
5
|
import type { HoveredTileState } from './hoveredTile';
|
|
6
|
+
import type { HoveredWordState } from './hoveredWord';
|
|
6
7
|
import type { I18nState } from './i18n';
|
|
7
8
|
import type { RackState } from './rack';
|
|
8
9
|
import type { ResultsState } from './results';
|
|
@@ -16,6 +17,7 @@ export type RootState = {
|
|
|
16
17
|
cellFilters: CellFiltersState;
|
|
17
18
|
dictionary: DictionaryState;
|
|
18
19
|
hoveredTile: HoveredTileState;
|
|
20
|
+
hoveredWord: HoveredWordState;
|
|
19
21
|
i18n: I18nState;
|
|
20
22
|
rack: RackState;
|
|
21
23
|
results: ResultsState;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { SortDirection, WordColumnId } from '@/types';
|
|
2
|
+
|
|
1
3
|
import { boardInitialState } from '../board';
|
|
2
4
|
|
|
3
5
|
import type { VerifyState } from './types';
|
|
@@ -8,5 +10,10 @@ export const verifyInitialState: VerifyState = {
|
|
|
8
10
|
board: boardInitialState,
|
|
9
11
|
},
|
|
10
12
|
invalidWords: [],
|
|
13
|
+
query: '',
|
|
14
|
+
sort: {
|
|
15
|
+
column: WordColumnId.Word,
|
|
16
|
+
direction: SortDirection.Ascending,
|
|
17
|
+
},
|
|
11
18
|
validWords: [],
|
|
12
19
|
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { SortDirection, type VerifiedWord, WordColumnId } from '../../types';
|
|
2
|
+
|
|
3
|
+
import { getWordCoordinates, sortWords } from './lib';
|
|
4
|
+
|
|
5
|
+
describe('getWordCoordinates', () => {
|
|
6
|
+
it('locates a horizontal word by its start cell', () => {
|
|
7
|
+
expect(getWordCoordinates(createWord({ x: 3, y: 3 }), 'original')).toBe('4D');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('locates a vertical word by its start cell', () => {
|
|
11
|
+
expect(getWordCoordinates(createWord({ direction: 'vertical', x: 3, y: 3 }), 'original')).toBe('D4');
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('sortWords', () => {
|
|
16
|
+
const cat = createWord({ word: 'cat', x: 3, y: 3 });
|
|
17
|
+
const zvq = createWord({ isValid: false, word: 'zvq', x: 3, y: 5 });
|
|
18
|
+
|
|
19
|
+
it('sorts by word', () => {
|
|
20
|
+
const sort = { column: WordColumnId.Word, direction: SortDirection.Ascending };
|
|
21
|
+
|
|
22
|
+
expect(sortWords([zvq, cat], sort, 'en-US', 'original')).toEqual([cat, zvq]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('sorts by coordinates', () => {
|
|
26
|
+
const sort = { column: WordColumnId.Coordinates, direction: SortDirection.Ascending };
|
|
27
|
+
|
|
28
|
+
expect(sortWords([zvq, cat], sort, 'en-US', 'original')).toEqual([cat, zvq]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('sorts invalid words first by validity', () => {
|
|
32
|
+
const sort = { column: WordColumnId.Validity, direction: SortDirection.Ascending };
|
|
33
|
+
|
|
34
|
+
expect(sortWords([cat, zvq], sort, 'en-US', 'original')).toEqual([zvq, cat]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('reverses the order when sorting descending', () => {
|
|
38
|
+
const sort = { column: WordColumnId.Word, direction: SortDirection.Descending };
|
|
39
|
+
|
|
40
|
+
expect(sortWords([cat, zvq], sort, 'en-US', 'original')).toEqual([zvq, cat]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('does not modify the input', () => {
|
|
44
|
+
const sort = { column: WordColumnId.Word, direction: SortDirection.Ascending };
|
|
45
|
+
const words = [zvq, cat];
|
|
46
|
+
|
|
47
|
+
sortWords(words, sort, 'en-US', 'original');
|
|
48
|
+
|
|
49
|
+
expect(words).toEqual([zvq, cat]);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
function createWord(overrides: Partial<VerifiedWord>): VerifiedWord {
|
|
54
|
+
return { direction: 'horizontal', isValid: true, word: 'cat', x: 0, y: 0, ...overrides };
|
|
55
|
+
}
|