@scrabble-solver/scrabble-solver 2.11.3 → 2.11.5

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 (137) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +7 -7
  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/export-marker.json +1 -1
  13. package/.next/next-server.js.nft.json +1 -1
  14. package/.next/prerender-manifest.json +1 -1
  15. package/.next/routes-manifest.json +1 -1
  16. package/.next/server/chunks/131.js +1 -1
  17. package/.next/server/chunks/277.js +736 -913
  18. package/.next/server/chunks/44.js +2 -30
  19. package/.next/server/chunks/636.js +286 -0
  20. package/.next/server/chunks/675.js +550 -0
  21. package/.next/server/middleware-build-manifest.js +1 -1
  22. package/.next/server/pages/404.html +2 -2
  23. package/.next/server/pages/404.js.nft.json +1 -1
  24. package/.next/server/pages/500.html +1 -1
  25. package/.next/server/pages/_app.js +73 -9
  26. package/.next/server/pages/_app.js.nft.json +1 -1
  27. package/.next/server/pages/_document.js.nft.json +1 -1
  28. package/.next/server/pages/_error.js +1 -280
  29. package/.next/server/pages/_error.js.nft.json +1 -1
  30. package/.next/server/pages/api/solve.js +22 -2
  31. package/.next/server/pages/index.html +1 -1
  32. package/.next/server/pages/index.js +382 -313
  33. package/.next/server/pages/index.js.nft.json +1 -1
  34. package/.next/server/pages/index.json +1 -1
  35. package/.next/static/UzQCOB6CHhyOupkEq8oZM/_buildManifest.js +1 -0
  36. package/.next/static/chunks/pages/{404-448ba28510855455.js → 404-d30fe85d005ce32b.js} +1 -1
  37. package/.next/static/chunks/pages/_app-e27464a187a58684.js +28 -0
  38. package/.next/static/chunks/pages/index-3fd280f406cc00fd.js +1 -0
  39. package/.next/static/css/4bd04cebe207859c.css +1 -0
  40. package/.next/static/css/5b3b78170f4c5875.css +2 -0
  41. package/.next/trace +50 -53
  42. package/next.config.js +1 -0
  43. package/package.json +12 -13
  44. package/src/@types/svg.d.ts +1 -1
  45. package/src/components/Board/Board.tsx +48 -44
  46. package/src/components/Board/components/Actions/Actions.tsx +4 -2
  47. package/src/components/Board/components/Cell/Cell.module.scss +59 -1
  48. package/src/components/Board/hooks/useGrid.ts +5 -3
  49. package/src/components/Button/Button.module.scss +1 -1
  50. package/src/components/Loading/Loading.module.scss +1 -1
  51. package/src/components/Loading/Loading.tsx +1 -1
  52. package/src/components/Logo/Logo.tsx +10 -12
  53. package/src/components/Logo/LogoBlueprint.tsx +21 -0
  54. package/src/components/Logo/index.ts +1 -1
  55. package/src/components/Modal/Modal.module.scss +1 -6
  56. package/src/components/Modal/Modal.tsx +15 -8
  57. package/src/components/NavButtons/NavButtons.tsx +2 -2
  58. package/src/components/Rack/Rack.module.scss +59 -0
  59. package/src/components/Results/HeaderButton.tsx +6 -6
  60. package/src/components/Results/Results.module.scss +3 -0
  61. package/src/components/Results/Results.tsx +7 -7
  62. package/src/components/Results/useColumns.ts +2 -5
  63. package/src/components/Solver/Solver.tsx +6 -23
  64. package/src/components/Tile/Tile.module.scss +2 -1
  65. package/src/components/Tile/Tile.tsx +8 -4
  66. package/src/components/index.ts +0 -3
  67. package/src/hooks/index.ts +6 -0
  68. package/src/hooks/useAppLayout.ts +62 -12
  69. package/src/hooks/useEffectOnce.ts +5 -0
  70. package/src/hooks/useIsTouchDevice.ts +1 -1
  71. package/src/hooks/useLatest.ts +13 -0
  72. package/src/hooks/useLocalStorage.ts +51 -0
  73. package/src/hooks/useMedia.ts +36 -0
  74. package/src/hooks/useMediaQueries.ts +13 -0
  75. package/src/hooks/useMediaQuery.ts +2 -1
  76. package/src/hooks/useOnWindowResize.ts +13 -0
  77. package/src/hooks/useViewportSize.ts +19 -0
  78. package/src/i18n/constants.ts +14 -14
  79. package/src/i18n/de.json +2 -2
  80. package/src/i18n/en.json +2 -2
  81. package/src/i18n/es.json +2 -2
  82. package/src/i18n/fa.json +1 -1
  83. package/src/i18n/fr.json +2 -2
  84. package/src/i18n/pl.json +2 -2
  85. package/src/lib/arrayEquals.ts +5 -0
  86. package/src/lib/index.ts +1 -0
  87. package/src/lib/zipCharactersAndTiles.ts +3 -1
  88. package/src/modals/DictionaryModal/DictionaryModal.tsx +2 -2
  89. package/src/modals/KeyMapModal/KeyMapModal.tsx +2 -2
  90. package/src/modals/KeyMapModal/keys.tsx +0 -2
  91. package/src/modals/MenuModal/MenuModal.module.scss +28 -4
  92. package/src/modals/MenuModal/MenuModal.tsx +4 -4
  93. package/src/modals/RemainingTilesModal/RemainingTilesModal.tsx +2 -2
  94. package/src/modals/ResultsModal/ResultsModal.module.scss +1 -5
  95. package/src/modals/ResultsModal/ResultsModal.tsx +10 -2
  96. package/src/modals/SettingsModal/SettingsModal.tsx +2 -2
  97. package/src/modals/SettingsModal/components/AutoGroupTilesSetting/lib.ts +3 -1
  98. package/src/modals/SettingsModal/components/LocaleSetting/LocaleSetting.module.scss +1 -1
  99. package/src/modals/WordsModal/WordsModal.tsx +2 -2
  100. package/src/pages/index.module.scss +3 -21
  101. package/src/pages/index.tsx +51 -67
  102. package/src/parameters/index.ts +29 -2
  103. package/src/state/localStorage.ts +13 -2
  104. package/src/state/sagas.ts +16 -8
  105. package/src/state/slices/boardInitialState.ts +5 -1
  106. package/src/state/slices/boardSlice.ts +2 -2
  107. package/src/state/slices/rackInitialState.ts +8 -2
  108. package/src/state/slices/rackSlice.ts +16 -13
  109. package/src/state/slices/settingsInitialState.ts +9 -4
  110. package/src/state/slices/settingsSlice.ts +3 -1
  111. package/src/styles/animations.scss +0 -20
  112. package/src/styles/global.scss +0 -7
  113. package/src/styles/mixins.scss +0 -59
  114. package/src/styles/variables.scss +11 -0
  115. package/src/types/index.ts +4 -0
  116. package/.next/static/USLkKOoHbITebIEHkMGX_/_buildManifest.js +0 -1
  117. package/.next/static/chunks/pages/_app-21c83ddb81fc09d0.js +0 -28
  118. package/.next/static/chunks/pages/index-0858deea02b2a417.js +0 -1
  119. package/.next/static/css/885da289cec275b3.css +0 -1
  120. package/.next/static/css/ea1c8134fe9a143e.css +0 -2
  121. package/src/components/LogoSplashScreen/LogoSplashScreen.module.scss +0 -65
  122. package/src/components/LogoSplashScreen/LogoSplashScreen.tsx +0 -31
  123. package/src/components/LogoSplashScreen/index.ts +0 -1
  124. package/src/components/Sizer/Sizer.module.scss +0 -10
  125. package/src/components/Sizer/Sizer.tsx +0 -10
  126. package/src/components/Sizer/index.ts +0 -1
  127. package/src/components/SplashScreen/SplashScreen.module.scss +0 -14
  128. package/src/components/SplashScreen/SplashScreen.tsx +0 -19
  129. package/src/components/SplashScreen/index.ts +0 -1
  130. package/src/hooks/useLocalStorage/index.ts +0 -1
  131. package/src/hooks/useLocalStorage/useLocalStorage.ts +0 -13
  132. package/src/hooks/useLocalStorage/useLocalStorageBoard.ts +0 -29
  133. package/src/hooks/useLocalStorage/useLocalStorageConfigId.ts +0 -29
  134. package/src/hooks/useLocalStorage/useLocalStorageLocale.ts +0 -32
  135. package/src/hooks/useLocalStorage/useLocalStorageRack.ts +0 -29
  136. /package/.next/static/{USLkKOoHbITebIEHkMGX_ → UzQCOB6CHhyOupkEq8oZM}/_ssgManifest.js +0 -0
  137. /package/{src/components/Logo/Logo.svg → public/logo.svg} +0 -0
