@scrabble-solver/scrabble-solver 2.10.2 → 2.10.4

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 (215) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +31 -19
  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/edge-server-production/0.pack +0 -0
  9. package/.next/cache/webpack/edge-server-production/index.pack +0 -0
  10. package/.next/cache/webpack/server-production/0.pack +0 -0
  11. package/.next/cache/webpack/server-production/index.pack +0 -0
  12. package/.next/next-server.js.nft.json +1 -1
  13. package/.next/prerender-manifest.json +1 -1
  14. package/.next/routes-manifest.json +1 -1
  15. package/.next/server/chunks/176.js +1269 -1559
  16. package/.next/server/chunks/579.js +50 -26
  17. package/.next/server/middleware-build-manifest.js +1 -1
  18. package/.next/server/middleware-react-loadable-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/api/dictionary/[locale]/[word].js +2 -1
  24. package/.next/server/pages/api/solve.js +22 -6
  25. package/.next/server/pages/index.html +2 -2
  26. package/.next/server/pages/index.js +1539 -52
  27. package/.next/server/pages/index.js.nft.json +1 -1
  28. package/.next/server/pages/index.json +1 -1
  29. package/.next/server/pages-manifest.json +1 -1
  30. package/.next/static/P7XhuDLmwJJqC8kgPjX42/_buildManifest.js +1 -0
  31. package/.next/static/{FjrbXpI5fkt4lmko-vL_7 → P7XhuDLmwJJqC8kgPjX42}/_ssgManifest.js +0 -0
  32. package/.next/static/chunks/490-d29992f1c264d70e.js +5 -0
  33. package/.next/static/chunks/528-9942ddad0031ff79.js +1 -0
  34. package/.next/static/chunks/pages/{404-24f9617eeb8d6dc1.js → 404-6c99a0c91257c60b.js} +1 -1
  35. package/.next/static/chunks/pages/_app-fa0661b072fc6af9.js +24 -0
  36. package/.next/static/chunks/pages/index-d761f0af070273d2.js +1 -0
  37. package/.next/static/css/78e42ad01f580f64.css +1 -0
  38. package/.next/static/css/97eb6ee0c4300c83.css +1 -0
  39. package/.next/static/css/dcca0c1a39cf5ef5.css +1 -0
  40. package/.next/trace +55 -52
  41. package/package.json +11 -9
  42. package/src/components/Badge/Badge.module.scss +4 -5
  43. package/src/components/Board/BoardPure.tsx +5 -5
  44. package/src/components/Board/components/Cell/Cell.module.scss +15 -2
  45. package/src/components/Button/Button.module.scss +10 -38
  46. package/src/components/Button/Button.tsx +6 -5
  47. package/src/components/Dictionary/Dictionary.module.scss +1 -2
  48. package/src/components/Dictionary/Dictionary.tsx +2 -6
  49. package/src/components/DictionaryInput/DictionaryInput.module.scss +1 -3
  50. package/src/components/EmptyState/EmptyState.module.scss +6 -7
  51. package/src/components/EmptyState/EmptyState.tsx +6 -6
  52. package/src/components/Key/Key.module.scss +0 -1
  53. package/src/components/{Splash/Splash.module.scss → LogoSplashScreen/LogoSplashScreen.module.scss} +1 -1
  54. package/src/components/{Splash/Splash.tsx → LogoSplashScreen/LogoSplashScreen.tsx} +7 -7
  55. package/src/components/LogoSplashScreen/index.ts +1 -0
  56. package/src/components/{Sidebar/Sidebar.module.scss → Modal/Modal.module.scss} +25 -20
  57. package/src/components/{Sidebar/Sidebar.tsx → Modal/Modal.tsx} +9 -9
  58. package/src/components/{Sidebar → Modal}/components/Section/Section.module.scss +0 -0
  59. package/src/components/{Sidebar → Modal}/components/Section/Section.tsx +0 -0
  60. package/src/components/{Sidebar → Modal}/components/Section/index.ts +0 -0
  61. package/src/components/{Sidebar → Modal}/components/index.ts +0 -0
  62. package/src/components/Modal/index.ts +1 -0
  63. package/src/components/NavButtons/NavButtons.tsx +22 -3
  64. package/src/components/Progress/Progress.module.scss +9 -0
  65. package/src/components/Progress/Progress.tsx +38 -0
  66. package/src/components/Progress/index.ts +1 -0
  67. package/src/components/Rack/Rack.module.scss +2 -0
  68. package/src/components/Rack/Rack.tsx +3 -1
  69. package/src/components/Rack/RackTile.tsx +3 -2
  70. package/src/components/ResultCandidatePicker/ResultCandidatePicker.module.scss +76 -0
  71. package/src/components/ResultCandidatePicker/ResultCandidatePicker.tsx +38 -0
  72. package/src/components/ResultCandidatePicker/index.ts +1 -0
  73. package/src/components/Results/Result.tsx +64 -43
  74. package/src/components/Results/Results.module.scss +9 -16
  75. package/src/components/Results/Results.tsx +45 -28
  76. package/src/components/Results/SolveButton.tsx +9 -7
  77. package/src/components/Results/{getColumns.ts → getLocaleColumns.ts} +2 -2
  78. package/src/components/Results/types.ts +16 -0
  79. package/src/components/Results/useColumns.ts +44 -0
  80. package/src/components/ResultsInput/ResultsInput.module.scss +1 -3
  81. package/src/components/Sizer/Sizer.module.scss +10 -0
  82. package/src/components/Sizer/Sizer.tsx +10 -0
  83. package/src/components/Sizer/index.ts +1 -0
  84. package/src/components/Solver/Solver.module.scss +83 -27
  85. package/src/components/Solver/Solver.tsx +157 -24
  86. package/src/components/Solver/components/ApplyButton/ApplyButton.module.scss +5 -0
  87. package/src/components/Solver/components/ApplyButton/ApplyButton.tsx +37 -0
  88. package/src/components/Solver/components/ApplyButton/index.ts +1 -0
  89. package/src/components/Solver/components/EmptyState/EmptyState.module.scss +59 -0
  90. package/src/components/Solver/components/EmptyState/EmptyState.tsx +41 -0
  91. package/src/components/Solver/components/EmptyState/index.ts +1 -0
  92. package/src/components/Solver/components/SolveButton/SolveButton.module.scss +4 -0
  93. package/src/components/Solver/components/SolveButton/SolveButton.tsx +42 -0
  94. package/src/components/Solver/components/SolveButton/index.ts +1 -0
  95. package/src/components/Solver/components/index.ts +3 -0
  96. package/src/components/{Screen/Screen.module.scss → SplashScreen/SplashScreen.module.scss} +2 -12
  97. package/src/components/{Screen/Screen.tsx → SplashScreen/SplashScreen.tsx} +4 -4
  98. package/src/components/SplashScreen/index.ts +1 -0
  99. package/src/components/SquareButton/SquareButton.module.scss +3 -3
  100. package/src/components/Tile/Tile.module.scss +7 -15
  101. package/src/components/Tooltip/Tooltip.module.scss +0 -1
  102. package/src/components/index.ts +6 -7
  103. package/src/hooks/index.ts +1 -1
  104. package/src/hooks/useMediaQuery.ts +11 -0
  105. package/src/hooks/usePortal.tsx +1 -1
  106. package/src/i18n/de.json +2 -0
  107. package/src/i18n/en.json +2 -0
  108. package/src/i18n/es.json +2 -0
  109. package/src/i18n/fa.json +2 -0
  110. package/src/i18n/fr.json +2 -0
  111. package/src/i18n/pl.json +2 -0
  112. package/src/icons/CardChecklist.svg +5 -0
  113. package/src/icons/Check.svg +2 -2
  114. package/src/icons/ChevronDown.svg +4 -0
  115. package/src/icons/CrossCircleFill.svg +4 -0
  116. package/src/icons/{CrossFill.svg → CrossSquareFill.svg} +0 -0
  117. package/src/icons/ExclamationTriangleFill.svg +4 -0
  118. package/src/icons/InfoCircleFill.svg +4 -0
  119. package/src/icons/List.svg +4 -0
  120. package/src/icons/Search.svg +4 -0
  121. package/src/icons/index.ts +8 -2
  122. package/src/{components/KeyMap/KeyMap.tsx → modals/KeyMapModal/KeyMapModal.tsx} +11 -13
  123. package/src/{components/KeyMap → modals/KeyMapModal}/components/Mapping/Mapping.module.scss +0 -0
  124. package/src/{components/KeyMap → modals/KeyMapModal}/components/Mapping/Mapping.tsx +0 -0
  125. package/src/{components/KeyMap → modals/KeyMapModal}/components/Mapping/index.ts +0 -0
  126. package/src/{components/KeyMap → modals/KeyMapModal}/components/index.ts +0 -0
  127. package/src/modals/KeyMapModal/index.ts +1 -0
  128. package/src/{components/KeyMap → modals/KeyMapModal}/keys.tsx +1 -2
  129. package/src/modals/MenuModal/MenuModal.module.scss +46 -0
  130. package/src/modals/MenuModal/MenuModal.tsx +54 -0
  131. package/src/modals/MenuModal/index.ts +1 -0
  132. package/src/modals/RemainingTilesModal/RemainingTilesModal.module.scss +28 -0
  133. package/src/{components/RemainingTiles/RemainingTiles.tsx → modals/RemainingTilesModal/RemainingTilesModal.tsx} +14 -12
  134. package/src/{components/RemainingTiles → modals/RemainingTilesModal/components/Character}/Character.module.scss +6 -2
  135. package/src/{components/RemainingTiles → modals/RemainingTilesModal/components/Character}/Character.tsx +11 -3
  136. package/src/modals/RemainingTilesModal/components/Character/index.ts +1 -0
  137. package/src/modals/RemainingTilesModal/components/index.ts +1 -0
  138. package/src/modals/RemainingTilesModal/index.ts +1 -0
  139. package/src/modals/ResultsModal/ResultsModal.module.scss +7 -0
  140. package/src/modals/ResultsModal/ResultsModal.tsx +58 -0
  141. package/src/modals/ResultsModal/index.ts +1 -0
  142. package/src/modals/SettingsModal/SettingsModal.tsx +34 -0
  143. package/src/{components/Settings → modals/SettingsModal}/components/AutoGroupTilesSetting/AutoGroupTilesSetting.module.scss +0 -0
  144. package/src/{components/Settings → modals/SettingsModal}/components/AutoGroupTilesSetting/AutoGroupTilesSetting.tsx +1 -2
  145. package/src/{components/Settings → modals/SettingsModal}/components/AutoGroupTilesSetting/constants.ts +0 -0
  146. package/src/{components/Settings → modals/SettingsModal}/components/AutoGroupTilesSetting/index.ts +0 -0
  147. package/src/{components/Settings → modals/SettingsModal}/components/AutoGroupTilesSetting/lib.ts +0 -0
  148. package/src/{components/Settings → modals/SettingsModal}/components/ConfigSetting/ConfigSetting.module.scss +0 -0
  149. package/src/{components/Settings → modals/SettingsModal}/components/ConfigSetting/ConfigSetting.tsx +1 -2
  150. package/src/{components/Settings → modals/SettingsModal}/components/ConfigSetting/index.ts +0 -0
  151. package/src/{components/Settings → modals/SettingsModal}/components/ConfigSetting/options.ts +0 -0
  152. package/src/{components/Settings → modals/SettingsModal}/components/ConfigSetting/types.ts +0 -0
  153. package/src/{components/Settings → modals/SettingsModal}/components/LocaleSetting/LocaleSetting.module.scss +0 -0
  154. package/src/{components/Settings → modals/SettingsModal}/components/LocaleSetting/LocaleSetting.tsx +1 -2
  155. package/src/{components/Settings → modals/SettingsModal}/components/LocaleSetting/index.ts +0 -0
  156. package/src/{components/Settings → modals/SettingsModal}/components/LocaleSetting/options.ts +0 -0
  157. package/src/{components/Settings → modals/SettingsModal}/components/LocaleSetting/types.ts +0 -0
  158. package/src/{components/Settings → modals/SettingsModal}/components/index.ts +0 -0
  159. package/src/modals/SettingsModal/index.ts +1 -0
  160. package/src/{components/Words/Words.module.scss → modals/WordsModal/WordsModal.module.scss} +7 -1
  161. package/src/{components/Words/Words.tsx → modals/WordsModal/WordsModal.tsx} +10 -12
  162. package/src/modals/WordsModal/index.ts +1 -0
  163. package/src/modals/index.ts +6 -0
  164. package/src/pages/api/dictionary/[locale]/[word].ts +2 -1
  165. package/src/pages/index.module.scss +6 -4
  166. package/src/pages/index.tsx +63 -26
  167. package/src/parameters/index.ts +28 -6
  168. package/src/state/createAppStore.ts +7 -4
  169. package/src/styles/mixins.scss +15 -13
  170. package/src/styles/variables.scss +15 -17
  171. package/src/types/index.ts +2 -0
  172. package/tsconfig.json +1 -0
  173. package/.next/cache/webpack/client-development/0.pack +0 -0
  174. package/.next/cache/webpack/client-development/1.pack +0 -0
  175. package/.next/cache/webpack/client-development/2.pack +0 -0
  176. package/.next/cache/webpack/client-development/3.pack +0 -0
  177. package/.next/cache/webpack/client-development/4.pack +0 -0
  178. package/.next/cache/webpack/client-development/index.pack +0 -0
  179. package/.next/cache/webpack/client-development/index.pack.old +0 -0
  180. package/.next/cache/webpack/server-development/0.pack +0 -0
  181. package/.next/cache/webpack/server-development/10.pack_ +0 -0
  182. package/.next/cache/webpack/server-development/5.pack_ +0 -0
  183. package/.next/cache/webpack/server-development/8.pack_ +0 -0
  184. package/.next/cache/webpack/server-development/index.pack +0 -0
  185. package/.next/static/FjrbXpI5fkt4lmko-vL_7/_buildManifest.js +0 -1
  186. package/.next/static/chunks/361-d16f336a9752a55a.js +0 -1
  187. package/.next/static/chunks/724-eb48df4d1ba3df8b.js +0 -5
  188. package/.next/static/chunks/amp.js +0 -720
  189. package/.next/static/chunks/main.js +0 -1076
  190. package/.next/static/chunks/pages/_app-959e495f0f221247.js +0 -24
  191. package/.next/static/chunks/pages/_app.js +0 -2121
  192. package/.next/static/chunks/pages/_error.js +0 -28
  193. package/.next/static/chunks/pages/index-1e30dafa41bddb80.js +0 -1
  194. package/.next/static/chunks/pages/index.js +0 -5314
  195. package/.next/static/chunks/react-refresh.js +0 -62
  196. package/.next/static/chunks/webpack.js +0 -1237
  197. package/.next/static/css/aafd07997120f1e4.css +0 -1
  198. package/.next/static/css/c8d26240c04079b9.css +0 -1
  199. package/.next/static/css/eb9d57f7103525ab.css +0 -1
  200. package/.next/static/development/_buildManifest.js +0 -1
  201. package/.next/static/development/_ssgManifest.js +0 -1
  202. package/.next/static/webpack/fb4b50d5e70ee127.webpack.hot-update.json +0 -1
  203. package/.next/static/webpack/pages/index.fb4b50d5e70ee127.hot-update.js +0 -171
  204. package/.next/static/webpack/webpack.fb4b50d5e70ee127.hot-update.js +0 -18
  205. package/src/components/KeyMap/index.ts +0 -1
  206. package/src/components/RemainingTiles/RemainingTiles.module.scss +0 -16
  207. package/src/components/RemainingTiles/index.ts +0 -1
  208. package/src/components/Screen/index.ts +0 -1
  209. package/src/components/Settings/Settings.tsx +0 -35
  210. package/src/components/Settings/index.ts +0 -1
  211. package/src/components/Sidebar/index.ts +0 -1
  212. package/src/components/Splash/index.ts +0 -1
  213. package/src/components/Words/index.ts +0 -1
  214. package/src/hooks/useIsTablet.ts +0 -10
  215. package/src/icons/Play.svg +0 -4
