@scrabble-solver/scrabble-solver 2.8.8 → 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 +12 -12
- 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 +350 -73
- package/.next/server/chunks/429.js +2 -13
- package/.next/server/chunks/44.js +802 -0
- package/.next/server/chunks/515.js +767 -322
- package/.next/server/chunks/911.js +77 -25
- 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 +226 -927
- package/.next/server/pages/api/solve.js.nft.json +1 -1
- package/.next/server/pages/api/verify.js +217 -0
- package/.next/server/pages/api/verify.js.nft.json +1 -0
- package/.next/server/pages/index.html +3 -3
- package/.next/server/pages/index.js +8 -2
- 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/A8A_Lmg8cM-Bkf-Jo1CLh/_buildManifest.js +1 -0
- package/.next/static/{z3J3qmq1nazbDv_ENIkCo → 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/{404-8eb3ba4f0ba17e08.js → 404-90c624da3c83fd17.js} +1 -1
- 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/static/css/{cdbc9e0afcff5473.css → ad2a08918868cad8.css} +1 -1
- package/.next/trace +42 -41
- package/package.json +12 -12
- package/src/components/Badge/Badge.module.scss +13 -0
- package/src/components/Badge/Badge.tsx +15 -0
- package/src/components/Badge/index.ts +1 -0
- 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/NavButtons/NavButtons.tsx +33 -14
- package/src/components/Rack/Rack.tsx +51 -11
- package/src/components/Rack/RackTile.tsx +33 -16
- package/src/components/RemainingTiles/RemainingTiles.module.scss +8 -7
- package/src/components/RemainingTiles/RemainingTiles.tsx +13 -4
- package/src/components/Results/Results.tsx +19 -3
- package/src/components/Sidebar/Sidebar.tsx +2 -2
- package/src/components/Sidebar/components/Section/Section.module.scss +0 -1
- package/src/components/Sidebar/components/Section/Section.tsx +1 -1
- package/src/components/SquareButton/Link.tsx +1 -1
- package/src/components/SquareButton/SquareButton.module.scss +5 -0
- 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/components/Words/Words.module.scss +35 -0
- package/src/components/Words/Words.tsx +57 -0
- package/src/components/Words/index.ts +1 -0
- package/src/components/index.ts +2 -0
- package/src/i18n/de.json +4 -1
- package/src/i18n/en.json +4 -1
- package/src/i18n/es.json +4 -1
- package/src/i18n/fr.json +4 -1
- package/src/i18n/pl.json +4 -1
- package/src/icons/BookHalf.svg +4 -0
- package/src/icons/Check.svg +4 -0
- package/src/icons/Cross.svg +2 -2
- package/src/icons/CrossFill.svg +4 -0
- package/src/icons/index.ts +3 -0
- 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 +3 -4
- package/src/pages/api/verify.ts +71 -0
- package/src/pages/index.tsx +5 -0
- package/src/sdk/fetchJson.ts +36 -0
- package/src/sdk/findWordDefinitions.ts +4 -3
- package/src/sdk/index.ts +1 -0
- package/src/sdk/solve.ts +8 -7
- package/src/sdk/verify.ts +23 -0
- package/src/state/rootReducer.ts +2 -0
- package/src/state/sagas.ts +35 -8
- package/src/state/selectors.ts +17 -1
- package/src/state/slices/dictionaryInitialState.ts +10 -2
- package/src/state/slices/dictionarySlice.ts +10 -16
- package/src/state/slices/index.ts +2 -0
- 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/state/slices/verifyInitialState.ts +12 -0
- package/src/state/slices/verifySlice.ts +31 -0
- package/src/styles/variables.scss +2 -1
- package/src/types/index.ts +6 -1
- package/.next/static/chunks/615-d258f6c528c18622.js +0 -1
- package/.next/static/chunks/758-f333b1dcdb941547.js +0 -1
- package/.next/static/chunks/pages/_app-4a663fd3d5ca4524.js +0 -1
- package/.next/static/chunks/pages/index-1a9826d740cc8830.js +0 -1
- package/.next/static/css/180c6c26317ac90f.css +0 -1
- package/.next/static/z3J3qmq1nazbDv_ENIkCo/_buildManifest.js +0 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import classNames from 'classnames';
|
|
2
2
|
import { FunctionComponent } from 'react';
|
|
3
3
|
|
|
4
|
-
import { Cog, Eraser, Github, Keyboard, Sack } from 'icons';
|
|
4
|
+
import { BookHalf, Cog, Eraser, Github, Keyboard, Sack } from 'icons';
|
|
5
5
|
import { GITHUB_PROJECT_URL } from 'parameters';
|
|
6
|
-
import { selectHasOverusedTiles, useTranslate, useTypedSelector } from 'state';
|
|
6
|
+
import { selectHasInvalidWords, selectHasOverusedTiles, useTranslate, useTypedSelector } from 'state';
|
|
7
7
|
|
|
8
8
|
import SquareButton from '../SquareButton';
|
|
9
9
|
|
|
@@ -14,27 +14,26 @@ interface Props {
|
|
|
14
14
|
onShowKeyMap: () => void;
|
|
15
15
|
onShowRemainingTiles: () => void;
|
|
16
16
|
onShowSettings: () => void;
|
|
17
|
+
onShowWords: () => void;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
const NavButtons: FunctionComponent<Props> = ({
|
|
20
|
+
const NavButtons: FunctionComponent<Props> = ({
|
|
21
|
+
onClear,
|
|
22
|
+
onShowKeyMap,
|
|
23
|
+
onShowRemainingTiles,
|
|
24
|
+
onShowSettings,
|
|
25
|
+
onShowWords,
|
|
26
|
+
}) => {
|
|
20
27
|
const translate = useTranslate();
|
|
21
28
|
const hasOverusedTiles = useTypedSelector(selectHasOverusedTiles);
|
|
29
|
+
const hasInvalidWords = useTypedSelector(selectHasInvalidWords);
|
|
22
30
|
|
|
23
31
|
return (
|
|
24
32
|
<div className={styles.navButtons}>
|
|
25
|
-
<SquareButton.
|
|
26
|
-
className={styles.button}
|
|
27
|
-
href={GITHUB_PROJECT_URL}
|
|
28
|
-
Icon={Github}
|
|
29
|
-
rel="noopener noreferrer"
|
|
30
|
-
target="_blank"
|
|
31
|
-
tooltip={translate('github')}
|
|
32
|
-
/>
|
|
33
|
+
<SquareButton className={styles.button} Icon={Eraser} tooltip={translate('common.clear')} onClick={onClear} />
|
|
33
34
|
|
|
34
35
|
<div className={styles.separator} />
|
|
35
36
|
|
|
36
|
-
<SquareButton className={styles.button} Icon={Keyboard} tooltip={translate('keyMap')} onClick={onShowKeyMap} />
|
|
37
|
-
|
|
38
37
|
<SquareButton
|
|
39
38
|
className={classNames(styles.button, {
|
|
40
39
|
[styles.error]: hasOverusedTiles,
|
|
@@ -44,9 +43,29 @@ const NavButtons: FunctionComponent<Props> = ({ onClear, onShowKeyMap, onShowRem
|
|
|
44
43
|
onClick={onShowRemainingTiles}
|
|
45
44
|
/>
|
|
46
45
|
|
|
46
|
+
<SquareButton
|
|
47
|
+
className={classNames(styles.button, {
|
|
48
|
+
[styles.error]: hasInvalidWords,
|
|
49
|
+
})}
|
|
50
|
+
Icon={BookHalf}
|
|
51
|
+
tooltip={translate('words')}
|
|
52
|
+
onClick={onShowWords}
|
|
53
|
+
/>
|
|
54
|
+
|
|
47
55
|
<div className={styles.separator} />
|
|
48
56
|
|
|
49
|
-
<SquareButton
|
|
57
|
+
<SquareButton.Link
|
|
58
|
+
className={styles.button}
|
|
59
|
+
href={GITHUB_PROJECT_URL}
|
|
60
|
+
Icon={Github}
|
|
61
|
+
rel="noopener noreferrer"
|
|
62
|
+
target="_blank"
|
|
63
|
+
tooltip={translate('github')}
|
|
64
|
+
/>
|
|
65
|
+
|
|
66
|
+
<div className={styles.separator} />
|
|
67
|
+
|
|
68
|
+
<SquareButton className={styles.button} Icon={Keyboard} tooltip={translate('keyMap')} onClick={onShowKeyMap} />
|
|
50
69
|
|
|
51
70
|
<SquareButton className={styles.button} Icon={Cog} tooltip={translate('settings')} onClick={onShowSettings} />
|
|
52
71
|
</div>
|
|
@@ -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,8 @@
|
|
|
1
|
-
.group {
|
|
2
|
-
margin-bottom: var(--spacing--l);
|
|
3
|
-
}
|
|
4
|
-
|
|
5
1
|
.title {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: space-between;
|
|
5
|
+
flex-wrap: wrap;
|
|
9
6
|
}
|
|
10
7
|
|
|
11
8
|
.content {
|
|
@@ -13,3 +10,7 @@
|
|
|
13
10
|
grid-template-columns: repeat(5, 1fr);
|
|
14
11
|
gap: var(--spacing--m);
|
|
15
12
|
}
|
|
13
|
+
|
|
14
|
+
.badge {
|
|
15
|
+
margin-left: var(--spacing--m);
|
|
16
|
+
}
|
|
@@ -2,6 +2,7 @@ import { FunctionComponent } from 'react';
|
|
|
2
2
|
|
|
3
3
|
import { selectRemainingTilesGroups, useTranslate, useTypedSelector } from 'state';
|
|
4
4
|
|
|
5
|
+
import Badge from '../Badge';
|
|
5
6
|
import Sidebar from '../Sidebar';
|
|
6
7
|
|
|
7
8
|
import Character from './Character';
|
|
@@ -20,15 +21,23 @@ const RemainingTiles: FunctionComponent<Props> = ({ className, isOpen, onClose }
|
|
|
20
21
|
return (
|
|
21
22
|
<Sidebar className={className} isOpen={isOpen} title={translate('remaining-tiles')} onClose={onClose}>
|
|
22
23
|
{groups.map(({ remainingCount, tiles, translationKey, totalCount }) => (
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
<Sidebar.Section
|
|
25
|
+
key={translationKey}
|
|
26
|
+
title={
|
|
27
|
+
<span className={styles.title}>
|
|
28
|
+
<span>{translate(translationKey)}</span>
|
|
29
|
+
<Badge className={styles.badge}>
|
|
30
|
+
{remainingCount} / {totalCount}
|
|
31
|
+
</Badge>
|
|
32
|
+
</span>
|
|
33
|
+
}
|
|
34
|
+
>
|
|
26
35
|
<div className={styles.content}>
|
|
27
36
|
{tiles.map((tile) => {
|
|
28
37
|
return <Character key={tile.character} tile={tile} />;
|
|
29
38
|
})}
|
|
30
39
|
</div>
|
|
31
|
-
</
|
|
40
|
+
</Sidebar.Section>
|
|
32
41
|
))}
|
|
33
42
|
</Sidebar>
|
|
34
43
|
);
|
|
@@ -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}
|
|
@@ -2,7 +2,7 @@ import classNames from 'classnames';
|
|
|
2
2
|
import { FunctionComponent, ReactNode } from 'react';
|
|
3
3
|
import Modal from 'react-modal';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { CrossFill } from 'icons';
|
|
6
6
|
import { TRANSITION_DURATION_LONG } from 'parameters';
|
|
7
7
|
import { useTranslate } from 'state';
|
|
8
8
|
|
|
@@ -41,7 +41,7 @@ const Sidebar: FunctionComponent<Props> = ({ children, className, isOpen, title,
|
|
|
41
41
|
|
|
42
42
|
<SquareButton
|
|
43
43
|
className={styles.closeButton}
|
|
44
|
-
Icon={
|
|
44
|
+
Icon={CrossFill}
|
|
45
45
|
tooltip={translate('common.close')}
|
|
46
46
|
onClick={onClose}
|
|
47
47
|
/>
|
|
@@ -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,35 @@
|
|
|
1
|
+
.title {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: space-between;
|
|
5
|
+
flex-wrap: wrap;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.badge {
|
|
9
|
+
margin-left: var(--spacing--m);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.word {
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
|
|
16
|
+
& + & {
|
|
17
|
+
margin-top: var(--spacing--s);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.invalid {
|
|
22
|
+
color: var(--color--error);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.valid {
|
|
26
|
+
color: var(--color--success);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.icon {
|
|
30
|
+
$size: 24px;
|
|
31
|
+
|
|
32
|
+
width: $size;
|
|
33
|
+
height: $size;
|
|
34
|
+
margin-right: var(--spacing--s);
|
|
35
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import { FunctionComponent } from 'react';
|
|
3
|
+
|
|
4
|
+
import { Check, Cross } from 'icons';
|
|
5
|
+
import { selectVerify, useTranslate, useTypedSelector } from 'state';
|
|
6
|
+
|
|
7
|
+
import Badge from '../Badge';
|
|
8
|
+
import Sidebar from '../Sidebar';
|
|
9
|
+
|
|
10
|
+
import styles from './Words.module.scss';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
className?: string;
|
|
14
|
+
isOpen: boolean;
|
|
15
|
+
onClose: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const Words: FunctionComponent<Props> = ({ className, isOpen, onClose }) => {
|
|
19
|
+
const translate = useTranslate();
|
|
20
|
+
const { invalidWords, validWords } = useTypedSelector(selectVerify);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Sidebar className={className} isOpen={isOpen} title={translate('words')} onClose={onClose}>
|
|
24
|
+
<Sidebar.Section
|
|
25
|
+
title={
|
|
26
|
+
<span className={styles.title}>
|
|
27
|
+
<span>{translate('words.invalid')}</span>
|
|
28
|
+
<Badge className={styles.badge}>{invalidWords.length}</Badge>
|
|
29
|
+
</span>
|
|
30
|
+
}
|
|
31
|
+
>
|
|
32
|
+
{invalidWords.map((word, index) => (
|
|
33
|
+
<div className={styles.word} key={index}>
|
|
34
|
+
<Cross className={classNames(styles.icon, styles.invalid)} /> {word}
|
|
35
|
+
</div>
|
|
36
|
+
))}
|
|
37
|
+
</Sidebar.Section>
|
|
38
|
+
|
|
39
|
+
<Sidebar.Section
|
|
40
|
+
title={
|
|
41
|
+
<span className={styles.title}>
|
|
42
|
+
<span>{translate('words.valid')}</span>
|
|
43
|
+
<Badge className={styles.badge}>{validWords.length}</Badge>
|
|
44
|
+
</span>
|
|
45
|
+
}
|
|
46
|
+
>
|
|
47
|
+
{validWords.map((word, index) => (
|
|
48
|
+
<div className={styles.word} key={index}>
|
|
49
|
+
<Check className={classNames(styles.icon, styles.valid)} /> {word}
|
|
50
|
+
</div>
|
|
51
|
+
))}
|
|
52
|
+
</Sidebar.Section>
|
|
53
|
+
</Sidebar>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export default Words;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './Words';
|
package/src/components/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { default as Badge } from './Badge';
|
|
1
2
|
export { default as Board } from './Board';
|
|
2
3
|
export { default as Button } from './Button';
|
|
3
4
|
export { default as Checkbox } from './Checkbox';
|
|
@@ -25,3 +26,4 @@ export { default as SvgFontCss } from './SvgFontCss';
|
|
|
25
26
|
export { default as Tile } from './Tile';
|
|
26
27
|
export { useTooltip } from './Tooltip';
|
|
27
28
|
export { default as Well } from './Well';
|
|
29
|
+
export { default as Words } from './Words';
|