@scrabble-solver/scrabble-solver 2.8.6 → 2.8.7

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 (85) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +9 -9
  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 +183 -94
  14. package/.next/server/chunks/{206.js → 429.js} +2 -4137
  15. package/.next/server/chunks/515.js +92 -70
  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/_document.js.nft.json +1 -1
  24. package/.next/server/pages/_error.js.nft.json +1 -1
  25. package/.next/server/pages/api/dictionary/[locale]/[word].js +33 -17
  26. package/.next/server/pages/api/dictionary/[locale]/[word].js.nft.json +1 -1
  27. package/.next/server/pages/api/solve.js +399 -56
  28. package/.next/server/pages/api/solve.js.nft.json +1 -1
  29. package/.next/server/pages/api/visit.js +3 -2
  30. package/.next/server/pages/api/visit.js.nft.json +1 -1
  31. package/.next/server/pages/index.html +3 -7
  32. package/.next/server/pages/index.js +12 -14
  33. package/.next/server/pages/index.js.nft.json +1 -1
  34. package/.next/server/pages/index.json +1 -1
  35. package/.next/static/chunks/56-cf37c430261bbea5.js +1 -0
  36. package/.next/static/chunks/pages/_app-0b12a65bea70a0df.js +1 -0
  37. package/.next/static/chunks/pages/index-fcb69802550afb81.js +1 -0
  38. package/.next/static/css/1f39b55d50f5b30b.css +1 -0
  39. package/.next/static/css/751e8a14776d05d8.css +1 -0
  40. package/.next/static/z_0_lqfmiI_ISokr6NNRq/_buildManifest.js +1 -0
  41. package/.next/static/{VjSpyGDWyVaO0muz54q_j → z_0_lqfmiI_ISokr6NNRq}/_ssgManifest.js +0 -0
  42. package/.next/trace +40 -42
  43. package/package.json +9 -9
  44. package/src/api/index.ts +3 -9
  45. package/src/api/isBoardValid.ts +43 -0
  46. package/src/api/isCellValid.ts +26 -0
  47. package/src/api/isRowValid.ts +19 -0
  48. package/src/components/Dictionary/Dictionary.module.scss +20 -0
  49. package/src/components/Dictionary/Dictionary.tsx +40 -29
  50. package/src/components/Results/Cell.tsx +3 -2
  51. package/src/components/Results/Result.tsx +16 -6
  52. package/src/components/ResultsInput/ResultsInput.tsx +11 -3
  53. package/src/hooks/useIsTablet.ts +2 -2
  54. package/src/lib/getRemainingTiles.ts +1 -1
  55. package/src/lib/index.ts +2 -1
  56. package/src/lib/isRegExp.ts +11 -0
  57. package/src/lib/isStringArray.ts +5 -0
  58. package/src/lib/sortResults.ts +5 -5
  59. package/src/pages/api/dictionary/[locale]/[word].ts +35 -11
  60. package/src/pages/api/solve.ts +39 -19
  61. package/src/pages/api/visit.ts +1 -0
  62. package/src/pages/index.module.scss +5 -11
  63. package/src/pages/index.tsx +5 -5
  64. package/src/sdk/{findWordDefinition.ts → findWordDefinitions.ts} +3 -3
  65. package/src/sdk/index.ts +1 -1
  66. package/src/state/sagas.ts +11 -11
  67. package/src/state/selectors.ts +6 -2
  68. package/src/state/slices/dictionaryInitialState.ts +3 -3
  69. package/src/state/slices/dictionarySlice.ts +4 -10
  70. package/.next/static/VjSpyGDWyVaO0muz54q_j/_buildManifest.js +0 -1
  71. package/.next/static/chunks/56-e2797384ae4b0fc0.js +0 -1
  72. package/.next/static/chunks/pages/_app-5136d33b9b007fd7.js +0 -1
  73. package/.next/static/chunks/pages/index-13ea7770a65c69ee.js +0 -1
  74. package/.next/static/css/3159cfe62ff742a3.css +0 -1
  75. package/.next/static/css/729bb37fe8f9bee6.css +0 -1
  76. package/src/api/validateBoard.ts +0 -45
  77. package/src/api/validateCell.ts +0 -40
  78. package/src/api/validateCharacter.ts +0 -14
  79. package/src/api/validateCharacters.ts +0 -24
  80. package/src/api/validateConfigId.ts +0 -9
  81. package/src/api/validateLocale.ts +0 -15
  82. package/src/api/validateRow.ts +0 -17
  83. package/src/api/validateTile.ts +0 -21
  84. package/src/api/validateWord.ts +0 -11
  85. 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.7",
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.7",
34
+ "@scrabble-solver/constants": "^2.8.7",
35
+ "@scrabble-solver/dictionaries": "^2.8.7",
36
+ "@scrabble-solver/logger": "^2.8.7",
37
+ "@scrabble-solver/solver": "^2.8.7",
38
+ "@scrabble-solver/types": "^2.8.7",
39
+ "@scrabble-solver/word-definitions": "^2.8.7",
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": "879bf9b68f27fa3d530d0582fd8fbbcd5c8b150b"
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;
@@ -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
  };
