@scrabble-solver/scrabble-solver 2.8.8 → 2.8.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +12 -12
  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 +350 -73
  14. package/.next/server/chunks/429.js +2 -13
  15. package/.next/server/chunks/44.js +802 -0
  16. package/.next/server/chunks/515.js +767 -322
  17. package/.next/server/chunks/911.js +77 -25
  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/_document.js.nft.json +1 -1
  24. package/.next/server/pages/_error.js.nft.json +1 -1
  25. package/.next/server/pages/api/solve.js +226 -927
  26. package/.next/server/pages/api/solve.js.nft.json +1 -1
  27. package/.next/server/pages/api/verify.js +217 -0
  28. package/.next/server/pages/api/verify.js.nft.json +1 -0
  29. package/.next/server/pages/index.html +3 -3
  30. package/.next/server/pages/index.js +8 -2
  31. package/.next/server/pages/index.js.nft.json +1 -1
  32. package/.next/server/pages/index.json +1 -1
  33. package/.next/server/pages-manifest.json +1 -0
  34. package/.next/static/A8A_Lmg8cM-Bkf-Jo1CLh/_buildManifest.js +1 -0
  35. package/.next/static/{z3J3qmq1nazbDv_ENIkCo → A8A_Lmg8cM-Bkf-Jo1CLh}/_ssgManifest.js +0 -0
  36. package/.next/static/chunks/317-95ab9051449362fa.js +1 -0
  37. package/.next/static/chunks/758-eff80059a1365d5d.js +1 -0
  38. package/.next/static/chunks/pages/{404-8eb3ba4f0ba17e08.js → 404-90c624da3c83fd17.js} +1 -1
  39. package/.next/static/chunks/pages/_app-0e358b5622cf9e66.js +1 -0
  40. package/.next/static/chunks/pages/index-0cc5e6eda5adac73.js +1 -0
  41. package/.next/static/css/9ac903004135f4b1.css +1 -0
  42. package/.next/static/css/{cdbc9e0afcff5473.css → ad2a08918868cad8.css} +1 -1
  43. package/.next/trace +42 -41
  44. package/package.json +12 -12
  45. package/src/components/Badge/Badge.module.scss +13 -0
  46. package/src/components/Badge/Badge.tsx +15 -0
  47. package/src/components/Badge/index.ts +1 -0
  48. package/src/components/Board/Board.tsx +4 -2
  49. package/src/components/Board/BoardPure.tsx +25 -5
  50. package/src/components/Board/hooks/useGrid.ts +212 -91
  51. package/src/components/Dictionary/Dictionary.tsx +8 -1
  52. package/src/components/NavButtons/NavButtons.tsx +33 -14
  53. package/src/components/Rack/Rack.tsx +51 -11
  54. package/src/components/Rack/RackTile.tsx +33 -16
  55. package/src/components/RemainingTiles/RemainingTiles.module.scss +8 -7
  56. package/src/components/RemainingTiles/RemainingTiles.tsx +13 -4
  57. package/src/components/Results/Results.tsx +19 -3
  58. package/src/components/Sidebar/Sidebar.tsx +2 -2
  59. package/src/components/Sidebar/components/Section/Section.module.scss +0 -1
  60. package/src/components/Sidebar/components/Section/Section.tsx +1 -1
  61. package/src/components/SquareButton/Link.tsx +1 -1
  62. package/src/components/SquareButton/SquareButton.module.scss +5 -0
  63. package/src/components/Tile/Tile.module.scss +4 -0
  64. package/src/components/Tile/Tile.tsx +13 -4
  65. package/src/components/Tile/TilePure.tsx +3 -4
  66. package/src/components/Words/Words.module.scss +35 -0
  67. package/src/components/Words/Words.tsx +57 -0
  68. package/src/components/Words/index.ts +1 -0
  69. package/src/components/index.ts +2 -0
  70. package/src/i18n/de.json +4 -1
  71. package/src/i18n/en.json +4 -1
  72. package/src/i18n/es.json +4 -1
  73. package/src/i18n/fr.json +4 -1
  74. package/src/i18n/pl.json +4 -1
  75. package/src/icons/BookHalf.svg +4 -0
  76. package/src/icons/Check.svg +4 -0
  77. package/src/icons/Cross.svg +2 -2
  78. package/src/icons/CrossFill.svg +4 -0
  79. package/src/icons/index.ts +3 -0
  80. package/src/lib/extractCharacters.ts +26 -0
  81. package/src/lib/extractInputValue.ts +17 -0
  82. package/src/lib/index.ts +2 -0
  83. package/src/lib/isCtrl.ts +1 -1
  84. package/src/lib/memoize.ts +15 -1
  85. package/src/pages/api/solve.ts +3 -4
  86. package/src/pages/api/verify.ts +71 -0
  87. package/src/pages/index.tsx +5 -0
  88. package/src/sdk/fetchJson.ts +36 -0
  89. package/src/sdk/findWordDefinitions.ts +4 -3
  90. package/src/sdk/index.ts +1 -0
  91. package/src/sdk/solve.ts +8 -7
  92. package/src/sdk/verify.ts +23 -0
  93. package/src/state/rootReducer.ts +2 -0
  94. package/src/state/sagas.ts +35 -8
  95. package/src/state/selectors.ts +17 -1
  96. package/src/state/slices/dictionaryInitialState.ts +10 -2
  97. package/src/state/slices/dictionarySlice.ts +10 -16
  98. package/src/state/slices/index.ts +2 -0
  99. package/src/state/slices/rackSlice.ts +7 -0
  100. package/src/state/slices/solveInitialState.ts +14 -2
  101. package/src/state/slices/solveSlice.ts +7 -4
  102. package/src/state/slices/verifyInitialState.ts +12 -0
  103. package/src/state/slices/verifySlice.ts +31 -0
  104. package/src/styles/variables.scss +2 -1
  105. package/src/types/index.ts +6 -1
  106. package/.next/static/chunks/615-d258f6c528c18622.js +0 -1
  107. package/.next/static/chunks/758-f333b1dcdb941547.js +0 -1
  108. package/.next/static/chunks/pages/_app-4a663fd3d5ca4524.js +0 -1
  109. package/.next/static/chunks/pages/index-1a9826d740cc8830.js +0 -1
  110. package/.next/static/css/180c6c26317ac90f.css +0 -1
  111. package/.next/static/z3J3qmq1nazbDv_ENIkCo/_buildManifest.js +0 -1
