@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.
Files changed (101) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +10 -10
  3. package/.next/cache/.tsbuildinfo +1 -1
  4. package/.next/cache/eslint/.cache_8dgz12 +1 -1
  5. package/.next/cache/next-server.js.nft.json +1 -1
  6. package/.next/cache/webpack/client-production/0.pack +0 -0
  7. package/.next/cache/webpack/client-production/index.pack +0 -0
  8. package/.next/cache/webpack/server-production/0.pack +0 -0
  9. package/.next/cache/webpack/server-production/index.pack +0 -0
  10. package/.next/next-server.js.nft.json +1 -1
  11. package/.next/prerender-manifest.json +1 -1
  12. package/.next/routes-manifest.json +1 -1
  13. package/.next/server/chunks/413.js +266 -110
  14. package/.next/server/chunks/{206.js → 429.js} +2 -4137
  15. package/.next/server/chunks/515.js +197 -91
  16. package/.next/server/chunks/{907.js → 911.js} +134 -367
  17. package/.next/server/chunks/939.js +218 -0
  18. package/.next/server/middleware-build-manifest.js +1 -1
  19. package/.next/server/pages/404.html +2 -2
  20. package/.next/server/pages/404.js.nft.json +1 -1
  21. package/.next/server/pages/500.html +2 -2
  22. package/.next/server/pages/_app.js.nft.json +1 -1
  23. package/.next/server/pages/api/dictionary/[locale]/[word].js +33 -17
  24. package/.next/server/pages/api/dictionary/[locale]/[word].js.nft.json +1 -1
  25. package/.next/server/pages/api/solve.js +399 -56
  26. package/.next/server/pages/api/solve.js.nft.json +1 -1
  27. package/.next/server/pages/api/visit.js +3 -2
  28. package/.next/server/pages/api/visit.js.nft.json +1 -1
  29. package/.next/server/pages/index.html +3 -7
  30. package/.next/server/pages/index.js +12 -14
  31. package/.next/server/pages/index.js.nft.json +1 -1
  32. package/.next/server/pages/index.json +1 -1
  33. package/.next/static/chunks/615-d258f6c528c18622.js +1 -0
  34. package/.next/static/chunks/pages/{404-30c06e61d256c5b2.js → 404-8eb3ba4f0ba17e08.js} +1 -1
  35. package/.next/static/chunks/pages/_app-4a663fd3d5ca4524.js +1 -0
  36. package/.next/static/chunks/pages/index-1a9826d740cc8830.js +1 -0
  37. package/.next/static/css/180c6c26317ac90f.css +1 -0
  38. package/.next/static/css/751e8a14776d05d8.css +1 -0
  39. package/.next/static/z3J3qmq1nazbDv_ENIkCo/_buildManifest.js +1 -0
  40. package/.next/static/{VjSpyGDWyVaO0muz54q_j → z3J3qmq1nazbDv_ENIkCo}/_ssgManifest.js +0 -0
  41. package/.next/trace +41 -42
  42. package/package.json +9 -9
  43. package/src/api/index.ts +3 -9
  44. package/src/api/isBoardValid.ts +43 -0
  45. package/src/api/isCellValid.ts +26 -0
  46. package/src/api/isRowValid.ts +19 -0
  47. package/src/components/Board/components/Cell/Cell.module.scss +34 -9
  48. package/src/components/Board/components/Cell/Cell.tsx +23 -4
  49. package/src/components/Board/components/Cell/CellPure.tsx +29 -1
  50. package/src/components/Board/hooks/useGrid.ts +1 -0
  51. package/src/components/Dictionary/Dictionary.module.scss +20 -0
  52. package/src/components/Dictionary/Dictionary.tsx +40 -29
  53. package/src/components/Results/Cell.tsx +3 -2
  54. package/src/components/Results/Result.tsx +16 -6
  55. package/src/components/ResultsInput/ResultsInput.tsx +11 -3
  56. package/src/hooks/useIsTablet.ts +2 -2
  57. package/src/i18n/de.json +1 -0
  58. package/src/i18n/en.json +1 -0
  59. package/src/i18n/es.json +1 -0
  60. package/src/i18n/fr.json +1 -0
  61. package/src/i18n/pl.json +1 -0
  62. package/src/icons/Flag.svg +4 -0
  63. package/src/icons/Star.svg +4 -0
  64. package/src/icons/index.ts +2 -0
  65. package/src/lib/getRemainingTiles.ts +1 -1
  66. package/src/lib/index.ts +2 -1
  67. package/src/lib/isRegExp.ts +11 -0
  68. package/src/lib/isStringArray.ts +5 -0
  69. package/src/lib/sortResults.ts +5 -5
  70. package/src/pages/api/dictionary/[locale]/[word].ts +35 -11
  71. package/src/pages/api/solve.ts +39 -19
  72. package/src/pages/api/visit.ts +1 -0
  73. package/src/pages/index.module.scss +5 -11
  74. package/src/pages/index.tsx +5 -5
  75. package/src/sdk/{findWordDefinition.ts → findWordDefinitions.ts} +3 -3
  76. package/src/sdk/index.ts +1 -1
  77. package/src/state/rootReducer.ts +10 -1
  78. package/src/state/sagas.ts +32 -12
  79. package/src/state/selectors.ts +41 -7
  80. package/src/state/slices/cellFilterInitialState.ts +7 -0
  81. package/src/state/slices/cellFilterSlice.ts +24 -0
  82. package/src/state/slices/dictionaryInitialState.ts +3 -3
  83. package/src/state/slices/dictionarySlice.ts +4 -10
  84. package/src/state/slices/index.ts +2 -0
  85. package/src/types/index.ts +1 -0
  86. package/.next/static/VjSpyGDWyVaO0muz54q_j/_buildManifest.js +0 -1
  87. package/.next/static/chunks/56-e2797384ae4b0fc0.js +0 -1
  88. package/.next/static/chunks/pages/_app-5136d33b9b007fd7.js +0 -1
  89. package/.next/static/chunks/pages/index-13ea7770a65c69ee.js +0 -1
  90. package/.next/static/css/3159cfe62ff742a3.css +0 -1
  91. package/.next/static/css/729bb37fe8f9bee6.css +0 -1
  92. package/src/api/validateBoard.ts +0 -45
  93. package/src/api/validateCell.ts +0 -40
  94. package/src/api/validateCharacter.ts +0 -14
  95. package/src/api/validateCharacters.ts +0 -24
  96. package/src/api/validateConfigId.ts +0 -9
  97. package/src/api/validateLocale.ts +0 -15
  98. package/src/api/validateRow.ts +0 -17
  99. package/src/api/validateTile.ts +0 -21
  100. package/src/api/validateWord.ts +0 -11
  101. 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.6",
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.6",
34
- "@scrabble-solver/constants": "^2.8.6",
35
- "@scrabble-solver/dictionaries": "^2.8.6",
36
- "@scrabble-solver/logger": "^2.8.6",
37
- "@scrabble-solver/solver": "^2.8.6",
38
- "@scrabble-solver/types": "^2.8.6",
39
- "@scrabble-solver/word-definitions": "^2.8.6",
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": "ec130aa77ac64b91ab82e79f130eb69ac561fffa"
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 validateBoard } from './validateBoard';
3
- export { default as validateCell } from './validateCell';
4
- export { default as validateCharacter } from './validateCharacter';
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--red--light);
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 { boardSlice, selectCellBonus, selectTilePoints, useTranslate, useTypedSelector } from 'state';
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 handleDirectionToggleClick = useCallback(() => {
68
+ const handleToggleFilterCellClick = useCallback(() => {
52
69
  if (inputRef.current) {
53
70
  inputRef.current.focus();
54
71
  }
55
72
 
56
- onDirectionToggle();
57
- }, [onDirectionToggle]);
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, {
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-lines, max-statements */
1
2
  import { EMPTY_CELL } from '@scrabble-solver/constants';
2
3
  import { Cell } from '@scrabble-solver/types';
3
4
  import { createRef, KeyboardEventHandler, RefObject, useCallback, useMemo, useState, useRef } from 'react';
@@ -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 { definitions, isAllowed, isLoading, word } = useTypedSelector(selectDictionary);
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]: isAllowed === true,
23
- [styles.isNotAllowed]: isAllowed === false,
23
+ [styles.isAllowed]: isFirstAllowed === true,
24
+ [styles.isNotAllowed]: isFirstAllowed === false,
24
25
  })}
