@scrabble-solver/scrabble-solver 2.18.0 → 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 +15 -15
- 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 +3 -2
- package/.next/server/pages/500.html +3 -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 +2 -2
- 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 +3 -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 +3 -2
- package/.next/server/pages/not-found.js.nft.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/pages/not-found-a59f9deeeff63d17.js +1 -0
- package/.next/static/chunks/webpack-9f5c9d275348829b.js +1 -0
- package/.next/static/css/65bcc9440e067113.css +1 -0
- package/.next/static/css/{abc2425d3ba607bc.css → 92f85ce358a0a468.css} +2 -1
- package/.next/static/css/a691348b788f8bc0.css +1 -0
- package/.next/static/css/cd4d7f2458487399.css +1 -0
- package/.next/static/{FL82obwzFHRzvV6z76I-U → 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 +23 -14
- package/public/robots.txt +1 -0
- package/public/site.webmanifest +14 -0
- 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/lib/waitForIdleOrFirstIntent.ts +19 -8
- 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/_app.tsx +40 -76
- package/src/pages/_document.tsx +21 -19
- package/src/pages/api/verify.ts +7 -6
- package/src/pages/index.tsx +58 -6
- package/src/pages/not-found.tsx +13 -2
- package/src/parameters/index.ts +9 -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 +30 -5
- 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-ba5d628e81a15461.js +0 -1
- package/.next/static/chunks/pages/index-5f9fa6ac3cd3d97b.js +0 -1
- package/.next/static/chunks/pages/not-found-23d61fd3ed791cad.js +0 -1
- package/.next/static/chunks/webpack-e34c3ae12275e978.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/src/components/SeoMessage/SeoMessage.tsx +0 -18
- package/src/components/SeoMessage/index.ts +0 -1
- /package/.next/static/{FL82obwzFHRzvV6z76I-U → nvGZh1gPuX98L07YmxlYD}/_ssgManifest.js +0 -0
|
@@ -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
|
@@ -11,15 +11,17 @@ import { call, delay, put, select, spawn, takeEvery, takeLatest } from 'redux-sa
|
|
|
11
11
|
import { LOCALE_FEATURES } from '@/i18n/constants';
|
|
12
12
|
import { loadTranslations } from '@/i18n/i18n';
|
|
13
13
|
import { memoize } from '@/lib/memoize';
|
|
14
|
-
import { waitForIdleOrFirstIntent } from '@/lib/waitForIdleOrFirstIntent';
|
|
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
|
}
|
|
@@ -139,7 +144,7 @@ function* onInitialize({ payload }: PayloadAction<{ version: string }>): AnyGene
|
|
|
139
144
|
const board = yield select(selectBoard);
|
|
140
145
|
const locale = yield select(selectLocale);
|
|
141
146
|
|
|
142
|
-
yield spawn(
|
|
147
|
+
yield spawn(prefetchDictionaryOnFirstIntent);
|
|
143
148
|
yield spawn(loadLocaleTranslations, locale);
|
|
144
149
|
yield spawn(preloadTranslationsWhenIdle);
|
|
145
150
|
yield spawn(visitWhenIdle);
|
|
@@ -202,8 +207,8 @@ function* visitWhenIdle(): AnyGenerator {
|
|
|
202
207
|
yield call(visit);
|
|
203
208
|
}
|
|
204
209
|
|
|
205
|
-
function*
|
|
206
|
-
yield call(
|
|
210
|
+
function* prefetchDictionaryOnFirstIntent(): AnyGenerator {
|
|
211
|
+
yield call(waitForFirstIntent);
|
|
207
212
|
const locale = yield select(selectLocale);
|
|
208
213
|
yield call(prefetchDictionary, locale);
|
|
209
214
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type BoardWord, type ShowCoordinates } from '@scrabble-solver/types';
|
|
2
|
+
|
|
3
|
+
import { createCoordinatesComparator } from '@/lib/createCoordinatesComparator';
|
|
4
|
+
import { createKeyComparator } from '@/lib/createKeyComparator';
|
|
5
|
+
import { createRegExp } from '@/lib/createRegExp';
|
|
6
|
+
import { createSortComparator } from '@/lib/createSortComparator';
|
|
7
|
+
import { getCoordinates } from '@/lib/getCoordinates';
|
|
8
|
+
import { numberComparator } from '@/lib/numberComparator';
|
|
9
|
+
import { type ComparatorFactory, type GroupedWords, type Sort, type VerifiedWord, WordColumnId } from '@/types';
|
|
10
|
+
|
|
11
|
+
export const getWordCoordinates = (word: BoardWord, showCoordinates: ShowCoordinates): string => {
|
|
12
|
+
return getCoordinates({ isHorizontal: word.direction === 'horizontal', x: word.x, y: word.y }, showCoordinates);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const groupWords = (words: VerifiedWord[], query: string): GroupedWords => {
|
|
16
|
+
const regExp = createRegExp(query);
|
|
17
|
+
|
|
18
|
+
return words.reduce<GroupedWords>(
|
|
19
|
+
(groupedWords, word) => {
|
|
20
|
+
if (word.word.match(regExp)) {
|
|
21
|
+
groupedWords.matching.push(word);
|
|
22
|
+
} else {
|
|
23
|
+
groupedWords.other.push(word);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return groupedWords;
|
|
27
|
+
},
|
|
28
|
+
{ matching: [], other: [] },
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const comparators: Record<WordColumnId, ComparatorFactory<VerifiedWord>> = {
|
|
33
|
+
[WordColumnId.Coordinates]: (locale: string, showCoordinates: ShowCoordinates) => {
|
|
34
|
+
return createCoordinatesComparator({ getItemCoordinates: getWordCoordinates, locale, showCoordinates });
|
|
35
|
+
},
|
|
36
|
+
[WordColumnId.Validity]: () => (a, b) => {
|
|
37
|
+
return numberComparator(Number(a.isValid), Number(b.isValid));
|
|
38
|
+
},
|
|
39
|
+
[WordColumnId.Word]: (locale: string) => createKeyComparator('word', locale),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const sortWords = (
|
|
43
|
+
words: VerifiedWord[],
|
|
44
|
+
sort: Sort<WordColumnId>,
|
|
45
|
+
locale: string,
|
|
46
|
+
showCoordinates: ShowCoordinates,
|
|
47
|
+
): VerifiedWord[] => {
|
|
48
|
+
return [...words].sort(createSortComparator({ comparators, locale, showCoordinates, sort }));
|
|
49
|
+
};
|
|
@@ -1,9 +1,46 @@
|
|
|
1
1
|
import { createSelector } from '@reduxjs/toolkit';
|
|
2
2
|
|
|
3
|
+
import { createRegExp } from '@/lib/createRegExp';
|
|
4
|
+
|
|
5
|
+
import { selectLocale, selectShowCoordinates } from '../settings';
|
|
3
6
|
import type { RootState } from '../types';
|
|
4
7
|
|
|
8
|
+
import { getWordCoordinates, groupWords, sortWords } from './lib';
|
|
9
|
+
|
|
10
|
+
const selectWordIndex = (_: unknown, index: number): number => index;
|
|
11
|
+
|
|
5
12
|
export const selectVerify = (state: RootState) => state.verify;
|
|
6
13
|
|
|
14
|
+
export const selectLastVerifiedBoard = createSelector([selectVerify], (verify) => verify.lastSolvedParameters.board);
|
|
15
|
+
|
|
7
16
|
export const selectInvalidWords = createSelector([selectVerify], (verify) => verify.invalidWords);
|
|
8
17
|
|
|
9
18
|
export const selectValidWords = createSelector([selectVerify], (verify) => verify.validWords);
|
|
19
|
+
|
|
20
|
+
export const selectWordsQuery = createSelector([selectVerify], (verify) => verify.query);
|
|
21
|
+
|
|
22
|
+
export const selectWordsSort = createSelector([selectVerify], (verify) => verify.sort);
|
|
23
|
+
|
|
24
|
+
export const selectCreatedWords = createSelector([selectInvalidWords, selectValidWords], (invalidWords, validWords) => {
|
|
25
|
+
return [...invalidWords, ...validWords];
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const selectSortedWords = createSelector(
|
|
29
|
+
[selectCreatedWords, selectWordsSort, selectLocale, selectShowCoordinates],
|
|
30
|
+
sortWords,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
export const selectProcessedWords = createSelector([selectSortedWords, selectWordsQuery], (words, query) => {
|
|
34
|
+
const { matching, other } = groupWords(words, query);
|
|
35
|
+
return [...matching, ...other];
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const selectIsWordMatching = createSelector(
|
|
39
|
+
[selectProcessedWords, selectWordsQuery, selectWordIndex],
|
|
40
|
+
(words, query, index) => createRegExp(query).test(words[index].word),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
export const selectWordCoordinates = createSelector(
|
|
44
|
+
[selectProcessedWords, selectShowCoordinates, selectWordIndex],
|
|
45
|
+
(words, showCoordinates, index) => getWordCoordinates(words[index], showCoordinates),
|
|
46
|
+
);
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
|
2
2
|
import { type Board } from '@scrabble-solver/types';
|
|
3
3
|
|
|
4
|
+
import { getNextSort } from '@/lib/getNextSort';
|
|
5
|
+
import { type VerifiedWord, type WordColumnId } from '@/types';
|
|
6
|
+
|
|
4
7
|
import { verifyInitialState } from './initialState';
|
|
5
8
|
|
|
6
9
|
interface VerifyParameters {
|
|
7
10
|
board: Board;
|
|
8
|
-
invalidWords:
|
|
9
|
-
validWords:
|
|
11
|
+
invalidWords: VerifiedWord[];
|
|
12
|
+
validWords: VerifiedWord[];
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
export const verifySlice = createSlice({
|
|
@@ -17,10 +20,18 @@ export const verifySlice = createSlice({
|
|
|
17
20
|
return { ...state, isLoading: true };
|
|
18
21
|
},
|
|
19
22
|
|
|
23
|
+
changeQuery: (state, action: PayloadAction<string>) => {
|
|
24
|
+
return { ...state, query: action.payload };
|
|
25
|
+
},
|
|
26
|
+
|
|
20
27
|
submitFailure: (state) => {
|
|
21
28
|
return { ...state, isLoading: false };
|
|
22
29
|
},
|
|
23
30
|
|
|
31
|
+
sort: (state, action: PayloadAction<WordColumnId>) => {
|
|
32
|
+
return { ...state, sort: getNextSort(state.sort, action.payload) };
|
|
33
|
+
},
|
|
34
|
+
|
|
24
35
|
submitSuccess: (state, action: PayloadAction<VerifyParameters>) => {
|
|
25
36
|
const { board, invalidWords, validWords } = action.payload;
|
|
26
37
|
return { ...state, isLoading: false, lastSolvedParameters: { board }, invalidWords, validWords };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { Sort, VerifiedWord, WordColumnId } from '@/types';
|
|
2
|
+
|
|
1
3
|
import type { BoardState } from '../board';
|
|
2
4
|
|
|
3
5
|
export interface VerifyState {
|
|
@@ -5,6 +7,8 @@ export interface VerifyState {
|
|
|
5
7
|
lastSolvedParameters: {
|
|
6
8
|
board: BoardState;
|
|
7
9
|
};
|
|
8
|
-
invalidWords:
|
|
9
|
-
|
|
10
|
+
invalidWords: VerifiedWord[];
|
|
11
|
+
query: string;
|
|
12
|
+
sort: Sort<WordColumnId>;
|
|
13
|
+
validWords: VerifiedWord[];
|
|
10
14
|
}
|
package/src/styles/_tokens.scss
CHANGED
package/src/styles/mixins.scss
CHANGED
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
),
|
|
11
11
|
$media-expressions: (
|
|
12
12
|
touch: '(hover: none)',
|
|
13
|
+
/**
|
|
14
|
+
* Keep in sync with useIsCompactLayout
|
|
15
|
+
*/
|
|
16
|
+
compact: '(max-width: #{$breakpoint-l - 1px}), (max-height: #{$breakpoint-height-l - 1px})',
|
|
17
|
+
desktop: '(min-width: #{$breakpoint-l}) and (min-height: #{$breakpoint-height-l})',
|
|
13
18
|
)
|
|
14
19
|
);
|
|
15
20
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
breakpointM: strip-unit($breakpoint-m);
|
|
7
7
|
breakpointL: strip-unit($breakpoint-l);
|
|
8
8
|
breakpointXl: strip-unit($breakpoint-xl);
|
|
9
|
+
breakpointHeightL: strip-unit($breakpoint-height-l);
|
|
9
10
|
|
|
10
11
|
borderColorLight: $border-color-light;
|
|
11
12
|
borderWidth: strip-unit($border-width);
|
|
@@ -153,7 +153,7 @@ $easeOutSine: cubic-bezier(0.61, 1, 0.88, 1);
|
|
|
153
153
|
--components-spacing: var(--spacing--l);
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
@include media('
|
|
156
|
+
@include media('compact') {
|
|
157
157
|
--dictionary--height: var(--dictionary--height--mobile);
|
|
158
158
|
--logo--height: #{$logo-height-small};
|
|
159
159
|
--solver-column-total: 0px;
|
package/src/types/index.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { type Result } from '@scrabble-solver/types';
|
|
2
|
-
import { type FunctionComponent, type SVGAttributes } from 'react';
|
|
1
|
+
import { type BoardWord, type Result, type ShowCoordinates } from '@scrabble-solver/types';
|
|
3
2
|
|
|
4
3
|
export type Comparator<T> = (a: T, B: T) => number;
|
|
5
4
|
|
|
5
|
+
export type ComparatorFactory<T> = (locale: string, showCoordinates: ShowCoordinates) => Comparator<T>;
|
|
6
|
+
|
|
6
7
|
export type AutoGroupTiles = 'left' | 'right' | null;
|
|
7
8
|
|
|
8
9
|
export type CellFilterType = 'include' | 'exclude';
|
|
@@ -26,8 +27,8 @@ export interface Point {
|
|
|
26
27
|
|
|
27
28
|
export type Rack = (string | null)[];
|
|
28
29
|
|
|
29
|
-
export interface Sort {
|
|
30
|
-
column:
|
|
30
|
+
export interface Sort<Id extends string = ResultColumnId> {
|
|
31
|
+
column: Id;
|
|
31
32
|
direction: SortDirection;
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -61,12 +62,10 @@ export enum ResultColumnId {
|
|
|
61
62
|
WordsCount = 'words-count',
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
export
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
translationKey: TranslationKey;
|
|
69
|
-
width?: number;
|
|
65
|
+
export enum WordColumnId {
|
|
66
|
+
Coordinates = 'coordinates',
|
|
67
|
+
Validity = 'validity',
|
|
68
|
+
Word = 'word',
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
export interface GroupedResults {
|
|
@@ -74,6 +73,15 @@ export interface GroupedResults {
|
|
|
74
73
|
other: Result[];
|
|
75
74
|
}
|
|
76
75
|
|
|
76
|
+
export interface GroupedWords {
|
|
77
|
+
matching: VerifiedWord[];
|
|
78
|
+
other: VerifiedWord[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface VerifiedWord extends BoardWord {
|
|
82
|
+
isValid: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
77
85
|
export type TranslationKey =
|
|
78
86
|
| 'cell.enter-word'
|
|
79
87
|
| 'cell.filter-cell.exclude'
|
|
@@ -135,6 +143,7 @@ export type TranslationKey =
|
|
|
135
143
|
| 'results.input.placeholder'
|
|
136
144
|
| 'results.insert'
|
|
137
145
|
| 'results.preview'
|
|
146
|
+
| 'results.select'
|
|
138
147
|
| 'results.solve'
|
|
139
148
|
| 'settings'
|
|
140
149
|
| 'settings.autoGroupTiles'
|
|
@@ -154,8 +163,12 @@ export type TranslationKey =
|
|
|
154
163
|
| 'settings.showCoordinates.alternative'
|
|
155
164
|
| 'settings.showCoordinates.original'
|
|
156
165
|
| 'words'
|
|
166
|
+
| 'words.empty-state.no-words'
|
|
167
|
+
| 'words.input.placeholder'
|
|
157
168
|
| 'words.invalid'
|
|
158
|
-
| 'words.
|
|
169
|
+
| 'words.preview'
|
|
170
|
+
| 'words.valid'
|
|
171
|
+
| 'words.validity';
|
|
159
172
|
|
|
160
173
|
export type Translate = (key: TranslationKey, replacements?: Record<string, string>) => string;
|
|
161
174
|
|