package/src/i18n/de.json CHANGED
@@ -49,5 +49,8 @@
49
49
  "settings.autoGroupTiles.right": "Rechte Seite",
50
50
  "settings.autoGroupTiles.null": "Nicht gruppieren",
51
51
  "settings.game": "Spiel",
52
- "settings.language": "Sprache"
52
+ "settings.language": "Sprache",
53
+ "words": "Gebildete Wörter",
54
+ "words.invalid": "Falsch",
55
+ "words.valid": "Korrekt"
53
56
  }
package/src/i18n/en.json CHANGED
@@ -49,5 +49,8 @@
49
49
  "settings.autoGroupTiles.right": "On the right",
50
50
  "settings.autoGroupTiles.null": "Do not group",
51
51
  "settings.game": "Game",
52
- "settings.language": "Language"
52
+ "settings.language": "Language",
53
+ "words": "Created words",
54
+ "words.invalid": "Invalid",
55
+ "words.valid": "Valid"
53
56
  }
package/src/i18n/es.json CHANGED
@@ -49,5 +49,8 @@
49
49
  "settings.autoGroupTiles.right": "A la derecha",
50
50
  "settings.autoGroupTiles.null": "No agrupar",
51
51
  "settings.game": "Juego",
52
- "settings.language": "Idioma"
52
+ "settings.language": "Idioma",
53
+ "words": "Palabras creadas",
54
+ "words.invalid": "Incorrecto",
55
+ "words.valid": "Correcto"
53
56
  }
package/src/i18n/fr.json CHANGED
@@ -49,5 +49,8 @@
49
49
  "settings.autoGroupTiles.right": "Vers la gauche",
50
50
  "settings.autoGroupTiles.null": "Ne pas grouper",
51
51
  "settings.game": "Jeu",
52
- "settings.language": "Langue"
52
+ "settings.language": "Langue",
53
+ "words": "Mots créés",
54
+ "words.invalid": "Incorrect",
55
+ "words.valid": "Corriger"
53
56
  }
package/src/i18n/pl.json CHANGED
@@ -49,5 +49,8 @@
49
49
  "settings.autoGroupTiles.right": "Po prawej",
50
50
  "settings.autoGroupTiles.null": "Nie grupuj",
51
51
  "settings.game": "Gra",
52
- "settings.language": "Język"
52
+ "settings.language": "Język",
53
+ "words": "Utworzone słowa",
54
+ "words.invalid": "Niepoprawne",
55
+ "words.valid": "Poprawne"
53
56
  }
