@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.
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +7 -7
- package/.next/cache/.tsbuildinfo +1 -1
- package/.next/cache/eslint/.cache_8dgz12 +1 -1
- package/.next/cache/next-server.js.nft.json +1 -1
- package/.next/cache/webpack/client-production/0.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack +0 -0
- package/.next/cache/webpack/edge-server-production/0.pack +0 -0
- package/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/0.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/export-marker.json +1 -1
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +1 -1
- package/.next/routes-manifest.json +1 -1
- package/.next/server/chunks/131.js +1 -1
- package/.next/server/chunks/277.js +736 -913
- package/.next/server/chunks/44.js +2 -30
- package/.next/server/chunks/636.js +286 -0
- package/.next/server/chunks/675.js +550 -0
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/pages/404.html +2 -2
- package/.next/server/pages/404.js.nft.json +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/pages/_app.js +73 -9
- package/.next/server/pages/_app.js.nft.json +1 -1
- package/.next/server/pages/_document.js.nft.json +1 -1
- package/.next/server/pages/_error.js +1 -280
- package/.next/server/pages/_error.js.nft.json +1 -1
- package/.next/server/pages/api/solve.js +22 -2
- package/.next/server/pages/index.html +1 -1
- package/.next/server/pages/index.js +382 -313
- package/.next/server/pages/index.js.nft.json +1 -1
- package/.next/server/pages/index.json +1 -1
- package/.next/static/UzQCOB6CHhyOupkEq8oZM/_buildManifest.js +1 -0
- package/.next/static/chunks/pages/{404-448ba28510855455.js → 404-d30fe85d005ce32b.js} +1 -1
- package/.next/static/chunks/pages/_app-e27464a187a58684.js +28 -0
- package/.next/static/chunks/pages/index-3fd280f406cc00fd.js +1 -0
- package/.next/static/css/4bd04cebe207859c.css +1 -0
- package/.next/static/css/5b3b78170f4c5875.css +2 -0
- package/.next/trace +50 -53
- package/next.config.js +1 -0
- package/package.json +12 -13
- package/src/@types/svg.d.ts +1 -1
- package/src/components/Board/Board.tsx +48 -44
- package/src/components/Board/components/Actions/Actions.tsx +4 -2
- package/src/components/Board/components/Cell/Cell.module.scss +59 -1
- package/src/components/Board/hooks/useGrid.ts +5 -3
- package/src/components/Button/Button.module.scss +1 -1
- package/src/components/Loading/Loading.module.scss +1 -1
- package/src/components/Loading/Loading.tsx +1 -1
- package/src/components/Logo/Logo.tsx +10 -12
- package/src/components/Logo/LogoBlueprint.tsx +21 -0
- package/src/components/Logo/index.ts +1 -1
- package/src/components/Modal/Modal.module.scss +1 -6
- package/src/components/Modal/Modal.tsx +15 -8
- package/src/components/NavButtons/NavButtons.tsx +2 -2
- package/src/components/Rack/Rack.module.scss +59 -0
- package/src/components/Results/HeaderButton.tsx +6 -6
- package/src/components/Results/Results.module.scss +3 -0
- package/src/components/Results/Results.tsx +7 -7
- package/src/components/Results/useColumns.ts +2 -5
- package/src/components/Solver/Solver.tsx +6 -23
- package/src/components/Tile/Tile.module.scss +2 -1
- package/src/components/Tile/Tile.tsx +8 -4
- package/src/components/index.ts +0 -3
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useAppLayout.ts +62 -12
- package/src/hooks/useEffectOnce.ts +5 -0
- package/src/hooks/useIsTouchDevice.ts +1 -1
- package/src/hooks/useLatest.ts +13 -0
- package/src/hooks/useLocalStorage.ts +51 -0
- package/src/hooks/useMedia.ts +36 -0
- package/src/hooks/useMediaQueries.ts +13 -0
- package/src/hooks/useMediaQuery.ts +2 -1
- package/src/hooks/useOnWindowResize.ts +13 -0
- package/src/hooks/useViewportSize.ts +19 -0
- package/src/i18n/constants.ts +14 -14
- package/src/i18n/de.json +2 -2
- package/src/i18n/en.json +2 -2
- package/src/i18n/es.json +2 -2
- package/src/i18n/fa.json +1 -1
- package/src/i18n/fr.json +2 -2
- package/src/i18n/pl.json +2 -2
- package/src/lib/arrayEquals.ts +5 -0
- package/src/lib/index.ts +1 -0
- package/src/lib/zipCharactersAndTiles.ts +3 -1
- package/src/modals/DictionaryModal/DictionaryModal.tsx +2 -2
- package/src/modals/KeyMapModal/KeyMapModal.tsx +2 -2
- package/src/modals/KeyMapModal/keys.tsx +0 -2
- package/src/modals/MenuModal/MenuModal.module.scss +28 -4
- package/src/modals/MenuModal/MenuModal.tsx +4 -4
- package/src/modals/RemainingTilesModal/RemainingTilesModal.tsx +2 -2
- package/src/modals/ResultsModal/ResultsModal.module.scss +1 -5
- package/src/modals/ResultsModal/ResultsModal.tsx +10 -2
- package/src/modals/SettingsModal/SettingsModal.tsx +2 -2
- package/src/modals/SettingsModal/components/AutoGroupTilesSetting/lib.ts +3 -1
- package/src/modals/SettingsModal/components/LocaleSetting/LocaleSetting.module.scss +1 -1
- package/src/modals/WordsModal/WordsModal.tsx +2 -2
- package/src/pages/index.module.scss +3 -21
- package/src/pages/index.tsx +51 -67
- package/src/parameters/index.ts +29 -2
- package/src/state/localStorage.ts +13 -2
- package/src/state/sagas.ts +16 -8
- package/src/state/slices/boardInitialState.ts +5 -1
- package/src/state/slices/boardSlice.ts +2 -2
- package/src/state/slices/rackInitialState.ts +8 -2
- package/src/state/slices/rackSlice.ts +16 -13
- package/src/state/slices/settingsInitialState.ts +9 -4
- package/src/state/slices/settingsSlice.ts +3 -1
- package/src/styles/animations.scss +0 -20
- package/src/styles/global.scss +0 -7
- package/src/styles/mixins.scss +0 -59
- package/src/styles/variables.scss +11 -0
- package/src/types/index.ts +4 -0
- package/.next/static/USLkKOoHbITebIEHkMGX_/_buildManifest.js +0 -1
- package/.next/static/chunks/pages/_app-21c83ddb81fc09d0.js +0 -28
- package/.next/static/chunks/pages/index-0858deea02b2a417.js +0 -1
- package/.next/static/css/885da289cec275b3.css +0 -1
- package/.next/static/css/ea1c8134fe9a143e.css +0 -2
- package/src/components/LogoSplashScreen/LogoSplashScreen.module.scss +0 -65
- package/src/components/LogoSplashScreen/LogoSplashScreen.tsx +0 -31
- package/src/components/LogoSplashScreen/index.ts +0 -1
- package/src/components/Sizer/Sizer.module.scss +0 -10
- package/src/components/Sizer/Sizer.tsx +0 -10
- package/src/components/Sizer/index.ts +0 -1
- package/src/components/SplashScreen/SplashScreen.module.scss +0 -14
- package/src/components/SplashScreen/SplashScreen.tsx +0 -19
- package/src/components/SplashScreen/index.ts +0 -1
- package/src/hooks/useLocalStorage/index.ts +0 -1
- package/src/hooks/useLocalStorage/useLocalStorage.ts +0 -13
- package/src/hooks/useLocalStorage/useLocalStorageBoard.ts +0 -29
- package/src/hooks/useLocalStorage/useLocalStorageConfigId.ts +0 -29
- package/src/hooks/useLocalStorage/useLocalStorageLocale.ts +0 -32
- package/src/hooks/useLocalStorage/useLocalStorageRack.ts +0 -29
- /package/.next/static/{USLkKOoHbITebIEHkMGX_ → UzQCOB6CHhyOupkEq8oZM}/_ssgManifest.js +0 -0
- /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
|
-
{
|
|
129
|
+
{showInput && <ResultsInput className={styles.input} />}
|
|
130
130
|
|
|
131
131
|
{isLoading && <Loading />}
|
|
132
132
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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 =
|
|
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
|
-
|
|
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 {
|
|
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={
|
|
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}
|
|
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-
|
|
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> = (
|
|
72
|
-
|
|
73
|
-
|
|
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) {
|
package/src/components/index.ts
CHANGED
|
@@ -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';
|
package/src/hooks/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 =
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
isBoardFullWidth: isLessThanM,
|
|
63
|
+
cellSize,
|
|
64
|
+
dictionaryHeight,
|
|
20
65
|
isModalFullWidth: isLessThanS,
|
|
21
|
-
|
|
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
|
|
74
|
+
showResultsInModal,
|
|
26
75
|
showShortNav: isLessThanS,
|
|
27
76
|
showTilePoints: !isLessThanXs,
|
|
77
|
+
tileSize,
|
|
28
78
|
};
|
|
29
79
|
};
|
|
30
80
|
|
|
@@ -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;
|
package/src/i18n/constants.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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": "
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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é.
|
|
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",
|