@scrabble-solver/scrabble-solver 2.8.5 → 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 (93) 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 +111 -78
  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 +3 -2
  23. package/.next/server/pages/_app.js.nft.json +1 -1
  24. package/.next/server/pages/_document.js.nft.json +1 -1
  25. package/.next/server/pages/_error.js.nft.json +1 -1
  26. package/.next/server/pages/api/dictionary/[locale]/[word].js +33 -17
  27. package/.next/server/pages/api/dictionary/[locale]/[word].js.nft.json +1 -1
  28. package/.next/server/pages/api/solve.js +404 -58
  29. package/.next/server/pages/api/solve.js.nft.json +1 -1
  30. package/.next/server/pages/api/visit.js +3 -2
  31. package/.next/server/pages/api/visit.js.nft.json +1 -1
  32. package/.next/server/pages/index.html +3 -7
  33. package/.next/server/pages/index.js +12 -14
  34. package/.next/server/pages/index.js.nft.json +1 -1
  35. package/.next/server/pages/index.json +1 -1
  36. package/.next/static/chunks/56-cf37c430261bbea5.js +1 -0
  37. package/.next/static/chunks/pages/_app-0b12a65bea70a0df.js +1 -0
  38. package/.next/static/chunks/pages/index-fcb69802550afb81.js +1 -0
  39. package/.next/static/css/1f39b55d50f5b30b.css +1 -0
  40. package/.next/static/css/751e8a14776d05d8.css +1 -0
  41. package/.next/static/z_0_lqfmiI_ISokr6NNRq/_buildManifest.js +1 -0
  42. package/.next/static/{TzKQ3IntkvaYmHBkWpfoi → z_0_lqfmiI_ISokr6NNRq}/_ssgManifest.js +0 -0
  43. package/.next/trace +40 -42
  44. package/package.json +9 -9
  45. package/src/api/index.ts +3 -9
  46. package/src/api/isBoardValid.ts +43 -0
  47. package/src/api/isCellValid.ts +26 -0
  48. package/src/api/isRowValid.ts +19 -0
  49. package/src/components/Board/Board.tsx +3 -1
  50. package/src/components/Board/BoardPure.tsx +4 -1
  51. package/src/components/Board/components/Cell/Cell.module.scss +28 -11
  52. package/src/components/Board/components/Cell/Cell.tsx +12 -1
  53. package/src/components/Board/components/Cell/CellPure.tsx +3 -1
  54. package/src/components/Board/components/Cell/lib.ts +10 -2
  55. package/src/components/Dictionary/Dictionary.module.scss +20 -0
  56. package/src/components/Dictionary/Dictionary.tsx +40 -29
  57. package/src/components/Results/Cell.tsx +3 -2
  58. package/src/components/Results/Result.tsx +16 -6
  59. package/src/components/ResultsInput/ResultsInput.tsx +11 -3
  60. package/src/hooks/useIsTablet.ts +2 -2
  61. package/src/lib/getRemainingTiles.ts +1 -1
  62. package/src/lib/index.ts +2 -1
  63. package/src/lib/isRegExp.ts +11 -0
  64. package/src/lib/isStringArray.ts +5 -0
  65. package/src/lib/sortResults.ts +5 -5
  66. package/src/pages/_app.tsx +6 -3
  67. package/src/pages/api/dictionary/[locale]/[word].ts +35 -11
  68. package/src/pages/api/solve.ts +39 -19
  69. package/src/pages/api/visit.ts +1 -0
  70. package/src/pages/index.module.scss +5 -11
  71. package/src/pages/index.tsx +5 -5
  72. package/src/sdk/{findWordDefinition.ts → findWordDefinitions.ts} +3 -3
  73. package/src/sdk/index.ts +1 -1
  74. package/src/state/sagas.ts +11 -11
  75. package/src/state/selectors.ts +6 -2
  76. package/src/state/slices/dictionaryInitialState.ts +3 -3
  77. package/src/state/slices/dictionarySlice.ts +4 -10
  78. package/.next/static/TzKQ3IntkvaYmHBkWpfoi/_buildManifest.js +0 -1
  79. package/.next/static/chunks/56-2d34867599a0ac66.js +0 -1
  80. package/.next/static/chunks/pages/_app-6ffa2ab900772b67.js +0 -1
  81. package/.next/static/chunks/pages/index-13ea7770a65c69ee.js +0 -1
  82. package/.next/static/css/3159cfe62ff742a3.css +0 -1
  83. package/.next/static/css/551d09cac435debb.css +0 -1
  84. package/src/api/validateBoard.ts +0 -45
  85. package/src/api/validateCell.ts +0 -40
  86. package/src/api/validateCharacter.ts +0 -14
  87. package/src/api/validateCharacters.ts +0 -24
  88. package/src/api/validateConfigId.ts +0 -9
  89. package/src/api/validateLocale.ts +0 -15
  90. package/src/api/validateRow.ts +0 -17
  91. package/src/api/validateTile.ts +0 -21
  92. package/src/api/validateWord.ts +0 -11
  93. 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.5",
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.5",
34
- "@scrabble-solver/constants": "^2.8.5",
35
- "@scrabble-solver/dictionaries": "^2.8.5",
36
- "@scrabble-solver/logger": "^2.8.5",
37
- "@scrabble-solver/solver": "^2.8.5",
38
- "@scrabble-solver/types": "^2.8.5",
39
- "@scrabble-solver/word-definitions": "^2.8.5",
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": "79ba3664458e7dc8d805599a3cf3cd1faba86309"
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;
@@ -1,6 +1,6 @@
1
1
  import { FunctionComponent, Ref } from 'react';