@@ -0,0 +1,4 @@
1
+ <!-- https://icons.getbootstrap.com/icons/book-half/ -->
2
+ <svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
3
+ <path d="M8.5 2.687c.654-.689 1.782-.886 3.112-.752 1.234.124 2.503.523 3.388.893v9.923c-.918-.35-2.107-.692-3.287-.81-1.094-.111-2.278-.039-3.213.492V2.687zM8 1.783C7.015.936 5.587.81 4.287.94c-1.514.153-3.042.672-3.994 1.105A.5.5 0 0 0 0 2.5v11a.5.5 0 0 0 .707.455c.882-.4 2.303-.881 3.68-1.02 1.409-.142 2.59.087 3.223.877a.5.5 0 0 0 .78 0c.633-.79 1.814-1.019 3.222-.877 1.378.139 2.8.62 3.681 1.02A.5.5 0 0 0 16 13.5v-11a.5.5 0 0 0-.293-.455c-.952-.433-2.48-.952-3.994-1.105C10.413.809 8.985.936 8 1.783z" fill="currentColor" />
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <!-- https://icons.getbootstrap.com/icons/check/ -->
2
+ <svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
3
+ <path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z" fill="currentColor" />
4
+ </svg>
@@ -1,4 +1,4 @@
1
- <!-- https://icons.getbootstrap.com/icons/x-square-fill/ -->
1
+ <!-- https://icons.getbootstrap.com/icons/x/ -->
2
2
  <svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
3
- <path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm3.354 4.646L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 1 1 .708-.708z" fill="currentColor" />
3
+ <path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" fill="currentColor" />
4
4
  </svg>
@@ -0,0 +1,4 @@
1
+ <!-- https://icons.getbootstrap.com/icons/x-square-fill/ -->
2
+ <svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
3
+ <path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm3.354 4.646L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 1 1 .708-.708z" fill="currentColor" />
4
+ </svg>
@@ -2,10 +2,13 @@ export { default as ArrowDown } from './ArrowDown.svg';
2
2
  export { default as ArrowLeft } from './ArrowLeft.svg';
3
3
  export { default as ArrowRight } from './ArrowRight.svg';
4
4
  export { default as ArrowUp } from './ArrowUp.svg';
5
+ export { default as BookHalf } from './BookHalf.svg';
6
+ export { default as Check } from './Check.svg';
5
7
  export { default as CheckboxChecked } from './CheckboxChecked.svg';
6
8
  export { default as CheckboxEmpty } from './CheckboxEmpty.svg';
7
9
  export { default as Cog } from './Cog.svg';
8
10
  export { default as Cross } from './Cross.svg';
11
+ export { default as CrossFill } from './CrossFill.svg';
9
12
  export { default as DashCircleFill } from './DashCircleFill.svg';
10
13
  export { default as Eraser } from './Eraser.svg';
11
14
  export { default as Flag } from './Flag.svg';
@@ -0,0 +1,26 @@
1
+ import { BLANK } from '@scrabble-solver/constants';
2
+ import { Config } from '@scrabble-solver/types';
3
+
4
+ const extractCharacters = (config: Config, value: string): string[] => {
5
+ let index = 0;
6
+ const characters: string[] = [];
7
+
8
+ while (index < value.length) {
9
+ const character = value[index];
10
+ const nextCharacter = value[index + 1];
11
+ const twoCharacterTileCandidate = `${character}${nextCharacter}`;
12
+
13
+ if (config.twoCharacterTiles.includes(twoCharacterTileCandidate)) {
14
+ characters.push(twoCharacterTileCandidate);
15
+ ++index;
16
+ } else if (config.hasCharacter(character) || character === BLANK) {
17
+ characters.push(character);
18
+ }
19
+
20
+ ++index;
21
+ }
22
+
23
+ return characters;
24
+ };
25
+
26
+ export default extractCharacters;
@@ -0,0 +1,17 @@
1
+ const extractInputValue = (input: HTMLInputElement) => {
2
+ const value = input.value.toLocaleLowerCase();
3
+
4
+ if (input.selectionStart === null || input.selectionEnd === null) {
5
+ return value;
6
+ }
7
+
8
+ const index = Math.min(input.selectionStart, input.selectionEnd);
9
+
10
+ if (index > 0) {
11
+ return value.substring(index - 1, index);
12
+ }
13
+
14
+ return value;
15
+ };
16
+
17
+ export default extractInputValue;
package/src/lib/index.ts CHANGED
@@ -6,6 +6,8 @@ export { default as createKeyboardNavigation } from './createKeyboardNavigation'
6
6
  export { default as createKeyComparator } from './createKeyComparator';