@@ -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 +
package/src/lib/index.ts CHANGED
@@ -15,8 +15,9 @@ export { default as getTotalRemainingTilesCount } from './getTotalRemainingTiles
15
15
  export { default as getTileSizes } from './getTileSizes';
16
16
  export { default as inverseDirection } from './inverseDirection';
17
17
  export { default as isCtrl } from './isCtrl';
18
- export { default as isLocale } from './isLocale';
19
18
  export { default as isMac } from './isMac';
19
+ export { default as isRegExp } from './isRegExp';
20
+ export { default as isStringArray } from './isStringArray';
20
21
  export { default as memoize } from './memoize';
21
22
  export { default as noop } from './noop';
22
23
  export { default as numberComparator } from './numberComparator';
@@ -0,0 +1,11 @@
1
+ const isRegExp = (value: string): boolean => {
2
+ try {
3
+ // eslint-disable-next-line no-new
4
+ new RegExp(value);
5
+ return true;
6
+ } catch {
7
+ return false;
8
+ }
9
+ };
10
+
11
+ export default isRegExp;
@@ -0,0 +1,5 @@
1
+ const isStringArray = (value: unknown): value is string[] => {
2
+ return Array.isArray(value) && value.every((item) => typeof item === 'string');
3
+ };
4
+
5
+ export default isStringArray;
@@ -6,13 +6,13 @@ import createKeyComparator from './createKeyComparator';
6
6
  import reverseComparator from './reverseComparator';
7
7
 
8
8
  const comparators: Record<ResultColumn, Comparator<Result>> = {
9
- [ResultColumn.BlanksCount]: createKeyComparator('numberOfBlanks'),
10
- [ResultColumn.ConsonantsCount]: createKeyComparator('numberOfConsonants'),
9
+ [ResultColumn.BlanksCount]: createKeyComparator('blanksCount'),
10
+ [ResultColumn.ConsonantsCount]: createKeyComparator('consonantsCount'),
11
11
  [ResultColumn.Points]: createKeyComparator('points'),
12
- [ResultColumn.TilesCount]: createKeyComparator('numberOfTiles'),
13
- [ResultColumn.VowelsCount]: createKeyComparator('numberOfVowels'),
12
+ [ResultColumn.TilesCount]: createKeyComparator('tilesCount'),
13
+ [ResultColumn.VowelsCount]: createKeyComparator('vowelsCount'),
14
14
  [ResultColumn.Word]: createKeyComparator('word'),
15
- [ResultColumn.WordsCount]: createKeyComparator('numberOfWords'),
15
+ [ResultColumn.WordsCount]: createKeyComparator('wordsCount'),
16
16
  };
17
17
 
