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.
Files changed (222) hide show
  1. package/.eslintrc.js +3 -1
  2. package/.eslintrc.prettier-only.js +1 -1
  3. package/.prettierignore +3 -0
  4. package/.prettierrc +3 -3
  5. package/babel.config.js +1 -1
  6. package/lerna.json +1 -1
  7. package/package.json +17 -17
  8. package/packages/configs/package.json +3 -3
  9. package/packages/constants/package.json +1 -1
  10. package/packages/dictionaries/package-lock.json +9 -9
  11. package/packages/dictionaries/package.json +5 -5
  12. package/packages/logger/package-lock.json +2 -2
  13. package/packages/logger/package.json +1 -1
  14. package/packages/scrabble-solver/.next/BUILD_ID +1 -1
  15. package/packages/scrabble-solver/.next/build-manifest.json +12 -12
  16. package/packages/scrabble-solver/.next/cache/.tsbuildinfo +1 -1
  17. package/packages/scrabble-solver/.next/cache/eslint/.cache_8dgz12 +1 -1
  18. package/packages/scrabble-solver/.next/cache/next-server.js.nft.json +1 -1
  19. package/packages/scrabble-solver/.next/cache/webpack/client-production/0.pack +0 -0
  20. package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack +0 -0
  21. package/packages/scrabble-solver/.next/cache/webpack/server-production/0.pack +0 -0
  22. package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack +0 -0
  23. package/packages/scrabble-solver/.next/next-server.js.nft.json +1 -1
  24. package/packages/scrabble-solver/.next/prerender-manifest.json +1 -1
  25. package/packages/scrabble-solver/.next/routes-manifest.json +1 -1
  26. package/packages/scrabble-solver/.next/server/chunks/413.js +350 -73
  27. package/packages/scrabble-solver/.next/server/chunks/429.js +2 -13
  28. package/packages/scrabble-solver/.next/server/chunks/44.js +802 -0
  29. package/packages/scrabble-solver/.next/server/chunks/515.js +767 -322
  30. package/packages/scrabble-solver/.next/server/chunks/911.js +77 -25
  31. package/packages/scrabble-solver/.next/server/middleware-build-manifest.js +1 -1
  32. package/packages/scrabble-solver/.next/server/pages/404.html +2 -2
  33. package/packages/scrabble-solver/.next/server/pages/404.js.nft.json +1 -1
  34. package/packages/scrabble-solver/.next/server/pages/500.html +2 -2
  35. package/packages/scrabble-solver/.next/server/pages/_app.js.nft.json +1 -1
  36. package/packages/scrabble-solver/.next/server/pages/_document.js.nft.json +1 -1
  37. package/packages/scrabble-solver/.next/server/pages/_error.js.nft.json +1 -1
  38. package/packages/scrabble-solver/.next/server/pages/api/solve.js +226 -927
  39. package/packages/scrabble-solver/.next/server/pages/api/solve.js.nft.json +1 -1
  40. package/packages/scrabble-solver/.next/server/pages/api/verify.js +217 -0
  41. package/packages/scrabble-solver/.next/server/pages/api/verify.js.nft.json +1 -0
  42. package/packages/scrabble-solver/.next/server/pages/index.html +3 -3
  43. package/packages/scrabble-solver/.next/server/pages/index.js +8 -2
  44. package/packages/scrabble-solver/.next/server/pages/index.js.nft.json +1 -1
  45. package/packages/scrabble-solver/.next/server/pages/index.json +1 -1
  46. package/packages/scrabble-solver/.next/server/pages-manifest.json +1 -0
  47. package/packages/scrabble-solver/.next/static/A8A_Lmg8cM-Bkf-Jo1CLh/_buildManifest.js +1 -0
  48. package/packages/scrabble-solver/.next/static/{z3J3qmq1nazbDv_ENIkCo → A8A_Lmg8cM-Bkf-Jo1CLh}/_ssgManifest.js +0 -0
  49. package/packages/scrabble-solver/.next/static/chunks/317-95ab9051449362fa.js +1 -0
  50. package/packages/scrabble-solver/.next/static/chunks/758-eff80059a1365d5d.js +1 -0
  51. package/packages/scrabble-solver/.next/static/chunks/pages/{404-8eb3ba4f0ba17e08.js → 404-90c624da3c83fd17.js} +1 -1
  52. package/packages/scrabble-solver/.next/static/chunks/pages/_app-0e358b5622cf9e66.js +1 -0
  53. package/packages/scrabble-solver/.next/static/chunks/pages/index-0cc5e6eda5adac73.js +1 -0
  54. package/packages/scrabble-solver/.next/static/css/9ac903004135f4b1.css +1 -0
  55. package/packages/scrabble-solver/.next/static/css/{cdbc9e0afcff5473.css → ad2a08918868cad8.css} +1 -1
  56. package/packages/scrabble-solver/.next/trace +42 -41
  57. package/packages/scrabble-solver/package-lock.json +298 -298
  58. package/packages/scrabble-solver/package.json +11 -11
  59. package/packages/scrabble-solver/src/components/Badge/Badge.module.scss +13 -0
  60. package/packages/scrabble-solver/src/components/Badge/Badge.tsx +15 -0
  61. package/packages/scrabble-solver/src/components/Badge/index.ts +1 -0
  62. package/packages/scrabble-solver/src/components/Board/Board.tsx +4 -2
  63. package/packages/scrabble-solver/src/components/Board/BoardPure.tsx +25 -5
  64. package/packages/scrabble-solver/src/components/Board/hooks/useGrid.ts +212 -91
  65. package/packages/scrabble-solver/src/components/Dictionary/Dictionary.tsx +8 -1
  66. package/packages/scrabble-solver/src/components/NavButtons/NavButtons.tsx +33 -14
  67. package/packages/scrabble-solver/src/components/Rack/Rack.tsx +51 -11
  68. package/packages/scrabble-solver/src/components/Rack/RackTile.tsx +33 -16
  69. package/packages/scrabble-solver/src/components/RemainingTiles/RemainingTiles.module.scss +8 -7
  70. package/packages/scrabble-solver/src/components/RemainingTiles/RemainingTiles.tsx +13 -4
  71. package/packages/scrabble-solver/src/components/Results/Results.tsx +19 -3
  72. package/packages/scrabble-solver/src/components/Sidebar/Sidebar.tsx +2 -2
  73. package/packages/scrabble-solver/src/components/Sidebar/components/Section/Section.module.scss +0 -1
  74. package/packages/scrabble-solver/src/components/Sidebar/components/Section/Section.tsx +1 -1
  75. package/packages/scrabble-solver/src/components/SquareButton/Link.tsx +1 -1
  76. package/packages/scrabble-solver/src/components/SquareButton/SquareButton.module.scss +5 -0
  77. package/packages/scrabble-solver/src/components/Tile/Tile.module.scss +4 -0
  78. package/packages/scrabble-solver/src/components/Tile/Tile.tsx +13 -4
  79. package/packages/scrabble-solver/src/components/Tile/TilePure.tsx +3 -4
  80. package/packages/scrabble-solver/src/components/Words/Words.module.scss +35 -0
  81. package/packages/scrabble-solver/src/components/Words/Words.tsx +57 -0
  82. package/packages/scrabble-solver/src/components/Words/index.ts +1 -0
  83. package/packages/scrabble-solver/src/components/index.ts +2 -0
  84. package/packages/scrabble-solver/src/i18n/de.json +4 -1
  85. package/packages/scrabble-solver/src/i18n/en.json +4 -1
  86. package/packages/scrabble-solver/src/i18n/es.json +4 -1
  87. package/packages/scrabble-solver/src/i18n/fr.json +4 -1
  88. package/packages/scrabble-solver/src/i18n/pl.json +4 -1
  89. package/packages/scrabble-solver/src/icons/BookHalf.svg +4 -0
  90. package/packages/scrabble-solver/src/icons/Check.svg +4 -0
  91. package/packages/scrabble-solver/src/icons/Cross.svg +2 -2
  92. package/packages/scrabble-solver/src/icons/CrossFill.svg +4 -0
  93. package/packages/scrabble-solver/src/icons/index.ts +3 -0
  94. package/packages/scrabble-solver/src/lib/extractCharacters.ts +26 -0
  95. package/packages/scrabble-solver/src/lib/extractInputValue.ts +17 -0
  96. package/packages/scrabble-solver/src/lib/index.ts +2 -0
  97. package/packages/scrabble-solver/src/lib/isCtrl.ts +1 -1
  98. package/packages/scrabble-solver/src/lib/memoize.ts +15 -1
  99. package/packages/scrabble-solver/src/pages/api/solve.ts +3 -4
  100. package/packages/scrabble-solver/src/pages/api/verify.ts +71 -0
  101. package/packages/scrabble-solver/src/pages/index.tsx +5 -0
  102. package/packages/scrabble-solver/src/sdk/fetchJson.ts +36 -0
  103. package/packages/scrabble-solver/src/sdk/findWordDefinitions.ts +4 -3
  104. package/packages/scrabble-solver/src/sdk/index.ts +1 -0
  105. package/packages/scrabble-solver/src/sdk/solve.ts +8 -7
  106. package/packages/scrabble-solver/src/sdk/verify.ts +23 -0
  107. package/packages/scrabble-solver/src/state/rootReducer.ts +2 -0
  108. package/packages/scrabble-solver/src/state/sagas.ts +35 -8
  109. package/packages/scrabble-solver/src/state/selectors.ts +17 -1
  110. package/packages/scrabble-solver/src/state/slices/dictionaryInitialState.ts +10 -2
  111. package/packages/scrabble-solver/src/state/slices/dictionarySlice.ts +10 -16
  112. package/packages/scrabble-solver/src/state/slices/index.ts +2 -0
  113. package/packages/scrabble-solver/src/state/slices/rackSlice.ts +7 -0
  114. package/packages/scrabble-solver/src/state/slices/solveInitialState.ts +14 -2
  115. package/packages/scrabble-solver/src/state/slices/solveSlice.ts +7 -4
  116. package/packages/scrabble-solver/src/state/slices/verifyInitialState.ts +12 -0
  117. package/packages/scrabble-solver/src/state/slices/verifySlice.ts +31 -0
  118. package/packages/scrabble-solver/src/styles/variables.scss +2 -1
  119. package/packages/scrabble-solver/src/types/index.ts +6 -1
  120. package/packages/solver/build/fillPattern.d.ts +5 -0
  121. package/packages/solver/build/fillPattern.js +52 -0
  122. package/packages/solver/build/generateEndIndices.d.ts +3 -0
  123. package/packages/solver/build/generateEndIndices.js +16 -0
  124. package/packages/solver/build/generateHorizontalPatterns.d.ts +3 -0
  125. package/packages/solver/build/generateHorizontalPatterns.js +18 -0
  126. package/packages/solver/build/generatePattern.d.ts +8 -0
  127. package/packages/solver/build/generatePattern.js +22 -0
  128. package/packages/solver/build/generatePatterns.d.ts +3 -0
  129. package/packages/solver/build/generatePatterns.js +13 -0
  130. package/packages/solver/build/generateStartIndices.d.ts +3 -0
  131. package/packages/solver/build/generateStartIndices.js +15 -0
  132. package/packages/solver/build/generateVectors.d.ts +7 -0
  133. package/packages/solver/build/generateVectors.js +6 -0
  134. package/packages/solver/build/generateVerticalPatterns.d.ts +3 -0
  135. package/packages/solver/build/generateVerticalPatterns.js +18 -0
  136. package/packages/solver/build/getCellsScore.d.ts +3 -0
  137. package/packages/solver/build/getCellsScore.js +16 -0
  138. package/packages/solver/build/getPatternHash.d.ts +3 -0
  139. package/packages/solver/build/getPatternHash.js +13 -0
  140. package/packages/solver/build/getPatternScore.d.ts +3 -0
  141. package/packages/solver/build/getPatternScore.js +15 -0
  142. package/packages/solver/build/getUniquePatterns.js +5 -11
  143. package/packages/solver/build/index.d.ts +1 -1
  144. package/packages/solver/build/index.js +3 -3
  145. package/packages/solver/build/solve.d.ts +4 -0
  146. package/packages/solver/build/solve.js +22 -0
  147. package/packages/solver/package-lock.json +9 -9
  148. package/packages/solver/package.json +6 -6
  149. package/packages/solver/src/fillPattern.test.ts +107 -0
  150. package/packages/solver/src/fillPattern.ts +70 -0
  151. package/packages/solver/src/generateEndIndices.test.ts +23 -0
  152. package/packages/solver/src/generateEndIndices.ts +21 -0
  153. package/packages/solver/src/generateHorizontalPatterns.test.ts +31 -0
  154. package/packages/solver/src/generateHorizontalPatterns.ts +17 -0
  155. package/packages/solver/src/generatePattern.ts +35 -0
  156. package/packages/solver/src/generatePatterns.ts +13 -0
  157. package/packages/solver/src/generateStartIndices.test.ts +19 -0
  158. package/packages/solver/src/generateStartIndices.ts +19 -0
  159. package/packages/solver/src/generateVectors.test.ts +12 -0
  160. package/packages/solver/src/generateVectors.ts +12 -0
  161. package/packages/solver/src/generateVerticalPatterns.test.ts +32 -0
  162. package/packages/solver/src/generateVerticalPatterns.ts +17 -0
  163. package/packages/solver/src/getCellsScore.ts +22 -0
  164. package/packages/solver/src/getPatternHash.test.ts +17 -0
  165. package/packages/solver/src/getPatternHash.ts +14 -0
  166. package/packages/solver/src/getPatternScore.test.ts +60 -0
  167. package/packages/solver/src/getPatternScore.ts +15 -0
  168. package/packages/solver/src/getUniquePatterns.ts +1 -10
  169. package/packages/solver/src/index.ts +1 -1
  170. package/packages/solver/src/{Solver.test.ts → solve.test.ts} +17 -16
  171. package/packages/solver/src/solve.ts +23 -0
  172. package/packages/types/build/Board.d.ts +1 -0
  173. package/packages/types/build/Board.js +19 -3
  174. package/packages/types/build/Config.js +1 -1
  175. package/packages/types/build/FinalPattern.d.ts +7 -0
  176. package/packages/types/build/FinalPattern.js +16 -0
  177. package/packages/types/build/HorizontalPattern.js +2 -5
  178. package/packages/types/build/Pattern.d.ts +1 -4
  179. package/packages/types/build/Pattern.js +2 -5
  180. package/packages/types/build/Result.js +3 -5
  181. package/packages/types/build/Tile.d.ts +2 -2
  182. package/packages/types/build/VerticalPattern.js +2 -5
  183. package/packages/types/build/index.d.ts +2 -0
  184. package/packages/types/build/index.js +5 -1
  185. package/packages/types/build/isError.d.ts +2 -0
  186. package/packages/types/build/isError.js +13 -0
  187. package/packages/types/package.json +2 -2
  188. package/packages/types/src/Board.ts +25 -3
  189. package/packages/types/src/Config.ts +1 -1
  190. package/packages/types/src/FinalPattern.ts +16 -0
  191. package/packages/types/src/HorizontalPattern.ts +5 -5
  192. package/packages/types/src/Pattern.ts +5 -5
  193. package/packages/types/src/Result.ts +3 -5
  194. package/packages/types/src/Tile.ts +2 -2
  195. package/packages/types/src/VerticalPattern.ts +5 -5
  196. package/packages/types/src/index.ts +2 -0
  197. package/packages/types/src/isError.ts +11 -0
  198. package/packages/word-definitions/package-lock.json +8 -8
  199. package/packages/word-definitions/package.json +2 -2
  200. package/packages/word-lists/package-lock.json +2 -2
  201. package/packages/word-lists/package.json +2 -2
  202. package/packages/scrabble-solver/.next/static/chunks/615-d258f6c528c18622.js +0 -1
  203. package/packages/scrabble-solver/.next/static/chunks/758-f333b1dcdb941547.js +0 -1
  204. package/packages/scrabble-solver/.next/static/chunks/pages/_app-4a663fd3d5ca4524.js +0 -1
  205. package/packages/scrabble-solver/.next/static/chunks/pages/index-1a9826d740cc8830.js +0 -1
  206. package/packages/scrabble-solver/.next/static/css/180c6c26317ac90f.css +0 -1
  207. package/packages/scrabble-solver/.next/static/z3J3qmq1nazbDv_ENIkCo/_buildManifest.js +0 -1
  208. package/packages/solver/build/PatternsFiller.d.ts +0 -12
  209. package/packages/solver/build/PatternsFiller.js +0 -70
  210. package/packages/solver/build/PatternsGenerator.d.ts +0 -32
  211. package/packages/solver/build/PatternsGenerator.js +0 -66
  212. package/packages/solver/build/ScoresCalculator.d.ts +0 -17
  213. package/packages/solver/build/ScoresCalculator.js +0 -37
  214. package/packages/solver/build/Solver.d.ts +0 -10
  215. package/packages/solver/build/Solver.js +0 -30
  216. package/packages/solver/src/PatternsFiller.test.ts +0 -92
  217. package/packages/solver/src/PatternsFiller.ts +0 -91
  218. package/packages/solver/src/PatternsGenerator.test.ts +0 -89
  219. package/packages/solver/src/PatternsGenerator.ts +0 -103
  220. package/packages/solver/src/ScoresCalculator.test.ts +0 -70
  221. package/packages/solver/src/ScoresCalculator.ts +0 -52
  222. package/packages/solver/src/Solver.ts +0 -39