@@ -0,0 +1,9 @@
1
+ .progress {
2
+ width: 100%;
3
+ min-height: 1px;
4
+ border-radius: var(--border--radius);
5
+
6
+ [dir='rtl'] & {
7
+ transform: scaleX(-1);
8
+ }
9
+ }
@@ -0,0 +1,38 @@
1
+ import classNames from 'classnames';
2
+ import { FunctionComponent, HTMLProps } from 'react';
3
+
4
+ import { PROGRESS_COLOR_BACKGROUND, PROGRESS_COLOR_VALUE } from 'parameters';
5
+ import { selectLocale, useTypedSelector } from 'state';
6
+
7
+ import styles from './Progress.module.scss';
8
+
9
+ interface Props extends HTMLProps<HTMLDivElement> {
10
+ max: number;
11
+ min?: number;
12
+ value: number;
13
+ }
14
+
15
+ const getGradient = (progress: number, color: string, background: string) => {
16
+ const percent = 100 * progress;
17
+ return `linear-gradient(90deg, ${color} 0%, ${color} ${percent}%, ${background} ${percent}%, ${background} 100%)`;
18
+ };
19
+
20
+ const Progress: FunctionComponent<Props> = ({ className, max, min = 0, style, value, ...props }) => {
21
+ const locale = useTypedSelector(selectLocale);
22
+ const progress = value / (max - min);
23
+ const percent = Math.round(100 * progress);
24
+
25
+ return (
26
+ <div
27
+ {...props}
28
+ className={classNames(styles.progress, className)}
29
+ style={{
30
+ ...style,
31
+ backgroundImage: getGradient(progress, PROGRESS_COLOR_VALUE, PROGRESS_COLOR_BACKGROUND),
32
+ }}
33
+ title={`${percent.toLocaleString(locale)}%`}
34
+ />
35
+ );
36
+ };
37
+
38
+ export default Progress;
@@ -0,0 +1 @@
1
+ export { default } from './Progress';
@@ -7,4 +7,6 @@
7
7
 
