@scrabble-solver/scrabble-solver 2.8.6 → 2.8.8
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 +266 -110
- package/.next/server/chunks/{206.js → 429.js} +2 -4137
- package/.next/server/chunks/515.js +197 -91
- package/.next/server/chunks/{907.js → 911.js} +134 -367
- package/.next/server/chunks/939.js +218 -0
- 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/dictionary/[locale]/[word].js +33 -17
- package/.next/server/pages/api/dictionary/[locale]/[word].js.nft.json +1 -1
- package/.next/server/pages/api/solve.js +399 -56
- package/.next/server/pages/api/solve.js.nft.json +1 -1
- package/.next/server/pages/api/visit.js +3 -2
- package/.next/server/pages/api/visit.js.nft.json +1 -1
- package/.next/server/pages/index.html +3 -7
- package/.next/server/pages/index.js +12 -14
- package/.next/server/pages/index.js.nft.json +1 -1
- package/.next/server/pages/index.json +1 -1
- package/.next/static/chunks/615-d258f6c528c18622.js +1 -0
- package/.next/static/chunks/pages/{404-30c06e61d256c5b2.js → 404-8eb3ba4f0ba17e08.js} +1 -1
- package/.next/static/chunks/pages/_app-4a663fd3d5ca4524.js +1 -0
- package/.next/static/chunks/pages/index-1a9826d740cc8830.js +1 -0
- package/.next/static/css/180c6c26317ac90f.css +1 -0
- package/.next/static/css/751e8a14776d05d8.css +1 -0
- package/.next/static/z3J3qmq1nazbDv_ENIkCo/_buildManifest.js +1 -0
- package/.next/static/{VjSpyGDWyVaO0muz54q_j → z3J3qmq1nazbDv_ENIkCo}/_ssgManifest.js +0 -0
- package/.next/trace +41 -42
- package/package.json +9 -9
- package/src/api/index.ts +3 -9
- package/src/api/isBoardValid.ts +43 -0
- package/src/api/isCellValid.ts +26 -0
- package/src/api/isRowValid.ts +19 -0
- package/src/components/Board/components/Cell/Cell.module.scss +34 -9
- package/src/components/Board/components/Cell/Cell.tsx +23 -4
- package/src/components/Board/components/Cell/CellPure.tsx +29 -1
- package/src/components/Board/hooks/useGrid.ts +1 -0
- package/src/components/Dictionary/Dictionary.module.scss +20 -0
- package/src/components/Dictionary/Dictionary.tsx +40 -29
- package/src/components/Results/Cell.tsx +3 -2
- package/src/components/Results/Result.tsx +16 -6
- package/src/components/ResultsInput/ResultsInput.tsx +11 -3
- package/src/hooks/useIsTablet.ts +2 -2
- package/src/i18n/de.json +1 -0
- package/src/i18n/en.json +1 -0
- package/src/i18n/es.json +1 -0
- package/src/i18n/fr.json +1 -0
- package/src/i18n/pl.json +1 -0
- package/src/icons/Flag.svg +4 -0
- package/src/icons/Star.svg +4 -0
- package/src/icons/index.ts +2 -0
- package/src/lib/getRemainingTiles.ts +1 -1
- package/src/lib/index.ts +2 -1
- package/src/lib/isRegExp.ts +11 -0
- package/src/lib/isStringArray.ts +5 -0
- package/src/lib/sortResults.ts +5 -5
- package/src/pages/api/dictionary/[locale]/[word].ts +35 -11
- package/src/pages/api/solve.ts +39 -19
- package/src/pages/api/visit.ts +1 -0
- package/src/pages/index.module.scss +5 -11
- package/src/pages/index.tsx +5 -5
- package/src/sdk/{findWordDefinition.ts → findWordDefinitions.ts} +3 -3
- package/src/sdk/index.ts +1 -1
- package/src/state/rootReducer.ts +10 -1
- package/src/state/sagas.ts +32 -12
- package/src/state/selectors.ts +41 -7
- package/src/state/slices/cellFilterInitialState.ts +7 -0
- package/src/state/slices/cellFilterSlice.ts +24 -0
- package/src/state/slices/dictionaryInitialState.ts +3 -3
- package/src/state/slices/dictionarySlice.ts +4 -10
- package/src/state/slices/index.ts +2 -0
- package/src/types/index.ts +1 -0
- package/.next/static/VjSpyGDWyVaO0muz54q_j/_buildManifest.js +0 -1
- package/.next/static/chunks/56-e2797384ae4b0fc0.js +0 -1
- package/.next/static/chunks/pages/_app-5136d33b9b007fd7.js +0 -1
- package/.next/static/chunks/pages/index-13ea7770a65c69ee.js +0 -1
- package/.next/static/css/3159cfe62ff742a3.css +0 -1
- package/.next/static/css/729bb37fe8f9bee6.css +0 -1
- package/src/api/validateBoard.ts +0 -45
- package/src/api/validateCell.ts +0 -40
- package/src/api/validateCharacter.ts +0 -14
- package/src/api/validateCharacters.ts +0 -24
- package/src/api/validateConfigId.ts +0 -9
- package/src/api/validateLocale.ts +0 -15
- package/src/api/validateRow.ts +0 -17
- package/src/api/validateTile.ts +0 -21
- package/src/api/validateWord.ts +0 -11
- package/src/lib/isLocale.ts +0 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scrabble-solver/scrabble-solver",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.8",
|
|
4
4
|
"description": "Scrabble Solver 2 - App",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=16"
|
|
@@ -30,13 +30,13 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@popperjs/core": "^2.11.6",
|
|
32
32
|
"@reduxjs/toolkit": "^1.8.5",
|
|
33
|
-
"@scrabble-solver/configs": "^2.8.
|
|
34
|
-
"@scrabble-solver/constants": "^2.8.
|
|
35
|
-
"@scrabble-solver/dictionaries": "^2.8.
|
|
36
|
-
"@scrabble-solver/logger": "^2.8.
|
|
37
|
-
"@scrabble-solver/solver": "^2.8.
|
|
38
|
-
"@scrabble-solver/types": "^2.8.
|
|
39
|
-
"@scrabble-solver/word-definitions": "^2.8.
|
|
33
|
+
"@scrabble-solver/configs": "^2.8.8",
|
|
34
|
+
"@scrabble-solver/constants": "^2.8.8",
|
|
35
|
+
"@scrabble-solver/dictionaries": "^2.8.8",
|
|
36
|
+
"@scrabble-solver/logger": "^2.8.8",
|
|
37
|
+
"@scrabble-solver/solver": "^2.8.8",
|
|
38
|
+
"@scrabble-solver/types": "^2.8.8",
|
|
39
|
+
"@scrabble-solver/word-definitions": "^2.8.8",
|
|
40
40
|
"classnames": "^2.3.2",
|
|
41
41
|
"next": "^12.3.1",
|
|
42
42
|
"normalize.css": "^8.0.1",
|
|
@@ -68,5 +68,5 @@
|
|
|
68
68
|
"env-cmd": "^10.1.0",
|
|
69
69
|
"sass": "^1.55.0"
|
|
70
70
|
},
|
|
71
|
-
"gitHead": "
|
|
71
|
+
"gitHead": "e6ced6e98204e49ba69750242643bda52a453635"
|
|
72
72
|
}
|
package/src/api/index.ts
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
export { default as getServerLoggingData } from './getServerLoggingData';
|
|
2
|
-
export { default as
|
|
3
|
-
export { default as
|
|
4
|
-
export { default as
|
|
5
|
-
export { default as validateCharacters } from './validateCharacters';
|
|
6
|
-
export { default as validateConfigId } from './validateConfigId';
|
|
7
|
-
export { default as validateLocale } from './validateLocale';
|
|
8
|
-
export { default as validateRow } from './validateRow';
|
|
9
|
-
export { default as validateTile } from './validateTile';
|
|
10
|
-
export { default as validateWord } from './validateWord';
|
|
2
|
+
export { default as isBoardValid } from './isBoardValid';
|
|
3
|
+
export { default as isCellValid } from './isCellValid';
|
|
4
|
+
export { default as isRowValid } from './isRowValid';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { BoardJson, CellJson, Config } from '@scrabble-solver/types';
|
|
2
|
+
|
|
3
|
+
import isRowValid from './isRowValid';
|
|
4
|
+
|
|
5
|
+
const isBoardValid = (board: BoardJson, config: Config): boolean => {
|
|
6
|
+
if (board.length !== config.boardHeight) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
for (const row of board) {
|
|
11
|
+
if (!isRowValid(row, config)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return areTwoCharacterTilesValid(board, config);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const areTwoCharacterTilesValid = (board: CellJson[][], config: Config): boolean => {
|
|
20
|
+
const cells: CellJson[] = board
|
|
21
|
+
.flat()
|
|
22
|
+
.filter((cell) => cell && cell.tile && config.isTwoCharacterTilePrefix(cell.tile.character));
|
|
23
|
+
|
|
24
|
+
for (const cell of cells) {
|
|
25
|
+
for (const characters of config.twoCharacterTiles) {
|
|
26
|
+
const canCheckDown = cell.y + 1 < board.length;
|
|
27
|
+
const canCheckRight = cell.x + 1 < board[0].length;
|
|
28
|
+
const cellDown = board[cell.y + 1][cell.x];
|
|
29
|
+
const cellRight = board[cell.y][cell.x + 1];
|
|
30
|
+
const collidesDown = canCheckDown && cellDown.tile && cellDown.tile.character === characters[1];
|
|
31
|
+
const collidesRight = canCheckRight && cellRight.tile && cellRight.tile.character === characters[1];
|
|
32
|
+
const collides = collidesDown || collidesRight;
|
|
33
|
+
|
|
34
|
+
if (cell.tile && characters.startsWith(cell.tile.character) && collides) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return true;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default isBoardValid;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { BLANK } from '@scrabble-solver/constants';
|
|
2
|
+
import { CellJson, Config } from '@scrabble-solver/types';
|
|
3
|
+
|
|
4
|
+
const isCellValid = (cell: CellJson, config: Config): boolean => {
|
|
5
|
+
const { isEmpty, tile, x, y } = cell;
|
|
6
|
+
|
|
7
|
+
if (x < 0 || x >= config.boardWidth) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (y < 0 || y >= config.boardHeight) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (isEmpty && tile !== null) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (tile !== null && !config.hasCharacter(tile.character) && tile.character !== BLANK) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return true;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default isCellValid;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { CellJson, Config } from '@scrabble-solver/types';
|
|
2
|
+
|
|
3
|
+
import isCellValid from './isCellValid';
|
|
4
|
+
|
|
5
|
+
const isRowValid = (row: CellJson[], config: Config): boolean => {
|
|
6
|
+
if (row.length !== config.boardWidth) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
for (const cell of row) {
|
|
11
|
+
if (!isCellValid(cell, config)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return true;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default isRowValid;
|
|
@@ -13,7 +13,6 @@ $icon-size: 16px;
|
|
|
13
13
|
transition: var(--transition);
|
|
14
14
|
background-clip: padding-box;
|
|
15
15
|
|
|
16
|
-
&.bonusStart,
|
|
17
16
|
&.bonusWord2,
|
|
18
17
|
&.bonusWord3 {
|
|
19
18
|
position: relative;
|
|
@@ -33,12 +32,7 @@ $icon-size: 16px;
|
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
&.bonusStart {
|
|
36
|
-
background-color: var(--color--
|
|
37
|
-
|
|
38
|
-
&:before {
|
|
39
|
-
content: '★';
|
|
40
|
-
color: var(--color--foreground--secondary);
|
|
41
|
-
}
|
|
35
|
+
background-color: var(--color--violet--light);
|
|
42
36
|
}
|
|
43
37
|
|
|
44
38
|
&.bonusCharacter1 {
|
|
@@ -168,6 +162,7 @@ $icon-size: 16px;
|
|
|
168
162
|
}
|
|
169
163
|
|
|
170
164
|
&,
|
|
165
|
+
.filterCell,
|
|
171
166
|
.toggleDirection {
|
|
172
167
|
width: $icon-size;
|
|
173
168
|
height: $icon-size;
|
|
@@ -180,8 +175,6 @@ $icon-size: 16px;
|
|
|
180
175
|
}
|
|
181
176
|
|
|
182
177
|
.toggleDirection {
|
|
183
|
-
transition: var(--transition);
|
|
184
|
-
|
|
185
178
|
&.right {
|
|
186
179
|
transform: rotate(-90deg);
|
|
187
180
|
}
|
|
@@ -194,3 +187,35 @@ $icon-size: 16px;
|
|
|
194
187
|
font-weight: bold;
|
|
195
188
|
}
|
|
196
189
|
}
|
|
190
|
+
|
|
191
|
+
.filterCell {
|
|
192
|
+
color: var(--color--inactive);
|
|
193
|
+
|
|
194
|
+
&.filtered {
|
|
195
|
+
color: var(--color--foreground--secondary);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.iconContainer {
|
|
200
|
+
position: absolute;
|
|
201
|
+
top: 0;
|
|
202
|
+
right: 0;
|
|
203
|
+
bottom: 0;
|
|
204
|
+
left: 0;
|
|
205
|
+
display: flex;
|
|
206
|
+
align-items: center;
|
|
207
|
+
justify-content: center;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.flagContainer {
|
|
211
|
+
background-color: var(--color--primary);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.flag,
|
|
215
|
+
.star {
|
|
216
|
+
$size: 24px;
|
|
217
|
+
|
|
218
|
+
width: $size;
|
|
219
|
+
height: $size;
|
|
220
|
+
color: white;
|
|
221
|
+
}
|
|
@@ -4,7 +4,15 @@ import { FunctionComponent, RefObject, useCallback, useMemo } from 'react';
|
|
|
4
4
|
import { useDispatch } from 'react-redux';
|
|
5
5
|
|
|
6
6
|
import { getTileSizes } from 'lib';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
boardSlice,
|
|
9
|
+
cellFilterSlice,
|
|
10
|
+
selectCellBonus,
|
|
11
|
+
selectCellIsFiltered,
|
|
12
|
+
selectTilePoints,
|
|
13
|
+
useTranslate,
|
|
14
|
+
useTypedSelector,
|
|
15
|
+
} from 'state';
|
|
8
16
|
|
|
9
17
|
import CellPure from './CellPure';
|
|
10
18
|
|
|
@@ -34,10 +42,19 @@ const Cell: FunctionComponent<Props> = ({
|
|
|
34
42
|
const translate = useTranslate();
|
|
35
43
|
const bonus = useTypedSelector((state) => selectCellBonus(state, cell));
|
|
36
44
|
const points = useTypedSelector((state) => selectTilePoints(state, cell.tile));
|
|
45
|
+
const isFiltered = useTypedSelector((state) => selectCellIsFiltered(state, cell));
|
|
37
46
|
const { tileFontSize } = getTileSizes(size);
|
|
38
47
|
const isEmpty = tile.character === EMPTY_CELL;
|
|
39
48
|
const style = useMemo(() => ({ fontSize: tileFontSize }), [tileFontSize]);
|
|
40
49
|
|
|
50
|
+
const handleDirectionToggleClick = useCallback(() => {
|
|
51
|
+
if (inputRef.current) {
|
|
52
|
+
inputRef.current.focus();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
onDirectionToggle();
|
|
56
|
+
}, [onDirectionToggle]);
|
|
57
|
+
|
|
41
58
|
const handleFocus = useCallback(() => onFocus(x, y), [x, y, onFocus]);
|
|
42
59
|
|
|
43
60
|
const handleToggleBlankClick = useCallback(() => {
|
|
@@ -48,13 +65,13 @@ const Cell: FunctionComponent<Props> = ({
|
|
|
48
65
|
dispatch(boardSlice.actions.toggleCellIsBlank({ x, y }));
|
|
49
66
|
}, [dispatch, x, y]);
|
|
50
67
|
|
|
51
|
-
const
|
|
68
|
+
const handleToggleFilterCellClick = useCallback(() => {
|
|
52
69
|
if (inputRef.current) {
|
|
53
70
|
inputRef.current.focus();
|
|
54
71
|
}
|
|
55
72
|
|
|
56
|
-
|
|
57
|
-
}, [
|
|
73
|
+
dispatch(cellFilterSlice.actions.toggle({ x, y }));
|
|
74
|
+
}, [dispatch, x, y]);
|
|
58
75
|
|
|
59
76
|
return (
|
|
60
77
|
<CellPure
|
|
@@ -65,6 +82,7 @@ const Cell: FunctionComponent<Props> = ({
|
|
|
65
82
|
inputRef={inputRef}
|
|
66
83
|
isCenter={isCenter}
|
|
67
84
|
isEmpty={isEmpty}
|
|
85
|
+
isFiltered={isFiltered}
|
|
68
86
|
points={points}
|
|
69
87
|
size={size}
|
|
70
88
|
style={style}
|
|
@@ -73,6 +91,7 @@ const Cell: FunctionComponent<Props> = ({
|
|
|
73
91
|
onDirectionToggleClick={handleDirectionToggleClick}
|
|
74
92
|
onFocus={handleFocus}
|
|
75
93
|
onToggleBlankClick={handleToggleBlankClick}
|
|
94
|
+
onToggleFilterCellClick={handleToggleFilterCellClick}
|
|
76
95
|
/>
|
|
77
96
|
);
|
|
78
97
|
};
|
|
@@ -2,7 +2,7 @@ import { Bonus, Cell, Tile as TileModel } from '@scrabble-solver/types';
|
|
|
2
2
|
import classNames from 'classnames';
|
|
3
3
|
import { CSSProperties, FocusEventHandler, FunctionComponent, memo, MouseEventHandler, RefObject } from 'react';
|
|
4
4
|
|
|
5
|
-
import { ArrowDown } from 'icons';
|
|
5
|
+
import { ArrowDown, Flag, Star } from 'icons';
|
|
6
6
|
import { Translate } from 'types';
|
|
7
7
|
|
|
8
8
|
import Tile from '../../../Tile';
|
|
@@ -19,6 +19,7 @@ interface Props {
|
|
|
19
19
|
inputRef: RefObject<HTMLInputElement>;
|
|
20
20
|
isCenter: boolean;
|
|
21
21
|
isEmpty: boolean;
|
|
22
|
+
isFiltered: boolean;
|
|
22
23
|
points?: number;
|
|
23
24
|
size: number;
|
|
24
25
|
style?: CSSProperties;
|
|
@@ -27,6 +28,7 @@ interface Props {
|
|
|
27
28
|
onDirectionToggleClick: MouseEventHandler<HTMLButtonElement>;
|
|
28
29
|
onFocus: FocusEventHandler<HTMLInputElement>;
|
|
29
30
|
onToggleBlankClick: MouseEventHandler<HTMLButtonElement>;
|
|
31
|
+
onToggleFilterCellClick: MouseEventHandler<HTMLButtonElement>;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
const CellPure: FunctionComponent<Props> = ({
|
|
@@ -37,6 +39,7 @@ const CellPure: FunctionComponent<Props> = ({
|
|
|
37
39
|
inputRef,
|
|
38
40
|
isCenter,
|
|
39
41
|
isEmpty,
|
|
42
|
+
isFiltered,
|
|
40
43
|
points,
|
|
41
44
|
size,
|
|
42
45
|
style,
|
|
@@ -45,6 +48,7 @@ const CellPure: FunctionComponent<Props> = ({
|
|
|
45
48
|
onDirectionToggleClick,
|
|
46
49
|
onFocus,
|
|
47
50
|
onToggleBlankClick,
|
|
51
|
+
onToggleFilterCellClick,
|
|
48
52
|
}) => (
|
|
49
53
|
<div
|
|
50
54
|
className={classNames(styles.cell, getBonusClassname(cell, bonus, isCenter), className, {
|
|
@@ -52,6 +56,18 @@ const CellPure: FunctionComponent<Props> = ({
|
|
|
52
56
|
})}
|
|
53
57
|
style={style}
|
|
54
58
|
>
|
|
59
|
+
{isCenter && (
|
|
60
|
+
<div className={classNames(styles.iconContainer)}>
|
|
61
|
+
<Star className={styles.star} />
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
{isFiltered && (
|
|
66
|
+
<div className={classNames(styles.iconContainer, styles.flagContainer)}>
|
|
67
|
+
<Flag className={styles.flag} />
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
|
|
55
71
|
<Tile
|
|
56
72
|
className={styles.tile}
|
|
57
73
|
character={isEmpty ? undefined : tile.character}
|
|
@@ -74,6 +90,18 @@ const CellPure: FunctionComponent<Props> = ({
|
|
|
74
90
|
/>
|
|
75
91
|
</Button>
|
|
76
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 />
|
|
102
|
+
</Button>
|
|
103
|
+
)}
|
|
104
|
+
|
|
77
105
|
{!isEmpty && (
|
|
78
106
|
<Button
|
|
79
107
|
className={classNames(styles.blank, {
|
|
@@ -7,6 +7,26 @@
|
|
|
7
7
|
font-size: var(--font--size--s);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
.result {
|
|
11
|
+
transition: var(--transition);
|
|
12
|
+
|
|
13
|
+
&.isAllowed {
|
|
14
|
+
& + & {
|
|
15
|
+
.content {
|
|
16
|
+
padding-top: 0;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
&.isNotAllowed {
|
|
22
|
+
& + & {
|
|
23
|
+
.content {
|
|
24
|
+
padding-top: 0;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
10
30
|
.content {
|
|
11
31
|
padding: var(--spacing--l);
|
|
12
32
|
}
|
|
@@ -14,44 +14,55 @@ interface Props {
|
|
|
14
14
|
|
|
15
15
|
const Dictionary: FunctionComponent<Props> = ({ className }) => {
|
|
16
16
|
const translate = useTranslate();
|
|
17
|
-
const {
|
|
17
|
+
const { results, isLoading } = useTypedSelector(selectDictionary);
|
|
18
|
+
const isFirstAllowed = results.length > 0 ? results[0].isAllowed : undefined;
|
|
18
19
|
|
|
19
20
|
return (
|
|
20
21
|
<div
|
|
21
22
|
className={classNames(styles.dictionary, className, {
|
|
22
|
-
[styles.isAllowed]:
|
|
23
|
-
[styles.isNotAllowed]:
|
|
23
|
+
[styles.isAllowed]: isFirstAllowed === true,
|
|
24
|
+
[styles.isNotAllowed]: isFirstAllowed === false,
|
|
24
25
|
})}
|
|
25
26
|
>
|
|
26
|
-
{
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
{
|
|
35
|
-
|
|
36
|
-
{isAllowed === true && (
|
|
37
|
-
<>
|
|
38
|
-
{definitions.length === 0 && <div>{translate('dictionary.empty-state.no-definitions')}</div>}
|
|
39
|
-
|
|
40
|
-
{definitions.length > 0 && (
|
|
41
|
-
<ul className={styles.definitions}>
|
|
42
|
-
{definitions.map((result, index) => (
|
|
43
|
-
<li key={index} className={styles.definition}>
|
|
44
|
-
{result}
|
|
45
|
-
</li>
|
|
46
|
-
))}
|
|
47
|
-
</ul>
|
|
48
|
-
)}
|
|
49
|
-
</>
|
|
27
|
+
{results.map(({ definitions, isAllowed, word }) => (
|
|
28
|
+
<div
|
|
29
|
+
className={classNames(styles.result, {
|
|
30
|
+
[styles.isAllowed]: isAllowed === true,
|
|
31
|
+
[styles.isNotAllowed]: isAllowed === false,
|
|
32
|
+
})}
|
|
33
|
+
key={word}
|
|
34
|
+
>
|
|
35
|
+
{typeof word === 'undefined' && (
|
|
36
|
+
<EmptyState type="info">{translate('dictionary.empty-state.uninitialized')}</EmptyState>
|
|
50
37
|
)}
|
|
51
38
|
|
|
52
|
-
{
|
|
39
|
+
{typeof word !== 'undefined' && (
|
|
40
|
+
<div className={styles.content}>
|
|
41
|
+
{word && <h2 className={styles.word}>{word}</h2>}
|
|
42
|
+
|
|
43
|
+
{isAllowed === false && <div>{translate('dictionary.empty-state.not-allowed')}</div>}
|
|
44
|
+
|
|
45
|
+
{isAllowed === true && (
|
|
46
|
+
<>
|
|
47
|
+
{definitions.length === 0 && <div>{translate('dictionary.empty-state.no-definitions')}</div>}
|
|
48
|
+
|
|
49
|
+
{definitions.length > 0 && (
|
|
50
|
+
<ul className={styles.definitions}>
|
|
51
|
+
{definitions.map((result, index) => (
|
|
52
|
+
<li key={index} className={styles.definition}>
|
|
53
|
+
{result}
|
|
54
|
+
</li>
|
|
55
|
+
))}
|
|
56
|
+
</ul>
|
|
57
|
+
)}
|
|
58
|
+
</>
|
|
59
|
+
)}
|
|
60
|
+
|
|
61
|
+
{!isLoading && isAllowed === null && <div>{translate('dictionary.empty-state.no-results')}</div>}
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
53
64
|
</div>
|
|
54
|
-
)}
|
|
65
|
+
))}
|
|
55
66
|
|
|
56
67
|
{isLoading && <Loading />}
|
|
57
68
|
</div>
|
|
@@ -11,12 +11,13 @@ import styles from './Results.module.scss';
|
|
|
11
11
|
interface Props {
|
|
12
12
|
className?: string;
|
|
13
13
|
translationKey: TranslationKey;
|
|
14
|
+
tooltip?: string | number;
|
|
14
15
|
value: string | number;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
const Cell: FunctionComponent<Props> = ({ className, translationKey, value }) => {
|
|
18
|
+
const Cell: FunctionComponent<Props> = ({ className, translationKey, tooltip, value }) => {
|
|
18
19
|
const translate = useTranslate();
|
|
19
|
-
const triggerProps = useTooltip(`${translate(translationKey)}: ${value}`);
|
|
20
|
+
const triggerProps = useTooltip(`${translate(translationKey)}: ${tooltip || value}`);
|
|
20
21
|
|
|
21
22
|
return (
|
|
22
23
|
<span className={classNames(styles.cell, className)} {...triggerProps}>
|
|
@@ -16,6 +16,7 @@ const Result = ({ index, style }: Props): ReactElement => {
|
|
|
16
16
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
17
17
|
const results = useTypedSelector(selectSortedFilteredResults)!;
|
|
18
18
|
const result = results[index];
|
|
19
|
+
const otherWords = result.words.slice(1).join(' / ').toLocaleUpperCase();
|
|
19
20
|
|
|
20
21
|
const handleClick = () => {
|
|
21
22
|
dispatch(resultsSlice.actions.applyResult(result));
|
|
@@ -49,12 +50,21 @@ const Result = ({ index, style }: Props): ReactElement => {
|
|
|
49
50
|
onMouseLeave={handleMouseLeave}
|
|
50
51
|
>
|
|
51
52
|
<span className={styles.resultContent}>
|
|
52
|
-
<Cell
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
<Cell className={styles.stat} translationKey="common.
|
|
53
|
+
<Cell
|
|
54
|
+
className={styles.word}
|
|
55
|
+
translationKey="common.word"
|
|
56
|
+
value={`${result.word.toLocaleUpperCase()}${otherWords.length > 0 ? ` (${otherWords})` : ''}`}
|
|
57
|
+
/>
|
|
58
|
+
<Cell className={styles.stat} translationKey="common.tiles" value={result.tilesCount} />
|
|
59
|
+
<Cell className={styles.stat} translationKey="common.consonants" value={result.consonantsCount} />
|
|
60
|
+
<Cell className={styles.stat} translationKey="common.vowels" value={result.vowelsCount} />
|
|
61
|
+
<Cell className={styles.stat} translationKey="common.blanks" value={result.blanksCount} />
|
|
62
|
+
<Cell
|
|
63
|
+
className={styles.stat}
|
|
64
|
+
translationKey="common.words"
|
|
65
|
+
tooltip={`${result.wordsCount} (${result.words.join(' / ').toLocaleUpperCase()})`}
|
|
66
|
+
value={result.wordsCount}
|
|
67
|
+
/>
|
|
58
68
|
<Cell className={styles.points} translationKey="common.points" value={result.points} />
|
|
59
69
|
</span>
|
|
60
70
|
</button>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import classNames from 'classnames';
|
|
2
|
-
import { ChangeEvent, FunctionComponent } from 'react';
|
|
2
|
+
import { ChangeEvent, FunctionComponent, useState } from 'react';
|
|
3
3
|
import { useDispatch } from 'react-redux';
|
|
4
4
|
|
|
5
|
+
import { isRegExp } from 'lib';
|
|
5
6
|
import { resultsSlice, selectResultsQuery, useTranslate, useTypedSelector } from 'state';
|
|
6
7
|
|
|
7
8
|
import styles from './ResultsInput.module.scss';
|
|
@@ -14,9 +15,16 @@ const ResultsInput: FunctionComponent<Props> = ({ className }) => {
|
|
|
14
15
|
const dispatch = useDispatch();
|
|
15
16
|
const translate = useTranslate();
|
|
16
17
|
const value = useTypedSelector(selectResultsQuery);
|
|
18
|
+
const [localValue, setLocalValue] = useState(value);
|
|
17
19
|
|
|
18
20
|
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
19
|
-
|
|
21
|
+
const newValue = event.target.value;
|
|
22
|
+
|
|
23
|
+
setLocalValue(newValue);
|
|
24
|
+
|
|
25
|
+
if (isRegExp(newValue)) {
|
|
26
|
+
dispatch(resultsSlice.actions.changeQuery(newValue));
|
|
27
|
+
}
|
|
20
28
|
};
|
|
21
29
|
|
|
22
30
|
return (
|
|
@@ -25,7 +33,7 @@ const ResultsInput: FunctionComponent<Props> = ({ className }) => {
|
|
|
25
33
|
className={styles.input}
|
|
26
34
|
placeholder={translate('results.input.placeholder')}
|
|
27
35
|
type="text"
|
|
28
|
-
value={
|
|
36
|
+
value={localValue}
|
|
29
37
|
onChange={handleChange}
|
|
30
38
|
/>
|
|
31
39
|
</form>
|
package/src/hooks/useIsTablet.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useMedia } from 'react-use';
|
|
2
2
|
|
|
3
3
|
const useIsTablet = (): boolean => {
|
|
4
|
-
const isTabletHeight = useMedia('(max-height: 800px)');
|
|
5
|
-
const isTabletWidth = useMedia('(max-width: 1024px)');
|
|
4
|
+
const isTabletHeight = useMedia('(max-height: 800px)', false);
|
|
5
|
+
const isTabletWidth = useMedia('(max-width: 1024px)', false);
|
|
6
6
|
const isTablet = isTabletHeight || isTabletWidth;
|
|
7
7
|
return isTablet;
|
|
8
8
|
};
|
package/src/i18n/de.json
CHANGED
package/src/i18n/en.json
CHANGED
package/src/i18n/es.json
CHANGED
package/src/i18n/fr.json
CHANGED
package/src/i18n/pl.json
CHANGED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<!-- https://icons.getbootstrap.com/icons/flag-fill/ -->
|
|
2
|
+
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
|
3
|
+
<path d="M14.778.085A.5.5 0 0 1 15 .5V8a.5.5 0 0 1-.314.464L14.5 8l.186.464-.003.001-.006.003-.023.009a12.435 12.435 0 0 1-.397.15c-.264.095-.631.223-1.047.35-.816.252-1.879.523-2.71.523-.847 0-1.548-.28-2.158-.525l-.028-.01C7.68 8.71 7.14 8.5 6.5 8.5c-.7 0-1.638.23-2.437.477A19.626 19.626 0 0 0 3 9.342V15.5a.5.5 0 0 1-1 0V.5a.5.5 0 0 1 1 0v.282c.226-.079.496-.17.79-.26C4.606.272 5.67 0 6.5 0c.84 0 1.524.277 2.121.519l.043.018C9.286.788 9.828 1 10.5 1c.7 0 1.638-.23 2.437-.477a19.587 19.587 0 0 0 1.349-.476l.019-.007.004-.002h.001" fill="currentColor" />
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<!-- https://icons.getbootstrap.com/icons/star-fill/ -->
|
|
2
|
+
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
|
3
|
+
<path d="M3.612 15.443c-.386.198-.824-.149-.746-.592l.83-4.73L.173 6.765c-.329-.314-.158-.888.283-.95l4.898-.696L7.538.792c.197-.39.73-.39.927 0l2.184 4.327 4.898.696c.441.062.612.636.282.95l-3.522 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256z" fill="currentColor" />
|
|
4
|
+
</svg>
|
package/src/icons/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ export { default as Cog } from './Cog.svg';
|
|
|
8
8
|
export { default as Cross } from './Cross.svg';
|
|
9
9
|
export { default as DashCircleFill } from './DashCircleFill.svg';
|
|
10
10
|
export { default as Eraser } from './Eraser.svg';
|
|
11
|
+
export { default as Flag } from './Flag.svg';
|
|
11
12
|
export { default as FlagEs } from './FlagEs.svg';
|
|
12
13
|
export { default as FlagFr } from './FlagFr.svg';
|
|
13
14
|
export { default as FlagGb } from './FlagGb.svg';
|
|
@@ -20,3 +21,4 @@ export { default as Play } from './Play.svg';
|
|
|
20
21
|
export { default as Sack } from './Sack.svg';
|
|
21
22
|
export { default as SortDown } from './SortDown.svg';
|
|
22
23
|
export { default as SortUp } from './SortUp.svg';
|
|
24
|
+
export { default as Star } from './Star.svg';
|
|
@@ -11,7 +11,7 @@ const getRemainingTiles = (config: Config, board: Board, characters: string[]):
|
|
|
11
11
|
const remainingTiles = Object.fromEntries(config.tiles.map((tile) => [tile.character, { ...tile, usedCount: 0 }]));
|
|
12
12
|
const blank: RemainingTile = {
|
|
13
13
|
character: BLANK,
|
|
14
|
-
count: config.
|
|
14
|
+
count: config.blanksCount,
|
|
15
15
|
score: config.blankScore,
|
|
16
16
|
usedCount:
|
|
17
17
|
nonEmptyCells.filter((cell) => cell.tile.isBlank).length +
|