@scrabble-solver/scrabble-solver 2.8.9 → 2.8.11
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 +11 -11
- 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 +189 -43
- package/.next/server/chunks/429.js +2 -13
- package/.next/server/chunks/515.js +469 -254
- 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/api/solve.js +77 -20
- 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/VJkrGviICslA_8zNVJ-g-/_buildManifest.js +1 -0
- package/.next/static/{yCxjzzYpw5JjJE53PO_s6 → VJkrGviICslA_8zNVJ-g-}/_ssgManifest.js +0 -0
- package/.next/static/chunks/317-8e8909dd2f587b64.js +1 -0
- package/.next/static/chunks/546-447e243fc9de2c59.js +1 -0
- package/.next/static/chunks/pages/{404-90c624da3c83fd17.js → 404-7082923654d5996f.js} +1 -1
- package/.next/static/chunks/pages/_app-57c77cad0f197d93.js +1 -0
- package/.next/static/chunks/pages/index-d3360e075ca3c222.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/components/Cell/CellPure.tsx +33 -31
- package/src/components/Board/hooks/useGrid.ts +217 -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/Sidebar/Sidebar.tsx +20 -1
- 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 +9 -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 -5
- 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}
|
|
@@ -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)}>
|
|
@@ -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;
|