18
18
  const sortResults = (
@@ -1,45 +1,69 @@
1
+ import { scrabble } from '@scrabble-solver/configs';
1
2
  import logger from '@scrabble-solver/logger';
2
- import { Locale } from '@scrabble-solver/types';
3
+ import { isLocale, Locale } from '@scrabble-solver/types';
3
4
  import { getWordDefinition } from '@scrabble-solver/word-definitions';
4
5
  import { NextApiRequest, NextApiResponse } from 'next';
5
6
 
6
- import { getServerLoggingData, validateLocale, validateWord } from 'api';
7
+ import { getServerLoggingData } from 'api';
7
8
 
8
9
  interface RequestData {
9
10
  locale: Locale;
10
- word: string;
11
+ words: string[];
11
12
  }
12
13
 
14
+ const MAXIMUM_WORDS_COUNT = scrabble['en-US'].maximumCharactersCount;
15
+
13
16
  const dictionary = async (request: NextApiRequest, response: NextApiResponse): Promise<void> => {
14
17
  const meta = getServerLoggingData(request);
15
18
 
16
19
  try {
17
- const { locale, word } = parseRequest(request);
20
+ const { locale, words } = parseRequest(request);
21
+
18
22
  logger.info('dictionary - request', {
19
23
  meta,
20
24
  payload: {
21
25
  locale,
22
- word,
26
+ words,
23
27
  },
24
28
  });
25
- const result = await getWordDefinition(locale, word);
26
- response.status(200).send(result.toJson());
29
+
30
+ const results = await Promise.all(words.map((word) => getWordDefinition(locale, word)));
31
+ response.status(200).send(results.map((result) => result.toJson()));
27
32
  } catch (error) {
28
33
  const message = error instanceof Error ? error.message : 'Unknown error';
29
34
  logger.error('dictionary - error', { error, meta });
30
35
  response.status(500).send({ error: 'Server error', message });
36
+ throw error;
31
37
  }
32
38
  };
33
39
 
34
40
  const parseRequest = (request: NextApiRequest): RequestData => {
35
41
  const { locale, word } = request.query;
36
42
 
37
- validateLocale(locale);
38
- validateWord(word);
43
+ if (!isLocale(locale)) {
44
+ throw new Error('Invalid "locale" parameter');
45
+ }
46
+
47
+ if (typeof word !== 'string' || word.length === 0) {
48
+ throw new Error('Invalid "word" parameter');
49
+ }
50
+
51
+ const words = Array.from(
52
+ new Set(
53
+ word
54
+ .split(',')
55
+ .map((part) => part.trim())
56
+ .filter(Boolean),
57
+ ),
58
+ );
59
+
60
+ if (words.length > MAXIMUM_WORDS_COUNT) {
61
+ throw new Error('Invalid "word" parameter');
62
+ }
39
63
 
40
64
  return {
41
- locale: locale as Locale,
42
- word: word as string,
65
+ locale,
66
+ words,
43
67
  };
44
68
  };
45
69
 
@@ -1,12 +1,13 @@
1
- import { getLocaleConfig } from '@scrabble-solver/configs';
1
+ 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
5
  import Solver from '@scrabble-solver/solver';
6
- import { Board, Config, Locale, Tile } from '@scrabble-solver/types';
6
+ import { Board, Config, isBoardJson, isLocale, Locale, Tile } from '@scrabble-solver/types';
7
7
  import { NextApiRequest, NextApiResponse } from 'next';
8
8
 
9
- import { getServerLoggingData, validateBoard, validateCharacters, validateConfigId, validateLocale } from 'api';
9
+ import { getServerLoggingData, isBoardValid } from 'api';
10
+ import { isStringArray } from 'lib';
10
11
 
11
12
  interface RequestData {
12
13
  board: Board;
@@ -20,6 +21,7 @@ const solve = async (request: NextApiRequest, response: NextApiResponse): Promis
20
21
 
21
22
  try {
22
23
  const { board, characters, config, locale } = parseRequest(request);
24
+
23
25
  logger.info('solve - request', {
24
26
  meta,
25
27
  payload: {
@@ -31,7 +33,7 @@ const solve = async (request: NextApiRequest, response: NextApiResponse): Promis
31
33
  locale,
32
34
  },
33
35
  });
34
- validateRequest({ board, characters, config, locale });
36
+
35
37
  const trie = await dictionaries.get(locale);
36
38
  const tiles = characters.map((character) => new Tile({ character, isBlank: character === BLANK }));
37
39
  const solver = new Solver(config, trie);
@@ -41,33 +43,51 @@ const solve = async (request: NextApiRequest, response: NextApiResponse): Promis
41
43
  const message = error instanceof Error ? error.message : 'Unknown error';
42
44
  logger.error('solve - error', { error, meta });
43
45
  response.status(500).send({ error: 'Server error', message });
46
+ throw error;
44
47
  }
45
48
  };
46
49
 
47
50
  const parseRequest = (request: NextApiRequest): RequestData => {
48
- const { board, characters, configId, locale } = request.body;
51
+ const { board: boardJson, characters, configId, locale } = request.body;
52
+
53
+ if (!isLocale(locale)) {
54
+ throw new Error('Invalid "locale" parameter');
55
+ }
56
+
57
+ if (!isConfigId(configId)) {
58
+ throw new Error('Invalid "configId" parameter');
59
+ }
49
60
 
50
- validateConfigId(configId);
51
- validateLocale(locale);
52
61
  const config = getLocaleConfig(configId, locale);
53
- validateBoard(board, config);
54
- validateCharacters(characters, config);
55
62
 
56
- return {
57
- board: Board.fromJson(board),
58
- characters,
59
- config,
60
- locale,
61
- };
62
- };
63
+ if (!isBoardJson(boardJson) || !isBoardValid(boardJson, config)) {
64
+ throw new Error('Invalid "board" parameter');
65
+ }
66
+
67
+ if (!isStringArray(characters) || characters.length === 0) {
68
+ throw new Error('Invalid "characters" parameter');
69
+ }
63
70
 
64
- const validateRequest = ({ board, characters, config }: RequestData): void => {
71
+ for (const character of characters) {
72
+ if (!config.hasCharacter(character) && character !== BLANK) {
73
+ throw new Error('Invalid "characters" parameter');
74
+ }
75
+ }
76
+
77
+ const board = Board.fromJson(boardJson);
65
78
  const blankTilesCount = characters.filter((character) => character === BLANK).length;
66
79
  const blanksCount = board.getBlanksCount() + blankTilesCount;
67
80
 
68
- if (blanksCount > config.numberOfBlanks) {
69
- throw new Error(`Too many blank tiles passed (board: ${board.getBlanksCount()}, tiles: ${blankTilesCount})`);
81
+ if (blanksCount > config.blanksCount) {
82
+ throw new Error('Too many blank tiles passed');
70
83
  }
84
+
85
+ return {
86
+ board,
87
+ characters,
88
+ config,
89
+ locale,
90
+ };
71
91
  };
72
92
 
73
93
  export default solve;
@@ -13,6 +13,7 @@ const visit = async (request: NextApiRequest, response: NextApiResponse): Promis
13
13
  const message = error instanceof Error ? error.message : 'Unknown error';
14
14
  logger.error('visit - error', { error, meta });
15
15
  response.status(500).send({ error: 'Server error', message });
16
+ throw error;
16
17
  }
17
18
  };
18
19
 
@@ -25,11 +25,15 @@
25
25
  padding: var(--spacing--l);
26
26
  }
27
27
 
28
- .logoContainer {
28
+ .navLogo {
29
29
  display: flex;
30
30
  flex: 1;
31
31
  }
32
32
 
33
+ .logoContainer {
34
+ display: flex;
35
+ }
36
+
33
37
  .logo {
34
38
  height: 60px;
35
39
  user-select: none;
@@ -127,13 +131,3 @@
127
131
  .submitInput {
128
132
  display: none;
129
133
  }
130
-
131
- .version {
132
- position: fixed;
133
- bottom: var(--spacing--m);
134
- left: 0;
135
- right: 0;
136
- font-size: var(--font--size--xs);
137
- color: var(--color--background);
138
- text-align: center;
139
- }