8
8
  .tile {
9
9
  @include focus-effect;
10
+
11
+ --background-color: var(--color--background);
10
12
  }
@@ -18,9 +18,10 @@ import RackTile from './RackTile';
18
18
 
19
19
  interface Props {
20
20
  className?: string;
21
+ tileSize: number;
21
22
  }
22
23
 
23
- const Rack: FunctionComponent<Props> = ({ className }) => {
24
+ const Rack: FunctionComponent<Props> = ({ className, tileSize }) => {
24
25
  const dispatch = useDispatch();
25
26
  const config = useTypedSelector(selectConfig);
26
27
  const locale = useTypedSelector(selectLocale);
@@ -120,6 +121,7 @@ const Rack: FunctionComponent<Props> = ({ className }) => {
120
121
  index={index}
121
122
  inputRef={tilesRefs[index]}
122
123
  key={index}
124
+ size={tileSize}
123
125
  tile={tile}
124
126
  onChange={handleChange}
125
127
  onKeyDown={handleKeyDown}
@@ -13,7 +13,6 @@ import {
13
13
  import { useDispatch } from 'react-redux';
14
14
 
15
15
  import { createKeyboardNavigation, extractCharacters, extractInputValue, isCtrl } from 'lib';
16
- import { TILE_SIZE } from 'parameters';
17
16
  import {
18
17
  rackSlice,
19
18
  selectCharacterIsValid,
@@ -32,6 +31,7 @@ interface Props {
32
31
  character: string | null;
33
32
  index: number;
34
33
  inputRef: RefObject<HTMLInputElement>;
34
+ size: number;
35
35
  tile: TileModel | null;
36
36
  onChange: ChangeEventHandler<HTMLInputElement>;
37
37
  onKeyDown: KeyboardEventHandler<HTMLInputElement>;
@@ -42,6 +42,7 @@ const RackTile: FunctionComponent<Props> = ({
42
42
  character,
43
43
  index,
44
44
  inputRef,
45
+ size,
45
46
  tile,
46
47
  onChange,
47
48
  onKeyDown,
@@ -101,7 +102,7 @@ const RackTile: FunctionComponent<Props> = ({
101
102
  placeholder={translate('rack.placeholder')[index]}
102
103
  points={points}
103
104
  raised
104
- size={TILE_SIZE}
105
+ size={size}
105
106
  tabIndex={index === 0 ? undefined : -1}
106
107
  onChange={handleChange}
107
108
  onFocus={handleFocus}
@@ -0,0 +1,76 @@
1
+ @import 'styles/mixins';
2
+
3
+ .resultCandidatePicker {
4
+ @include button-reset;
5
+ @include focus-effect;
6
+
7
+ display: flex;
8
+ align-items: center;
9
+ min-width: 0;
10
+ width: 100%;
11
+ border: var(--border);
12
+ border-radius: var(--border--radius);
13
+ background-color: white;
14
+ box-shadow: var(--box-shadow);
15
+ cursor: pointer;
16
+
17
+ &[disabled] {
18
+ opacity: var(--opacity--disabled);
19
+ box-shadow: none;
20
+ cursor: not-allowed;
21
+ }
22
+ }
23
+
24
+ .content {
25
+ display: flex;
26
+ align-items: center;
27
+ width: 100%;
28
+ overflow: hidden;
29
+ }
30
+
31
+ .points,
32
+ .word {
33
+ padding: var(--spacing--m) var(--spacing--l);
34
+ }
35
+
36
+ .points {
37
+ flex: 0 0 auto;
38
+ font-weight: bold;
39
+
40
+ [dir='ltr'] & {
41
+ border-right: var(--border);
42
+ }
43
+
44
+ [dir='rtl'] & {
45
+ border-left: var(--border);
46
+ }
47
+ }
48
+
49
+ .word {
50
+ @include ellipsis;
51
+
52
+ flex: 1;
53
+ text-transform: uppercase;
54
+ text-align: center;
55
+ }
56
+
57
+ .iconContainer {
58
+ flex: 0 0 auto;
59
+ display: flex;
60
+
61
+ [dir='ltr'] & {
62
+ padding-right: var(--spacing--l);
63
+ }
64
+
65
+ [dir='rtl'] & {
66
+ padding-left: var(--spacing--l);
67
+ }
68
+ }
69
+
70
+ .icon {
71
+ $size: 20px;
72
+
73
+ width: $size;
74
+ height: $size;
75
+ color: var(--color--inactive);
76
+ }
@@ -0,0 +1,38 @@
1
+ import classNames from 'classnames';
2
+ import { FunctionComponent, HTMLProps } from 'react';
3
+
4
+ import { ChevronDown } from 'icons';
5
+ import { selectAreResultsOutdated, selectLocale, selectResultCandidate, useTypedSelector } from 'state';
6
+
7
+ import styles from './ResultCandidatePicker.module.scss';
8
+
9
+ interface Props extends HTMLProps<HTMLButtonElement> {
10
+ type?: 'button' | 'submit' | 'reset' | undefined;
11
+ }
12
+
13
+ const ResultCandidatePicker: FunctionComponent<Props> = ({ className, ...props }) => {
14
+ const locale = useTypedSelector(selectLocale);
15
+ const isOutdated = useTypedSelector(selectAreResultsOutdated);
16
+ const resultCandidate = useTypedSelector(selectResultCandidate);
17
+
18
+ if (!resultCandidate) {
19
+ return null;
20
+ }
21
+
22
+ return (
23
+ <button
24
+ className={classNames(styles.resultCandidatePicker, className)}
25
+ disabled={isOutdated}
26
+ type="button"
27
+ {...props}
28
+ >
29
+ <div className={styles.points}>{resultCandidate.points.toLocaleString(locale)}</div>
30
+ <div className={styles.word}>{resultCandidate.word}</div>
31
+ <div className={styles.iconContainer}>
32
+ <ChevronDown className={styles.icon} />
33
+ </div>
34
+ </button>
35
+ );
36
+ };
37
+
38
+ export default ResultCandidatePicker;
@@ -0,0 +1 @@
1
+ export { default } from './ResultCandidatePicker';
@@ -1,49 +1,52 @@
1
- import { CSSProperties, ReactElement } from 'react';
2
- import { useDispatch } from 'react-redux';
1
+ import classNames from 'classnames';
2
+ import { CSSProperties, FocusEventHandler, MouseEventHandler, ReactElement, useRef } from 'react';
3
3
 
4
4
  import { LOCALE_FEATURES } from 'i18n';
5
- import { resultsSlice, selectLocale, selectSortedFilteredResults, useTypedSelector } from 'state';
5
+ import { noop } from 'lib';
6
+ import { selectLocale, useTypedSelector } from 'state';
7
+ import { ResultColumn } from 'types';
6
8
 
7
9
  import Cell from './Cell';
8
10
  import styles from './Results.module.scss';
11
+ import { ResultData } from './types';
12
+ import useColumns from './useColumns';
9
13
 
10
14
  interface Props {
15
+ data: ResultData;
11
16
  index: number;
12
17
  style?: CSSProperties;
13
18
  }
14
19
 
15
- const Result = ({ index, style }: Props): ReactElement => {
16
- const dispatch = useDispatch();
20
+ const Result = ({ data, index, style }: Props): ReactElement => {
21
+ const {
22
+ highlightedIndex,
23
+ results,
24
+ onBlur = noop,
25
+ onClick = noop,
26
+ onFocus = noop,
27
+ onMouseEnter = noop,
28
+ onMouseLeave = noop,
29
+ } = data;
30
+ const ref = useRef<HTMLButtonElement>(null);
17
31
  const locale = useTypedSelector(selectLocale);
18
32
  const { consonants, vowels } = LOCALE_FEATURES[locale];
19
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
20
- const results = useTypedSelector(selectSortedFilteredResults)!;
21
33
  const result = results[index];
22
34
  const otherWords = result.words.slice(1).join(' / ').toLocaleUpperCase();
35
+ const columns = useColumns();
36
+ const enabledColumns = Object.fromEntries(columns.map((column) => [column.id, true]));
23
37
 
24
- const handleClick = () => {
25
- dispatch(resultsSlice.actions.applyResult(result));
26
- };
27
-
28
- const handleMouseEnter = () => {
29
- dispatch(resultsSlice.actions.changeResultCandidate(result));
30
- };
31
-
32
- const handleMouseLeave = () => {
33
- dispatch(resultsSlice.actions.changeResultCandidate(null));
34
- };
35
-
36
- const handleFocus = () => {
37
- dispatch(resultsSlice.actions.changeResultCandidate(result));
38
- };
39
-
40
- const handleBlur = () => {
41
- dispatch(resultsSlice.actions.changeResultCandidate(null));
42
- };
38
+ const handleClick: MouseEventHandler = (event) => onClick(result, event);
39
+ const handleMouseEnter: MouseEventHandler = (event) => onMouseEnter(result, event);
40
+ const handleMouseLeave: MouseEventHandler = (event) => onMouseLeave(result, event);
41
+ const handleBlur: FocusEventHandler = (event) => onBlur(result, event);
42
+ const handleFocus: FocusEventHandler = (event) => onFocus(result, event);
43
43
 
44
44
  return (
45
45
  <button
46
- className={styles.result}
46
+ className={classNames(styles.result, {
47
+ [styles.highlighted]: index === highlightedIndex,
48
+ })}
49
+ ref={ref}
47
50
  style={style}
48
51
  type="button"
49
52
  onBlur={handleBlur}
@@ -53,24 +56,42 @@ const Result = ({ index, style }: Props): ReactElement => {
53
56
  onMouseLeave={handleMouseLeave}
54
57
  >
55
58
  <span className={styles.resultContent}>
56
- <Cell
57
- className={styles.word}
58
- translationKey="common.word"
59
- value={`${result.word.toLocaleUpperCase()}${otherWords.length > 0 ? ` (${otherWords})` : ''}`}
60
- />
61
- <Cell className={styles.stat} translationKey="common.tiles" value={result.tilesCount} />
62
- {consonants && (
59
+ {enabledColumns[ResultColumn.Word] && (
60
+ <Cell
61
+ className={styles.word}
62
+ translationKey="common.word"
63
+ value={`${result.word.toLocaleUpperCase()}${otherWords.length > 0 ? ` (${otherWords})` : ''}`}
64
+ />
65
+ )}
66
+
67
+ {enabledColumns[ResultColumn.TilesCount] && (
68
+ <Cell className={styles.stat} translationKey="common.tiles" value={result.tilesCount} />
69
+ )}
70
+
71
+ {enabledColumns[ResultColumn.ConsonantsCount] && consonants && (
63
72
  <Cell className={styles.stat} translationKey="common.consonants" value={result.consonantsCount} />
64
73
  )}
65
- {vowels && <Cell className={styles.stat} translationKey="common.vowels" value={result.vowelsCount} />}
66
- <Cell className={styles.stat} translationKey="common.blanks" value={result.blanksCount} />
67
- <Cell
68
- className={styles.stat}
69
- translationKey="common.words"
70
- tooltip={`${result.wordsCount} (${result.words.join(' / ').toLocaleUpperCase()})`}
71
- value={result.wordsCount}
72
- />
73
- <Cell className={styles.points} translationKey="common.points" value={result.points} />
74
+
75
+ {enabledColumns[ResultColumn.VowelsCount] && vowels && (
76
+ <Cell className={styles.stat} translationKey="common.vowels" value={result.vowelsCount} />
77
+ )}
78
+
79
+ {enabledColumns[ResultColumn.BlanksCount] && (
80
+ <Cell className={styles.stat} translationKey="common.blanks" value={result.blanksCount} />
81
+ )}
82
+
83
+ {enabledColumns[ResultColumn.WordsCount] && (
84
+ <Cell
85
+ className={styles.stat}
86
+ translationKey="common.words"
87
+ tooltip={`${result.wordsCount} (${result.words.join(' / ').toLocaleUpperCase()})`}
88
+ value={result.wordsCount}
89
+ />
90
+ )}
91
+
92
+ {enabledColumns[ResultColumn.Points] && (
93
+ <Cell className={styles.points} translationKey="common.points" value={result.points} />
94
+ )}
74
95
  </span>
75
96
  </button>
76
97
  );
@@ -8,7 +8,6 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
8
8
  flex-direction: column;
9
9
  height: 100%;
10
10
  font-family: var(--font--family--title);
11
- font-size: var(--font--size--s);
12
11
  }
13
12
 
14
13
  .emptyState {
@@ -16,6 +15,7 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
16
15
  }
17
16
 
18
17
  .list {
18
+ flex: 1 1 auto;
19
19
  transition: var(--transition);
20
20
 
21
21
  &.outdated {
@@ -30,8 +30,8 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
30
30
  align-items: center;
31
31
  justify-content: space-between;
32
32
  border-bottom: var(--border);
33
- font-size: var(--font--size--s);
34
33
  font-weight: 700;
34
+ overflow: auto;
35
35
  }
36
36
 
37
37
  .headerButton {
@@ -41,6 +41,7 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
41
41
  cursor: pointer;
42
42
  text-transform: uppercase;
43
43
  transition: var(--transition);
44
+ padding: var(--spacing--s) 0;
44
45
 
45
46
  &:focus,
46
47
  &:hover {
@@ -69,7 +70,8 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
69
70
  font-weight: 400;
70
71
 
71
72
  &:focus,
72
- &:hover {
73
+ &:hover,
74
+ &.highlighted {
73
75
  &:not(:disabled) {
74
76
  background-color: var(--color--primary);
75
77
  color: white;
@@ -82,10 +84,6 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
82
84
  align-items: center;
83
85
  justify-content: space-between;
84
86
 
85
- [dir='rtl'] & {
86
- flex-direction: row-reverse;
87
- }
88
-
89
87
  .word {
90
88
  @include ellipsis;
91
89
  }
@@ -95,7 +93,7 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
95
93
  display: flex;
96
94
  align-items: center;
97
95
  justify-content: center;
98
- padding: var(--spacing--s);
96
+ padding: 0 var(--spacing--s);
99
97
  gap: var(--spacing--s);
100
98
 
101
99
  .result &:first-child,
@@ -150,18 +148,13 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
150
148
  font-weight: bold;
151
149
  }
152
150
 
153
- .outdatedButton {
151
+ .solveButton {
154
152
  display: block;
155
- margin: 0 auto;
156
- margin-top: var(--spacing--xxl);
157
-
158
- @include tablet {
159
- margin-top: var(--spacing--l);
160
- }
153
+ margin: var(--spacing--xl) auto 0;
161
154
  }
162
155
 
163
156
  .sortIcon {
164
- $size: 15px;
157
+ $size: 19px;
165
158
 
166
159
  flex: 0 0 auto;
167
160
  width: $size;
@@ -1,9 +1,9 @@
1
1
  import classNames from 'classnames';
2
- import { FunctionComponent, useLayoutEffect, useRef } from 'react';
2
+ import { FunctionComponent, useEffect, useMemo, useState } from 'react';
3
3
  import { FixedSizeList } from 'react-window';
4
4
 
5
5
  import { LOCALE_FEATURES } from 'i18n';
6
- import { RESULTS_HEADER_HEIGHT, RESULTS_INPUT_HEIGHT, RESULTS_ITEM_HEIGHT } from 'parameters';
6
+ import { BORDER_WIDTH, RESULTS_HEADER_HEIGHT, RESULTS_INPUT_HEIGHT, RESULTS_ITEM_HEIGHT } from 'parameters';
7
7
  import {
8
8
  selectAreResultsOutdated,
9
9
  selectIsLoading,
@@ -19,33 +19,48 @@ import EmptyState from '../EmptyState';
19
19
  import Loading from '../Loading';
20
20
  import ResultsInput from '../ResultsInput';
21
21
 
22
- import getColumns from './getColumns';
23
22
  import HeaderButton from './HeaderButton';
24
23
  import Result from './Result';
25
24
  import styles from './Results.module.scss';
26
25
  import SolveButton from './SolveButton';
26
+ import { ResultCallbacks, ResultData } from './types';
27
+ import useColumns from './useColumns';
27
28
 
28
29
  interface Props {
30
+ callbacks: ResultCallbacks;
29
31
  height: number;
32
+ highlightedIndex?: number;
30
33
  width: number;
31
34
  }
32
35
 
33
- const Results: FunctionComponent<Props> = ({ height, width }) => {
34
- const locale = useTypedSelector(selectLocale);
36
+ const Results: FunctionComponent<Props> = ({ callbacks, height, highlightedIndex, width }) => {
35
37
  const translate = useTranslate();
38
+ const locale = useTypedSelector(selectLocale);
39
+ const { direction } = LOCALE_FEATURES[locale];
36
40
  const allResults = useTypedSelector(selectSortedResults);
37
- const results = useTypedSelector(selectSortedFilteredResults);
41
+ const filteredResults = useTypedSelector(selectSortedFilteredResults);
42
+ const results = filteredResults || [];
38
43
  const isLoading = useTypedSelector(selectIsLoading);
39
44
  const isOutdated = useTypedSelector(selectAreResultsOutdated);
40
45
  const error = useTypedSelector(selectSolveError);
41
- const listRef = useRef<HTMLElement>();
42
- const columns = getColumns(LOCALE_FEATURES[locale]);
43
-
44
- useLayoutEffect(() => {
45
- if (listRef.current) {
46
- listRef.current.scrollTo(0, 0);
47
- }
48
- }, [listRef, results]);
46
+ const itemData = useMemo(() => ({ ...callbacks, highlightedIndex, results }), [callbacks, highlightedIndex, results]);
47
+ const [listRef, setListRef] = useState<FixedSizeList<ResultData> | null>(null);
48
+ const columns = useColumns();
49
+ const scrollToIndex = typeof highlightedIndex === 'number' ? highlightedIndex : 0;
50
+
51
+ useEffect(() => {
52
+ // without setTimeout, the initial scrolling offset is calculated
53
+ // incorrectly, as the list is not fully rendered by the browser yet
54
+ const timeout = globalThis.setTimeout(() => {
55
+ if (listRef) {
56
+ listRef.scrollToItem(scrollToIndex, 'center');
57
+ }
58
+ }, 0);
59
+
60
+ return () => {
61
+ globalThis.clearTimeout(timeout);
62
+ };
63
+ }, [listRef, scrollToIndex]);
49
64
 
50
65
  return (
51
66
  <div className={styles.results}>
@@ -56,52 +71,54 @@ const Results: FunctionComponent<Props> = ({ height, width }) => {
56
71
  </div>
57
72
 
58
73
  {typeof error !== 'undefined' && (
59
- <EmptyState className={styles.emptyState} type="error">
74
+ <EmptyState className={styles.emptyState} variant="error">
60
75
  {error.message}
61
76
  </EmptyState>
62
77
  )}
63
78
 
64
- {typeof error === 'undefined' && typeof results === 'undefined' && (
65
- <EmptyState className={styles.emptyState} type="info">
79
+ {typeof error === 'undefined' && typeof filteredResults === 'undefined' && (
80
+ <EmptyState className={styles.emptyState} variant="info">
66
81
  {translate('results.empty-state.uninitialized')}
67
82
 
68
- <SolveButton />
83
+ <SolveButton className={styles.solveButton} />
69
84
  </EmptyState>
70
85
  )}
71
86
 
72
- {typeof error === 'undefined' && typeof results !== 'undefined' && typeof allResults !== 'undefined' && (
87
+ {typeof error === 'undefined' && typeof filteredResults !== 'undefined' && typeof allResults !== 'undefined' && (
73
88
  <>
74
89
  {isOutdated && (
75
- <EmptyState className={styles.emptyState} type="info">
90
+ <EmptyState className={styles.emptyState} variant="info">
76
91
  {translate('results.empty-state.outdated')}
77
92
 
78
- <SolveButton />
93
+ <SolveButton className={styles.solveButton} />
79
94
  </EmptyState>
80
95
  )}
81
96
 
82
97
  {!isOutdated && (
83
98
  <>
84
99
  {allResults.length === 0 && (
85
- <EmptyState className={styles.emptyState} type="warning">
100
+ <EmptyState className={styles.emptyState} variant="warning">
86
101
  {translate('results.empty-state.no-results')}
87
102
  </EmptyState>
88
103
  )}
89
104
 
90
- {allResults.length > 0 && results.length === 0 && (
91
- <EmptyState className={styles.emptyState} type="info">
105
+ {allResults.length > 0 && filteredResults.length === 0 && (
106
+ <EmptyState className={styles.emptyState} variant="info">
92
107
  {translate('results.empty-state.no-filtered-results')}
93
108
  </EmptyState>
94
109
  )}
95
110
 
96
- {allResults.length > 0 && results.length > 0 && (
111
+ {allResults.length > 0 && filteredResults.length > 0 && (
97
112
  <FixedSizeList
98
113
  className={classNames(styles.list, {
99
114
  [styles.outdated]: isOutdated,
100
115
  })}
101
- height={height - RESULTS_HEADER_HEIGHT - RESULTS_INPUT_HEIGHT}
102
- innerRef={listRef}
103
- itemCount={results.length}
116
+ direction={direction}
117
+ height={height - RESULTS_HEADER_HEIGHT - RESULTS_INPUT_HEIGHT - 2 * BORDER_WIDTH}
118
+ itemCount={filteredResults.length}
119
+ itemData={itemData}
104
120
  itemSize={RESULTS_ITEM_HEIGHT}
121
+ ref={setListRef}
105
122
  width={width}
106
123
  >
107
124
  {Result}
@@ -1,7 +1,7 @@
1
1
  import { FunctionComponent } from 'react';
2
2
  import { useDispatch } from 'react-redux';
3
3
 
4
- import { Play } from 'icons';
4
+ import { Search } from 'icons';
5
5
  import {
6
6
  selectAreResultsOutdated,
7
7
  selectIsLoading,
@@ -13,9 +13,11 @@ import {
13
13
 
14
14
  import Button from '../Button';
15
15
 
16
- import styles from './Results.module.scss';
16
+ interface Props {
17
+ className?: string;
18
+ }
17
19
 
18
- const SolveButton: FunctionComponent = () => {
20
+ const SolveButton: FunctionComponent<Props> = ({ className }) => {
19
21
  const dispatch = useDispatch();
20
22
  const translate = useTranslate();
21
23
  const isLoading = useTypedSelector(selectIsLoading);
@@ -23,17 +25,17 @@ const SolveButton: FunctionComponent = () => {
23
25
  const isOutdated = useTypedSelector(selectAreResultsOutdated);
24
26
  const hasTiles = rack.some((tile) => tile !== null);
25
27
 
26
- const handleRefresh = () => {
28
+ const handleClick = () => {
27
29
  dispatch(solveSlice.actions.submit());
28
30
  };
29
31
 
30
32
  return (
31
33
  <Button
32
- className={styles.outdatedButton}
34
+ className={className}
33
35
  disabled={isLoading || !isOutdated || !hasTiles}
34
- Icon={Play}
36
+ Icon={Search}
35
37
  type="submit"
36
- onClick={handleRefresh}
38
+ onClick={handleClick}
37
39
  >
38
40
  {translate('results.solve')}
39
41
  </Button>
@@ -3,7 +3,7 @@ import { ResultColumn } from 'types';
3
3
  import styles from './Results.module.scss';
4
4
  import { Column } from './types';
5
5
 
6
- const getColumns = (options: { consonants: boolean; vowels: boolean }): Column[] => {
6
+ const getLocaleColumns = (options: { consonants: boolean; vowels: boolean }): Column[] => {
7
7
  const { consonants, vowels } = options;
8
8
  const columns: Column[] = [
9
9
  {
@@ -55,4 +55,4 @@ const getColumns = (options: { consonants: boolean; vowels: boolean }): Column[]
55
55
  return columns;
56
56
  };
57
57
 
58
- export default getColumns;
58
+ export default getLocaleColumns;
@@ -1,3 +1,6 @@
1
+ import { Result } from '@scrabble-solver/types';
2
+ import { FocusEvent, MouseEvent } from 'react';
3
+
1
4
  import { ResultColumn, TranslationKey } from 'types';
2
5
 
3
6
  export interface Column {
@@ -5,3 +8,16 @@ export interface Column {
5
8
  id: ResultColumn;
6
9
  translationKey: TranslationKey;
7
10
  }
11
+
12
+ export interface ResultCallbacks {
13
+ onBlur?: (result: Result, event: FocusEvent) => void;
14
+ onClick?: (result: Result, event: MouseEvent) => void;
15
+ onFocus?: (result: Result, event: FocusEvent) => void;
16
+ onMouseEnter?: (result: Result, event: MouseEvent) => void;
17
+ onMouseLeave?: (result: Result, event: MouseEvent) => void;
18
+ }
19
+
20
+ export interface ResultData extends ResultCallbacks {
21
+ highlightedIndex?: number;
22
+ results: Result[];
23
+ }