7
7
  export { default as createNullMovingComparator } from './createNullMovingComparator';
8
8
  export { default as detectLocale } from './detectLocale';
9
+ export { default as extractCharacters } from './extractCharacters';
10
+ export { default as extractInputValue } from './extractInputValue';
9
11
  export { default as findCell } from './findCell';
10
12
  export { default as getCellSize } from './getCellSize';
11
13
  export { default as getRemainingTiles } from './getRemainingTiles';
package/src/lib/isCtrl.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { KeyboardEvent } from 'react';
2
2
 
3
- const isCtrl = <T>(event: KeyboardEvent<T>): boolean => {
3
+ const isCtrl = <T>(event: KeyboardEvent<T> | globalThis.KeyboardEvent): boolean => {
4
4
  return event.ctrlKey || event.metaKey;
5
5
  };
6
6
 
@@ -1,5 +1,5 @@
1
1
  interface AnyFunction {
2
- (...parameters: any[]): any; // eslint-disable-line @typescript-eslint/no-explicit-any
2
+ (...parameters: any[]): any | Promise<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
3
3
  }
4
4
 
5
5
  interface AnyCachedFunction<T extends AnyFunction> extends AnyFunction {
@@ -20,6 +20,14 @@ const memoize = <T extends AnyFunction>(fn: T): AnyCachedFunction<T> => {
20
20
  return cache.find((entry) => parametersEqual(entry.parameters, parameters))?.result;
21
21
  };
22
22
 
23
+ const removeCache = (parameters: Parameters<T>): void => {
24
+ const index = cache.findIndex((entry) => parametersEqual(entry.parameters, parameters));
25
+
26
+ if (index >= 0) {
27
+ cache.splice(index, 1);
28
+ }
29
+ };
30
+
23
31
  const writeCache = (parameters: Parameters<T>, result: ReturnType<T>): void => {
24
32
  cache.push({ parameters, result });
25
33
  };
@@ -33,6 +41,12 @@ const memoize = <T extends AnyFunction>(fn: T): AnyCachedFunction<T> => {
33
41
 
34
42
  const result = fn(...parameters);
35
43
 
44
+ if (result instanceof Promise) {
45
+ result.catch(() => {
46
+ removeCache(parameters);
47
+ });
48
+ }
49
+
36
50
  writeCache(parameters, result);
37
51
 
38
52
  return result;
@@ -2,7 +2,7 @@ import { getLocaleConfig, isConfigId } from '@scrabble-solver/configs';
2
2
  import { BLANK } from '@scrabble-solver/constants';
3
3
  import { dictionaries } from '@scrabble-solver/dictionaries';
4
4
  import logger from '@scrabble-solver/logger';
5
- import Solver from '@scrabble-solver/solver';
5
+ import { solve as solveScrabble } from '@scrabble-solver/solver';
6
6
  import { Board, Config, isBoardJson, isLocale, Locale, Tile } from '@scrabble-solver/types';
7
7
  import { NextApiRequest, NextApiResponse } from 'next';
8
8
 
@@ -36,9 +36,8 @@ const solve = async (request: NextApiRequest, response: NextApiResponse): Promis
36
36
 
37
37
  const trie = await dictionaries.get(locale);
38
38
  const tiles = characters.map((character) => new Tile({ character, isBlank: character === BLANK }));
39
- const solver = new Solver(config, trie);
40
- const results = solver.solve(board, tiles);
41
- response.status(200).send(results.map((result) => result.toJson()));
39
+ const results = solveScrabble(trie, config, board, tiles);
40
+ response.status(200).send(results);
42
41
  } catch (error) {
43
42
  const message = error instanceof Error ? error.message : 'Unknown error';
44
43
  logger.error('solve - error', { error, meta });
@@ -0,0 +1,71 @@
1
+ import { getLocaleConfig, isConfigId } from '@scrabble-solver/configs';
2
+ import { dictionaries } from '@scrabble-solver/dictionaries';
3
+ import logger from '@scrabble-solver/logger';
4
+ import { Board, Config, isBoardJson, isLocale, Locale } from '@scrabble-solver/types';
5
+ import { NextApiRequest, NextApiResponse } from 'next';
6
+
7
+ import { getServerLoggingData, isBoardValid } from 'api';
8
+
9
+ interface RequestData {
10
+ board: Board;
11
+ config: Config;
12
+ locale: Locale;
13
+ }
14
+
15
+ const verify = async (request: NextApiRequest, response: NextApiResponse): Promise<void> => {
16
+ const meta = getServerLoggingData(request);
17
+
18
+ try {
19
+ const { board, locale } = parseRequest(request);
20
+
21
+ logger.info('verify - request', {
22
+ meta,
23
+ payload: {
24
+ board: board.toString(),
25
+ boardBlanksCount: board.getBlanksCount(),
26
+ boardTilesCount: board.getTilesCount(),
27
+ configId: request.body.configId,
28
+ locale,
29
+ },
30
+ });
31
+
32
+ const trie = await dictionaries.get(locale);
33
+ const words = board.getWords().sort((a, b) => a.localeCompare(b));
34
+ const invalidWords = words.filter((word) => !trie.has(word));
35
+ const validWords = words.filter((word) => trie.has(word));
36
+ response.status(200).send({ invalidWords, validWords });
37
+ } catch (error) {
38
+ const message = error instanceof Error ? error.message : 'Unknown error';
39
+ logger.error('verify - error', { error, meta });
40
+ response.status(500).send({ error: 'Server error', message });
41
+ throw error;
42
+ }
43
+ };
44
+
45
+ const parseRequest = (request: NextApiRequest): RequestData => {
46
+ const { board: boardJson, configId, locale } = request.body;
47
+
48
+ if (!isLocale(locale)) {
49
+ throw new Error('Invalid "locale" parameter');
50
+ }
51
+
52
+ if (!isConfigId(configId)) {
53
+ throw new Error('Invalid "configId" parameter');
54
+ }
55
+
56
+ const config = getLocaleConfig(configId, locale);
57
+
58
+ if (!isBoardJson(boardJson) || !isBoardValid(boardJson, config)) {
59
+ throw new Error('Invalid "board" parameter');
60
+ }
61
+
62
+ const board = Board.fromJson(boardJson);
63
+
64
+ return {
65
+ board,
66
+ config,
67
+ locale,
68
+ };
69
+ };
70
+
71
+ export default verify;
@@ -19,6 +19,7 @@ import {
19
19
  Settings,
20
20
  Splash,
21
21
  Well,
22
+ Words,
22
23
  } from 'components';
23
24
  import { useIsTablet, useLocalStorage } from 'hooks';
24
25
  import { getCellSize } from 'lib';
@@ -39,6 +40,7 @@ const Index: FunctionComponent<Props> = ({ version }) => {
39
40
  const [showKeyMap, setShowKeyMap] = useState(false);
40
41
  const [showRemainingTiles, setShowRemainingTiles] = useState(false);
41
42
  const [showSettings, setShowSettings] = useState(false);
43
+ const [showWords, setShowWords] = useState(false);
42
44
  const [boardRef, { height: boardHeight }] = useMeasure<HTMLDivElement>();
43
45
  const [contentRef, { height: contentHeight, width: contentWidth }] = useMeasure<HTMLDivElement>();
44
46
  const [resultsContainerRef, { height: resultsContainerHeight, width: resultsContainerWidth }] =
@@ -85,6 +87,7 @@ const Index: FunctionComponent<Props> = ({ version }) => {
85
87
  onShowKeyMap={() => setShowKeyMap(true)}
86
88
  onShowRemainingTiles={() => setShowRemainingTiles(true)}
87
89
  onShowSettings={() => setShowSettings(true)}
90
+ onShowWords={() => setShowWords(true)}
88
91
  />
89
92
  </div>
90
93
 
@@ -122,6 +125,8 @@ const Index: FunctionComponent<Props> = ({ version }) => {
122
125
 
123
126
  <KeyMap isOpen={showKeyMap} onClose={() => setShowKeyMap(false)} />
124
127
 
128
+ <Words isOpen={showWords} onClose={() => setShowWords(false)} />
129
+
125
130
  <RemainingTiles isOpen={showRemainingTiles} onClose={() => setShowRemainingTiles(false)} />
126
131
 
127
132
  <Splash forceShow={!isInitialized} onAnimationEnd={handleSplashAnimationEnd} />
@@ -0,0 +1,36 @@
1
+ import { isError } from '@scrabble-solver/types';
2
+
3
+ const fetchJson = async <T>(input: RequestInfo | URL, init?: RequestInit): Promise<T> => {
4
+ let response: Response;
5
+
6
+ try {
7
+ response = await fetch(input, {
8
+ ...init,
9
+ headers: {
10
+ 'Content-Type': 'application/json',
11
+ ...init?.headers,
12
+ },
13
+ });
14
+ } catch (error) {
15
+ const message = isError(error) ? error.message : 'Unknown error';
16
+ throw new Error(`Network error: ${message}`);
17
+ }
18
+
19
+ if (response.ok) {
20
+ return response.json();
21
+ }
22
+
23
+ try {
24
+ const json = await response.json();
25
+
26
+ if (isError(json)) {
27
+ throw new Error(json.message);
28
+ }
29
+ } finally {
30
+ // do nothing
31
+ }
32
+
33
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
34
+ };
35
+
36
+ export default fetchJson;
@@ -1,8 +1,9 @@
1
- import { Locale, WordDefinition } from '@scrabble-solver/types';
1
+ import { Locale, WordDefinition, WordDefinitionJson } from '@scrabble-solver/types';
2
+
3
+ import fetchJson from './fetchJson';
2
4
 
3
5
  const findWordDefinitions = async (locale: Locale, word: string): Promise<WordDefinition[]> => {
4
- const url = `/api/dictionary/${locale}/${word}`;
5
- const json = await fetch(url).then((response) => response.json());
6
+ const json = await fetchJson<WordDefinitionJson[]>(`/api/dictionary/${locale}/${word}`);
6
7
  return json.map(WordDefinition.fromJson);
7
8
  };
8
9
 
package/src/sdk/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { default as findWordDefinitions } from './findWordDefinitions';
2
2
  export { default as solve } from './solve';
3
+ export { default as verify } from './verify';
3
4
  export { default as visit } from './visit';
package/src/sdk/solve.ts CHANGED
@@ -1,4 +1,6 @@
1
- import { BoardJson, Locale, ResultJson } from '@scrabble-solver/types';
1
+ import { BoardJson, Locale, Result, ResultJson } from '@scrabble-solver/types';
2
+
3
+ import fetchJson from './fetchJson';
2
4
 
3
5
  interface Payload {
4
6
  board: BoardJson;
@@ -7,14 +9,13 @@ interface Payload {
7
9
  locale: Locale;
8
10
  }
9
11
 
10
- const solve = ({ board, characters, configId, locale }: Payload): Promise<ResultJson[]> => {
11
- return fetch('/api/solve', {
12
+ const solve = async ({ board, characters, configId, locale }: Payload): Promise<Result[]> => {
13
+ const json = await fetchJson<ResultJson[]>('/api/solve', {
12
14
  method: 'POST',
13
- headers: {
14
- 'Content-Type': 'application/json',
15
- },
16
15
  body: JSON.stringify({ board, characters, configId, locale }),
17
- }).then((response) => response.json());
16
+ });
17
+
18
+ return json.map(Result.fromJson);
18
19
  };
19
20
 
20
21
  export default solve;
@@ -0,0 +1,23 @@
1
+ import { BoardJson, Locale } from '@scrabble-solver/types';
2
+
3
+ import fetchJson from './fetchJson';
4
+
5
+ interface Payload {
6
+ board: BoardJson;
7
+ configId: string;
8
+ locale: Locale;
9
+ }
10
+
11
+ interface Response {
12
+ invalidWords: string[];
13
+ validWords: string[];
14
+ }
15
+
16
+ const verify = async ({ board, configId, locale }: Payload): Promise<Response> => {
17
+ return fetchJson<Response>('/api/verify', {
18
+ method: 'POST',
19
+ body: JSON.stringify({ board, configId, locale }),
20
+ });
21
+ };
22
+
23
+ export default verify;
@@ -8,6 +8,7 @@ import {
8
8
  resultsSlice,
9
9
  settingsSlice,
10
10
  solveSlice,
11
+ verifySlice,
11
12
  } from './slices';
12
13
 
13
14
  const rootReducer = combineReducers({
@@ -18,6 +19,7 @@ const rootReducer = combineReducers({
18
19
  results: resultsSlice.reducer,
19
20
  settings: settingsSlice.reducer,
20
21
  solve: solveSlice.reducer,
22
+ verify: verifySlice.reducer,
21
23
  });
22
24
 
23
25
  export default rootReducer;
@@ -3,7 +3,7 @@ import { Result } from '@scrabble-solver/types';
3
3
  import { call, delay, put, select, takeEvery, takeLatest } from 'redux-saga/effects';
4
4
 
5
5
  import { memoize } from 'lib';
6
- import { findWordDefinitions, solve, visit } from 'sdk';
6
+ import { findWordDefinitions, solve, verify, visit } from 'sdk';
7
7
 
8
8
  import { initialize, reset } from './actions';
9
9
  import {
@@ -23,6 +23,7 @@ import {
23
23
  resultsSlice,
24
24
  settingsSlice,
25
25
  solveSlice,
26
+ verifySlice,
26
27
  } from './slices';
27
28
 
28
29
  const SUBMIT_DELAY = 150;
@@ -43,7 +44,8 @@ export function* rootSaga(): AnyGenerator {
43
44
  yield takeLatest(dictionarySlice.actions.submit.type, onDictionarySubmit);
44
45
  yield takeLatest(initialize.type, onInitialize);
45
46
  yield takeLatest(reset.type, onReset);
46
- yield takeLatest(solveSlice.actions.submit.type, onSubmit);
47
+ yield takeLatest(solveSlice.actions.submit.type, onSolve);
48
+ yield takeLatest(verifySlice.actions.submit.type, onVerify);
47
49
  }
48
50
 
49
51
  function* onCellValueChange({ payload }: PayloadAction<{ value: string; x: number; y: number }>): AnyGenerator {
@@ -52,6 +54,8 @@ function* onCellValueChange({ payload }: PayloadAction<{ value: string; x: numbe
52
54
  if (isFiltered) {
53
55
  yield put(cellFilterSlice.actions.toggle(payload));
54
56
  }
57
+
58
+ yield put(verifySlice.actions.submit());
55
59
  }
56
60
 
57
61
  function* onApplyResult({ payload: result }: PayloadAction<Result>): AnyGenerator {
@@ -60,11 +64,13 @@ function* onApplyResult({ payload: result }: PayloadAction<Result>): AnyGenerato
60
64
  yield put(cellFilterSlice.actions.reset());
61
65
  yield put(rackSlice.actions.removeTiles(result.tiles));
62
66
  yield put(rackSlice.actions.groupTiles(autoGroupTiles));
67
+ yield put(verifySlice.actions.submit());
63
68
  }
64
69
 
65
70
  function* onConfigIdChange(): AnyGenerator {
66
71
  yield put(resultsSlice.actions.reset());
67
72
  yield put(solveSlice.actions.submit());
73
+ yield put(verifySlice.actions.submit());
68
74
  yield* ensureProperTilesCount();
69
75
  }
70
76
 
@@ -80,7 +86,7 @@ function* onDictionarySubmit(): AnyGenerator {
80
86
  const wordDefinitions = yield call(memoizedFindWordDefinitions, locale, word);
81
87
  yield put(dictionarySlice.actions.submitSuccess(wordDefinitions));
82
88
  } catch (error) {
83
- yield put(dictionarySlice.actions.submitFailure());
89
+ yield put(dictionarySlice.actions.submitFailure(error));
84
90
  }
85
91
  }
86
92
 
@@ -95,12 +101,14 @@ function* onReset(): AnyGenerator {
95
101
  yield put(dictionarySlice.actions.reset());
96
102
  yield put(rackSlice.actions.reset());
97
103
  yield put(resultsSlice.actions.reset());
104
+ yield put(verifySlice.actions.submit());
98
105
  }
99
106
 
100
107
  function* onLocaleChange(): AnyGenerator {
101
- yield put(solveSlice.actions.submit());
102
- yield put(resultsSlice.actions.changeResultCandidate(null));
103
108
  yield put(dictionarySlice.actions.reset());
109
+ yield put(resultsSlice.actions.changeResultCandidate(null));
110
+ yield put(solveSlice.actions.submit());
111
+ yield put(verifySlice.actions.submit());
104
112
  }
105
113
 
106
114
  function* onResultCandidateChange({ payload: result }: PayloadAction<Result | null>): AnyGenerator {
@@ -110,7 +118,7 @@ function* onResultCandidateChange({ payload: result }: PayloadAction<Result | nu
110
118
  }
111
119
  }
112
120
 
113
- function* onSubmit(): AnyGenerator {
121
+ function* onSolve(): AnyGenerator {
114
122
  const board = yield select(selectBoard);
115
123
  const { config } = yield select(selectConfig);
116
124
  const locale = yield select(selectLocale);
@@ -129,11 +137,30 @@ function* onSubmit(): AnyGenerator {
129
137
  configId: config.id,
130
138
  locale,
131
139
  });
140
+ yield put(resultsSlice.actions.changeResults(results));
132
141
  yield put(solveSlice.actions.submitSuccess({ board, characters }));
133
- yield put(resultsSlice.actions.changeResults(results.map(Result.fromJson)));
134
142
  } catch (error) {
135
143
  yield put(resultsSlice.actions.changeResults([]));
136
- yield put(solveSlice.actions.submitFailure());
144
+ yield put(solveSlice.actions.submitFailure(error));
145
+ }
146
+ }
147
+
148
+ function* onVerify(): AnyGenerator {
149
+ yield delay(SUBMIT_DELAY);
150
+
151
+ const board = yield select(selectBoard);
152
+ const { config } = yield select(selectConfig);
153
+ const locale = yield select(selectLocale);
154
+
155
+ try {
156
+ const { invalidWords, validWords } = yield call(verify, {
157
+ board: board.toJson(),
158
+ configId: config.id,
159
+ locale,
160
+ });
161
+ yield put(verifySlice.actions.submitSuccess({ board, invalidWords, validWords }));
162
+ } catch (error) {
163
+ yield put(verifySlice.actions.submitFailure());
137
164
  }
138
165
  }
139
166
 
@@ -1,6 +1,6 @@
1
1
  import { createSelector } from '@reduxjs/toolkit';
2
2
  import { getLocaleConfig } from '@scrabble-solver/configs';
3
- import { Cell, Config, Result, Tile } from '@scrabble-solver/types';
3
+ import { Cell, Config, isError, Result, Tile } from '@scrabble-solver/types';
4
4
 
5
5
  import i18n from 'i18n';
6
6
  import { findCell, getRemainingTiles, getRemainingTilesGroups, sortResults, unorderedArraysEqual } from 'lib';
@@ -30,8 +30,14 @@ const selectSettingsRoot = (state: RootState): RootState['settings'] => state.se
30
30
 
31
31
  const selectSolveRoot = (state: RootState): RootState['solve'] => state.solve;
32
32
 
33
+ const selectVerifyRoot = (state: RootState): RootState['verify'] => state.verify;
34
+
33
35
  export const selectDictionary = selectDictionaryRoot;
34
36
 
37
+ export const selectDictionaryError = createSelector([selectDictionaryRoot], (dictionary) => {
38
+ return isError(dictionary.error) ? dictionary.error : undefined;
39
+ });
40
+
35
41
  export const selectAutoGroupTiles = createSelector([selectSettingsRoot], (settings) => settings.autoGroupTiles);
36
42
 
37
43
  export const selectLocale = createSelector([selectSettingsRoot], (settings) => settings.locale);
@@ -156,6 +162,10 @@ export const selectLastSolvedParameters = createSelector([selectSolveRoot], (sol
156
162
 
157
163
  export const selectIsLoading = createSelector([selectSolveRoot], (solve) => solve.isLoading);
158
164
 
165
+ export const selectSolveError = createSelector([selectSolveRoot], (solve) => {
166
+ return isError(solve.error) ? solve.error : undefined;
167
+ });
168
+
159
169
  export const selectHaveCharactersChanged = createSelector(
160
170
  [selectLastSolvedParameters, selectCharacters],
161
171
  (lastSolvedParameters, characters) => {
@@ -183,3 +193,9 @@ export const selectHasOverusedTiles = createSelector([selectRemainingTiles], (re
183
193
  });
184
194
 
185
195
  export const selectRemainingTilesGroups = createSelector([selectRemainingTiles], getRemainingTilesGroups);
196
+
197
+ export const selectVerify = selectVerifyRoot;
198
+
199
+ export const selectHasInvalidWords = createSelector([selectVerify], ({ invalidWords }) => {
200
+ return invalidWords.length > 0;
201
+ });
@@ -1,9 +1,17 @@
1
1
  import { WordDefinition } from '@scrabble-solver/types';
2
2
 
3
- const dictionaryInitialState = {
3
+ interface DictionaryInitialState {
4
+ error: unknown | undefined;
5
+ input: string;
6
+ isLoading: boolean;
7
+ results: WordDefinition[];
8
+ }
9
+
10
+ const dictionaryInitialState: DictionaryInitialState = {
11
+ error: undefined,
4
12
  input: '',
5
13
  isLoading: false,
6
- results: [] as WordDefinition[],
14
+ results: [],
7
15
  };
8
16
 
9
17
  export default dictionaryInitialState;