@scrabble-solver/scrabble-solver 2.8.10 → 2.9.0
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/InjectManifest.js.nft.json +1 -0
- package/.next/build-manifest.json +9 -9
- package/.next/cache/.tsbuildinfo +1 -1
- package/.next/cache/eslint/.cache_8dgz12 +1 -1
- package/.next/cache/next-server.js.nft.json +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/edge-server-production/0.pack +0 -0
- package/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/0.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +1 -1
- package/.next/routes-manifest.json +1 -1
- package/.next/server/InjectManifest.js.nft.json +1 -0
- package/.next/server/chunks/413.js +44 -17
- package/.next/server/chunks/452.js +894 -0
- package/.next/server/chunks/515.js +82 -45
- package/.next/server/chunks/911.js +0 -887
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/pages/404.html +3 -3
- package/.next/server/pages/404.js.nft.json +1 -1
- package/.next/server/pages/500.html +2 -2
- package/.next/server/pages/_app.js.nft.json +1 -1
- package/.next/server/pages/_document.js.nft.json +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 +200 -0
- package/.next/server/pages/api/dictionary/[locale].js.nft.json +1 -0
- package/.next/server/pages/api/solve.js +32 -2
- 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 +7 -7
- package/.next/server/pages/index.js +74 -29
- package/.next/server/pages/index.js.nft.json +1 -1
- package/.next/server/pages/index.json +1 -1
- package/.next/server/pages-manifest.json +1 -0
- package/.next/static/BiJ0RS1mh1CdONVQg_p20/_buildManifest.js +1 -0
- package/.next/static/{A8A_Lmg8cM-Bkf-Jo1CLh → BiJ0RS1mh1CdONVQg_p20}/_ssgManifest.js +0 -0
- package/.next/static/chunks/317-c5d262202c17d519.js +1 -0
- package/.next/static/chunks/{758-eff80059a1365d5d.js → 546-447e243fc9de2c59.js} +1 -1
- package/.next/static/chunks/pages/{404-90c624da3c83fd17.js → 404-7082923654d5996f.js} +1 -1
- package/.next/static/chunks/pages/_app-1878e12521f2d115.js +1 -0
- package/.next/static/chunks/pages/index-14d33636a0746c22.js +1 -0
- package/.next/trace +52 -42
- package/next.config.js +11 -0
- package/package.json +17 -11
- package/src/components/Board/components/Cell/CellPure.tsx +33 -31
- package/src/components/Board/hooks/useGrid.ts +23 -18
- package/src/components/Sidebar/Sidebar.tsx +20 -1
- package/src/components/SvgFontCss/SvgFontCss.tsx +2 -1
- package/src/pages/api/dictionary/[locale]/index.ts +53 -0
- package/src/pages/api/solve.ts +6 -0
- package/src/pages/index.tsx +8 -1
- package/src/sdk/fetch.ts +30 -0
- package/src/sdk/fetchJson.ts +10 -31
- package/src/sdk/getDictionary.ts +11 -0
- package/src/service-worker/dictionaries/constants.ts +3 -0
- package/src/service-worker/dictionaries/expirationManager.ts +9 -0
- package/src/service-worker/dictionaries/getDictionary.ts +22 -0
- package/src/service-worker/dictionaries/getDictionaryUrl.ts +7 -0
- package/src/service-worker/dictionaries/index.ts +2 -0
- package/src/service-worker/dictionaries/revalidateDictionary.ts +35 -0
- package/src/service-worker/getTrie.ts +26 -0
- package/src/service-worker/index.ts +22 -0
- package/src/service-worker/routeSolveRequests.ts +37 -0
- package/src/service-worker/routeVerifyRequests.ts +35 -0
- package/src/serviceWorkerManager.ts +26 -0
- package/src/state/sagas.ts +7 -0
- package/src/state/slices/rackSlice.ts +0 -5
- package/src/state/slices/settingsInitialState.ts +20 -1
- package/tsconfig.json +1 -0
- package/.next/static/A8A_Lmg8cM-Bkf-Jo1CLh/_buildManifest.js +0 -1
- package/.next/static/chunks/317-95ab9051449362fa.js +0 -1
- package/.next/static/chunks/pages/_app-0e358b5622cf9e66.js +0 -1
- package/.next/static/chunks/pages/index-0cc5e6eda5adac73.js +0 -1
package/next.config.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const WorkboxPlugin = require('workbox-webpack-plugin');
|
|
5
6
|
|
|
6
7
|
const tsConfig = fs.readFileSync(path.resolve(__dirname, 'tsconfig.json'), 'utf-8');
|
|
7
8
|
const tsConfigJson = JSON.parse(tsConfig);
|
|
@@ -39,5 +40,15 @@ module.exports = {
|
|
|
39
40
|
},
|
|
40
41
|
],
|
|
41
42
|
},
|
|
43
|
+
plugins: [
|
|
44
|
+
...config.plugins,
|
|
45
|
+
process.env.NODE_ENV === 'production'
|
|
46
|
+
? new WorkboxPlugin.InjectManifest({
|
|
47
|
+
swSrc: path.join(__dirname, 'src/service-worker/index.ts'),
|
|
48
|
+
swDest: path.join(__dirname, 'public/service-worker.js'),
|
|
49
|
+
exclude: [/\.map$/, /\.next/, /_next/, /manifest/, /\.htaccess$/, /.*\/static\/.*/, /service-worker\.js$/],
|
|
50
|
+
})
|
|
51
|
+
: undefined,
|
|
52
|
+
].filter(Boolean),
|
|
42
53
|
}),
|
|
43
54
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scrabble-solver/scrabble-solver",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"description": "Scrabble Solver 2 - App",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=16"
|
|
@@ -28,15 +28,16 @@
|
|
|
28
28
|
"start": "env-cmd next start -p 3333"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
+
"@kamilmielnik/trie": "^2.0.1",
|
|
31
32
|
"@popperjs/core": "^2.11.6",
|
|
32
33
|
"@reduxjs/toolkit": "^1.8.6",
|
|
33
|
-
"@scrabble-solver/configs": "^2.
|
|
34
|
-
"@scrabble-solver/constants": "^2.
|
|
35
|
-
"@scrabble-solver/dictionaries": "^2.
|
|
36
|
-
"@scrabble-solver/logger": "^2.
|
|
37
|
-
"@scrabble-solver/solver": "^2.
|
|
38
|
-
"@scrabble-solver/types": "^2.
|
|
39
|
-
"@scrabble-solver/word-definitions": "^2.
|
|
34
|
+
"@scrabble-solver/configs": "^2.9.0",
|
|
35
|
+
"@scrabble-solver/constants": "^2.9.0",
|
|
36
|
+
"@scrabble-solver/dictionaries": "^2.9.0",
|
|
37
|
+
"@scrabble-solver/logger": "^2.9.0",
|
|
38
|
+
"@scrabble-solver/solver": "^2.9.0",
|
|
39
|
+
"@scrabble-solver/types": "^2.9.0",
|
|
40
|
+
"@scrabble-solver/word-definitions": "^2.9.0",
|
|
40
41
|
"classnames": "^2.3.2",
|
|
41
42
|
"next": "^12.3.1",
|
|
42
43
|
"normalize.css": "^8.0.1",
|
|
@@ -51,7 +52,11 @@
|
|
|
51
52
|
"redux": "^4.2.0",
|
|
52
53
|
"redux-saga": "^1.2.1",
|
|
53
54
|
"store2": "^2.14.2",
|
|
54
|
-
"uuid": "^9.0.0"
|
|
55
|
+
"uuid": "^9.0.0",
|
|
56
|
+
"workbox-expiration": "^6.5.4",
|
|
57
|
+
"workbox-precaching": "^6.5.4",
|
|
58
|
+
"workbox-routing": "^6.5.4",
|
|
59
|
+
"workbox-window": "^6.5.4"
|
|
55
60
|
},
|
|
56
61
|
"devDependencies": {
|
|
57
62
|
"@svgr/webpack": "^6.5.0",
|
|
@@ -66,7 +71,8 @@
|
|
|
66
71
|
"@types/redux-saga": "^0.10.5",
|
|
67
72
|
"@types/uuid": "^8.3.4",
|
|
68
73
|
"env-cmd": "^10.1.0",
|
|
69
|
-
"sass": "^1.55.0"
|
|
74
|
+
"sass": "^1.55.0",
|
|
75
|
+
"workbox-webpack-plugin": "^6.5.4"
|
|
70
76
|
},
|
|
71
|
-
"gitHead": "
|
|
77
|
+
"gitHead": "f100d9d52460a68f1fb5a1059cd335029d5e46a8"
|
|
72
78
|
}
|
|
@@ -81,39 +81,41 @@ const CellPure: FunctionComponent<Props> = ({
|
|
|
81
81
|
onFocus={onFocus}
|
|
82
82
|
/>
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
<
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
{isEmpty && (
|
|
94
|
-
<Button
|
|
95
|
-
className={classNames(styles.filterCell, {
|
|
96
|
-
[styles.filtered]: isFiltered,
|
|
97
|
-
})}
|
|
98
|
-
tooltip={translate('cell.filter-cell')}
|
|
99
|
-
onClick={onToggleFilterCellClick}
|
|
100
|
-
>
|
|
101
|
-
<Flag />
|
|
84
|
+
{!cell.isCandidate() && (
|
|
85
|
+
<div className={styles.actions}>
|
|
86
|
+
<Button tooltip={translate('cell.toggle-direction')} onClick={onDirectionToggleClick}>
|
|
87
|
+
<ArrowDown
|
|
88
|
+
className={classNames(styles.toggleDirection, {
|
|
89
|
+
[styles.right]: direction === 'horizontal',
|
|
90
|
+
})}
|
|
91
|
+
/>
|
|
102
92
|
</Button>
|
|
103
|
-
)}
|
|
104
93
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
94
|
+
{isEmpty && (
|
|
95
|
+
<Button
|
|
96
|
+
className={classNames(styles.filterCell, {
|
|
97
|
+
[styles.filtered]: isFiltered,
|
|
98
|
+
})}
|
|
99
|
+
tooltip={translate('cell.filter-cell')}
|
|
100
|
+
onClick={onToggleFilterCellClick}
|
|
101
|
+
>
|
|
102
|
+
<Flag />
|
|
103
|
+
</Button>
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
{!isEmpty && (
|
|
107
|
+
<Button
|
|
108
|
+
className={classNames(styles.blank, {
|
|
109
|
+
[styles.active]: tile.isBlank,
|
|
110
|
+
})}
|
|
111
|
+
tooltip={tile.isBlank ? translate('cell.set-not-blank') : translate('cell.set-blank')}
|
|
112
|
+
onClick={onToggleBlankClick}
|
|
113
|
+
>
|
|
114
|
+
B
|
|
115
|
+
</Button>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
117
119
|
</div>
|
|
118
120
|
);
|
|
119
121
|
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from 'react';
|
|
16
16
|
import { useDispatch } from 'react-redux';
|
|
17
17
|
import { useLatest } from 'react-use';
|
|
18
|
+
import { AnyAction } from 'redux';
|
|
18
19
|
|
|
19
20
|
import { createGridOf, createKeyboardNavigation, extractCharacters, extractInputValue, isCtrl } from 'lib';
|
|
20
21
|
import { boardSlice, selectConfig, useTypedSelector } from 'state';
|
|
@@ -82,6 +83,7 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
|
|
|
82
83
|
const insertValue = useCallback(
|
|
83
84
|
(position: Point, value: string) => {
|
|
84
85
|
const characters = value ? extractCharacters(config, value).filter((character) => character !== BLANK) : [BLANK];
|
|
86
|
+
const actions: AnyAction[] = [];
|
|
85
87
|
let board = new Board({ rows: rows.map((row) => row.map((cell) => cell.clone())) });
|
|
86
88
|
let { x, y } = position;
|
|
87
89
|
|
|
@@ -108,10 +110,9 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
|
|
|
108
110
|
const twoCharacterCandidate = cellUp.tile.character + character;
|
|
109
111
|
|
|
110
112
|
if (!cellUp.tile.isBlank && config.twoCharacterTiles.includes(twoCharacterCandidate)) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
);
|
|
113
|
+
const action = boardSlice.actions.changeCellValue({ x, y: y - 1, value: twoCharacterCandidate });
|
|
114
|
+
board = boardSlice.reducer(board, action);
|
|
115
|
+
actions.push(action);
|
|
115
116
|
return;
|
|
116
117
|
}
|
|
117
118
|
}
|
|
@@ -121,8 +122,10 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
|
|
|
121
122
|
const twoCharacterCandidate = character + cellDown.tile.character;
|
|
122
123
|
|
|
123
124
|
if (!cellDown.tile.isBlank && config.twoCharacterTiles.includes(twoCharacterCandidate)) {
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
const action1 = boardSlice.actions.changeCellValue({ x, y, value: character });
|
|
126
|
+
const action2 = boardSlice.actions.changeCellValue({ x, y: y + 1, value: EMPTY_CELL });
|
|
127
|
+
board = boardSlice.reducer(boardSlice.reducer(board, action1), action2);
|
|
128
|
+
actions.push(action1, action2);
|
|
126
129
|
scheduleMoveFocus();
|
|
127
130
|
return;
|
|
128
131
|
}
|
|
@@ -133,10 +136,9 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
|
|
|
133
136
|
const twoCharacterCandidate = cellLeft.tile.character + character;
|
|
134
137
|
|
|
135
138
|
if (!cellLeft.tile.isBlank && config.twoCharacterTiles.includes(twoCharacterCandidate)) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
);
|
|
139
|
+
const action = boardSlice.actions.changeCellValue({ x: x - 1, y, value: twoCharacterCandidate });
|
|
140
|
+
board = boardSlice.reducer(board, action);
|
|
141
|
+
actions.push(action);
|
|
140
142
|
return;
|
|
141
143
|
}
|
|
142
144
|
}
|
|
@@ -146,8 +148,10 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
|
|
|
146
148
|
const twoCharacterCandidate = character + cellRight.tile.character;
|
|
147
149
|
|
|
148
150
|
if (!cellRight.tile.isBlank && config.twoCharacterTiles.includes(twoCharacterCandidate)) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
+
const action1 = boardSlice.actions.changeCellValue({ x, y, value: character });
|
|
152
|
+
const action2 = boardSlice.actions.changeCellValue({ x: x + 1, y, value: EMPTY_CELL });
|
|
153
|
+
board = boardSlice.reducer(boardSlice.reducer(board, action1), action2);
|
|
154
|
+
actions.push(action1, action2);
|
|
151
155
|
scheduleMoveFocus();
|
|
152
156
|
return;
|
|
153
157
|
}
|
|
@@ -158,20 +162,21 @@ const useGrid = (rows: Cell[][]): [State, Actions] => {
|
|
|
158
162
|
const twoCharacterCandidate = cell.tile.character + character;
|
|
159
163
|
|
|
160
164
|
if (!cell.tile.isBlank && config.twoCharacterTiles.includes(twoCharacterCandidate)) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
);
|
|
165
|
+
const action = boardSlice.actions.changeCellValue({ x, y, value: twoCharacterCandidate });
|
|
166
|
+
board = boardSlice.reducer(board, action);
|
|
167
|
+
actions.push(action);
|
|
165
168
|
return;
|
|
166
169
|
}
|
|
167
170
|
}
|
|
168
171
|
|
|
169
|
-
|
|
172
|
+
const action = boardSlice.actions.changeCellValue({ x, y, value: character });
|
|
173
|
+
board = boardSlice.reducer(board, action);
|
|
174
|
+
actions.push(action);
|
|
170
175
|
scheduleMoveFocus();
|
|
171
176
|
});
|
|
172
177
|
|
|
173
178
|
moveFocus(Math.abs(position.x - x) + Math.abs(position.y - y));
|
|
174
|
-
|
|
179
|
+
actions.forEach(dispatch);
|
|
175
180
|
},
|
|
176
181
|
[config, directionRef, dispatch, moveFocus, rows],
|
|
177
182
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import classNames from 'classnames';
|
|
2
|
-
import { FunctionComponent, ReactNode } from 'react';
|
|
2
|
+
import { FunctionComponent, ReactNode, useEffect, useState } from 'react';
|
|
3
3
|
import Modal from 'react-modal';
|
|
4
|
+
import { useKey } from 'react-use';
|
|
4
5
|
|
|
5
6
|
import { CrossFill } from 'icons';
|
|
6
7
|
import { TRANSITION_DURATION_LONG } from 'parameters';
|
|
@@ -21,6 +22,23 @@ export interface Props {
|
|
|
21
22
|
|
|
22
23
|
const Sidebar: FunctionComponent<Props> = ({ children, className, isOpen, title, onClose }) => {
|
|
23
24
|
const translate = useTranslate();
|
|
25
|
+
const [shouldReturnFocusAfterClose, setShouldReturnFocusAfterClose] = useState(true);
|
|
26
|
+
|
|
27
|
+
useKey(
|
|
28
|
+
'Escape',
|
|
29
|
+
() => {
|
|
30
|
+
setShouldReturnFocusAfterClose(false);
|
|
31
|
+
onClose();
|
|
32
|
+
},
|
|
33
|
+
undefined,
|
|
34
|
+
[onClose],
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (isOpen) {
|
|
39
|
+
setShouldReturnFocusAfterClose(true);
|
|
40
|
+
}
|
|
41
|
+
}, [isOpen]);
|
|
24
42
|
|
|
25
43
|
return (
|
|
26
44
|
<Modal
|
|
@@ -33,6 +51,7 @@ const Sidebar: FunctionComponent<Props> = ({ children, className, isOpen, title,
|
|
|
33
51
|
contentLabel={title}
|
|
34
52
|
isOpen={isOpen}
|
|
35
53
|
overlayClassName={styles.overlay}
|
|
54
|
+
shouldReturnFocusAfterClose={shouldReturnFocusAfterClose}
|
|
36
55
|
onRequestClose={onClose}
|
|
37
56
|
>
|
|
38
57
|
<div className={classNames(styles.sidebar, className)}>
|
|
@@ -7,6 +7,7 @@ text {
|
|
|
7
7
|
font-family: 'Open Sans';
|
|
8
8
|
}`;
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
// eslint-disable-next-line react/no-danger
|
|
11
|
+
const SvgFontCss: FunctionComponent = () => <style type="text/css" dangerouslySetInnerHTML={{ __html: CSS }} />;
|
|
11
12
|
|
|
12
13
|
export default SvgFontCss;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { dictionaries } from '@scrabble-solver/dictionaries';
|
|
2
|
+
import logger from '@scrabble-solver/logger';
|
|
3
|
+
import { isLocale, Locale } from '@scrabble-solver/types';
|
|
4
|
+
import { NextApiRequest, NextApiResponse } from 'next';
|
|
5
|
+
|
|
6
|
+
import { getServerLoggingData } from 'api';
|
|
7
|
+
|
|
8
|
+
interface RequestData {
|
|
9
|
+
locale: Locale;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const dictionary = async (request: NextApiRequest, response: NextApiResponse): Promise<void> => {
|
|
13
|
+
const meta = getServerLoggingData(request);
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const { locale } = parseRequest(request);
|
|
17
|
+
|
|
18
|
+
logger.info('dictionary - request', {
|
|
19
|
+
meta,
|
|
20
|
+
payload: {
|
|
21
|
+
locale,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const trie = await dictionaries.get(locale);
|
|
26
|
+
response.status(200).send(trie.serialize());
|
|
27
|
+
} catch (error) {
|
|
28
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
29
|
+
logger.error('dictionary - error', { error, meta });
|
|
30
|
+
response.status(500).send({ error: 'Server error', message });
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const parseRequest = (request: NextApiRequest): RequestData => {
|
|
36
|
+
const { locale } = request.query;
|
|
37
|
+
|
|
38
|
+
if (!isLocale(locale)) {
|
|
39
|
+
throw new Error('Invalid "locale" parameter');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
locale,
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const config = {
|
|
48
|
+
api: {
|
|
49
|
+
responseLimit: '25mb',
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default dictionary;
|
package/src/pages/api/solve.ts
CHANGED
package/src/pages/index.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import classNames from 'classnames';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import { AnimationEvent, FormEvent, FunctionComponent, useState } from 'react';
|
|
4
|
+
import { AnimationEvent, FormEvent, FunctionComponent, useEffect, useState } from 'react';
|
|
5
5
|
import Modal from 'react-modal';
|
|
6
6
|
import { useDispatch } from 'react-redux';
|
|
7
7
|
import { useEffectOnce, useMeasure } from 'react-use';
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
import { useIsTablet, useLocalStorage } from 'hooks';
|
|
25
25
|
import { getCellSize } from 'lib';
|
|
26
26
|
import { COMPONENTS_SPACING, COMPONENTS_SPACING_MOBILE, DICTIONARY_HEIGHT } from 'parameters';
|
|
27
|
+
import { registerServiceWorker } from 'serviceWorkerManager';
|
|
27
28
|
import { initialize, localStorage, reset, selectConfig, solveSlice, useTypedSelector } from 'state';
|
|
28
29
|
|
|
29
30
|
import styles from './index.module.scss';
|
|
@@ -72,6 +73,12 @@ const Index: FunctionComponent<Props> = ({ version }) => {
|
|
|
72
73
|
dispatch(initialize());
|
|
73
74
|
});
|
|
74
75
|
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (process.env.NODE_ENV === 'production') {
|
|
78
|
+
registerServiceWorker();
|
|
79
|
+
}
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
75
82
|
return (
|
|
76
83
|
<>
|
|
77
84
|
<div className={classNames(styles.index, { [styles.initialized]: isInitialized })}>
|
package/src/sdk/fetch.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { isError } from '@scrabble-solver/types';
|
|
2
|
+
|
|
3
|
+
const fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
4
|
+
let response: Response;
|
|
5
|
+
|
|
6
|
+
try {
|
|
7
|
+
response = await window.fetch(input, init);
|
|
8
|
+
} catch (error) {
|
|
9
|
+
const message = isError(error) ? error.message : 'Unknown error';
|
|
10
|
+
throw new Error(`Network error: ${message}`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (response.ok) {
|
|
14
|
+
return response;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const json = await response.json();
|
|
19
|
+
|
|
20
|
+
if (isError(json)) {
|
|
21
|
+
throw new Error(json.message);
|
|
22
|
+
}
|
|
23
|
+
} finally {
|
|
24
|
+
// do nothing
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default fetch;
|
package/src/sdk/fetchJson.ts
CHANGED
|
@@ -1,36 +1,15 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fetch from './fetch';
|
|
2
2
|
|
|
3
3
|
const fetchJson = async <T>(input: RequestInfo | URL, init?: RequestInit): Promise<T> => {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
...init,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
});
|
|
14
|
-
} catch (error) {
|
|
15
|
-
const message = isError(error) ? error.message : 'Unknown error';
|
|
16
|
-
throw new Error(`Network error: ${message}`);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (response.ok) {
|
|
20
|
-
return response.json();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
const json = await response.json();
|
|
25
|
-
|
|
26
|
-
if (isError(json)) {
|
|
27
|
-
throw new Error(json.message);
|
|
28
|
-
}
|
|
29
|
-
} finally {
|
|
30
|
-
// do nothing
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
4
|
+
const response = await fetch(input, {
|
|
5
|
+
...init,
|
|
6
|
+
headers: {
|
|
7
|
+
'Content-Type': 'application/json',
|
|
8
|
+
...init?.headers,
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
return response.json();
|
|
34
13
|
};
|
|
35
14
|
|
|
36
15
|
export default fetchJson;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Trie } from '@kamilmielnik/trie';
|
|
2
|
+
import { Locale } from '@scrabble-solver/types';
|
|
3
|
+
|
|
4
|
+
import fetchJson from './fetchJson';
|
|
5
|
+
|
|
6
|
+
const getDictionary = async (locale: Locale): Promise<Trie> => {
|
|
7
|
+
const serialized = await fetchJson<string>(`/api/dictionary/${locale}`);
|
|
8
|
+
return Trie.deserialize(serialized);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default getDictionary;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { CacheExpiration } from 'workbox-expiration';
|
|
2
|
+
|
|
3
|
+
import { DICTIONARY_CACHE, DICTIONARY_CACHE_MAX_AGE } from './constants';
|
|
4
|
+
|
|
5
|
+
const expirationManager = new CacheExpiration(DICTIONARY_CACHE, {
|
|
6
|
+
maxAgeSeconds: DICTIONARY_CACHE_MAX_AGE / 1000,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export default expirationManager;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Locale } from '@scrabble-solver/types';
|
|
2
|
+
|
|
3
|
+
import { DICTIONARY_CACHE } from './constants';
|
|
4
|
+
import expirationManager from './expirationManager';
|
|
5
|
+
import getDictionaryUrl from './getDictionaryUrl';
|
|
6
|
+
|
|
7
|
+
const getDictionary = async (locale: Locale): Promise<string | undefined> => {
|
|
8
|
+
await expirationManager.expireEntries();
|
|
9
|
+
|
|
10
|
+
const url = getDictionaryUrl(locale);
|
|
11
|
+
const cache = await caches.open(DICTIONARY_CACHE);
|
|
12
|
+
const cached = await cache.match(url);
|
|
13
|
+
|
|
14
|
+
if (typeof cached === 'undefined') {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const serialized = await cached.clone().text();
|
|
19
|
+
return serialized;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default getDictionary;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Locale } from '@scrabble-solver/types';
|
|
2
|
+
|
|
3
|
+
import { DICTIONARY_CACHE } from './constants';
|
|
4
|
+
import expirationManager from './expirationManager';
|
|
5
|
+
import getDictionaryUrl from './getDictionaryUrl';
|
|
6
|
+
|
|
7
|
+
const requests: Partial<Record<Locale, Promise<Response> | undefined>> = {};
|
|
8
|
+
|
|
9
|
+
const revalidateDictionary = async (locale: Locale): Promise<void> => {
|
|
10
|
+
if (requests[locale] instanceof Promise) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let response: Response | undefined;
|
|
15
|
+
const url = getDictionaryUrl(locale);
|
|
16
|
+
const request = fetch(url);
|
|
17
|
+
requests[locale] = request;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
response = await request;
|
|
21
|
+
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
requests[locale] = undefined;
|
|
24
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const cache = await caches.open(DICTIONARY_CACHE);
|
|
28
|
+
await cache.put(url, response.clone());
|
|
29
|
+
await expirationManager.updateTimestamp(url);
|
|
30
|
+
} finally {
|
|
31
|
+
requests[locale] = undefined;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default revalidateDictionary;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Trie } from '@kamilmielnik/trie';
|
|
2
|
+
import { Locale } from '@scrabble-solver/types';
|
|
3
|
+
|
|
4
|
+
import { getDictionary } from './dictionaries';
|
|
5
|
+
|
|
6
|
+
const cache: Partial<Record<Locale, { trie: Trie; dictionary: string } | undefined>> = {};
|
|
7
|
+
|
|
8
|
+
const getTrie = async (locale: Locale): Promise<Trie | undefined> => {
|
|
9
|
+
const dictionary = await getDictionary(locale);
|
|
10
|
+
|
|
11
|
+
if (typeof dictionary === 'undefined') {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const cached = cache[locale];
|
|
16
|
+
|
|
17
|
+
if (typeof cached === 'undefined' || cached.dictionary !== dictionary) {
|
|
18
|
+
const trie = Trie.deserialize(dictionary);
|
|
19
|
+
cache[locale] = { dictionary, trie };
|
|
20
|
+
return trie;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return cached.trie;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default getTrie;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/// <reference lib="WebWorker" />
|
|
2
|
+
|
|
3
|
+
import { cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching';
|
|
4
|
+
|
|
5
|
+
import routeSolveRequests from './routeSolveRequests';
|
|
6
|
+
import routeVerifyRequests from './routeVerifyRequests';
|
|
7
|
+
|
|
8
|
+
declare const self: ServiceWorkerGlobalScope;
|
|
9
|
+
|
|
10
|
+
self.addEventListener('install', () => {
|
|
11
|
+
self.skipWaiting();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
self.addEventListener('activate', () => {
|
|
15
|
+
self.clients.claim();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
cleanupOutdatedCaches();
|
|
19
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
20
|
+
precacheAndRoute(self.__WB_MANIFEST);
|
|
21
|
+
routeSolveRequests();
|
|
22
|
+
routeVerifyRequests();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { getConfig } from '@scrabble-solver/configs';
|
|
2
|
+
import { BLANK } from '@scrabble-solver/constants';
|
|
3
|
+
import { solve } from '@scrabble-solver/solver';
|
|
4
|
+
import { Board, Locale, Tile } from '@scrabble-solver/types';
|
|
5
|
+
import { registerRoute } from 'workbox-routing';
|
|
6
|
+
|
|
7
|
+
import { revalidateDictionary } from './dictionaries';
|
|
8
|
+
import getTrie from './getTrie';
|
|
9
|
+
|
|
10
|
+
const headers = {
|
|
11
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const routeSolveRequests = () => {
|
|
15
|
+
registerRoute(
|
|
16
|
+
({ url }) => url.origin === location.origin && url.pathname === '/api/solve',
|
|
17
|
+
async ({ request }) => {
|
|
18
|
+
const { board, characters, configId, locale } = await request.clone().json();
|
|
19
|
+
const trie = await getTrie(locale);
|
|
20
|
+
|
|
21
|
+
if (!trie) {
|
|
22
|
+
const response = await fetch(request);
|
|
23
|
+
revalidateDictionary(locale);
|
|
24
|
+
return response;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const config = getConfig(configId)[locale as Locale];
|
|
28
|
+
const tiles = characters.map((character: string) => new Tile({ character, isBlank: character === BLANK }));
|
|
29
|
+
const resultsJson = solve(trie, config, Board.fromJson(board), tiles);
|
|
30
|
+
const json = JSON.stringify(resultsJson);
|
|
31
|
+
return new Response(json, { headers });
|
|
32
|
+
},
|
|
33
|
+
'POST',
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default routeSolveRequests;
|