@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
@@ -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';