@@ -1,10 +1,10 @@
1
1
  import classNames from 'classnames';
2
2
  import { FunctionComponent, useEffect, useMemo, useState } from 'react';
3
- import { useLatest, useMeasure } from 'react-use';
4
3
  import { FixedSizeList } from 'react-window';
5
4
 
5
+ import { useAppLayout, useLatest } from 'hooks';
6
6
  import { LOCALE_FEATURES } from 'i18n';
7
- import { RESULTS_ITEM_HEIGHT } from 'parameters';
7
+ import { BORDER_WIDTH, RESULTS_HEADER_HEIGHT, RESULTS_ITEM_HEIGHT, TEXT_INPUT_HEIGHT } from 'parameters';
8
8
  import {
9
9
  selectAreResultsOutdated,
10
10
  selectIsLoading,
@@ -18,7 +18,6 @@ import {
18
18
  import EmptyState from '../EmptyState';
19
19
  import Loading from '../Loading';
20
20
  import ResultsInput from '../ResultsInput';
21
- import Sizer from '../Sizer';
22
21
 
23
22
  import HeaderButton from './HeaderButton';
24
23
  import Result from './Result';
@@ -35,6 +34,7 @@ interface Props {
35
34
 
36
35
  const Results: FunctionComponent<Props> = ({ callbacks, className, highlightedIndex }) => {
37
36
  const translate = useTranslate();
37
+ const { resultsHeight, resultsWidth } = useAppLayout();
38
38
  const locale = useTypedSelector(selectLocale);
39
39
  const { direction } = LOCALE_FEATURES[locale];
40
40
  const results = useTypedSelector(selectResults);
@@ -42,12 +42,14 @@ const Results: FunctionComponent<Props> = ({ callbacks, className, highlightedIn
42
42
  const isOutdated = useTypedSelector(selectAreResultsOutdated);
43
43
  const error = useTypedSelector(selectSolveError);
44
44
  const itemData = useMemo(() => ({ ...callbacks, highlightedIndex, results }), [callbacks, highlightedIndex, results]);
45
- const [sizerRef, { height, width }] = useMeasure<HTMLDivElement>();
46
45
  const [listRef, setListRef] = useState<FixedSizeList<ResultData> | null>(null);
47
46
  const columns = useColumns();
48
47
  const scrollToIndex = typeof highlightedIndex === 'number' ? highlightedIndex : 0;
49
48
  const scrollToIndexRef = useLatest(scrollToIndex);
50
49
  const hasResults = typeof error === 'undefined' && typeof results !== 'undefined';
50
+ const showInput = hasResults && results.length > 0 && !isOutdated;
51
+ const height = resultsHeight - RESULTS_HEADER_HEIGHT - (showInput ? TEXT_INPUT_HEIGHT : 0) - 2 * BORDER_WIDTH;
52
+ const width = resultsWidth - 2 * BORDER_WIDTH;
51
53
 
52
54
  useEffect(() => {
53
55
  // without setTimeout, the initial scrolling offset is calculated
@@ -72,8 +74,6 @@ const Results: FunctionComponent<Props> = ({ callbacks, className, highlightedIn
72
74
  </div>
73
75
 
74
76
  <div className={styles.content}>
75
- <Sizer ref={sizerRef} />
76
-
77
77
  {typeof error !== 'undefined' && (
78
78
  <EmptyState className={styles.emptyState} variant="error">
79
79
  {error.message}
@@ -126,7 +126,7 @@ const Results: FunctionComponent<Props> = ({ callbacks, className, highlightedIn
126
126
  )}
127
127
  </div>
128
128
 
129
- {hasResults && results.length > 0 && !isOutdated && <ResultsInput className={styles.input} />}
129
+ {showInput && <ResultsInput className={styles.input} />}
130
130
 
131
131
  {isLoading && <Loading />}
132
132
  </div>
@@ -1,4 +1,4 @@
1
- import { useMediaQuery } from 'hooks';
1
+ import { useMediaQueries } from 'hooks';
2
2
  import { LOCALE_FEATURES } from 'i18n';
3
3
  import { selectLocale, useTypedSelector } from 'state';
4
4
  import { ResultColumn } from 'types';
@@ -17,10 +17,7 @@ const COLUMNS_L = [...COLUMNS_XS];
17
17
  const useColumns = (): Column[] => {
18
18
  const locale = useTypedSelector(selectLocale);
19
19
  const localeColumns = getLocaleColumns(LOCALE_FEATURES[locale]);
20
- const isLessThanXs = useMediaQuery('<xs');
21
- const isLessThanS = useMediaQuery('<s');
22
- const isLessThanM = useMediaQuery('<m');
23
- const isLessThanL = useMediaQuery('<l');
20
+ const { isLessThanXs, isLessThanS, isLessThanM, isLessThanL } = useMediaQueries();
24
21
 
25
22
  if (isLessThanXs) {
26
23
  return localeColumns.filter((column) => COLUMNS_XS.includes(column.id));
@@ -1,15 +1,12 @@
1
1
  import { Result } from '@scrabble-solver/types';
2
2
  import classNames from 'classnames';
3
- import { FunctionComponent, SyntheticEvent, useEffect, useMemo } from 'react';
3
+ import { FunctionComponent, memo, SyntheticEvent, useEffect, useMemo } from 'react';
4
4
  import { useDispatch } from 'react-redux';
5
- import { useMeasure } from 'react-use';
6
5
 
7
6
  import { useAppLayout, useIsTouchDevice } from 'hooks';
8
- import { BOARD_TILE_SIZE_MAX, BOARD_TILE_SIZE_MIN, BORDER_WIDTH, RACK_TILE_SIZE_MAX } from 'parameters';
9
7
  import {
10
8
  resultsSlice,
11
9
  selectAreResultsOutdated,
12
- selectConfig,
13
10
  selectResultCandidate,
14
11
  selectResults,
15
12
  selectSolveError,
@@ -30,33 +27,19 @@ import styles from './Solver.module.scss';
30
27
 
31
28
  interface Props {
32
29
  className?: string;
33
- height: number;
34
- width: number;
35
30
  onShowResults: () => void;
36
31
  }
37
32
 
38
- // eslint-disable-next-line max-statements
39
- const Solver: FunctionComponent<Props> = ({ className, height, width, onShowResults }) => {
33
+ const Solver: FunctionComponent<Props> = ({ className, onShowResults }) => {
40
34
  const dispatch = useDispatch();
41
35
  const translate = useTranslate();
42
36
  const isTouchDevice = useIsTouchDevice();
43
- const { columnWidth, componentsSpacing, isBoardFullWidth, showColumn, showCompactControls, showFloatingSolveButton } =
44
- useAppLayout();
45
- const config = useTypedSelector(selectConfig);
37
+ const { cellSize, maxControlsWidth, showCompactControls, showFloatingSolveButton, tileSize } = useAppLayout();
46
38
  const error = useTypedSelector(selectSolveError);
47
39
  const isOutdated = useTypedSelector(selectAreResultsOutdated);
48
40
  const resultCandidate = useTypedSelector(selectResultCandidate);
49
41
  const results = useTypedSelector(selectResults);
50
- const [bottomContainerRef, { height: bottomContainerHeight }] = useMeasure<HTMLDivElement>();
51
- const maxBoardWidth = width - columnWidth - (showColumn ? componentsSpacing : 0) - 2 * componentsSpacing;
52
- const maxBoardHeight = isBoardFullWidth ? Number.POSITIVE_INFINITY : Math.max(height - bottomContainerHeight, 0);
53
42
  const [bestResult] = results || [];
54
- const cellWidth = (maxBoardWidth - (config.boardWidth + 1) * BORDER_WIDTH) / config.boardWidth;
55
- const cellHeight = (maxBoardHeight - (config.boardHeight + 1) * BORDER_WIDTH) / config.boardHeight;
56
- const cellSize = Math.min(cellWidth, cellHeight);
57
- const cellSizeSafe = Math.min(Math.max(cellSize, BOARD_TILE_SIZE_MIN), BOARD_TILE_SIZE_MAX);
58
- const tileSize = Math.min((maxBoardWidth - 2 * BORDER_WIDTH) / config.maximumCharactersCount, RACK_TILE_SIZE_MAX);
59
- const maxControlsWidth = tileSize * config.maximumCharactersCount + 2 * BORDER_WIDTH;
60
43
  const touchCallbacks = useMemo(
61
44
  () => ({
62
45
  onClick: (result: Result) => {
@@ -110,7 +93,7 @@ const Solver: FunctionComponent<Props> = ({ className, height, width, onShowResu
110
93
  <div className={styles.container}>
111
94
  <div className={styles.content}>
112
95
  <form className={styles.boardContainer} onSubmit={handleSubmit}>
113
- <Board cellSize={cellSizeSafe} className={styles.board} />
96
+ <Board cellSize={cellSize} className={styles.board} />
114
97
  <input className={styles.submitInput} tabIndex={-1} type="submit" />
115
98
  </form>
116
99
 
@@ -125,7 +108,7 @@ const Solver: FunctionComponent<Props> = ({ className, height, width, onShowResu
125
108
  </div>
126
109
  </div>
127
110
 
128
- <div className={styles.bottomContainer} ref={bottomContainerRef}>
111
+ <div className={styles.bottomContainer}>
129
112
  <div className={styles.bottomContent}>
130
113
  <form onSubmit={handleSubmit}>
131
114
  <Rack className={styles.rack} tileSize={tileSize} />
@@ -157,4 +140,4 @@ const Solver: FunctionComponent<Props> = ({ className, height, width, onShowResu
157
140
  );
158
141
  };
159
142
 
160
- export default Solver;
143
+ export default memo(Solver);
@@ -14,6 +14,7 @@
14
14
  text-transform: uppercase;
15
15
  text-align: center;
16
16
  transition: var(--transition);
17
+ transition-property: background-color, color, box-shadow;
17
18
  user-select: none;
18
19
 
19
20
  &.points1 {
@@ -130,7 +131,7 @@
130
131
  [dir='ltr'] & {
131
132
  top: 0;
132
133
  right: 0;
133
- border-bottom-right-radius: inherit;
134
+ border-top-right-radius: inherit;
134
135
  }
135
136
 
136
137
  [dir='rtl'] & {
@@ -6,6 +6,7 @@ import {
6
6
  FunctionComponent,
7
7
  KeyboardEventHandler,
8
8
  Ref,
9
+ useCallback,
9
10
  useEffect,
10
11
  useMemo,
11
12
  useRef,
@@ -68,10 +69,13 @@ const Tile: FunctionComponent<Props> = ({
68
69
  const canShowPoints = showTilePoints && (!isEmpty || isBlank) && typeof points !== 'undefined';
69
70
  const pointsFormatted = typeof points === 'number' ? points.toLocaleString(locale) : '';
70
71
 
71
- const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (event) => {
72
- ref.current?.select();
73
- onKeyDown(event);
74
- };
72
+ const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = useCallback(
73
+ (event) => {
74
+ ref.current?.select();
75
+ onKeyDown(event);
76
+ },
77
+ [onKeyDown],
78
+ );
75
79
 
76
80
  useEffect(() => {
77
81
  if (autoFocus && ref.current) {
@@ -10,7 +10,6 @@ export { default as IconButton } from './IconButton';
10
10
  export { default as Key } from './Key';
11
11
  export { default as Loading } from './Loading';
12
12
  export { default as Logo } from './Logo';
13
- export { default as LogoSplashScreen } from './LogoSplashScreen';
14
13
  export { default as Modal } from './Modal';
15
14
  export { default as NavButtons } from './NavButtons';
16
15
  export { default as NotFound } from './NotFound';
@@ -21,10 +20,8 @@ export { default as Radio } from './Radio';
21
20
  export { default as Results } from './Results';
22
21
  export { default as ResultsInput } from './ResultsInput';
23
22
  export { default as SeoMessage } from './SeoMessage';
24
- export { default as Sizer } from './Sizer';
25
23
  export { default as Solver } from './Solver';
26
24
  export { default as Spinner } from './Spinner';
27
- export { default as SplashScreen } from './SplashScreen';
28
25
  export { default as SvgFontCss } from './SvgFontCss';
29
26
  export { default as SvgFontFix } from './SvgFontFix';
30
27
  export { default as Tile } from './Tile';
@@ -1,7 +1,13 @@
1
1
  export { default as useAppLayout } from './useAppLayout';
2
2
  export { default as useDirection } from './useDirection';
3
+ export { default as useEffectOnce } from './useEffectOnce';
3
4
  export { default as useIsTouchDevice } from './useIsTouchDevice';
4
5
  export { default as useLanguage } from './useLanguage';
6
+ export { default as useLatest } from './useLatest';
5
7
  export { default as useLocalStorage } from './useLocalStorage';
8
+ export { default as useMedia } from './useMedia';
9
+ export { default as useMediaQueries } from './useMediaQueries';
6
10
  export { default as useMediaQuery } from './useMediaQuery';
11
+ export { default as useOnWindowResize } from './useOnWindowResize';
7
12
  export { default as usePortal } from './usePortal';
13
+ export { default as useViewportSize } from './useViewportSize';
@@ -1,30 +1,80 @@
1
- import { COMPONENTS_SPACING, COMPONENTS_SPACING_SMALL, SOLVER_COLUMN_WIDTH } from 'parameters';
1
+ /* eslint-disable max-statements */
2
+
3
+ import {
4
+ BOARD_TILE_SIZE_MAX,
5
+ BOARD_TILE_SIZE_MIN,
6
+ BORDER_WIDTH,
7
+ BUTTON_HEIGHT,
8
+ COMPONENTS_SPACING,
9
+ COMPONENTS_SPACING_SMALL,
10
+ DICTIONARY_HEIGHT,
11
+ DICTIONARY_HEIGHT_MOBILE,
12
+ LOGO_ASPECT_RATIO,
13
+ LOGO_HEIGHT,
14
+ LOGO_HEIGHT_SMALL,
15
+ MODAL_HEADER_HEIGHT,
16
+ MODAL_WIDTH,
17
+ NAV_PADDING,
18
+ RACK_TILE_SIZE_MAX,
19
+ SOLVER_COLUMN_WIDTH,
20
+ } from 'parameters';
21
+ import { selectConfig, useTypedSelector } from 'state';
2
22
 
3
23
  import useIsTouchDevice from './useIsTouchDevice';
4
- import useMediaQuery from './useMediaQuery';
24
+ import useMediaQueries from './useMediaQueries';
25
+ import useViewportSize from './useViewportSize';
5
26
 
6
27
  const useAppLayout = () => {
28
+ const { viewportHeight, viewportWidth } = useViewportSize();
29
+ const config = useTypedSelector(selectConfig);
7
30
  const isTouchDevice = useIsTouchDevice();
8
- const isLessThanXs = useMediaQuery('<xs');
9
- const isLessThanS = useMediaQuery('<s');
10
- const isLessThanM = useMediaQuery('<m');
11
- const isLessThanL = useMediaQuery('<l');
12
- const isLessThanXl = useMediaQuery('<xl');
31
+ const { isLessThanXs, isLessThanS, isLessThanM, isLessThanL, isLessThanXl } = useMediaQueries();
32
+ const isBoardFullWidth = isLessThanM;
33
+ const showResultCandidatePicker = isLessThanL;
34
+ const componentsSpacing = isLessThanXl ? COMPONENTS_SPACING_SMALL : COMPONENTS_SPACING;
13
35
  const showColumn = !isLessThanL;
36
+ const columnWidth = showColumn ? SOLVER_COLUMN_WIDTH : 0;
37
+ const logoHeight = isLessThanL ? LOGO_HEIGHT_SMALL : LOGO_HEIGHT;
38
+ const navHeight = 2 * NAV_PADDING + logoHeight;
39
+ const solverHeight = viewportHeight - navHeight;
40
+ const solverWidth = viewportWidth;
41
+ const maxBoardWidth = solverWidth - columnWidth - (showColumn ? componentsSpacing : 0) - 2 * componentsSpacing;
42
+ const tileSize = Math.min((maxBoardWidth - 2 * BORDER_WIDTH) / config.maximumCharactersCount, RACK_TILE_SIZE_MAX);
43
+ const candidatePickerHeight = showResultCandidatePicker ? BUTTON_HEIGHT + componentsSpacing : 0;
44
+ const bottomContainerHeight = candidatePickerHeight + tileSize + 2 * componentsSpacing;
45
+ const maxBoardHeight = isBoardFullWidth
46
+ ? Number.POSITIVE_INFINITY
47
+ : Math.max(solverHeight - bottomContainerHeight, 0);
48
+ const cellWidth = (maxBoardWidth - (config.boardWidth + 1) * BORDER_WIDTH) / config.boardWidth;
49
+ const cellHeight = (maxBoardHeight - (config.boardHeight + 1) * BORDER_WIDTH) / config.boardHeight;
50
+ const cellSize = Math.min(Math.max(Math.min(cellWidth, cellHeight), BOARD_TILE_SIZE_MIN), BOARD_TILE_SIZE_MAX);
51
+ const boardSize = (cellSize + BORDER_WIDTH) * config.boardWidth + BORDER_WIDTH;
52
+ const maxControlsWidth = tileSize * config.maximumCharactersCount + 2 * BORDER_WIDTH;
53
+ const showResultsInModal = isLessThanL;
54
+ const dictionaryHeight = showResultsInModal ? DICTIONARY_HEIGHT_MOBILE : DICTIONARY_HEIGHT;
55
+ const modalWidth = isLessThanS ? viewportWidth : MODAL_WIDTH;
56
+ const resultsHeight = isLessThanL
57
+ ? viewportHeight - dictionaryHeight - BUTTON_HEIGHT - MODAL_HEADER_HEIGHT - 5 * componentsSpacing
58
+ : boardSize - componentsSpacing - dictionaryHeight;
14
59
 
15
60
  return {
61
+ actionsWidth: 2 * BUTTON_HEIGHT - BORDER_WIDTH,
16
62
  animateTile: !isLessThanXs,
17
- columnWidth: showColumn ? SOLVER_COLUMN_WIDTH : 0,
18
- componentsSpacing: isLessThanXl ? COMPONENTS_SPACING_SMALL : COMPONENTS_SPACING,
19
- isBoardFullWidth: isLessThanM,
63
+ cellSize,
64
+ dictionaryHeight,
20
65
  isModalFullWidth: isLessThanS,
21
- showColumn,
66
+ logoHeight,
67
+ logoWidth: logoHeight * LOGO_ASPECT_RATIO,
68
+ maxControlsWidth,
69
+ resultsHeight,
70
+ resultsWidth: isLessThanL ? modalWidth - 2 * componentsSpacing : SOLVER_COLUMN_WIDTH,
22
71
  showCompactControls: !showColumn,
23
72
  showFloatingSolveButton: isTouchDevice,
24
73
  showKeyMap: !isTouchDevice,
25
- showResultsInModal: isLessThanL,
74
+ showResultsInModal,
26
75
  showShortNav: isLessThanS,
27
76
  showTilePoints: !isLessThanXs,
77
+ tileSize,
28
78
  };
29
79
  };
30
80
 
@@ -0,0 +1,5 @@
1
+ import { EffectCallback, useEffect } from 'react';
2
+
3
+ const useEffectOnce = (effect: EffectCallback) => useEffect(effect, []);
4
+
5
+ export default useEffectOnce;
@@ -1,4 +1,4 @@
1
- import { useMedia } from 'react-use';
1
+ import useMedia from './useMedia';
2
2
 
3
3
  const useIsTouchDevice = () => {
4
4
  return useMedia('(hover: none)', false);
@@ -0,0 +1,13 @@
1
+ import { useRef } from 'react';
2
+
3
+ interface Latest<T> {
4
+ readonly current: T;
5
+ }
6
+
7
+ const useLatest = <T>(value: T): Latest<T> => {
8
+ const ref = useRef(value);
9
+ ref.current = value;
10
+ return ref;
11
+ };
12
+
13
+ export default useLatest;
@@ -0,0 +1,51 @@
1
+ import { useEffect } from 'react';
2
+
3
+ import {
4
+ localStorage,
5
+ selectAutoGroupTiles,
6
+ selectBoard,
7
+ selectConfigId,
8
+ selectLocale,
9
+ selectRack,
10
+ useTypedSelector,
11
+ } from 'state';
12
+
13
+ const useLocalStorage = () => {
14
+ const autoGroupTiles = useTypedSelector(selectAutoGroupTiles);
15
+ const board = useTypedSelector(selectBoard);
16
+ const configId = useTypedSelector(selectConfigId);
17
+ const locale = useTypedSelector(selectLocale);
18
+ const rack = useTypedSelector(selectRack);
19
+
20
+ useEffect(() => {
21
+ if (autoGroupTiles) {
22
+ localStorage.setAutoGroupTiles(autoGroupTiles);
23
+ }
24
+ }, [autoGroupTiles]);
25
+
26
+ useEffect(() => {
27
+ if (board) {
28
+ localStorage.setBoard(board);
29
+ }
30
+ }, [board]);
31
+
32
+ useEffect(() => {
33
+ if (configId) {
34
+ localStorage.setConfigId(configId);
35
+ }
36
+ }, [configId]);
37
+
38
+ useEffect(() => {
39
+ if (locale) {
40
+ localStorage.setLocale(locale);
41
+ }
42
+ }, [locale]);
43
+
44
+ useEffect(() => {
45
+ if (rack) {
46
+ localStorage.setRack(rack);
47
+ }
48
+ }, [rack]);
49
+ };
50
+
51
+ export default useLocalStorage;
@@ -0,0 +1,36 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ const getInitialState = (query: string, defaultState?: boolean) => {
4
+ if (typeof defaultState !== 'undefined') {
5
+ return defaultState;
6
+ }
7
+
8
+ if (typeof window === 'undefined') {
9
+ return false;
10
+ }
11
+
12
+ return window.matchMedia(query).matches;
13
+ };
14
+
15
+ const useMedia = (query: string, defaultState?: boolean) => {
16
+ const [state, setState] = useState(getInitialState(query, defaultState));
17
+
18
+ useEffect(() => {
19
+ const mediaQuery = window.matchMedia(query);
20
+
21
+ const handleChange = () => {
22
+ setState(mediaQuery.matches);
23
+ };
24
+
25
+ setState(mediaQuery.matches);
26
+ mediaQuery.addEventListener('change', handleChange);
27
+
28
+ return () => {
29
+ mediaQuery.removeEventListener('change', handleChange);
30
+ };
31
+ }, [query]);
32
+
33
+ return state;
34
+ };
35
+
36
+ export default useMedia;
@@ -0,0 +1,13 @@
1
+ import useMediaQuery from './useMediaQuery';
2
+
3
+ const useMediaQueries = () => {
4
+ const isLessThanXs = useMediaQuery('<xs');
5
+ const isLessThanS = useMediaQuery('<s');
6
+ const isLessThanM = useMediaQuery('<m');
7
+ const isLessThanL = useMediaQuery('<l');
8
+ const isLessThanXl = useMediaQuery('<xl');
9
+
10
+ return { isLessThanXs, isLessThanS, isLessThanM, isLessThanL, isLessThanXl };
11
+ };
12
+
13
+ export default useMediaQueries;
@@ -1,8 +1,9 @@
1
1
  import { buildMediaQuery } from 'include-media-query-builder';
2
- import { useMedia } from 'react-use';
3
2
 
4
3
  import { BREAKPOINTS } from 'parameters';
5
4
 
5
+ import useMedia from './useMedia';
6
+
6
7
  const useMediaQuery = (query: string | string[], defaultState?: boolean | undefined): boolean => {
7
8
  const mediaQuery = buildMediaQuery(BREAKPOINTS, query);
8
9
  return useMedia(mediaQuery, defaultState);
@@ -0,0 +1,13 @@
1
+ import { useEffect } from 'react';
2
+
3
+ const useOnWindowResize = (onResize: (event: Event) => void) => {
4
+ useEffect(() => {
5
+ window.addEventListener('resize', onResize);
6
+
7
+ return () => {
8
+ window.removeEventListener('resize', onResize);
9
+ };
10
+ }, [onResize]);
11
+ };
12
+
13
+ export default useOnWindowResize;
@@ -0,0 +1,19 @@
1
+ import { useCallback, useState } from 'react';
2
+
3
+ import useOnWindowResize from './useOnWindowResize';
4
+
5
+ const useViewportSize = () => {
6
+ const [viewportHeight, setViewportHeight] = useState(typeof window === 'undefined' ? 0 : window.innerHeight);
7
+ const [viewportWidth, setViewportWidth] = useState(typeof window === 'undefined' ? 0 : window.innerWidth);
8
+
9
+ const handleWindowResize = useCallback(() => {
10
+ setViewportHeight(window.innerHeight);
11
+ setViewportWidth(window.innerWidth);
12
+ }, []);
13
+
14
+ useOnWindowResize(handleWindowResize);
15
+
16
+ return { viewportHeight, viewportWidth };
17
+ };
18
+
19
+ export default useViewportSize;
@@ -66,6 +66,13 @@ interface Flag {
66
66
  }
67
67
 
68
68
  export const LOCALE_FLAGS: Record<Locale, Flag> = {
69
+ [Locale.DE_DE]: {
70
+ className: styles.de,
71
+ Icon: FlagDe,
72
+ label: 'Deutsch',
73
+ name: 'German',
74
+ value: Locale.DE_DE,
75
+ },
69
76
  [Locale.EN_GB]: {
70
77
  className: styles.gb,
71
78
  Icon: FlagGb,
@@ -80,6 +87,13 @@ export const LOCALE_FLAGS: Record<Locale, Flag> = {
80
87
  name: 'English (US)',
81
88
  value: Locale.EN_US,
82
89
  },
90
+ [Locale.ES_ES]: {
91
+ className: styles.es,
92
+ Icon: FlagEs,
93
+ label: 'Español',
94
+ name: 'Spanish',
95
+ value: Locale.ES_ES,
96
+ },
83
97
  [Locale.FA_IR]: {
84
98
  className: styles.fa,
85
99
  Icon: FlagFa,
@@ -94,13 +108,6 @@ export const LOCALE_FLAGS: Record<Locale, Flag> = {
94
108
  name: 'French',
95
109
  value: Locale.FR_FR,
96
110
  },
97
- [Locale.DE_DE]: {
98
- className: styles.de,
99
- Icon: FlagDe,
100
- label: 'Deutsch',
101
- name: 'German',
102
- value: Locale.DE_DE,
103
- },
104
111
  [Locale.PL_PL]: {
105
112
  className: styles.pl,
106
113
  Icon: FlagPl,
@@ -108,11 +115,4 @@ export const LOCALE_FLAGS: Record<Locale, Flag> = {
108
115
  name: 'Polish',
109
116
  value: Locale.PL_PL,
110
117
  },
111
- [Locale.ES_ES]: {
112
- className: styles.es,
113
- Icon: FlagEs,
114
- label: 'Español',
115
- name: 'Spanish',
116
- value: Locale.ES_ES,
117
- },
118
118
  };
package/src/i18n/de.json CHANGED
@@ -21,7 +21,7 @@
21
21
  "dictionary.empty-state.no-definitions": "Wort existiert im Wörterbuch aber hat keine Definition.",
22
22
  "dictionary.empty-state.no-results": "Wort kann nicht im Wörterbuch gefunden werden.",
23
23
  "dictionary.empty-state.not-allowed": "Dieses Wort ist nicht erlaubt.",
24
- "dictionary.empty-state.uninitialized": "Die Wörterbuchdefinition des letzten markierten Wortes wird hier angezeigt.",
24
+ "dictionary.empty-state.uninitialized": "Die Wörterbuchdéfinition wird hier angezeigt.",
25
25
  "dictionary.input.placeholder": "Durchsuche Wörterbuch...",
26
26
  "dictionary.input.title": "Durch Kommas getrennte Wörter",
27
27
  "empty-state.error": "Fehler",
@@ -46,7 +46,7 @@
46
46
  "remaining-tiles": "Restliche Steine",
47
47
  "results": "Ergebnisse",
48
48
  "results.empty-state.no-results": "Keine Ergebnisse - kein Wort konnte generiert werden.",
49
- "results.empty-state.outdated": "Ergebnisse sind alt. Klicken zum Aktualisieren.",
49
+ "results.empty-state.outdated": "Ergebnisse sind alt.",
50
50
  "results.empty-state.uninitialized": "Wörter die aus deinen Buchstaben generiert wurden erscheinen hier.",
51
51
  "results.input.placeholder": "Suchergebnisse... (RegExp)",
52
52
  "results.insert": "Hinzufügen",
package/src/i18n/en.json CHANGED
@@ -21,7 +21,7 @@
21
21
  "dictionary.empty-state.no-definitions": "Word exists in the dictionary but it does not have a definition.",
22
22
  "dictionary.empty-state.no-results": "Unable to find word definition in the dictionary.",
23
23
  "dictionary.empty-state.not-allowed": "This word is not allowed.",
24
- "dictionary.empty-state.uninitialized": "Dictionary definition of the most recently highlighted word will be shown here.",
24
+ "dictionary.empty-state.uninitialized": "Word definition will be shown here.",
25
25
  "dictionary.input.placeholder": "Search dictionary...",
26
26
  "dictionary.input.title": "Comma-separated words",
27
27
  "empty-state.error": "Error",
@@ -46,7 +46,7 @@
46
46
  "remaining-tiles": "Remaining tiles",
47
47
  "results": "Results",
48
48
  "results.empty-state.no-results": "No results - unable to generate any words.",
49
- "results.empty-state.outdated": "Results are outdated. Click below to update.",
49
+ "results.empty-state.outdated": "Results are outdated.",
50
50
  "results.empty-state.uninitialized": "Words generated from your letters will be shown here.",
51
51
  "results.input.placeholder": "Search results... (RegExp)",
52
52
  "results.insert": "Insert",
package/src/i18n/es.json CHANGED
@@ -21,7 +21,7 @@
21
21
  "dictionary.empty-state.no-definitions": "La palabra existe en el diccionario pero no tiene una definición.",
22
22
  "dictionary.empty-state.no-results": "No se puede encontrar la definición de palabra en el diccionario.",
23
23
  "dictionary.empty-state.not-allowed": "Esta palabra no es aceptable.",
24
- "dictionary.empty-state.uninitialized": "Aquí se mostrará la definición del diccionario de la última palabra resaltada.",
24
+ "dictionary.empty-state.uninitialized": "Aquí se mostrará la definición del diccionario.",
25
25
  "dictionary.input.placeholder": "Busca el diccionario...",
26
26
  "dictionary.input.title": "Palabras separadas por comas",
27
27
  "empty-state.error": "Error",
@@ -46,7 +46,7 @@
46
46
  "remaining-tiles": "Casillas restantes",
47
47
  "results": "Resultados",
48
48
  "results.empty-state.no-results": "No hay resultados; no se pueden generar palabras",
49
- "results.empty-state.outdated": "Los resultados están desactualizados. Haga clic a continuación para actualizar.",
49
+ "results.empty-state.outdated": "Los resultados están desactualizados.",
50
50
  "results.empty-state.uninitialized": "Aquí se mostrarán las palabras generadas a partir de sus letras.",
51
51
  "results.input.placeholder": "Busque una solución... (RegExp)",
52
52
  "results.insert": "Insertar",
package/src/i18n/fa.json CHANGED
@@ -46,7 +46,7 @@
46
46
  "remaining-tiles": "کاشی های باقی مانده",
47
47
  "results": "نتایج",
48
48
  "results.empty-state.no-results": "کلمه قابل استفاده پیدا نشد.",
49
- "results.empty-state.outdated": "نتایج به روز نیستند، برای بروز رسانی کلیک کنید.",
49
+ "results.empty-state.outdated": "نتایج به روز نیستند، برای بروز.",
50
50
  "results.empty-state.uninitialized": "کلمات تولید شده از حروف شما اینجا نمایش داده خواهد شد.",
51
51
  "results.input.placeholder": "جستجو در نتایج (RegExp)",
52
52
  "results.insert": "وارد کردن",
package/src/i18n/fr.json CHANGED
@@ -21,7 +21,7 @@
21
21
  "dictionary.empty-state.no-definitions": "Le mot existe dans le dictionary mais n'a pas de définition.",
22
22
  "dictionary.empty-state.no-results": "Impossible de trouver une définition pour ce mot dans le dictionaire.",
23
23
  "dictionary.empty-state.not-allowed": "Ce mot n'est pas pas acceptable.",
24
- "dictionary.empty-state.uninitialized": "La définition dictionaire du dernier mot surligné sera affichée ici.",
24
+ "dictionary.empty-state.uninitialized": "La définition dictionaire sera affichée ici.",
25
25
  "dictionary.input.placeholder": "Rechercher dans le dictionnaire...",
26
26
  "dictionary.input.title": "Mots séparées par des virgules",
27
27
  "empty-state.error": "Erreur",
@@ -46,7 +46,7 @@
46
46
  "remaining-tiles": "Cases restantes",
47
47
  "results": "Résultats",
48
48
  "results.empty-state.no-results": "Pas de résultats - impossible de générer des mots.",
49
- "results.empty-state.outdated": "Les résultats sont dépassé. Cliquer ci-dessous pour mettre à jour.",
49
+ "results.empty-state.outdated": "Les résultats sont dépassé.",
50
50
  "results.empty-state.uninitialized": "Les mots générés à partir de vos lettres seront affichés ici.",
51
51
  "results.input.placeholder": "Rechercher les résultats... (RegExp)",
52
52
  "results.insert": "Inserer",