scrabble-solver 2.13.11 → 2.13.13
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/.eslintrc.js +13 -4
- package/.github/workflows/cypress.yml +24 -0
- package/.github/workflows/eslint.yml +9 -10
- package/.github/workflows/jest.yml +27 -0
- package/.nx/cache/nx_files.nxt +0 -0
- package/.nx/workspace-data/file-map.json +1410 -1326
- package/.nx/workspace-data/lockfile.hash +1 -1
- package/.nx/workspace-data/parsed-lock-file.json +5765 -5261
- package/.nx/workspace-data/project-graph.json +4745 -3485
- package/README.md +5 -3
- package/cypress/e2e/app.spec.cy.ts +107 -0
- package/cypress/e2e/bugs/112.spec.cy.ts +29 -0
- package/cypress/e2e/bugs/129.spec.cy.ts +13 -0
- package/cypress/e2e/bugs/2.spec.cy.ts +15 -0
- package/cypress/e2e/bugs/3.spec.cy.ts +15 -0
- package/cypress/e2e/bugs/342.spec.cy.ts +37 -0
- package/cypress/e2e/bugs/348.spec.cy.ts +73 -0
- package/cypress/support/commands.ts +4 -0
- package/cypress/support/e2e.ts +1 -0
- package/cypress/support/index.ts +1 -0
- package/cypress/support/lib/actions.ts +57 -0
- package/cypress/support/lib/index.ts +2 -0
- package/cypress/support/lib/selectors.ts +70 -0
- package/cypress/tsconfig.json +9 -0
- package/cypress.config.ts +9 -0
- package/jest.config.js +2 -3
- package/lerna.json +1 -1
- package/package.json +27 -20
- package/packages/configs/package.json +3 -3
- package/packages/constants/package.json +1 -1
- package/packages/dictionaries/package-lock.json +2 -2
- package/packages/dictionaries/package.json +4 -4
- package/packages/logger/package-lock.json +3 -3
- package/packages/logger/package.json +2 -2
- package/packages/scrabble-solver/.next/BUILD_ID +1 -1
- package/packages/scrabble-solver/.next/build-manifest.json +14 -14
- package/packages/scrabble-solver/.next/cache/.tsbuildinfo +1 -1
- package/packages/scrabble-solver/.next/cache/eslint/.cache_8dgz12 +1 -1
- package/packages/scrabble-solver/.next/cache/webpack/client-production/0.pack +0 -0
- package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack +0 -0
- package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack.old +0 -0
- package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/0.pack +0 -0
- package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack.old +0 -0
- package/packages/scrabble-solver/.next/cache/webpack/server-production/0.pack +0 -0
- package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack +0 -0
- package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack.old +0 -0
- package/packages/scrabble-solver/.next/prerender-manifest.js +1 -1
- package/packages/scrabble-solver/.next/prerender-manifest.json +1 -1
- package/packages/scrabble-solver/.next/routes-manifest.json +1 -1
- package/packages/scrabble-solver/.next/server/chunks/807.js +1 -1
- package/packages/scrabble-solver/.next/server/middleware-build-manifest.js +1 -1
- package/packages/scrabble-solver/.next/server/pages/404.html +1 -1
- package/packages/scrabble-solver/.next/server/pages/500.html +1 -1
- package/packages/scrabble-solver/.next/server/pages/api/solve.js +1 -1
- package/packages/scrabble-solver/.next/server/pages/index.html +1 -1
- package/packages/scrabble-solver/.next/server/pages/index.js +1 -1
- package/packages/scrabble-solver/.next/server/pages/index.json +1 -1
- package/packages/scrabble-solver/.next/server/pages-manifest.json +1 -1
- package/packages/scrabble-solver/.next/static/chunks/{main-b5b360c6afb66b05.js → main-8b0b4e610892a916.js} +1 -1
- package/packages/scrabble-solver/.next/static/chunks/pages/{404-0c9f3e0f8b15f487.js → 404-b0c2ccded2455be0.js} +1 -1
- package/packages/scrabble-solver/.next/static/chunks/pages/_app-42ce6b4032e931ff.js +17 -0
- package/packages/scrabble-solver/.next/static/chunks/pages/index-3718075f2ba2220c.js +1 -0
- package/packages/scrabble-solver/.next/static/chunks/webpack-c4acd79e87956a0e.js +1 -0
- package/packages/scrabble-solver/.next/static/css/2adc9736d823979b.css +2 -0
- package/packages/scrabble-solver/.next/static/{5T-kyZzpLLGYA9Qzg0-Sn → qwJjm2FeDHHGY92CY5oQQ}/_buildManifest.js +1 -1
- package/packages/scrabble-solver/.next/trace +45 -45
- package/packages/scrabble-solver/package-lock.json +6 -6
- package/packages/scrabble-solver/package.json +12 -12
- package/packages/scrabble-solver/public/service-worker.js +1 -1
- package/packages/scrabble-solver/src/components/Board/Board.module.scss +3 -9
- package/packages/scrabble-solver/src/components/Board/Board.tsx +2 -2
- package/packages/scrabble-solver/src/components/Board/BoardPure.tsx +3 -2
- package/packages/scrabble-solver/src/components/Board/components/ToggleDirectionButton/ToggleDirectionButton.tsx +4 -2
- package/packages/scrabble-solver/src/components/Board/hooks/useBackgroundImage.tsx +2 -6
- package/packages/scrabble-solver/src/components/Board/hooks/useBoardStyle.ts +10 -4
- package/packages/scrabble-solver/src/components/Dictionary/Dictionary.module.scss +10 -7
- package/packages/scrabble-solver/src/components/Dictionary/Dictionary.tsx +38 -36
- package/packages/scrabble-solver/src/components/Loading/Loading.module.scss +1 -0
- package/packages/scrabble-solver/src/components/Loading/Loading.tsx +1 -1
- package/packages/scrabble-solver/src/components/Modal/components/Section/Section.tsx +3 -2
- package/packages/scrabble-solver/src/components/NavButtons/NavButtons.tsx +1 -0
- package/packages/scrabble-solver/src/components/Rack/Rack.tsx +1 -0
- package/packages/scrabble-solver/src/components/Results/Cell.tsx +7 -3
- package/packages/scrabble-solver/src/components/Results/Header.tsx +99 -0
- package/packages/scrabble-solver/src/components/Results/HeaderButton.tsx +18 -13
- package/packages/scrabble-solver/src/components/Results/Result.tsx +23 -16
- package/packages/scrabble-solver/src/components/Results/Results.module.scss +24 -12
- package/packages/scrabble-solver/src/components/Results/Results.tsx +3 -9
- package/packages/scrabble-solver/src/components/Results/types.ts +0 -8
- package/packages/scrabble-solver/src/components/Solver/Solver.tsx +1 -1
- package/packages/scrabble-solver/src/components/Tile/TilePure.tsx +1 -0
- package/packages/scrabble-solver/src/hooks/index.ts +1 -0
- package/packages/scrabble-solver/src/hooks/useAppLayout.ts +12 -1
- package/packages/scrabble-solver/src/hooks/useColumns.ts +47 -0
- package/packages/scrabble-solver/src/i18n/languages/english.json +1 -0
- package/packages/scrabble-solver/src/i18n/languages/french.json +1 -0
- package/packages/scrabble-solver/src/i18n/languages/german.json +1 -0
- package/packages/scrabble-solver/src/i18n/languages/persian.json +1 -0
- package/packages/scrabble-solver/src/i18n/languages/polish.json +1 -0
- package/packages/scrabble-solver/src/i18n/languages/romanian.json +1 -0
- package/packages/scrabble-solver/src/i18n/languages/spanish.json +1 -0
- package/packages/scrabble-solver/src/icons/GeoAlt.svg +5 -0
- package/packages/scrabble-solver/src/icons/OneTwoThree.svg +4 -0
- package/packages/scrabble-solver/src/icons/SquareA.svg +6 -0
- package/packages/scrabble-solver/src/icons/SquareB.svg +6 -0
- package/packages/scrabble-solver/src/icons/Squares.svg +34 -0
- package/packages/scrabble-solver/src/icons/Words.svg +22 -0
- package/packages/scrabble-solver/src/icons/index.ts +6 -0
- package/packages/scrabble-solver/src/lib/groupResults.ts +1 -1
- package/packages/scrabble-solver/src/lib/index.ts +0 -1
- package/packages/scrabble-solver/src/lib/sortResults.ts +10 -10
- package/packages/scrabble-solver/src/modals/KeyMapModal/KeyMapModal.tsx +8 -9
- package/packages/scrabble-solver/src/modals/RemainingTilesModal/RemainingTilesModal.tsx +1 -0
- package/packages/scrabble-solver/src/modals/SettingsModal/SettingsModal.tsx +5 -5
- package/packages/scrabble-solver/src/modals/WordsModal/WordsModal.tsx +2 -0
- package/packages/scrabble-solver/src/parameters/index.ts +12 -0
- package/packages/scrabble-solver/src/state/selectors.ts +26 -1
- package/packages/scrabble-solver/src/state/slices/resultsInitialState.ts +2 -2
- package/packages/scrabble-solver/src/state/slices/resultsSlice.ts +2 -2
- package/packages/scrabble-solver/src/state/useTranslate.ts +5 -1
- package/packages/scrabble-solver/src/styles/variables.scss +1 -0
- package/packages/scrabble-solver/src/types/index.ts +12 -2
- package/packages/solver/package-lock.json +2 -2
- package/packages/solver/package.json +5 -5
- package/packages/types/package.json +2 -2
- package/packages/word-definitions/build/lib/request.d.ts +0 -1
- package/packages/word-definitions/package-lock.json +2 -2
- package/packages/word-definitions/package.json +2 -2
- package/packages/word-lists/package-lock.json +2 -2
- package/packages/word-lists/package.json +2 -2
- package/tsconfig.jest.json +2 -1
- package/.github/workflows/test.yml +0 -28
- package/packages/scrabble-solver/.next/static/chunks/pages/_app-264cd7dc7c7b5cc2.js +0 -17
- package/packages/scrabble-solver/.next/static/chunks/pages/index-65bfe83d121535ab.js +0 -1
- package/packages/scrabble-solver/.next/static/chunks/webpack-6ef43a8d4a395f49.js +0 -1
- package/packages/scrabble-solver/.next/static/css/2f727b21d1331ea5.css +0 -2
- package/packages/scrabble-solver/src/components/Results/getCoordinatesColumn.ts +0 -14
- package/packages/scrabble-solver/src/components/Results/getLocaleColumns.ts +0 -58
- package/packages/scrabble-solver/src/components/Results/useColumns.ts +0 -44
- package/packages/scrabble-solver/src/lib/dataUrlToBlob.ts +0 -20
- /package/packages/scrabble-solver/.next/static/{5T-kyZzpLLGYA9Qzg0-Sn → qwJjm2FeDHHGY92CY5oQQ}/_ssgManifest.js +0 -0
|
@@ -29,7 +29,7 @@ const Loading: FunctionComponent<Props> = ({ className, wave = true }) => {
|
|
|
29
29
|
const content = useMemo(() => prepareContent(message), [message]);
|
|
30
30
|
|
|
31
31
|
return (
|
|
32
|
-
<div aria-label={translation} className={classNames(styles.loading, className)} role="status">
|
|
32
|
+
<div aria-label={translation} className={classNames(styles.loading, className)} data-testid="loading" role="status">
|
|
33
33
|
<div className={styles.dim} />
|
|
34
34
|
<div className={styles.text}>
|
|
35
35
|
<PlainTiles className={classNames(styles.tiles)} content={content} dropShadow wave={wave} />
|
|
@@ -6,11 +6,12 @@ import styles from './Section.module.scss';
|
|
|
6
6
|
interface Props {
|
|
7
7
|
children: ReactNode;
|
|
8
8
|
className?: string;
|
|
9
|
+
label: string;
|
|
9
10
|
title: ReactNode;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
const Section: FunctionComponent<Props> = ({ children, className, title }) => (
|
|
13
|
-
<section className={classNames(styles.section, className)}>
|
|
13
|
+
const Section: FunctionComponent<Props> = ({ children, className, label, title }) => (
|
|
14
|
+
<section aria-label={label} className={classNames(styles.section, className)}>
|
|
14
15
|
<h2 className={styles.heading}>{title}</h2>
|
|
15
16
|
<div>{children}</div>
|
|
16
17
|
</section>
|
|
@@ -168,6 +168,7 @@ const Rack: FunctionComponent<Props> = ({ className, tileSize }) => {
|
|
|
168
168
|
className={classNames(styles.rack, className, {
|
|
169
169
|
[styles.hidden]: showInputPrompt,
|
|
170
170
|
})}
|
|
171
|
+
data-testid="rack"
|
|
171
172
|
ref={ref}
|
|
172
173
|
style={{ fontSize: tileFontSize }}
|
|
173
174
|
onPaste={handlePaste}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import classNames from 'classnames';
|
|
2
|
-
import { FunctionComponent, ReactNode } from 'react';
|
|
2
|
+
import { CSSProperties, FunctionComponent, ReactNode } from 'react';
|
|
3
3
|
|
|
4
4
|
import { selectLocale, useTranslate, useTypedSelector } from 'state';
|
|
5
5
|
import { TranslationKey } from 'types';
|
|
@@ -11,19 +11,23 @@ import styles from './Results.module.scss';
|
|
|
11
11
|
interface Props {
|
|
12
12
|
children?: ReactNode;
|
|
13
13
|
className?: string;
|
|
14
|
+
dataTestId?: string;
|
|
15
|
+
style?: CSSProperties;
|
|
14
16
|
translationKey: TranslationKey;
|
|
15
17
|
tooltip?: string | number;
|
|
16
18
|
value: string | number;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
const Cell: FunctionComponent<Props> = ({ children, className, translationKey, tooltip, value }) => {
|
|
21
|
+
const Cell: FunctionComponent<Props> = ({ children, className, dataTestId, style, translationKey, tooltip, value }) => {
|
|
20
22
|
const translate = useTranslate();
|
|
21
23
|
const locale = useTypedSelector(selectLocale);
|
|
22
24
|
const formattedValue = value.toLocaleString(locale);
|
|
23
25
|
|
|
24
26
|
return (
|
|
25
27
|
<Tooltip tooltip={`${translate(translationKey)}: ${tooltip || formattedValue}`}>
|
|
26
|
-
<div className={classNames(styles.cell, className)}
|
|
28
|
+
<div className={classNames(styles.cell, className)} data-testid={dataTestId} style={style}>
|
|
29
|
+
{children || formattedValue}
|
|
30
|
+
</div>
|
|
27
31
|
</Tooltip>
|
|
28
32
|
);
|
|
29
33
|
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { FunctionComponent } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useAppLayout, useColumns } from 'hooks';
|
|
4
|
+
import { GeoAlt, OneTwoThree, Square, SquareA, SquareB, Squares, Words } from 'icons';
|
|
5
|
+
import { RESULTS_COLUMN_WIDTH } from 'parameters';
|
|
6
|
+
import { ResultColumnId } from 'types';
|
|
7
|
+
|
|
8
|
+
import HeaderButton from './HeaderButton';
|
|
9
|
+
import styles from './Results.module.scss';
|
|
10
|
+
|
|
11
|
+
const Results: FunctionComponent = () => {
|
|
12
|
+
const { resultWordWidth } = useAppLayout();
|
|
13
|
+
const columns = useColumns();
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className={styles.header}>
|
|
17
|
+
{columns[ResultColumnId.Coordinates] && (
|
|
18
|
+
<HeaderButton
|
|
19
|
+
className={styles.coordinates}
|
|
20
|
+
Icon={GeoAlt}
|
|
21
|
+
id={ResultColumnId.Coordinates}
|
|
22
|
+
style={{ flexBasis: RESULTS_COLUMN_WIDTH[ResultColumnId.Coordinates] }}
|
|
23
|
+
translationKey="settings.showCoordinates"
|
|
24
|
+
/>
|
|
25
|
+
)}
|
|
26
|
+
|
|
27
|
+
{columns[ResultColumnId.Word] && (
|
|
28
|
+
<HeaderButton
|
|
29
|
+
className={styles.word}
|
|
30
|
+
id={ResultColumnId.Word}
|
|
31
|
+
style={{ flexBasis: resultWordWidth }}
|
|
32
|
+
translationKey="common.word"
|
|
33
|
+
/>
|
|
34
|
+
)}
|
|
35
|
+
|
|
36
|
+
{columns[ResultColumnId.TilesCount] && (
|
|
37
|
+
<HeaderButton
|
|
38
|
+
className={styles.stat}
|
|
39
|
+
Icon={Squares}
|
|
40
|
+
id={ResultColumnId.TilesCount}
|
|
41
|
+
style={{ flexBasis: RESULTS_COLUMN_WIDTH[ResultColumnId.TilesCount] }}
|
|
42
|
+
translationKey="common.tiles"
|
|
43
|
+
/>
|
|
44
|
+
)}
|
|
45
|
+
|
|
46
|
+
{columns[ResultColumnId.VowelsCount] && (
|
|
47
|
+
<HeaderButton
|
|
48
|
+
className={styles.stat}
|
|
49
|
+
Icon={SquareA}
|
|
50
|
+
id={ResultColumnId.VowelsCount}
|
|
51
|
+
style={{ flexBasis: RESULTS_COLUMN_WIDTH[ResultColumnId.VowelsCount] }}
|
|
52
|
+
translationKey="common.vowels"
|
|
53
|
+
/>
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
{columns[ResultColumnId.ConsonantsCount] && (
|
|
57
|
+
<HeaderButton
|
|
58
|
+
className={styles.stat}
|
|
59
|
+
Icon={SquareB}
|
|
60
|
+
id={ResultColumnId.ConsonantsCount}
|
|
61
|
+
style={{ flexBasis: RESULTS_COLUMN_WIDTH[ResultColumnId.ConsonantsCount] }}
|
|
62
|
+
translationKey="common.consonants"
|
|
63
|
+
/>
|
|
64
|
+
)}
|
|
65
|
+
|
|
66
|
+
{columns[ResultColumnId.BlanksCount] && (
|
|
67
|
+
<HeaderButton
|
|
68
|
+
className={styles.stat}
|
|
69
|
+
Icon={Square}
|
|
70
|
+
id={ResultColumnId.BlanksCount}
|
|
71
|
+
style={{ flexBasis: RESULTS_COLUMN_WIDTH[ResultColumnId.BlanksCount] }}
|
|
72
|
+
translationKey="common.blanks"
|
|
73
|
+
/>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
{columns[ResultColumnId.WordsCount] && (
|
|
77
|
+
<HeaderButton
|
|
78
|
+
className={styles.stat}
|
|
79
|
+
Icon={Words}
|
|
80
|
+
id={ResultColumnId.WordsCount}
|
|
81
|
+
style={{ flexBasis: RESULTS_COLUMN_WIDTH[ResultColumnId.WordsCount] }}
|
|
82
|
+
translationKey="common.words"
|
|
83
|
+
/>
|
|
84
|
+
)}
|
|
85
|
+
|
|
86
|
+
{columns[ResultColumnId.Points] && (
|
|
87
|
+
<HeaderButton
|
|
88
|
+
className={styles.points}
|
|
89
|
+
Icon={OneTwoThree}
|
|
90
|
+
id={ResultColumnId.Points}
|
|
91
|
+
style={{ flexBasis: RESULTS_COLUMN_WIDTH[ResultColumnId.Points] }}
|
|
92
|
+
translationKey="common.points"
|
|
93
|
+
/>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export default Results;
|
|
@@ -1,42 +1,47 @@
|
|
|
1
1
|
import classNames from 'classnames';
|
|
2
|
-
import { ReactElement, useCallback } from 'react';
|
|
2
|
+
import { CSSProperties, FunctionComponent, ReactElement, SVGAttributes, useCallback } from 'react';
|
|
3
3
|
import { useDispatch } from 'react-redux';
|
|
4
4
|
|
|
5
5
|
import { SortDown, SortUp } from 'icons';
|
|
6
6
|
import { resultsSlice, selectResultsSort, useTranslate, useTypedSelector } from 'state';
|
|
7
|
-
import { SortDirection } from 'types';
|
|
7
|
+
import { ResultColumnId, SortDirection, TranslationKey } from 'types';
|
|
8
8
|
|
|
9
9
|
import { Tooltip } from '../Tooltip';
|
|
10
10
|
|
|
11
11
|
import styles from './Results.module.scss';
|
|
12
|
-
import { Column } from './types';
|
|
13
12
|
|
|
14
13
|
interface Props {
|
|
15
|
-
|
|
14
|
+
className: string;
|
|
15
|
+
Icon?: FunctionComponent<SVGAttributes<SVGElement>>;
|
|
16
|
+
id: ResultColumnId;
|
|
17
|
+
translationKey: TranslationKey;
|
|
18
|
+
style?: CSSProperties;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
|
-
const HeaderButton = ({
|
|
21
|
+
const HeaderButton = ({ className, Icon, id, translationKey, style }: Props): ReactElement => {
|
|
19
22
|
const dispatch = useDispatch();
|
|
20
23
|
const translate = useTranslate();
|
|
21
24
|
const sort = useTypedSelector(selectResultsSort);
|
|
22
25
|
|
|
23
26
|
const handleClick = useCallback(() => {
|
|
24
|
-
dispatch(resultsSlice.actions.sort(
|
|
25
|
-
}, [
|
|
27
|
+
dispatch(resultsSlice.actions.sort(id));
|
|
28
|
+
}, [dispatch, id]);
|
|
26
29
|
|
|
27
30
|
return (
|
|
28
|
-
<Tooltip tooltip={translate(
|
|
31
|
+
<Tooltip tooltip={translate(translationKey)}>
|
|
29
32
|
<button
|
|
30
|
-
aria-label={translate(
|
|
31
|
-
className={classNames(styles.headerButton,
|
|
32
|
-
|
|
33
|
+
aria-label={translate(translationKey)}
|
|
34
|
+
className={classNames(styles.headerButton, className)}
|
|
35
|
+
style={style}
|
|
33
36
|
type="button"
|
|
34
37
|
onClick={handleClick}
|
|
35
38
|
>
|
|
36
39
|
<span className={styles.cell}>
|
|
37
|
-
<
|
|
40
|
+
{Icon && <Icon className={styles.headerButtonIcon} />}
|
|
38
41
|
|
|
39
|
-
{
|
|
42
|
+
{!Icon && <span className={styles.headerButtonLabel}>{translate(translationKey)}</span>}
|
|
43
|
+
|
|
44
|
+
{sort.column === id && (
|
|
40
45
|
<>
|
|
41
46
|
{sort.direction === SortDirection.Ascending && <SortUp className={styles.sortIcon} />}
|
|
42
47
|
{sort.direction === SortDirection.Descending && <SortDown className={styles.sortIcon} />}
|
|
@@ -2,6 +2,7 @@ import classNames from 'classnames';
|
|
|
2
2
|
import { CSSProperties, FocusEventHandler, MouseEventHandler, ReactElement, useMemo, useRef } from 'react';
|
|
3
3
|
import Highlighter from 'react-highlight-words';
|
|
4
4
|
|
|
5
|
+
import { useAppLayout, useColumns } from 'hooks';
|
|
5
6
|
import { LOCALE_FEATURES } from 'i18n';
|
|
6
7
|
import { getCoordinates, noop } from 'lib';
|
|
7
8
|
import {
|
|
@@ -11,12 +12,11 @@ import {
|
|
|
11
12
|
selectShowCoordinates,
|
|
12
13
|
useTypedSelector,
|
|
13
14
|
} from 'state';
|
|
14
|
-
import {
|
|
15
|
+
import { ResultColumnId } from 'types';
|
|
15
16
|
|
|
16
17
|
import Cell from './Cell';
|
|
17
18
|
import styles from './Results.module.scss';
|
|
18
19
|
import { ResultData } from './types';
|
|
19
|
-
import useColumns from './useColumns';
|
|
20
20
|
|
|
21
21
|
interface Props {
|
|
22
22
|
data: ResultData;
|
|
@@ -34,16 +34,16 @@ const Result = ({ data, index, style }: Props): ReactElement => {
|
|
|
34
34
|
onMouseEnter = noop,
|
|
35
35
|
onMouseLeave = noop,
|
|
36
36
|
} = data;
|
|
37
|
+
const { resultWordWidth } = useAppLayout();
|
|
37
38
|
const ref = useRef<HTMLButtonElement>(null);
|
|
38
39
|
const columns = useColumns();
|
|
39
40
|
const locale = useTypedSelector(selectLocale);
|
|
40
41
|
const showCoordinates = useTypedSelector(selectShowCoordinates);
|
|
41
42
|
const query = useTypedSelector(selectResultsQuery);
|
|
42
|
-
const {
|
|
43
|
+
const { direction, separator } = LOCALE_FEATURES[locale];
|
|
43
44
|
const result = results[index];
|
|
44
45
|
const isMatching = useTypedSelector((state) => selectIsResultMatching(state, index));
|
|
45
46
|
const words = direction === 'rtl' ? [...result.words].reverse() : result.words;
|
|
46
|
-
const enabledColumns = Object.fromEntries(columns.map((column) => [column.id, true]));
|
|
47
47
|
const coordinates = useMemo(() => getCoordinates(result, showCoordinates), [result, showCoordinates]);
|
|
48
48
|
|
|
49
49
|
const handleClick: MouseEventHandler = (event) => onClick(result, event);
|
|
@@ -54,11 +54,13 @@ const Result = ({ data, index, style }: Props): ReactElement => {
|
|
|
54
54
|
|
|
55
55
|
return (
|
|
56
56
|
<button
|
|
57
|
+
aria-hidden={isMatching ? undefined : 'true'}
|
|
57
58
|
aria-label={result.word}
|
|
58
59
|
className={classNames(styles.result, {
|
|
59
60
|
[styles.highlighted]: index === highlightedIndex,
|
|
60
61
|
[styles.notMatching]: !isMatching,
|
|
61
62
|
})}
|
|
63
|
+
data-testid="result"
|
|
62
64
|
ref={ref}
|
|
63
65
|
style={style}
|
|
64
66
|
type="button"
|
|
@@ -69,33 +71,38 @@ const Result = ({ data, index, style }: Props): ReactElement => {
|
|
|
69
71
|
onMouseLeave={handleMouseLeave}
|
|
70
72
|
>
|
|
71
73
|
<span className={styles.resultContent}>
|
|
72
|
-
{
|
|
74
|
+
{columns[ResultColumnId.Coordinates] && (
|
|
73
75
|
<Cell className={styles.coordinates} translationKey="settings.showCoordinates" value={coordinates} />
|
|
74
76
|
)}
|
|
75
77
|
|
|
76
|
-
{
|
|
77
|
-
<Cell
|
|
78
|
+
{columns[ResultColumnId.Word] && (
|
|
79
|
+
<Cell
|
|
80
|
+
className={styles.word}
|
|
81
|
+
style={{ flexBasis: resultWordWidth }}
|
|
82
|
+
translationKey="common.word"
|
|
83
|
+
value={result.word}
|
|
84
|
+
>
|
|
78
85
|
<Highlighter highlightClassName={styles.highlight} searchWords={[query]} textToHighlight={result.word} />
|
|
79
86
|
</Cell>
|
|
80
87
|
)}
|
|
81
88
|
|
|
82
|
-
{
|
|
89
|
+
{columns[ResultColumnId.TilesCount] && (
|
|
83
90
|
<Cell className={styles.stat} translationKey="common.tiles" value={result.tilesCount} />
|
|
84
91
|
)}
|
|
85
92
|
|
|
86
|
-
{
|
|
87
|
-
<Cell className={styles.stat} translationKey="common.
|
|
93
|
+
{columns[ResultColumnId.VowelsCount] && (
|
|
94
|
+
<Cell className={styles.stat} translationKey="common.vowels" value={result.vowelsCount} />
|
|
88
95
|
)}
|
|
89
96
|
|
|
90
|
-
{
|
|
91
|
-
<Cell className={styles.stat} translationKey="common.
|
|
97
|
+
{columns[ResultColumnId.ConsonantsCount] && (
|
|
98
|
+
<Cell className={styles.stat} translationKey="common.consonants" value={result.consonantsCount} />
|
|
92
99
|
)}
|
|
93
100
|
|
|
94
|
-
{
|
|
101
|
+
{columns[ResultColumnId.BlanksCount] && (
|
|
95
102
|
<Cell className={styles.stat} translationKey="common.blanks" value={result.blanksCount} />
|
|
96
103
|
)}
|
|
97
104
|
|
|
98
|
-
{
|
|
105
|
+
{columns[ResultColumnId.WordsCount] && (
|
|
99
106
|
<Cell
|
|
100
107
|
className={styles.stat}
|
|
101
108
|
translationKey="common.words"
|
|
@@ -104,8 +111,8 @@ const Result = ({ data, index, style }: Props): ReactElement => {
|
|
|
104
111
|
/>
|
|
105
112
|
)}
|
|
106
113
|
|
|
107
|
-
{
|
|
108
|
-
<Cell className={styles.points} translationKey="common.points" value={result.points} />
|
|
114
|
+
{columns[ResultColumnId.Points] && (
|
|
115
|
+
<Cell className={styles.points} dataTestId="points" translationKey="common.points" value={result.points} />
|
|
109
116
|
)}
|
|
110
117
|
</span>
|
|
111
118
|
</button>
|
|
@@ -44,7 +44,6 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
|
|
|
44
44
|
display: flex;
|
|
45
45
|
align-items: center;
|
|
46
46
|
justify-content: space-between;
|
|
47
|
-
font-weight: 700;
|
|
48
47
|
border-bottom: var(--border);
|
|
49
48
|
border-top-left-radius: inherit;
|
|
50
49
|
border-top-right-radius: inherit;
|
|
@@ -53,11 +52,13 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
|
|
|
53
52
|
.headerButton {
|
|
54
53
|
@include button-reset;
|
|
55
54
|
@include focus-effect;
|
|
55
|
+
|
|
56
56
|
cursor: pointer;
|
|
57
57
|
|
|
58
58
|
text-transform: uppercase;
|
|
59
59
|
transition: var(--transition);
|
|
60
60
|
background-color: var(--color--background);
|
|
61
|
+
height: 100%;
|
|
61
62
|
|
|
62
63
|
&:first-child {
|
|
63
64
|
border-start-start-radius: inherit;
|
|
@@ -72,6 +73,17 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
|
|
|
72
73
|
background-color: var(--color--primary);
|
|
73
74
|
color: var(--color--primary--opposite);
|
|
74
75
|
}
|
|
76
|
+
|
|
77
|
+
&.points {
|
|
78
|
+
@include scrollbars;
|
|
79
|
+
|
|
80
|
+
overflow-y: scroll;
|
|
81
|
+
|
|
82
|
+
&,
|
|
83
|
+
&:hover {
|
|
84
|
+
scrollbar-color: transparent transparent;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
75
87
|
}
|
|
76
88
|
|
|
77
89
|
.headerButtonLabel {
|
|
@@ -81,6 +93,12 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
|
|
|
81
93
|
text-align: start;
|
|
82
94
|
}
|
|
83
95
|
|
|
96
|
+
.headerButtonIcon {
|
|
97
|
+
flex: 0 0 auto;
|
|
98
|
+
width: var(--results--icon--size);
|
|
99
|
+
height: var(--results--icon--size);
|
|
100
|
+
}
|
|
101
|
+
|
|
84
102
|
.result {
|
|
85
103
|
@include button-reset;
|
|
86
104
|
|
|
@@ -98,7 +116,7 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
|
|
|
98
116
|
}
|
|
99
117
|
}
|
|
100
118
|
|
|
101
|
-
|
|
119
|
+
&[aria-hidden='true'] {
|
|
102
120
|
color: var(--color--inactive);
|
|
103
121
|
|
|
104
122
|
&:focus,
|
|
@@ -138,24 +156,18 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
|
|
|
138
156
|
justify-content: flex-start;
|
|
139
157
|
}
|
|
140
158
|
|
|
141
|
-
.result
|
|
142
|
-
.headerButton:first-child & {
|
|
143
|
-
text-align: start;
|
|
159
|
+
.result &.word:first-child,
|
|
160
|
+
.headerButton.word:first-child & {
|
|
144
161
|
padding-inline-start: $row-padding-horizontal;
|
|
145
162
|
}
|
|
146
163
|
|
|
147
|
-
.result &:last-child,
|
|
148
|
-
.headerButton:last-child & {
|
|
149
|
-
padding-inline-end: $row-padding-horizontal;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
164
|
&:last-child {
|
|
153
165
|
flex: 1;
|
|
154
166
|
}
|
|
155
167
|
}
|
|
156
168
|
|
|
157
169
|
.word {
|
|
158
|
-
flex:
|
|
170
|
+
flex: 0 0;
|
|
159
171
|
text-transform: uppercase;
|
|
160
172
|
}
|
|
161
173
|
|
|
@@ -176,7 +188,7 @@ $row-padding-horizontal: calc(var(--spacing--m) + var(--spacing--s));
|
|
|
176
188
|
}
|
|
177
189
|
|
|
178
190
|
.coordinates {
|
|
179
|
-
$width:
|
|
191
|
+
$width: 55px;
|
|
180
192
|
|
|
181
193
|
flex: 0 0 $width;
|
|
182
194
|
max-width: $width;
|
|
@@ -20,12 +20,11 @@ import EmptyState from '../EmptyState';
|
|
|
20
20
|
import Loading from '../Loading';
|
|
21
21
|
import ResultsInput from '../ResultsInput';
|
|
22
22
|
|
|
23
|
-
import
|
|
23
|
+
import Header from './Header';
|
|
24
24
|
import Result from './Result';
|
|
25
25
|
import styles from './Results.module.scss';
|
|
26
26
|
import SolveButton from './SolveButton';
|
|
27
27
|
import { ResultCallbacks, ResultData } from './types';
|
|
28
|
-
import useColumns from './useColumns';
|
|
29
28
|
|
|
30
29
|
interface Props {
|
|
31
30
|
callbacks: ResultCallbacks;
|
|
@@ -47,7 +46,6 @@ const Results: FunctionComponent<Props> = ({ callbacks, className, highlightedIn
|
|
|
47
46
|
const error = useTypedSelector(selectSolveError);
|
|
48
47
|
const itemData = useMemo(() => ({ ...callbacks, highlightedIndex, results }), [callbacks, highlightedIndex, results]);
|
|
49
48
|
const [listRef, setListRef] = useState<FixedSizeList<ResultData> | null>(null);
|
|
50
|
-
const columns = useColumns();
|
|
51
49
|
const scrollToIndex = typeof highlightedIndex === 'number' ? highlightedIndex : 0;
|
|
52
50
|
const scrollToIndexRef = useLatest(scrollToIndex);
|
|
53
51
|
const hasResults = typeof error === 'undefined' && typeof results !== 'undefined';
|
|
@@ -70,12 +68,8 @@ const Results: FunctionComponent<Props> = ({ callbacks, className, highlightedIn
|
|
|
70
68
|
}, [results, listRef, scrollToIndexRef]);
|
|
71
69
|
|
|
72
70
|
return (
|
|
73
|
-
<div className={classNames(styles.results, className)}>
|
|
74
|
-
<
|
|
75
|
-
{columns.map((column) => (
|
|
76
|
-
<HeaderButton column={column} key={column.id} />
|
|
77
|
-
))}
|
|
78
|
-
</div>
|
|
71
|
+
<div className={classNames(styles.results, className)} data-testid="results">
|
|
72
|
+
<Header />
|
|
79
73
|
|
|
80
74
|
<div className={styles.content}>
|
|
81
75
|
{typeof error !== 'undefined' && (
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
import { Result } from '@scrabble-solver/types';
|
|
2
2
|
import { FocusEvent, MouseEvent } from 'react';
|
|
3
3
|
|
|
4
|
-
import { ResultColumn, TranslationKey } from 'types';
|
|
5
|
-
|
|
6
|
-
export interface Column {
|
|
7
|
-
className: string;
|
|
8
|
-
id: ResultColumn;
|
|
9
|
-
translationKey: TranslationKey;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
4
|
export interface ResultCallbacks {
|
|
13
5
|
onBlur?: (result: Result, event: FocusEvent) => void;
|
|
14
6
|
onClick?: (result: Result, event: MouseEvent) => void;
|
|
@@ -100,7 +100,7 @@ const Solver: FunctionComponent<Props> = ({ className, onShowResults }) => {
|
|
|
100
100
|
<div className={styles.column}>
|
|
101
101
|
<Results callbacks={callbacks} className={styles.results} />
|
|
102
102
|
|
|
103
|
-
<div className={styles.dictionaryContainer}>
|
|
103
|
+
<div data-testid="dictionary" className={styles.dictionaryContainer}>
|
|
104
104
|
<Dictionary className={styles.dictionary} />
|
|
105
105
|
<DictionaryInput className={styles.dictionaryInput} />
|
|
106
106
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { default as useAppLayout } from './useAppLayout';
|
|
2
|
+
export { default as useColumns } from './useColumns';
|
|
2
3
|
export { default as useDirection } from './useDirection';
|
|
3
4
|
export { default as useEffectOnce } from './useEffectOnce';
|
|
4
5
|
export { default as useIsTouchDevice } from './useIsTouchDevice';
|
|
@@ -15,10 +15,13 @@ import {
|
|
|
15
15
|
MODAL_WIDTH,
|
|
16
16
|
NAV_PADDING,
|
|
17
17
|
RACK_TILE_SIZE_MAX,
|
|
18
|
+
RESULTS_COLUMN_WIDTH,
|
|
18
19
|
SOLVER_COLUMN_WIDTH,
|
|
19
20
|
} from 'parameters';
|
|
20
21
|
import { selectConfig, selectShowCoordinates, useTypedSelector } from 'state';
|
|
22
|
+
import { ResultColumnId } from 'types';
|
|
21
23
|
|
|
24
|
+
import useColumns from './useColumns';
|
|
22
25
|
import useIsTouchDevice from './useIsTouchDevice';
|
|
23
26
|
import useMediaQueries from './useMediaQueries';
|
|
24
27
|
import useViewportSize from './useViewportSize';
|
|
@@ -29,6 +32,7 @@ const useAppLayout = () => {
|
|
|
29
32
|
const showCoordinates = useTypedSelector(selectShowCoordinates);
|
|
30
33
|
const isTouchDevice = useIsTouchDevice();
|
|
31
34
|
const { isLessThanXs, isLessThanS, isLessThanM, isLessThanL, isLessThanXl } = useMediaQueries();
|
|
35
|
+
const columns = useColumns();
|
|
32
36
|
const isBoardFullWidth = isLessThanM;
|
|
33
37
|
const showResultCandidatePicker = isLessThanL;
|
|
34
38
|
const componentsSpacing = isLessThanXl ? COMPONENTS_SPACING_SMALL : COMPONENTS_SPACING;
|
|
@@ -68,6 +72,12 @@ const useAppLayout = () => {
|
|
|
68
72
|
? viewportHeight - dictionaryHeight - BUTTON_HEIGHT - MODAL_HEADER_HEIGHT - 5 * componentsSpacing
|
|
69
73
|
: boardSize - componentsSpacing - dictionaryHeight;
|
|
70
74
|
const rackWidth = tileSize * config.rackSize;
|
|
75
|
+
const resultsWidth = isLessThanL ? modalWidth - 2 * componentsSpacing : SOLVER_COLUMN_WIDTH;
|
|
76
|
+
const columnsWidth = Object.keys(columns).reduce(
|
|
77
|
+
(sum, column) => sum + (RESULTS_COLUMN_WIDTH[column as ResultColumnId] ?? 0),
|
|
78
|
+
0,
|
|
79
|
+
);
|
|
80
|
+
const resultWordWidth = resultsWidth - 2 * BORDER_WIDTH - columnsWidth;
|
|
71
81
|
|
|
72
82
|
return {
|
|
73
83
|
actionsWidth: 2 * BUTTON_HEIGHT - BORDER_WIDTH,
|
|
@@ -83,7 +93,8 @@ const useAppLayout = () => {
|
|
|
83
93
|
rackHeight: tileSize,
|
|
84
94
|
rackWidth,
|
|
85
95
|
resultsHeight,
|
|
86
|
-
resultsWidth
|
|
96
|
+
resultsWidth,
|
|
97
|
+
resultWordWidth,
|
|
87
98
|
showCompactControls: !showColumn,
|
|
88
99
|
showKeyMap: !isTouchDevice,
|
|
89
100
|
showResultsInModal,
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import { selectColumns, useTypedSelector } from 'state';
|
|
4
|
+
import { ResultColumnId } from 'types';
|
|
5
|
+
|
|
6
|
+
import useMediaQueries from './useMediaQueries';
|
|
7
|
+
|
|
8
|
+
const COLUMNS_XS = [ResultColumnId.Coordinates, ResultColumnId.Word, ResultColumnId.Points];
|
|
9
|
+
|
|
10
|
+
const COLUMNS_S = [...COLUMNS_XS, ResultColumnId.BlanksCount, ResultColumnId.WordsCount];
|
|
11
|
+
|
|
12
|
+
const COLUMNS_M = [...COLUMNS_XS];
|
|
13
|
+
|
|
14
|
+
const COLUMNS_L = [...COLUMNS_XS];
|
|
15
|
+
|
|
16
|
+
const useColumns = (): Partial<Record<ResultColumnId, boolean>> => {
|
|
17
|
+
const columns = useTypedSelector(selectColumns);
|
|
18
|
+
const { isLessThanXs, isLessThanS, isLessThanM, isLessThanL } = useMediaQueries();
|
|
19
|
+
|
|
20
|
+
const filteredColumns = useMemo(() => {
|
|
21
|
+
if (isLessThanXs) {
|
|
22
|
+
return columns.filter((columnId) => COLUMNS_XS.includes(columnId));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (isLessThanS) {
|
|
26
|
+
return columns.filter((columnId) => COLUMNS_S.includes(columnId));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (isLessThanM) {
|
|
30
|
+
return columns.filter((columnId) => COLUMNS_M.includes(columnId));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (isLessThanL) {
|
|
34
|
+
return columns.filter((columnId) => COLUMNS_L.includes(columnId));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return columns;
|
|
38
|
+
}, [columns, isLessThanXs, isLessThanS, isLessThanM, isLessThanL]);
|
|
39
|
+
|
|
40
|
+
const columnsMap = useMemo(() => {
|
|
41
|
+
return Object.fromEntries(filteredColumns.map((column) => [column, true]));
|
|
42
|
+
}, [filteredColumns]);
|
|
43
|
+
|
|
44
|
+
return columnsMap;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export default useColumns;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"cell.set-blank": "Mark it a blank",
|
|
6
6
|
"cell.set-not-blank": "Mark it not a blank",
|
|
7
7
|
"cell.tile.location": "Board: tile ({{x}}, {{y}})",
|
|
8
|
+
"cell.toggle-direction": "Typing direction",
|
|
8
9
|
"common.arrows": "Arrow keys",
|
|
9
10
|
"common.blanks": "Blanks",
|
|
10
11
|
"common.clear": "Clear",
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"cell.set-blank": "Marquer comme vide",
|
|
6
6
|
"cell.set-not-blank": "Marquer comme non vide",
|
|
7
7
|
"cell.tile.location": "Plateau: la case ({{x}}, {{y}})",
|
|
8
|
+
"cell.toggle-direction": "Direction d'écriture",
|
|
8
9
|
"common.arrows": "Touches fléchées",
|
|
9
10
|
"common.blanks": "Cases vides",
|
|
10
11
|
"common.clear": "Effacer",
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"cell.set-blank": "Als Blanko markieren",
|
|
6
6
|
"cell.set-not-blank": "Nicht als Blanko markieren",
|
|
7
7
|
"cell.tile.location": "Brett: Stein ({{x}}, {{y}})",
|
|
8
|
+
"cell.toggle-direction": "Schreibrichtung",
|
|
8
9
|
"common.arrows": "Pfeiltasten",
|
|
9
10
|
"common.blanks": "Blankos",
|
|
10
11
|
"common.clear": "Löschen",
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"cell.set-blank": "علامت گذاری به عنوان خالی",
|
|
6
6
|
"cell.set-not-blank": "علامت گذاری به عنوان غیر خالی",
|
|
7
7
|
"cell.tile.location": "({{x}}، {{y}}) کاشی: صفحه",
|
|
8
|
+
"cell.toggle-direction": "جهت تایپ",
|
|
8
9
|
"common.arrows": "کلیدهای جهت دار",
|
|
9
10
|
"common.blanks": "خالی",
|
|
10
11
|
"common.clear": "پاک کردن",
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"cell.set-blank": "Oznacz jako blank",
|
|
6
6
|
"cell.set-not-blank": "Oznacz jako nie blank",
|
|
7
7
|
"cell.tile.location": "Plansza: płytka ({{x}}, {{y}})",
|
|
8
|
+
"cell.toggle-direction": "Kierunek wpisywania",
|
|
8
9
|
"common.arrows": "Klawisze strzałek",
|
|
9
10
|
"common.blanks": "Blanki",
|
|
10
11
|
"common.clear": "Wyczyść",
|