2
2
 
3
- import { selectRowsWithCandidate, useTypedSelector } from 'state';
3
+ import { selectBoard, selectRowsWithCandidate, useTypedSelector } from 'state';
4
4
 
5
5
  import BoardPure from './BoardPure';
6
6
  import { useGrid } from './hooks';
@@ -13,12 +13,14 @@ interface Props {
13
13
 
14
14
  const Board: FunctionComponent<Props> = ({ cellSize, className, innerRef }) => {
15
15
  const rows = useTypedSelector(selectRowsWithCandidate);
16
+ const board = useTypedSelector(selectBoard);
16
17
  const [{ lastDirection, refs }, { onDirectionToggle, onFocus, onKeyDown }] = useGrid(rows);
17
18
 
18
19
  return (
19
20
  <BoardPure
20
21
  className={className}
21
22
  cellSize={cellSize}
23
+ center={board.center}
22
24
  innerRef={innerRef}
23
25
  lastDirection={lastDirection}
24
26
  refs={refs}
@@ -8,6 +8,7 @@ import { Cell as CellComponent } from './components';
8
8
  interface Props {
9
9
  className?: string;
10
10
  cellSize: number;
11
+ center: Cell;
11
12
  innerRef?: Ref<HTMLDivElement>;
12
13
  lastDirection: 'horizontal' | 'vertical';
13
14
  refs: RefObject<HTMLInputElement>[][];
@@ -19,11 +20,12 @@ interface Props {
19
20
 
20
21
  const BoardPure: FunctionComponent<Props> = ({
21
22
  className,
23
+ cellSize,
24
+ center,
22
25
  innerRef,
23
26
  lastDirection,
24
27
  refs,
25
28
  rows,
26
- cellSize,
27
29
  onDirectionToggle,
28
30
  onFocus,
29
31
  onKeyDown,
@@ -37,6 +39,7 @@ const BoardPure: FunctionComponent<Props> = ({
37
39
  cell={cell}
38
40
  direction={lastDirection}
39
41
  inputRef={refs[y][x]}
42
+ isCenter={center.x === x && center.y === y}
40
43
  key={x}
41
44
  size={cellSize}
42
45
  onDirectionToggle={onDirectionToggle}
@@ -13,6 +13,34 @@ $icon-size: 16px;
13
13
  transition: var(--transition);
14
14
  background-clip: padding-box;
15
15
 
16
+ &.bonusStart,
17
+ &.bonusWord2,
18
+ &.bonusWord3 {
19
+ position: relative;
20
+
21
+ &:before {
22
+ display: flex;
23
+ justify-content: center;
24
+ align-items: center;
25
+ position: absolute;
26
+ top: 0;
27
+ bottom: 0;
28
+ left: 0;
29
+ right: 0;
30
+ font-weight: bold;
31
+ pointer-events: none;
32
+ }
33
+ }
34
+
35
+ &.bonusStart {
36
+ background-color: var(--color--red--light);
37
+
38
+ &:before {
39
+ content: '★';
40
+ color: var(--color--foreground--secondary);
41
+ }
42
+ }
43
+
16
44
  &.bonusCharacter1 {
17
45
  background-color: var(--color--yellow--light);
18
46
  }
@@ -39,22 +67,11 @@ $icon-size: 16px;
39
67
 
40
68
  &.bonusWord2,
41
69
  &.bonusWord3 {
42
- position: relative;
43
70
  background-color: var(--color--inactive);
44
71
 
45
72
  &:before {
46
- display: flex;
47
- justify-content: center;
48
- align-items: center;
49
- position: absolute;
50
- top: 0;
51
- bottom: 0;
52
- left: 0;
53
- right: 0;
54
73
  font-size: 75%;
55
- font-weight: bold;
56
74
  color: white;
57
- pointer-events: none;
58
75
  }
59
76
  }
60
77
 
@@ -13,12 +13,22 @@ interface Props {
13
13
  className?: string;
14
14
  direction: 'horizontal' | 'vertical';
15
15
  inputRef: RefObject<HTMLInputElement>;
16
+ isCenter: boolean;
16
17
  size: number;
17
18
  onDirectionToggle: () => void;
18
19
  onFocus: (x: number, y: number) => void;
19
20
  }
20
21
 
21
- const Cell: FunctionComponent<Props> = ({ cell, className, direction, inputRef, size, onDirectionToggle, onFocus }) => {
22
+ const Cell: FunctionComponent<Props> = ({
23
+ cell,
24
+ className,
25
+ direction,
26
+ inputRef,
27
+ isCenter,
28
+ size,
29
+ onDirectionToggle,
30
+ onFocus,
31
+ }) => {
22
32
  const { tile, x, y } = cell;
23
33
  const dispatch = useDispatch();
24
34
  const translate = useTranslate();
@@ -53,6 +63,7 @@ const Cell: FunctionComponent<Props> = ({ cell, className, direction, inputRef,
53
63
  className={className}
54
64
  direction={direction}
55
65
  inputRef={inputRef}
66
+ isCenter={isCenter}
56
67
  isEmpty={isEmpty}
57
68
  points={points}
58
69
  size={size}
@@ -17,6 +17,7 @@ interface Props {
17
17
  className?: string;
18
18
  direction: 'horizontal' | 'vertical';
19
19
  inputRef: RefObject<HTMLInputElement>;
20
+ isCenter: boolean;
20
21
  isEmpty: boolean;
21
22
  points?: number;
22
23
  size: number;
@@ -34,6 +35,7 @@ const CellPure: FunctionComponent<Props> = ({
34
35
  className,
35
36
  direction,
36
37
  inputRef,
38
+ isCenter,
37
39
  isEmpty,
38
40
  points,
39
41
  size,
@@ -45,7 +47,7 @@ const CellPure: FunctionComponent<Props> = ({
45
47
  onToggleBlankClick,
46
48
  }) => (
47
49
  <div
48
- className={classNames(styles.cell, getBonusClassname(cell, bonus), className, {
50
+ className={classNames(styles.cell, getBonusClassname(cell, bonus, isCenter), className, {
49
51
  [styles.candidate]: cell.isCandidate(),
50
52
  })}
51
53
  style={style}
@@ -20,8 +20,16 @@ const CHARACTER_MULTIPLIER_CLASSNAMES: Record<number, string> = {
20
20
  3: styles.bonusCharacterMultiplier3,
21
21
  };
22
22
 
23
- export const getBonusClassname = (cell: Cell, bonus: Bonus | undefined): string | undefined => {
24
- if (!bonus || !cell.isEmpty) {
23
+ export const getBonusClassname = (cell: Cell, bonus: Bonus | undefined, isCenter: boolean): string | undefined => {
24
+ if (!cell.isEmpty) {
25
+ return undefined;
26
+ }
27
+
28
+ if (isCenter) {
29
+ return styles.bonusStart;
30
+ }
31
+
32
+ if (!bonus) {
25
33
  return undefined;
26
34
  }
27
35
 
@@ -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 = (
@@ -7,7 +7,9 @@ import { createAppStore } from 'state';
7
7
 
8
8
  import 'styles/global.scss';
9
9
 
10
- const DESCRIPTION = 'Scrabble Solver 2 - The ultimate, open-source cheating app for Scrabble and Literaki';
10
+ const DESCRIPTION =
11
+ // eslint-disable-next-line max-len
12
+ 'Scrabble Solver 2 - Free and open-source analysis tool for Scrabble and Literaki. Quickly find top scoring words using given letters and board state. Available in English, French, German, Polish & Spanish.';
11
13
  const KEYWORDS = [
12
14
  'Scrabble',
13
15
  'Solver',
@@ -54,8 +56,9 @@ const App: FunctionComponent<AppProps> = ({ Component, pageProps }) => (
54
56
 
55
57
  <Provider store={store}>
56
58
  <p style={{ fontSize: 0 }}>
57
- Scrabble Solver 2 is the most popular, online, open-source, cheating app for Scrabble. It's available in
58
- English, French, German, Polish &amp; Spanish! Source code is available on GitHub - contributions are welcome!
59
+ Scrabble Solver 2 is a free and open-source analysis tool for Scrabble and Literaki. Quickly find top scoring
60
+ words using given letters and board state. Available in English, French, German, Polish & Spanish. Source code
61
+ is available on GitHub - contributions are welcome!
59
62
  </p>
60
63
  <Component {...pageProps} />
61
64
  </Provider>