@@ -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> = ({ onClear, onShowKeyMap, onShowRemainingTiles, onShowSettings }) => {
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.Link
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 className={styles.button} Icon={Eraser} tooltip={translate('common.clear')} onClick={onClear} />
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 { createArray, createKeyboardNavigation, zipCharactersAndTiles } from 'lib';
6
- import { selectConfig, selectRack, selectResultCandidateTiles, useTypedSelector } from 'state';
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 onKeyDown = useMemo(() => {
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
- const character = event.key.toLowerCase();
53
-
54
- if (config.hasCharacter(character) || character === BLANK) {
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
- onKeyDown={onKeyDown}
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> = ({ activeIndexRef, character, index, inputRef, tile, onKeyDown }) => {
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 handleCharacterChange = useCallback(
41
- (value: string | null) => {
42
- dispatch(rackSlice.actions.changeCharacter({ character: value, index }));
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: () => handleCharacterChange(null),
50
- onDelete: () => handleCharacterChange(null),
65
+ onBackspace: (event) => {
66
+ event.preventDefault();
67
+ dispatch(rackSlice.actions.changeCharacter({ character: null, index }));
68
+ },
51
69
  onKeyDown: (event) => {
52
- const newCharacter = event.key.toLowerCase();
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
- handleCharacterChange(twoCharacterTile);
58
- } else if (config.hasCharacter(newCharacter) || newCharacter === BLANK) {
59
- handleCharacterChange(newCharacter);
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
- }, [config, handleCharacterChange, onKeyDown]);
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
- margin-bottom: var(--spacing--m);
7
- font-size: var(--font--size--h2);
8
- font-weight: 300;
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
- <div className={styles.group} key={translationKey}>
24
- <h2 className={styles.title}>{`${translate(translationKey)} (${remainingCount} / ${totalCount})`}</h2>
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
- </div>
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 results === 'undefined' && (
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 { Cross } from 'icons';
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={Cross}
44
+ Icon={CrossFill}
45
45
  tooltip={translate('common.close')}
46
46
  onClick={onClose}
47
47
  />
@@ -7,6 +7,5 @@
7
7
  .heading {
8
8
  margin-bottom: var(--spacing--m);
9
9
  font-size: var(--font--size--h2);
10
- font-family: var(--font--family--title);
11
10
  font-weight: 300;
12
11
  }
@@ -6,7 +6,7 @@ import styles from './Section.module.scss';
6
6
  interface Props {
7
7
  children: ReactNode;
8
8
  className?: string;
9
- title: string;
9
+ title: ReactNode;
10
10
  }
11
11
 
12
12
  const Section: FunctionComponent<Props> = ({ children, className, title }) => (
@@ -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)} type="button" {...props} {...triggerProps}>
19
+ <a className={classNames(styles.squareButton, className)} {...props} {...triggerProps}>
20
20
  <span className={styles.content}>
21
21
  <Icon className={styles.icon} />
22
22
  </span>
@@ -18,6 +18,11 @@
18
18
  &:active {
19
19
  color: var(--color--foreground);
20
20
  }
21
+
22
+ &[disabled] {
23
+ pointer-events: none;
24
+ opacity: 0.25;
25
+ }
21
26
  }
22
27
 
23
28
  .content {
@@ -62,6 +62,10 @@
62
62
  color: var(--color--inactive);
63
63
  }
64
64
 
65
+ &::selection {
66
+ background: transparent;
67
+ }
68
+
65
69
  &:disabled {
66
70
  color: inherit;
67
71
  }
@@ -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
- onFocus,
47
- onKeyDown,
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={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={handleChange}
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';
@@ -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';