@scrabble-solver/scrabble-solver 2.8.9 → 2.8.10
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 +10 -10
- 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/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/chunks/413.js +181 -39
- package/.next/server/chunks/429.js +2 -13
- package/.next/server/chunks/515.js +408 -227
- package/.next/server/chunks/911.js +25 -3
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/pages/404.html +2 -2
- 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/solve.js +52 -19
- package/.next/server/pages/index.html +3 -3
- package/.next/server/pages/index.js +1 -1
- package/.next/server/pages/index.js.nft.json +1 -1
- package/.next/server/pages/index.json +1 -1
- package/.next/static/A8A_Lmg8cM-Bkf-Jo1CLh/_buildManifest.js +1 -0
- package/.next/static/{yCxjzzYpw5JjJE53PO_s6 → A8A_Lmg8cM-Bkf-Jo1CLh}/_ssgManifest.js +0 -0
- package/.next/static/chunks/317-95ab9051449362fa.js +1 -0
- package/.next/static/chunks/758-eff80059a1365d5d.js +1 -0
- package/.next/static/chunks/pages/_app-0e358b5622cf9e66.js +1 -0
- package/.next/static/chunks/pages/index-0cc5e6eda5adac73.js +1 -0
- package/.next/static/css/9ac903004135f4b1.css +1 -0
- package/.next/trace +42 -42
- package/package.json +12 -12
- package/src/components/Badge/Badge.module.scss +1 -1
- package/src/components/Board/Board.tsx +4 -2
- package/src/components/Board/BoardPure.tsx +25 -5
- package/src/components/Board/hooks/useGrid.ts +212 -91
- package/src/components/Dictionary/Dictionary.tsx +8 -1
- package/src/components/Rack/Rack.tsx +51 -11
- package/src/components/Rack/RackTile.tsx +33 -16
- package/src/components/Results/Results.tsx +19 -3
- package/src/components/SquareButton/Link.tsx +1 -1
- package/src/components/Tile/Tile.module.scss +4 -0
- package/src/components/Tile/Tile.tsx +13 -4
- package/src/components/Tile/TilePure.tsx +3 -4
- package/src/lib/extractCharacters.ts +26 -0
- package/src/lib/extractInputValue.ts +17 -0
- package/src/lib/index.ts +2 -0
- package/src/lib/isCtrl.ts +1 -1
- package/src/lib/memoize.ts +15 -1
- package/src/pages/api/solve.ts +1 -1
- package/src/sdk/fetchJson.ts +36 -0
- package/src/sdk/findWordDefinitions.ts +4 -3
- package/src/sdk/solve.ts +8 -7
- package/src/sdk/verify.ts +5 -6
- package/src/state/sagas.ts +3 -3
- package/src/state/selectors.ts +9 -1
- package/src/state/slices/dictionaryInitialState.ts +10 -2
- package/src/state/slices/dictionarySlice.ts +10 -16
- package/src/state/slices/rackSlice.ts +7 -0
- package/src/state/slices/solveInitialState.ts +14 -2
- package/src/state/slices/solveSlice.ts +7 -4
- package/src/types/index.ts +2 -0
- package/.next/static/chunks/317-a33dd38e9b9a17ed.js +0 -1
- package/.next/static/chunks/758-f333b1dcdb941547.js +0 -1
- package/.next/static/chunks/pages/_app-f8f360878e1c2aff.js +0 -1
- package/.next/static/chunks/pages/index-ecea697d3e5d8a6f.js +0 -1
- package/.next/static/css/64dc2ce1811912f1.css +0 -1
- package/.next/static/yCxjzzYpw5JjJE53PO_s6/_buildManifest.js +0 -1
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import { BLANK } from '@scrabble-solver/constants';
|
|
2
1
|
import classNames from 'classnames';
|
|
3
|
-
import { createRef, FunctionComponent, useCallback, useMemo, useRef } from 'react';
|
|
2
|
+
import { ChangeEvent, ClipboardEvent, createRef, FunctionComponent, useCallback, useMemo, useRef } from 'react';
|
|
3
|
+
import { useDispatch } from 'react-redux';
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
|
|
5
|
+
import {
|
|
6
|
+
createArray,
|
|
7
|
+
createKeyboardNavigation,
|
|
8
|
+
extractCharacters,
|
|
9
|
+
extractInputValue,
|
|
10
|
+
isCtrl,
|
|
11
|
+
zipCharactersAndTiles,
|
|
12
|
+
} from 'lib';
|
|
13
|
+
import { rackSlice, selectConfig, selectRack, selectResultCandidateTiles, useTypedSelector } from 'state';
|
|
7
14
|
|
|
8
15
|
import styles from './Rack.module.scss';
|
|
9
16
|
import RackTile from './RackTile';
|
|
@@ -13,6 +20,7 @@ interface Props {
|
|
|
13
20
|
}
|
|
14
21
|
|
|
15
22
|
const Rack: FunctionComponent<Props> = ({ className }) => {
|
|
23
|
+
const dispatch = useDispatch();
|
|
16
24
|
const config = useTypedSelector(selectConfig);
|
|
17
25
|
const rack = useTypedSelector(selectRack);
|
|
18
26
|
const resultCandidateTiles = useTypedSelector(selectResultCandidateTiles);
|
|
@@ -35,7 +43,33 @@ const Rack: FunctionComponent<Props> = ({ className }) => {
|
|
|
35
43
|
[activeIndexRef, tilesCount, tilesRefs],
|
|
36
44
|
);
|
|
37
45
|
|
|
38
|
-
const
|
|
46
|
+
const handleChange = useCallback(
|
|
47
|
+
(event: ChangeEvent<HTMLInputElement>) => {
|
|
48
|
+
const value = extractInputValue(event.target);
|
|
49
|
+
const characters = value ? extractCharacters(config, value) : [];
|
|
50
|
+
changeActiveIndex(value ? characters.length : -1);
|
|
51
|
+
},
|
|
52
|
+
[changeActiveIndex, config],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const handlePaste = useCallback(
|
|
56
|
+
(event: ClipboardEvent<HTMLInputElement>) => {
|
|
57
|
+
const index = activeIndexRef.current;
|
|
58
|
+
|
|
59
|
+
if (typeof index === 'undefined') {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
const value = event.clipboardData.getData('text/plain').toLocaleLowerCase();
|
|
65
|
+
const characters = value ? extractCharacters(config, value) : [];
|
|
66
|
+
changeActiveIndex(value ? characters.length : -1);
|
|
67
|
+
dispatch(rackSlice.actions.changeCharacters({ characters, index }));
|
|
68
|
+
},
|
|
69
|
+
[changeActiveIndex, config, dispatch],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const handleKeyDown = useMemo(() => {
|
|
39
73
|
return createKeyboardNavigation({
|
|
40
74
|
onArrowLeft: (event) => {
|
|
41
75
|
event.preventDefault();
|
|
@@ -45,13 +79,18 @@ const Rack: FunctionComponent<Props> = ({ className }) => {
|
|
|
45
79
|
event.preventDefault();
|
|
46
80
|
changeActiveIndex(1);
|
|
47
81
|
},
|
|
48
|
-
onBackspace: () => {
|
|
82
|
+
onBackspace: (event) => {
|
|
83
|
+
event.preventDefault();
|
|
49
84
|
changeActiveIndex(-1);
|
|
50
85
|
},
|
|
51
86
|
onKeyDown: (event) => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (
|
|
87
|
+
if (isCtrl(event) && config.isTwoCharacterTilePrefix(event.key)) {
|
|
88
|
+
changeActiveIndex(1);
|
|
89
|
+
} else if (event.currentTarget.value === event.key) {
|
|
90
|
+
// change event did not fire because the same character was typed over the current one
|
|
91
|
+
// but we still want to move the caret
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
event.stopPropagation();
|
|
55
94
|
changeActiveIndex(1);
|
|
56
95
|
}
|
|
57
96
|
},
|
|
@@ -59,7 +98,7 @@ const Rack: FunctionComponent<Props> = ({ className }) => {
|
|
|
59
98
|
}, [changeActiveIndex, config]);
|
|
60
99
|
|
|
61
100
|
return (
|
|
62
|
-
<div className={classNames(styles.rack, className)}>
|
|
101
|
+
<div className={classNames(styles.rack, className)} onPaste={handlePaste}>
|
|
63
102
|
{tiles.map(({ character, tile }, index) => (
|
|
64
103
|
<RackTile
|
|
65
104
|
activeIndexRef={activeIndexRef}
|
|
@@ -68,7 +107,8 @@ const Rack: FunctionComponent<Props> = ({ className }) => {
|
|
|
68
107
|
inputRef={tilesRefs[index]}
|
|
69
108
|
key={index}
|
|
70
109
|
tile={tile}
|
|
71
|
-
|
|
110
|
+
onChange={handleChange}
|
|
111
|
+
onKeyDown={handleKeyDown}
|
|
72
112
|
/>
|
|
73
113
|
))}
|
|
74
114
|
</div>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { BLANK } from '@scrabble-solver/constants';
|
|
2
2
|
import { Tile as TileModel } from '@scrabble-solver/types';
|
|
3
3
|
import {
|
|
4
|
+
ChangeEvent,
|
|
5
|
+
ChangeEventHandler,
|
|
4
6
|
FunctionComponent,
|
|
5
7
|
KeyboardEventHandler,
|
|
6
8
|
MutableRefObject,
|
|
@@ -10,7 +12,7 @@ import {
|
|
|
10
12
|
} from 'react';
|
|
11
13
|
import { useDispatch } from 'react-redux';
|
|
12
14
|
|
|
13
|
-
import { createKeyboardNavigation, isCtrl } from 'lib';
|
|
15
|
+
import { createKeyboardNavigation, extractCharacters, extractInputValue, isCtrl } from 'lib';
|
|
14
16
|
import { TILE_SIZE } from 'parameters';
|
|
15
17
|
import { rackSlice, selectCharacterPoints, selectConfig, useTranslate, useTypedSelector } from 'state';
|
|
16
18
|
|
|
@@ -24,10 +26,19 @@ interface Props {
|
|
|
24
26
|
index: number;
|
|
25
27
|
inputRef: RefObject<HTMLInputElement>;
|
|
26
28
|
tile: TileModel | null;
|
|
29
|
+
onChange: ChangeEventHandler<HTMLInputElement>;
|
|
27
30
|
onKeyDown: KeyboardEventHandler<HTMLInputElement>;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
const RackTile: FunctionComponent<Props> = ({
|
|
33
|
+
const RackTile: FunctionComponent<Props> = ({
|
|
34
|
+
activeIndexRef,
|
|
35
|
+
character,
|
|
36
|
+
index,
|
|
37
|
+
inputRef,
|
|
38
|
+
tile,
|
|
39
|
+
onChange,
|
|
40
|
+
onKeyDown,
|
|
41
|
+
}) => {
|
|
31
42
|
const dispatch = useDispatch();
|
|
32
43
|
const translate = useTranslate();
|
|
33
44
|
const config = useTypedSelector(selectConfig);
|
|
@@ -37,32 +48,37 @@ const RackTile: FunctionComponent<Props> = ({ activeIndexRef, character, index,
|
|
|
37
48
|
activeIndexRef.current = index;
|
|
38
49
|
}, [index]);
|
|
39
50
|
|
|
40
|
-
const
|
|
41
|
-
(
|
|
42
|
-
|
|
51
|
+
const handleChange = useCallback(
|
|
52
|
+
(event: ChangeEvent<HTMLInputElement>) => {
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
event.stopPropagation();
|
|
55
|
+
const value = extractInputValue(event.target);
|
|
56
|
+
const characters = value ? extractCharacters(config, value) : [null];
|
|
57
|
+
dispatch(rackSlice.actions.changeCharacters({ characters, index }));
|
|
58
|
+
onChange(event);
|
|
43
59
|
},
|
|
44
|
-
[index],
|
|
60
|
+
[config, index, onChange],
|
|
45
61
|
);
|
|
46
62
|
|
|
47
63
|
const handleKeyDown = useMemo(() => {
|
|
48
64
|
return createKeyboardNavigation({
|
|
49
|
-
onBackspace: () =>
|
|
50
|
-
|
|
65
|
+
onBackspace: (event) => {
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
dispatch(rackSlice.actions.changeCharacter({ character: null, index }));
|
|
68
|
+
},
|
|
51
69
|
onKeyDown: (event) => {
|
|
52
|
-
|
|
53
|
-
const twoCharacterTile = config.getTwoCharacterTileByPrefix(newCharacter);
|
|
54
|
-
|
|
55
|
-
if (isCtrl(event) && twoCharacterTile) {
|
|
70
|
+
if (isCtrl(event) && config.isTwoCharacterTilePrefix(event.key)) {
|
|
56
71
|
event.preventDefault();
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
72
|
+
event.stopPropagation();
|
|
73
|
+
const twoTilesCharacter = config.getTwoCharacterTileByPrefix(event.key);
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
75
|
+
dispatch(rackSlice.actions.changeCharacter({ character: twoTilesCharacter!, index }));
|
|
60
76
|
}
|
|
61
77
|
|
|
62
78
|
onKeyDown(event);
|
|
63
79
|
},
|
|
64
80
|
});
|
|
65
|
-
}, [
|
|
81
|
+
}, [index, onKeyDown]);
|
|
66
82
|
|
|
67
83
|
return (
|
|
68
84
|
<Tile
|
|
@@ -78,6 +94,7 @@ const RackTile: FunctionComponent<Props> = ({ activeIndexRef, character, index,
|
|
|
78
94
|
raised
|
|
79
95
|
size={TILE_SIZE}
|
|
80
96
|
tabIndex={index === 0 ? undefined : -1}
|
|
97
|
+
onChange={handleChange}
|
|
81
98
|
onFocus={handleFocus}
|
|
82
99
|
onKeyDown={handleKeyDown}
|
|
83
100
|
/>
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import classNames from 'classnames';
|
|
2
|
-
import { FunctionComponent } from 'react';
|
|
2
|
+
import { FunctionComponent, useLayoutEffect, useRef } from 'react';
|
|
3
3
|
import { FixedSizeList } from 'react-window';
|
|
4
4
|
|
|
5
5
|
import { RESULTS_HEADER_HEIGHT, RESULTS_INPUT_HEIGHT, RESULTS_ITEM_HEIGHT } from 'parameters';
|
|
6
6
|
import {
|
|
7
7
|
selectAreResultsOutdated,
|
|
8
8
|
selectIsLoading,
|
|
9
|
+
selectSolveError,
|
|
9
10
|
selectSortedFilteredResults,
|
|
10
11
|
selectSortedResults,
|
|
11
12
|
useTranslate,
|
|
@@ -33,6 +34,14 @@ const Results: FunctionComponent<Props> = ({ height, width }) => {
|
|
|
33
34
|
const results = useTypedSelector(selectSortedFilteredResults);
|
|
34
35
|
const isLoading = useTypedSelector(selectIsLoading);
|
|
35
36
|
const isOutdated = useTypedSelector(selectAreResultsOutdated);
|
|
37
|
+
const error = useTypedSelector(selectSolveError);
|
|
38
|
+
const listRef = useRef<HTMLElement>();
|
|
39
|
+
|
|
40
|
+
useLayoutEffect(() => {
|
|
41
|
+
if (listRef.current) {
|
|
42
|
+
listRef.current.scrollTo(0, 0);
|
|
43
|
+
}
|
|
44
|
+
}, [listRef, results]);
|
|
36
45
|
|
|
37
46
|
return (
|
|
38
47
|
<div className={styles.results}>
|
|
@@ -42,7 +51,13 @@ const Results: FunctionComponent<Props> = ({ height, width }) => {
|
|
|
42
51
|
))}
|
|
43
52
|
</div>
|
|
44
53
|
|
|
45
|
-
{typeof
|
|
54
|
+
{typeof error !== 'undefined' && (
|
|
55
|
+
<EmptyState className={styles.emptyState} type="error">
|
|
56
|
+
{error.message}
|
|
57
|
+
</EmptyState>
|
|
58
|
+
)}
|
|
59
|
+
|
|
60
|
+
{typeof results === 'undefined' && typeof error === 'undefined' && (
|
|
46
61
|
<EmptyState className={styles.emptyState} type="info">
|
|
47
62
|
{translate('results.empty-state.uninitialized')}
|
|
48
63
|
|
|
@@ -50,7 +65,7 @@ const Results: FunctionComponent<Props> = ({ height, width }) => {
|
|
|
50
65
|
</EmptyState>
|
|
51
66
|
)}
|
|
52
67
|
|
|
53
|
-
{typeof results !== 'undefined' && typeof allResults !== 'undefined' && (
|
|
68
|
+
{typeof results !== 'undefined' && typeof allResults !== 'undefined' && typeof error === 'undefined' && (
|
|
54
69
|
<>
|
|
55
70
|
{isOutdated && (
|
|
56
71
|
<EmptyState className={styles.emptyState} type="info">
|
|
@@ -80,6 +95,7 @@ const Results: FunctionComponent<Props> = ({ height, width }) => {
|
|
|
80
95
|
[styles.outdated]: isOutdated,
|
|
81
96
|
})}
|
|
82
97
|
height={height - RESULTS_HEADER_HEIGHT - RESULTS_INPUT_HEIGHT}
|
|
98
|
+
innerRef={listRef}
|
|
83
99
|
itemCount={results.length}
|
|
84
100
|
itemSize={RESULTS_ITEM_HEIGHT}
|
|
85
101
|
width={width}
|
|
@@ -16,7 +16,7 @@ const Link: FunctionComponent<Props> = ({ className, Icon, tooltip, ...props })
|
|
|
16
16
|
const triggerProps = useTooltip(tooltip, props);
|
|
17
17
|
|
|
18
18
|
return (
|
|
19
|
-
<a className={classNames(styles.squareButton, className)}
|
|
19
|
+
<a className={classNames(styles.squareButton, className)} {...props} {...triggerProps}>
|
|
20
20
|
<span className={styles.content}>
|
|
21
21
|
<Icon className={styles.icon} />
|
|
22
22
|
</span>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EMPTY_CELL } from '@scrabble-solver/constants';
|
|
2
2
|
import {
|
|
3
|
+
ChangeEventHandler,
|
|
3
4
|
createRef,
|
|
4
5
|
FocusEventHandler,
|
|
5
6
|
FunctionComponent,
|
|
@@ -9,7 +10,7 @@ import {
|
|
|
9
10
|
useMemo,
|
|
10
11
|
} from 'react';
|
|
11
12
|
|
|
12
|
-
import { getTileSizes } from 'lib';
|
|
13
|
+
import { getTileSizes, noop } from 'lib';
|
|
13
14
|
|
|
14
15
|
import TilePure from './TilePure';
|
|
15
16
|
|
|
@@ -26,6 +27,7 @@ interface Props {
|
|
|
26
27
|
raised?: boolean;
|
|
27
28
|
size: number;
|
|
28
29
|
tabIndex?: number;
|
|
30
|
+
onChange?: ChangeEventHandler<HTMLInputElement>;
|
|
29
31
|
onFocus?: FocusEventHandler<HTMLInputElement>;
|
|
30
32
|
onKeyDown?: KeyboardEventHandler<HTMLInputElement>;
|
|
31
33
|
}
|
|
@@ -43,8 +45,9 @@ const Tile: FunctionComponent<Props> = ({
|
|
|
43
45
|
raised,
|
|
44
46
|
size,
|
|
45
47
|
tabIndex,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
onChange,
|
|
49
|
+
onFocus = noop,
|
|
50
|
+
onKeyDown = noop,
|
|
48
51
|
}) => {
|
|
49
52
|
const { pointsFontSize, tileFontSize, tileSize } = getTileSizes(size);
|
|
50
53
|
const style = useMemo(() => ({ height: tileSize, width: tileSize }), [tileSize]);
|
|
@@ -54,6 +57,11 @@ const Tile: FunctionComponent<Props> = ({
|
|
|
54
57
|
const isEmpty = !character || character === EMPTY_CELL;
|
|
55
58
|
const canShowPoints = (isBlank || !isEmpty) && typeof points !== 'undefined';
|
|
56
59
|
|
|
60
|
+
const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (event) => {
|
|
61
|
+
inputRef.current?.select();
|
|
62
|
+
onKeyDown(event);
|
|
63
|
+
};
|
|
64
|
+
|
|
57
65
|
useEffect(() => {
|
|
58
66
|
if (autoFocus && inputRef.current) {
|
|
59
67
|
inputRef.current.focus();
|
|
@@ -77,8 +85,9 @@ const Tile: FunctionComponent<Props> = ({
|
|
|
77
85
|
raised={raised}
|
|
78
86
|
style={style}
|
|
79
87
|
tabIndex={tabIndex}
|
|
88
|
+
onChange={onChange}
|
|
80
89
|
onFocus={onFocus}
|
|
81
|
-
onKeyDown={
|
|
90
|
+
onKeyDown={handleKeyDown}
|
|
82
91
|
/>
|
|
83
92
|
);
|
|
84
93
|
};
|
|
@@ -27,12 +27,11 @@ interface Props {
|
|
|
27
27
|
raised?: boolean;
|
|
28
28
|
style?: CSSProperties;
|
|
29
29
|
tabIndex?: number;
|
|
30
|
+
onChange?: ChangeEventHandler<HTMLInputElement>;
|
|
30
31
|
onFocus?: FocusEventHandler<HTMLInputElement>;
|
|
31
32
|
onKeyDown?: KeyboardEventHandler<HTMLInputElement>;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
const handleChange: ChangeEventHandler = (event) => event.preventDefault();
|
|
35
|
-
|
|
36
35
|
const TilePure: FunctionComponent<Props> = ({
|
|
37
36
|
autoFocus,
|
|
38
37
|
canShowPoints,
|
|
@@ -49,6 +48,7 @@ const TilePure: FunctionComponent<Props> = ({
|
|
|
49
48
|
raised,
|
|
50
49
|
style,
|
|
51
50
|
tabIndex,
|
|
51
|
+
onChange,
|
|
52
52
|
onFocus,
|
|
53
53
|
onKeyDown,
|
|
54
54
|
}) => (
|
|
@@ -72,14 +72,13 @@ const TilePure: FunctionComponent<Props> = ({
|
|
|
72
72
|
autoFocus={autoFocus}
|
|
73
73
|
className={styles.character}
|
|
74
74
|
disabled={disabled}
|
|
75
|
-
maxLength={1}
|
|
76
75
|
placeholder={placeholder}
|
|
77
76
|
ref={inputRef}
|
|
78
77
|
spellCheck={false}
|
|
79
78
|
style={inputStyle}
|
|
80
79
|
tabIndex={tabIndex}
|
|
81
80
|
value={character || ''}
|
|
82
|
-
onChange={
|
|
81
|
+
onChange={onChange}
|
|
83
82
|
onFocus={onFocus}
|
|
84
83
|
onKeyDown={onKeyDown}
|
|
85
84
|
/>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { BLANK } from '@scrabble-solver/constants';
|
|
2
|
+
import { Config } from '@scrabble-solver/types';
|
|
3
|
+
|
|
4
|
+
const extractCharacters = (config: Config, value: string): string[] => {
|
|
5
|
+
let index = 0;
|
|
6
|
+
const characters: string[] = [];
|
|
7
|
+
|
|
8
|
+
while (index < value.length) {
|
|
9
|
+
const character = value[index];
|
|
10
|
+
const nextCharacter = value[index + 1];
|
|
11
|
+
const twoCharacterTileCandidate = `${character}${nextCharacter}`;
|
|
12
|
+
|
|
13
|
+
if (config.twoCharacterTiles.includes(twoCharacterTileCandidate)) {
|
|
14
|
+
characters.push(twoCharacterTileCandidate);
|
|
15
|
+
++index;
|
|
16
|
+
} else if (config.hasCharacter(character) || character === BLANK) {
|
|
17
|
+
characters.push(character);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
++index;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return characters;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default extractCharacters;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const extractInputValue = (input: HTMLInputElement) => {
|
|
2
|
+
const value = input.value.toLocaleLowerCase();
|
|
3
|
+
|
|
4
|
+
if (input.selectionStart === null || input.selectionEnd === null) {
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const index = Math.min(input.selectionStart, input.selectionEnd);
|
|
9
|
+
|
|
10
|
+
if (index > 0) {
|
|
11
|
+
return value.substring(index - 1, index);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return value;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default extractInputValue;
|
package/src/lib/index.ts
CHANGED
|
@@ -6,6 +6,8 @@ export { default as createKeyboardNavigation } from './createKeyboardNavigation'
|
|
|
6
6
|
export { default as createKeyComparator } from './createKeyComparator';
|
|
7
7
|
export { default as createNullMovingComparator } from './createNullMovingComparator';
|
|
8
8
|
export { default as detectLocale } from './detectLocale';
|
|
9
|
+
export { default as extractCharacters } from './extractCharacters';
|
|
10
|
+
export { default as extractInputValue } from './extractInputValue';
|
|
9
11
|
export { default as findCell } from './findCell';
|
|
10
12
|
export { default as getCellSize } from './getCellSize';
|
|
11
13
|
export { default as getRemainingTiles } from './getRemainingTiles';
|
package/src/lib/isCtrl.ts
CHANGED
package/src/lib/memoize.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
interface AnyFunction {
|
|
2
|
-
(...parameters: any[]): any
|
|
2
|
+
(...parameters: any[]): any | Promise<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
3
3
|
}
|
|
4
4
|
|
|
5
5
|
interface AnyCachedFunction<T extends AnyFunction> extends AnyFunction {
|
|
@@ -20,6 +20,14 @@ const memoize = <T extends AnyFunction>(fn: T): AnyCachedFunction<T> => {
|
|
|
20
20
|
return cache.find((entry) => parametersEqual(entry.parameters, parameters))?.result;
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
const removeCache = (parameters: Parameters<T>): void => {
|
|
24
|
+
const index = cache.findIndex((entry) => parametersEqual(entry.parameters, parameters));
|
|
25
|
+
|
|
26
|
+
if (index >= 0) {
|
|
27
|
+
cache.splice(index, 1);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
23
31
|
const writeCache = (parameters: Parameters<T>, result: ReturnType<T>): void => {
|
|
24
32
|
cache.push({ parameters, result });
|
|
25
33
|
};
|
|
@@ -33,6 +41,12 @@ const memoize = <T extends AnyFunction>(fn: T): AnyCachedFunction<T> => {
|
|
|
33
41
|
|
|
34
42
|
const result = fn(...parameters);
|
|
35
43
|
|
|
44
|
+
if (result instanceof Promise) {
|
|
45
|
+
result.catch(() => {
|
|
46
|
+
removeCache(parameters);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
writeCache(parameters, result);
|
|
37
51
|
|
|
38
52
|
return result;
|
package/src/pages/api/solve.ts
CHANGED
|
@@ -37,7 +37,7 @@ const solve = async (request: NextApiRequest, response: NextApiResponse): Promis
|
|
|
37
37
|
const trie = await dictionaries.get(locale);
|
|
38
38
|
const tiles = characters.map((character) => new Tile({ character, isBlank: character === BLANK }));
|
|
39
39
|
const results = solveScrabble(trie, config, board, tiles);
|
|
40
|
-
response.status(200).send(results
|
|
40
|
+
response.status(200).send(results);
|
|
41
41
|
} catch (error) {
|
|
42
42
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
43
43
|
logger.error('solve - error', { error, meta });
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { isError } from '@scrabble-solver/types';
|
|
2
|
+
|
|
3
|
+
const fetchJson = async <T>(input: RequestInfo | URL, init?: RequestInit): Promise<T> => {
|
|
4
|
+
let response: Response;
|
|
5
|
+
|
|
6
|
+
try {
|
|
7
|
+
response = await fetch(input, {
|
|
8
|
+
...init,
|
|
9
|
+
headers: {
|
|
10
|
+
'Content-Type': 'application/json',
|
|
11
|
+
...init?.headers,
|
|
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}`);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default fetchJson;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { Locale, WordDefinition } from '@scrabble-solver/types';
|
|
1
|
+
import { Locale, WordDefinition, WordDefinitionJson } from '@scrabble-solver/types';
|
|
2
|
+
|
|
3
|
+
import fetchJson from './fetchJson';
|
|
2
4
|
|
|
3
5
|
const findWordDefinitions = async (locale: Locale, word: string): Promise<WordDefinition[]> => {
|
|
4
|
-
const
|
|
5
|
-
const json = await fetch(url).then((response) => response.json());
|
|
6
|
+
const json = await fetchJson<WordDefinitionJson[]>(`/api/dictionary/${locale}/${word}`);
|
|
6
7
|
return json.map(WordDefinition.fromJson);
|
|
7
8
|
};
|
|
8
9
|
|
package/src/sdk/solve.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { BoardJson, Locale, ResultJson } from '@scrabble-solver/types';
|
|
1
|
+
import { BoardJson, Locale, Result, ResultJson } from '@scrabble-solver/types';
|
|
2
|
+
|
|
3
|
+
import fetchJson from './fetchJson';
|
|
2
4
|
|
|
3
5
|
interface Payload {
|
|
4
6
|
board: BoardJson;
|
|
@@ -7,14 +9,13 @@ interface Payload {
|
|
|
7
9
|
locale: Locale;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
|
-
const solve = ({ board, characters, configId, locale }: Payload): Promise<
|
|
11
|
-
|
|
12
|
+
const solve = async ({ board, characters, configId, locale }: Payload): Promise<Result[]> => {
|
|
13
|
+
const json = await fetchJson<ResultJson[]>('/api/solve', {
|
|
12
14
|
method: 'POST',
|
|
13
|
-
headers: {
|
|
14
|
-
'Content-Type': 'application/json',
|
|
15
|
-
},
|
|
16
15
|
body: JSON.stringify({ board, characters, configId, locale }),
|
|
17
|
-
})
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return json.map(Result.fromJson);
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
export default solve;
|
package/src/sdk/verify.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { BoardJson, Locale } from '@scrabble-solver/types';
|
|
2
2
|
|
|
3
|
+
import fetchJson from './fetchJson';
|
|
4
|
+
|
|
3
5
|
interface Payload {
|
|
4
6
|
board: BoardJson;
|
|
5
7
|
configId: string;
|
|
@@ -11,14 +13,11 @@ interface Response {
|
|
|
11
13
|
validWords: string[];
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
const verify = ({ board, configId, locale }: Payload): Promise<Response> => {
|
|
15
|
-
return
|
|
16
|
+
const verify = async ({ board, configId, locale }: Payload): Promise<Response> => {
|
|
17
|
+
return fetchJson<Response>('/api/verify', {
|
|
16
18
|
method: 'POST',
|
|
17
|
-
headers: {
|
|
18
|
-
'Content-Type': 'application/json',
|
|
19
|
-
},
|
|
20
19
|
body: JSON.stringify({ board, configId, locale }),
|
|
21
|
-
})
|
|
20
|
+
});
|
|
22
21
|
};
|
|
23
22
|
|
|
24
23
|
export default verify;
|
package/src/state/sagas.ts
CHANGED
|
@@ -86,7 +86,7 @@ function* onDictionarySubmit(): AnyGenerator {
|
|
|
86
86
|
const wordDefinitions = yield call(memoizedFindWordDefinitions, locale, word);
|
|
87
87
|
yield put(dictionarySlice.actions.submitSuccess(wordDefinitions));
|
|
88
88
|
} catch (error) {
|
|
89
|
-
yield put(dictionarySlice.actions.submitFailure());
|
|
89
|
+
yield put(dictionarySlice.actions.submitFailure(error));
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
@@ -137,11 +137,11 @@ function* onSolve(): AnyGenerator {
|
|
|
137
137
|
configId: config.id,
|
|
138
138
|
locale,
|
|
139
139
|
});
|
|
140
|
-
yield put(resultsSlice.actions.changeResults(results
|
|
140
|
+
yield put(resultsSlice.actions.changeResults(results));
|
|
141
141
|
yield put(solveSlice.actions.submitSuccess({ board, characters }));
|
|
142
142
|
} catch (error) {
|
|
143
143
|
yield put(resultsSlice.actions.changeResults([]));
|
|
144
|
-
yield put(solveSlice.actions.submitFailure());
|
|
144
|
+
yield put(solveSlice.actions.submitFailure(error));
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
|
package/src/state/selectors.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createSelector } from '@reduxjs/toolkit';
|
|
2
2
|
import { getLocaleConfig } from '@scrabble-solver/configs';
|
|
3
|
-
import { Cell, Config, Result, Tile } from '@scrabble-solver/types';
|
|
3
|
+
import { Cell, Config, isError, Result, Tile } from '@scrabble-solver/types';
|
|
4
4
|
|
|
5
5
|
import i18n from 'i18n';
|
|
6
6
|
import { findCell, getRemainingTiles, getRemainingTilesGroups, sortResults, unorderedArraysEqual } from 'lib';
|
|
@@ -34,6 +34,10 @@ const selectVerifyRoot = (state: RootState): RootState['verify'] => state.verify
|
|
|
34
34
|
|
|
35
35
|
export const selectDictionary = selectDictionaryRoot;
|
|
36
36
|
|
|
37
|
+
export const selectDictionaryError = createSelector([selectDictionaryRoot], (dictionary) => {
|
|
38
|
+
return isError(dictionary.error) ? dictionary.error : undefined;
|
|
39
|
+
});
|
|
40
|
+
|
|
37
41
|
export const selectAutoGroupTiles = createSelector([selectSettingsRoot], (settings) => settings.autoGroupTiles);
|
|
38
42
|
|
|
39
43
|
export const selectLocale = createSelector([selectSettingsRoot], (settings) => settings.locale);
|
|
@@ -158,6 +162,10 @@ export const selectLastSolvedParameters = createSelector([selectSolveRoot], (sol
|
|
|
158
162
|
|
|
159
163
|
export const selectIsLoading = createSelector([selectSolveRoot], (solve) => solve.isLoading);
|
|
160
164
|
|
|
165
|
+
export const selectSolveError = createSelector([selectSolveRoot], (solve) => {
|
|
166
|
+
return isError(solve.error) ? solve.error : undefined;
|
|
167
|
+
});
|
|
168
|
+
|
|
161
169
|
export const selectHaveCharactersChanged = createSelector(
|
|
162
170
|
[selectLastSolvedParameters, selectCharacters],
|
|
163
171
|
(lastSolvedParameters, characters) => {
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { WordDefinition } from '@scrabble-solver/types';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
interface DictionaryInitialState {
|
|
4
|
+
error: unknown | undefined;
|
|
5
|
+
input: string;
|
|
6
|
+
isLoading: boolean;
|
|
7
|
+
results: WordDefinition[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const dictionaryInitialState: DictionaryInitialState = {
|
|
11
|
+
error: undefined,
|
|
4
12
|
input: '',
|
|
5
13
|
isLoading: false,
|
|
6
|
-
results: []
|
|
14
|
+
results: [],
|
|
7
15
|
};
|
|
8
16
|
|
|
9
17
|
export default dictionaryInitialState;
|