scrabble-solver 2.13.12 → 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 +632 -548
- 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/{N8hSsS6Ppzlj3ebHMSZvI → 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 +2 -0
- 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/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 +11 -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-8246f5b39b6a5e59.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/{N8hSsS6Ppzlj3ebHMSZvI → qwJjm2FeDHHGY92CY5oQQ}/_ssgManifest.js +0 -0
|
@@ -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;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<!-- https://icons.getbootstrap.com/icons/geo-alt/ -->
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16">
|
|
3
|
+
<path d="M12.166 8.94c-.524 1.062-1.234 2.12-1.96 3.07A32 32 0 0 1 8 14.58a32 32 0 0 1-2.206-2.57c-.726-.95-1.436-2.008-1.96-3.07C3.304 7.867 3 6.862 3 6a5 5 0 0 1 10 0c0 .862-.305 1.867-.834 2.94M8 16s6-5.686 6-10A6 6 0 0 0 2 6c0 4.314 6 10 6 10" />
|
|
4
|
+
<path d="M8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4m0 1a3 3 0 1 0 0-6 3 3 0 0 0 0 6" />
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<!-- https://icons.getbootstrap.com/icons/123/ -->
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16">
|
|
3
|
+
<path d="M2.873 11.297V4.142H1.699L0 5.379v1.137l1.64-1.18h.06v5.961zm3.213-5.09v-.063c0-.618.44-1.169 1.196-1.169.676 0 1.174.44 1.174 1.106 0 .624-.42 1.101-.807 1.526L4.99 10.553v.744h4.78v-.99H6.643v-.069L8.41 8.252c.65-.724 1.237-1.332 1.237-2.27C9.646 4.849 8.723 4 7.308 4c-1.573 0-2.36 1.064-2.36 2.15v.057zm6.559 1.883h.786c.823 0 1.374.481 1.379 1.179.01.707-.55 1.216-1.421 1.21-.77-.005-1.326-.419-1.379-.953h-1.095c.042 1.053.938 1.918 2.464 1.918 1.478 0 2.642-.839 2.62-2.144-.02-1.143-.922-1.651-1.551-1.714v-.063c.535-.09 1.347-.66 1.326-1.678-.026-1.053-.933-1.855-2.359-1.845-1.5.005-2.317.88-2.348 1.898h1.116c.032-.498.498-.944 1.206-.944.703 0 1.206.435 1.206 1.07.005.64-.504 1.106-1.2 1.106h-.75z" />
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<!-- https://icons.getbootstrap.com/icons/square/ -->
|
|
2
|
+
<!-- https://icons.getbootstrap.com/icons/alphabet-uppercase/ -->
|
|
3
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16">
|
|
4
|
+
<path d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h12zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2z" />
|
|
5
|
+
<path d="M 7.2949219,4.8691409 5.2382813,11.130859 H 6.4648438 L 6.9453125,9.5195319 h 2.046875 L 9.4707032,11.130859 H 10.761719 L 8.7148438,4.8691409 Z m 0.6484375,1.199219 h 0.054687 l 0.75,2.53125 h -1.554687 z" />
|
|
6
|
+
</svg>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<!-- https://icons.getbootstrap.com/icons/square/ -->
|
|
2
|
+
<!-- https://icons.getbootstrap.com/icons/alphabet-uppercase/ -->
|
|
3
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16">
|
|
4
|
+
<path d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h12zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2z" />
|
|
5
|
+
<path d="m 5.658203,4.8691408 v 6.2617182 h 2.5507813 c 1.2429974,0 2.1328127,-0.646627 2.1328127,-1.7656244 0,-0.9409982 -0.629236,-1.4533438 -1.3652346,-1.5273438 V 7.7812502 C 9.5825612,7.6292506 10.027344,7.172155 10.027344,6.4101564 c 0,-0.957998 -0.7185963,-1.5410156 -1.8085941,-1.5410156 z m 1.203125,0.9316406 h 1.0742187 c 0.569999,1e-7 0.8964844,0.3103136 0.8964844,0.8203126 0,0.5449988 -0.3579236,0.8535156 -1.1699218,0.8535156 H 6.861328 Z m 0,2.5371094 h 1.1503906 c 0.7129986,0 1.0957031,0.3621262 1.0957031,0.953125 10e-8,0.5969988 -0.3927518,0.9082032 -1.34375,0.9082032 H 6.861328 Z" />
|
|
6
|
+
</svg>
|