25
26
  >
26
- {typeof word === 'undefined' && (
27
- <EmptyState type="info">{translate('dictionary.empty-state.uninitialized')}</EmptyState>
28
- )}
29
-
30
- {typeof word !== 'undefined' && (
31
- <div className={styles.content}>
32
- {word && <h2 className={styles.word}>{word}</h2>}
33
-
34
- {isAllowed === false && <div>{translate('dictionary.empty-state.not-allowed')}</div>}
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
- {!isLoading && isAllowed === null && <div>{translate('dictionary.empty-state.no-results')}</div>}
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 className={styles.word} translationKey="common.word" value={result.word} />
53
- <Cell className={styles.stat} translationKey="common.tiles" value={result.numberOfTiles} />
54
- <Cell className={styles.stat} translationKey="common.consonants" value={result.numberOfConsonants} />
55
- <Cell className={styles.stat} translationKey="common.vowels" value={result.numberOfVowels} />
56
- <Cell className={styles.stat} translationKey="common.blanks" value={result.numberOfBlanks} />
57
- <Cell className={styles.stat} translationKey="common.words" value={result.numberOfWords} />
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
- dispatch(resultsSlice.actions.changeQuery(event.target.value));
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={value}
36
+ value={localValue}
29
37
  onChange={handleChange}
30
38
  />
31
39
  </form>
@@ -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
@@ -1,4 +1,5 @@
1
1
  {
2
+ "cell.filter-cell": "Zielort - klicken zum Wechseln",
2
3
  "cell.set-blank": "Als Blanko markieren",
3
4
  "cell.set-not-blank": "Nicht als Blanko markieren",
4
5
  "cell.toggle-direction": "Schreibrichtung - klicken zum Wechseln",
package/src/i18n/en.json CHANGED
@@ -1,4 +1,5 @@
1
1
  {
2
+ "cell.filter-cell": "Target destination - click to toggle",
2
3
  "cell.set-blank": "Mark it a blank",
3
4
  "cell.set-not-blank": "Mark it not a blank",
4
5
  "cell.toggle-direction": "Typing direction - click to toggle",
package/src/i18n/es.json CHANGED
@@ -1,4 +1,5 @@
1
1
  {
2
+ "cell.filter-cell": "Destino objetivo: haga clic para alternar",
2
3
  "cell.set-blank": "Marcar como en blanco",
3
4
  "cell.set-not-blank": "Marcar como no en blanco",
4
5
  "cell.toggle-direction": "Dirección de escritura: haga clic para alternar",
package/src/i18n/fr.json CHANGED
@@ -1,4 +1,5 @@
1
1
  {
2
+ "cell.filter-cell": "Destination cible - cliquer pour changer",
2
3
  "cell.set-blank": "Marquer comme vide",
3
4
  "cell.set-not-blank": "Marquer comme non vide",
4
5
  "cell.toggle-direction": "Direction d'écriture - cliquer pour changer",
package/src/i18n/pl.json CHANGED
@@ -1,4 +1,5 @@
1
1
  {
2
+ "cell.filter-cell": "Miejsce docelowe - kliknij aby zmienić",
2
3
  "cell.set-blank": "Oznacz jako blank",
3
4
  "cell.set-not-blank": "Oznacz jako nie blank",
4
5
  "cell.toggle-direction": "Kierunek wpisywania - kliknij aby zmienić",
@@ -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>
@@ -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.numberOfBlanks,
14
+ count: config.blanksCount,
15
15
  score: config.blankScore,
16
16
  usedCount:
17
17
  nonEmptyCells.filter((cell) => cell.tile.isBlank).length +