@scrabble-solver/scrabble-solver 2.13.8 → 2.13.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +6 -6
  3. package/.next/cache/.tsbuildinfo +1 -1
  4. package/.next/cache/eslint/.cache_8dgz12 +1 -1
  5. package/.next/cache/webpack/client-production/0.pack +0 -0
  6. package/.next/cache/webpack/client-production/index.pack +0 -0
  7. package/.next/cache/webpack/edge-server-production/0.pack +0 -0
  8. package/.next/cache/webpack/edge-server-production/index.pack +0 -0
  9. package/.next/cache/webpack/server-production/0.pack +0 -0
  10. package/.next/cache/webpack/server-production/index.pack +0 -0
  11. package/.next/prerender-manifest.js +1 -1
  12. package/.next/prerender-manifest.json +1 -1
  13. package/.next/routes-manifest.json +1 -1
  14. package/.next/server/chunks/807.js +1 -1
  15. package/.next/server/middleware-build-manifest.js +1 -1
  16. package/.next/server/pages/404.html +1 -1
  17. package/.next/server/pages/500.html +1 -1
  18. package/.next/server/pages/_app.js +1 -1
  19. package/.next/server/pages/_error.js +1 -1
  20. package/.next/server/pages/api/solve.js +1 -1
  21. package/.next/server/pages/index.html +1 -1
  22. package/.next/server/pages/index.js +1 -1
  23. package/.next/server/pages/index.json +1 -1
  24. package/.next/static/{7zESQYo9UAqNh9LV0b7Sd → 0kOqO_aASkcT2xjhiptyo}/_buildManifest.js +1 -1
  25. package/.next/static/chunks/pages/{404-63b972b24be99c62.js → 404-b447c5ca188dd7c1.js} +1 -1
  26. package/.next/static/chunks/pages/_app-0bbddaa93fde16ea.js +17 -0
  27. package/.next/static/chunks/pages/index-24b84719cf22731c.js +1 -0
  28. package/.next/static/css/841a5b5f0b2fb131.css +2 -0
  29. package/.next/trace +44 -44
  30. package/LICENSE +1 -1
  31. package/package.json +10 -9
  32. package/src/components/Board/Board.tsx +3 -3
  33. package/src/components/Board/BoardPure.tsx +27 -19
  34. package/src/components/Board/components/Actions/Actions.tsx +8 -12
  35. package/src/components/Board/components/Actions/lib.ts +30 -0
  36. package/src/components/Board/hooks/useBackgroundImage.tsx +3 -24
  37. package/src/components/Radio/Radio.module.scss +2 -1
  38. package/src/components/Results/Results.tsx +5 -1
  39. package/src/components/Tile/Tile.module.scss +0 -2
  40. package/src/components/Tile/Tile.tsx +1 -15
  41. package/src/components/Tooltip/Tooltip.module.scss +1 -0
  42. package/src/hooks/useAppLayout.ts +0 -1
  43. package/src/i18n/languages/english.json +2 -1
  44. package/src/i18n/languages/french.json +2 -1
  45. package/src/i18n/languages/german.json +2 -1
  46. package/src/i18n/languages/persian.json +2 -1
  47. package/src/i18n/languages/polish.json +2 -1
  48. package/src/i18n/languages/romanian.json +2 -1
  49. package/src/i18n/languages/spanish.json +2 -1
  50. package/src/icons/Ban.svg +4 -0
  51. package/src/icons/index.ts +1 -0
  52. package/src/lib/groupResults.ts +7 -13
  53. package/src/lib/index.ts +2 -0
  54. package/src/lib/resultMatchesCellFilter.ts +23 -0
  55. package/src/lib/sortGroupedResults.ts +22 -0
  56. package/src/parameters/index.ts +0 -9
  57. package/src/state/sagas.ts +4 -4
  58. package/src/state/selectors.ts +13 -13
  59. package/src/state/slices/cellFilterInitialState.ts +2 -2
  60. package/src/state/slices/cellFilterSlice.ts +29 -4
  61. package/src/types/index.ts +18 -1
  62. package/.next/static/chunks/pages/_app-a2848b7efa6bb6b0.js +0 -17
  63. package/.next/static/chunks/pages/index-7b73be2915cc7099.js +0 -1
  64. package/.next/static/css/b37850c8d5270d91.css +0 -2
  65. /package/.next/static/{7zESQYo9UAqNh9LV0b7Sd → 0kOqO_aASkcT2xjhiptyo}/_ssgManifest.js +0 -0
package/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2022 Kamil Mielnik <kamil@kamilmielnik.com>
1
+ Copyright (c) 2024 Kamil Mielnik <kamil@kamilmielnik.com>
2
2
 
3
3
  Attribution-NonCommercial-NoDerivatives 4.0 International
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scrabble-solver/scrabble-solver",
3
- "version": "2.13.8",
3
+ "version": "2.13.10",
4
4
  "description": "Scrabble Solver 2 - App",
5
5
  "engines": {
6
6
  "node": ">=16"
@@ -30,13 +30,13 @@
30
30
  "@floating-ui/react": "^0.26.17",
31
31
  "@kamilmielnik/trie": "^3.0.0",
32
32
  "@reduxjs/toolkit": "^2.2.5",
33
- "@scrabble-solver/configs": "^2.13.8",
34
- "@scrabble-solver/constants": "^2.13.8",
35
- "@scrabble-solver/dictionaries": "^2.13.8",
36
- "@scrabble-solver/logger": "^2.13.8",
37
- "@scrabble-solver/solver": "^2.13.8",
38
- "@scrabble-solver/types": "^2.13.8",
39
- "@scrabble-solver/word-definitions": "^2.13.8",
33
+ "@scrabble-solver/configs": "^2.13.10",
34
+ "@scrabble-solver/constants": "^2.13.10",
35
+ "@scrabble-solver/dictionaries": "^2.13.10",
36
+ "@scrabble-solver/logger": "^2.13.10",
37
+ "@scrabble-solver/solver": "^2.13.10",
38
+ "@scrabble-solver/types": "^2.13.10",
39
+ "@scrabble-solver/word-definitions": "^2.13.10",
40
40
  "classnames": "^2.5.1",
41
41
  "env-cmd": "^10.1.0",
42
42
  "include-media": "^2.0.0",
@@ -52,6 +52,7 @@
52
52
  "react-window": "^1.8.10",
53
53
  "redux-saga": "^1.3.0",
54
54
  "store2": "^2.14.3",
55
+ "use-debounce": "^10.0.1",
55
56
  "workbox-expiration": "^7.1.0",
56
57
  "workbox-precaching": "^7.1.0",
57
58
  "workbox-routing": "^7.1.0",
@@ -72,5 +73,5 @@
72
73
  "@types/redux-saga": "^0.10.5",
73
74
  "sass": "^1.77.5"
74
75
  },
75
- "gitHead": "294a7a790843ef9d260be52a3144e323c8ac0641"
76
+ "gitHead": "ebc7207c650ab1fdbb0bca3df53a445eaa6de268"
76
77
  }
@@ -9,7 +9,7 @@ import { useDispatch } from 'react-redux';
9
9
 
10
10
  import { useAppLayout } from 'hooks';
11
11
  import { LOCALE_FEATURES } from 'i18n';
12
- import { TRANSITION } from 'parameters';
12
+ import { BORDER_WIDTH, TRANSITION } from 'parameters';
13
13
  import {
14
14
  boardSlice,
15
15
  cellFilterSlice,
@@ -176,8 +176,8 @@ const Board: FunctionComponent<Props> = ({ className }) => {
176
176
  ref={floatingFocus.refs.setFloating}
177
177
  style={{
178
178
  position: floatingFocus.strategy,
179
- top: floatingFocus.y ? floatingFocus.y + cellSize : 0,
180
- left: floatingFocus.x ?? 0,
179
+ top: (floatingFocus.y ? floatingFocus.y + cellSize : 0) - (showCoordinates === 'hidden' ? 0 : BORDER_WIDTH),
180
+ left: (floatingFocus.x ?? 0) - (showCoordinates === 'hidden' ? 0 : BORDER_WIDTH),
181
181
  width: cellSize,
182
182
  height: cellSize,
183
183
  opacity: hasFocus ? 1 : 0,
@@ -12,10 +12,10 @@ import {
12
12
  memo,
13
13
  } from 'react';
14
14
 
15
- import { FlagFill } from 'icons';
15
+ import { Ban, FlagFill } from 'icons';
16
16
  import { getCoordinate } from 'lib';
17
17
  import { BORDER_WIDTH } from 'parameters';
18
- import { Point } from 'types';
18
+ import { CellFilterEntry } from 'types';
19
19
 
20
20
  import styles from './Board.module.scss';
21
21
  import { Cell } from './components';
@@ -26,7 +26,7 @@ interface Props {
26
26
  coordinatesFontSize: number;
27
27
  coordinatesSize: number;
28
28
  direction: 'ltr' | 'rtl';
29
- filteredCells: Point[];
29
+ filteredCells: CellFilterEntry[];
30
30
  inputRefs: RefObject<HTMLInputElement>[][];
31
31
  rows: CellModel[][];
32
32
  showCoordinates: ShowCoordinates;
@@ -87,22 +87,30 @@ const BoardPure = forwardRef<HTMLDivElement, Props>(
87
87
  </>
88
88
  )}
89
89
 
90
- {filteredCells.map(({ x, y }) => (
91
- <div
92
- className={styles.iconContainer}
93
- key={[x, y].join('-')}
94
- style={{
95
- height: cellSize,
96
- width: cellSize,
97
- left: direction === 'ltr' ? coordinatesSize + x * (cellSize + BORDER_WIDTH) : undefined,
98
- right: direction === 'rtl' ? coordinatesSize + x * (cellSize + BORDER_WIDTH) : undefined,
99
- top: coordinatesSize + y * (cellSize + BORDER_WIDTH),
100
- }}
101
- >
102
- <div className={styles.iconBackground} />
103
- <FlagFill className={styles.icon} />
104
- </div>
105
- ))}
90
+ {/* The dynamic changes to the board presentation need to be outside of useBackgroundImage
91
+ to prevent flickering on blob URL change (i.e. when flagging a field,
92
+ but not when changing game type since user's attention is not on the board
93
+ when that happens)*/}
94
+ {filteredCells.map(({ x, y, type }) => {
95
+ const Icon = type === 'exclude' ? Ban : FlagFill;
96
+
97
+ return (
98
+ <div
99
+ className={styles.iconContainer}
100
+ key={[x, y].join('-')}
101
+ style={{
102
+ height: cellSize,
103
+ width: cellSize,
104
+ left: direction === 'ltr' ? coordinatesSize + BORDER_WIDTH + x * (cellSize + BORDER_WIDTH) : undefined,
105
+ right: direction === 'rtl' ? coordinatesSize + BORDER_WIDTH + x * (cellSize + BORDER_WIDTH) : undefined,
106
+ top: coordinatesSize + BORDER_WIDTH + y * (cellSize + BORDER_WIDTH),
107
+ }}
108
+ >
109
+ <div className={styles.iconBackground} />
110
+ <Icon className={styles.icon} />
111
+ </div>
112
+ );
113
+ })}
106
114
 
107
115
  {rows.map((cells, y) => (
108
116
  <Fragment key={y}>
@@ -3,21 +3,16 @@ import { Cell } from '@scrabble-solver/types';
3
3
  import classNames from 'classnames';
4
4
  import { forwardRef, HTMLProps, MouseEventHandler } from 'react';
5
5
 
6
- import { Flag, FlagFill, Keyboard, Square, SquareFill } from 'icons';
6
+ import { Keyboard, Square, SquareFill } from 'icons';
7
7
  import { findCell } from 'lib';
8
- import {
9
- selectCellIsFiltered,
10
- selectInputMode,
11
- selectResultCandidateCells,
12
- useTranslate,
13
- useTypedSelector,
14
- } from 'state';
8
+ import { selectCellFilter, selectInputMode, selectResultCandidateCells, useTranslate, useTypedSelector } from 'state';
15
9
  import { Direction } from 'types';
16
10
 
17
11
  import Button from '../../../Button';
18
12
  import ToggleDirectionButton from '../ToggleDirectionButton';
19
13
 
20
14
  import styles from './Actions.module.scss';
15
+ import { getNextCellFilter } from './lib';
21
16
 
22
17
  interface Props extends HTMLProps<HTMLDivElement> {
23
18
  cell: Cell;
@@ -35,10 +30,11 @@ const Actions = forwardRef<HTMLDivElement, Props>(
35
30
  ) => {
36
31
  const translate = useTranslate();
37
32
  const inputMode = useTypedSelector(selectInputMode);
38
- const isFiltered = useTypedSelector((state) => selectCellIsFiltered(state, cell));
33
+ const filter = useTypedSelector((state) => selectCellFilter(state, cell));
39
34
  const resultCandidateCells = useTypedSelector(selectResultCandidateCells);
40
35
  const isBlank = cell.tile.isBlank;
41
36
  const isEmpty = cell.tile.character === EMPTY_CELL || Boolean(findCell(resultCandidateCells, cell.x, cell.y));
37
+ const { Icon, labelTranslationKey } = getNextCellFilter(filter);
42
38
 
43
39
  // On iOS it helps with losing focus too early which makes Actions disappear
44
40
  const handleMouseDown: MouseEventHandler = (event) => event.preventDefault();
@@ -67,10 +63,10 @@ const Actions = forwardRef<HTMLDivElement, Props>(
67
63
 
68
64
  {isEmpty && (
69
65
  <Button
70
- aria-label={translate('cell.filter-cell')}
66
+ aria-label={translate(labelTranslationKey)}
71
67
  className={classNames(styles.action)}
72
- Icon={isFiltered ? Flag : FlagFill}
73
- tooltip={translate('cell.filter-cell')}
68
+ Icon={Icon}
69
+ tooltip={translate(labelTranslationKey)}
74
70
  onClick={onToggleFilterCell}
75
71
  onMouseDown={handleMouseDown}
76
72
  />
@@ -0,0 +1,30 @@
1
+ import { FunctionComponent, SVGAttributes } from 'react';
2
+
3
+ import { Ban, Eraser, FlagFill } from 'icons';
4
+ import { CellFilterEntry, TranslationKey } from 'types';
5
+
6
+ export const getNextCellFilter = (
7
+ filter: CellFilterEntry | undefined,
8
+ ): {
9
+ Icon: FunctionComponent<SVGAttributes<SVGElement>>;
10
+ labelTranslationKey: TranslationKey;
11
+ } => {
12
+ if (filter?.type === 'exclude') {
13
+ return {
14
+ Icon: Eraser,
15
+ labelTranslationKey: 'common.clear',
16
+ };
17
+ }
18
+
19
+ if (filter?.type === 'include') {
20
+ return {
21
+ Icon: Ban,
22
+ labelTranslationKey: 'cell.filter-cell.exclude',
23
+ };
24
+ }
25
+
26
+ return {
27
+ Icon: FlagFill,
28
+ labelTranslationKey: 'cell.filter-cell.include',
29
+ };
30
+ };
@@ -7,16 +7,9 @@ import { Provider } from 'react-redux';
7
7
 
8
8
  import { useAppLayout, useMediaQueries } from 'hooks';
9
9
  import { LOCALE_FEATURES } from 'i18n';
10
- import { FlagFill, Star } from 'icons';
10
+ import { Star } from 'icons';
11
11
  import { dataUrlToBlob, getTileSizes } from 'lib';
12
- import {
13
- BORDER_COLOR_LIGHT,
14
- BORDER_RADIUS,
15
- BORDER_WIDTH,
16
- COLOR_BACKGROUND,
17
- COLOR_BONUS_START,
18
- COLOR_FILTERED,
19
- } from 'parameters';
12
+ import { BORDER_COLOR_LIGHT, BORDER_RADIUS, BORDER_WIDTH, COLOR_BACKGROUND, COLOR_BONUS_START } from 'parameters';
20
13
  import { selectConfig, selectLocale, selectShowCoordinates, store, useTypedSelector } from 'state';
21
14
  import { Point } from 'types';
22
15
 
@@ -30,7 +23,6 @@ const BONUS = 'b';
30
23
  const BONUS_WORD_2 = 'b2';
31
24
  const BONUS_WORD_3 = 'b3';
32
25
  const BONUS_WORD_4 = 'b4';
33
- const CELL_FILTER = 'c';
34
26
 
35
27
  const useBackgroundImage = () => {
36
28
  const { boardSize, cellSize, coordinatesSize } = useAppLayout();
@@ -131,19 +123,6 @@ const useBackgroundImage = () => {
131
123
  x4
132
124
  </text>
133
125
  </symbol>
134
-
135
- <symbol id={CELL_FILTER}>
136
- <rect
137
- fill={COLOR_FILTERED}
138
- height={bonusSize}
139
- rx={borderRadius}
140
- width={bonusSize}
141
- x={bonusOffset}
142
- y={bonusOffset}
143
- />
144
-
145
- <FlagFill color="white" height={iconSize} width={iconSize} x={iconOffset} y={iconOffset} />
146
- </symbol>
147
126
  </defs>
148
127
 
149
128
  {showCoordinates === 'hidden' && (
@@ -230,7 +209,7 @@ const useBackgroundImage = () => {
230
209
  </Provider>,
231
210
  );
232
211
 
233
- const encodedSvg = globalThis.btoa(backgroundSvg);
212
+ const encodedSvg = useMemo(() => globalThis.btoa(backgroundSvg), [backgroundSvg]);
234
213
  const dataUrl = `data:image/svg+xml;base64,${encodedSvg}`;
235
214
  const blob = useMemo(() => dataUrlToBlob(dataUrl), [dataUrl]);
236
215
  const blobUrl = useMemo(() => URL.createObjectURL(blob), [blob]);
@@ -25,6 +25,7 @@ $radio-box-size: $radio-size + 2 * $radio-inner-border;
25
25
 
26
26
  &.disabled {
27
27
  opacity: var(--opacity--disabled);
28
+ cursor: not-allowed;
28
29
  }
29
30
  }
30
31
 
@@ -35,7 +36,7 @@ $radio-box-size: $radio-size + 2 * $radio-inner-border;
35
36
  width: 100%;
36
37
  height: 100%;
37
38
  opacity: 0;
38
- cursor: pointer;
39
+ cursor: inherit;
39
40
  }
40
41
 
41
42
  .icon {
@@ -1,6 +1,7 @@
1
1
  import classNames from 'classnames';
2
2
  import { FunctionComponent, useEffect, useMemo, useState } from 'react';
3
3
  import { FixedSizeList } from 'react-window';
4
+ import { useDebounce } from 'use-debounce';
4
5
 
5
6
  import { useAppLayout, useLatest } from 'hooks';
6
7
  import { LOCALE_FEATURES } from 'i18n';
@@ -32,6 +33,8 @@ interface Props {
32
33
  highlightedIndex?: number;
33
34
  }
34
35
 
36
+ const IS_LOADING_DEBOUNCE = 100;
37
+
35
38
  const Results: FunctionComponent<Props> = ({ callbacks, className, highlightedIndex }) => {
36
39
  const translate = useTranslate();
37
40
  const { resultsHeight, resultsWidth } = useAppLayout();
@@ -39,6 +42,7 @@ const Results: FunctionComponent<Props> = ({ callbacks, className, highlightedIn
39
42
  const { direction } = LOCALE_FEATURES[locale];
40
43
  const results = useTypedSelector(selectResults);
41
44
  const isLoading = useTypedSelector(selectIsLoading);
45
+ const [isLoadingDebounced] = useDebounce(isLoading, IS_LOADING_DEBOUNCE);
42
46
  const isOutdated = useTypedSelector(selectAreResultsOutdated);
43
47
  const error = useTypedSelector(selectSolveError);
44
48
  const itemData = useMemo(() => ({ ...callbacks, highlightedIndex, results }), [callbacks, highlightedIndex, results]);
@@ -128,7 +132,7 @@ const Results: FunctionComponent<Props> = ({ callbacks, className, highlightedIn
128
132
 
129
133
  {showInput && <ResultsInput className={styles.input} />}
130
134
 
131
- {isLoading && <Loading />}
135
+ {isLoadingDebounced && <Loading />}
132
136
  </div>
133
137
  );
134
138
  };
@@ -13,8 +13,6 @@
13
13
  font-weight: bold;
14
14
  text-transform: uppercase;
15
15
  text-align: center;
16
- transition: var(--transition);
17
- transition-property: background-color, color, box-shadow;
18
16
  user-select: none;
19
17
 
20
18
  @include media('<xs') {
@@ -9,14 +9,12 @@ import {
9
9
  Ref,
10
10
  TouchEventHandler,
11
11
  useCallback,
12
- useEffect,
13
12
  useMemo,
14
13
  useRef,
15
14
  } from 'react';
16
15
 
17
16
  import { useAppLayout } from 'hooks';
18
17
  import { getTileSizes, noop } from 'lib';
19
- import { EASE_OUT_CUBIC, TILE_APPEAR_DURATION, TILE_APPEAR_KEYFRAMES } from 'parameters';
20
18
  import { selectLocale, useTypedSelector } from 'state';
21
19
 
22
20
  import TilePure from './TilePure';
@@ -65,7 +63,7 @@ const Tile: FunctionComponent<Props> = ({
65
63
  onTouchStart = noop,
66
64
  }) => {
67
65
  const locale = useTypedSelector(selectLocale);
68
- const { animateTile, showTilePoints } = useAppLayout();
66
+ const { showTilePoints } = useAppLayout();
69
67
  const { pointsFontSize, tileSize } = getTileSizes(size);
70
68
  const style = useMemo(() => ({ height: tileSize, width: tileSize }), [tileSize]);
71
69
  const pointsStyle = useMemo(() => ({ fontSize: pointsFontSize }), [pointsFontSize]);
@@ -83,18 +81,6 @@ const Tile: FunctionComponent<Props> = ({
83
81
  [onKeyDown],
84
82
  );
85
83
 
86
- useEffect(() => {
87
- if (!ref.current?.parentElement || !character || !animateTile) {
88
- return;
89
- }
90
-
91
- ref.current.parentElement.animate(TILE_APPEAR_KEYFRAMES, {
92
- duration: TILE_APPEAR_DURATION,
93
- easing: EASE_OUT_CUBIC,
94
- fill: 'forwards',
95
- });
96
- }, [character, animateTile]);
97
-
98
84
  return (
99
85
  <TilePure
100
86
  aria-label={ariaLabel}
@@ -7,6 +7,7 @@
7
7
  color: var(--color--tooltip--foreground);
8
8
  z-index: var(--z-index--tooltip);
9
9
  text-align: center;
10
+ pointer-events: none;
10
11
  }
11
12
 
12
13
  .arrow {
@@ -71,7 +71,6 @@ const useAppLayout = () => {
71
71
 
72
72
  return {
73
73
  actionsWidth: 2 * BUTTON_HEIGHT - BORDER_WIDTH,
74
- animateTile: !isLessThanXs,
75
74
  boardSize,
76
75
  cellSize,
77
76
  coordinatesFontSize: coordinatesSize * 0.6,
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "cell.enter-word": "Enter word",
3
- "cell.filter-cell": "Target destination",
3
+ "cell.filter-cell.exclude": "Exclude destination",
4
+ "cell.filter-cell.include": "Target destination",
4
5
  "cell.set-blank": "Mark it a blank",
5
6
  "cell.set-not-blank": "Mark it not a blank",
6
7
  "cell.tile.location": "Board: tile ({{x}}, {{y}})",
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "cell.enter-word": "Entrez un mot",
3
- "cell.filter-cell": "Destination cible",
3
+ "cell.filter-cell.exclude": "Exclure la destination",
4
+ "cell.filter-cell.include": "Destination cible",
4
5
  "cell.set-blank": "Marquer comme vide",
5
6
  "cell.set-not-blank": "Marquer comme non vide",
6
7
  "cell.tile.location": "Plateau: la case ({{x}}, {{y}})",
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "cell.enter-word": "Wort eingeben",
3
- "cell.filter-cell": "Zielort",
3
+ "cell.filter-cell.exclude": "Ziel ausschließen",
4
+ "cell.filter-cell.include": "Ziel anvisieren",
4
5
  "cell.set-blank": "Als Blanko markieren",
5
6
  "cell.set-not-blank": "Nicht als Blanko markieren",
6
7
  "cell.tile.location": "Brett: Stein ({{x}}, {{y}})",
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "cell.enter-word": "کلمه را وارد کنید",
3
- "cell.filter-cell": "مقصد",
3
+ "cell.filter-cell.exclude": "مقصد را حذف کنید",
4
+ "cell.filter-cell.include": "مقصد مورد نظر",
4
5
  "cell.set-blank": "علامت گذاری به عنوان خالی",
5
6
  "cell.set-not-blank": "علامت گذاری به عنوان غیر خالی",
6
7
  "cell.tile.location": "({{x}}، {{y}}) کاشی: صفحه",
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "cell.enter-word": "Wprowadź słowo",
3
- "cell.filter-cell": "Miejsce docelowe",
3
+ "cell.filter-cell.exclude": "Wyklucz miejsce",
4
+ "cell.filter-cell.include": "Miejsce docelowe",
4
5
  "cell.set-blank": "Oznacz jako blank",
5
6
  "cell.set-not-blank": "Oznacz jako nie blank",
6
7
  "cell.tile.location": "Plansza: płytka ({{x}}, {{y}})",
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "cell.enter-word": "Introducere cuvant",
3
- "cell.filter-cell": "Destinatia",
3
+ "cell.filter-cell.exclude": "Excludeți destinația",
4
+ "cell.filter-cell.include": "Destinație țintă",
4
5
  "cell.set-blank": "Marcare camp liber",
5
6
  "cell.set-not-blank": "Marcare camp ocupat ",
6
7
  "cell.tile.location": "Tabla: camp ({{x}}, {{y}})",
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "cell.enter-word": "Ingresar palabra",
3
- "cell.filter-cell": "Destino objetivo",
3
+ "cell.filter-cell.exclude": "Excluir destino",
4
+ "cell.filter-cell.include": "Destino objetivo",
4
5
  "cell.set-blank": "Marcar como en blanco",
5
6
  "cell.set-not-blank": "Marcar como no en blanco",
6
7
  "cell.tile.location": "Tablero: espacio ({{x}}, {{y}})",
@@ -0,0 +1,4 @@
1
+ <!-- https://icons.getbootstrap.com/icons/ban/ -->
2
+ <svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
3
+ <path d="M15 8a6.97 6.97 0 0 0-1.71-4.584l-9.874 9.875A7 7 0 0 0 15 8M2.71 12.584l9.874-9.875a7 7 0 0 0-9.874 9.874ZM16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0" fill="currentColor" />
4
+ </svg>
@@ -2,6 +2,7 @@ export { default as ArrowDown } from './ArrowDown.svg';
2
2
  export { default as ArrowLeft } from './ArrowLeft.svg';
3
3
  export { default as ArrowRight } from './ArrowRight.svg';
4
4
  export { default as ArrowUp } from './ArrowUp.svg';
5
+ export { default as Ban } from './Ban.svg';
5
6
  export { default as BookHalf } from './BookHalf.svg';
6
7
  export { default as CardChecklist } from './CardChecklist.svg';
7
8
  export { default as Check } from './Check.svg';
@@ -1,18 +1,14 @@
1
1
  import { Result } from '@scrabble-solver/types';
2
2
 
3
- import { Point } from 'types';
3
+ import { CellFilterEntry, GroupedResults } from 'types';
4
4
 
5
5
  import createRegExp from './createRegExp';
6
-
7
- interface GroupedResults {
8
- matching: Result[];
9
- other: Result[];
10
- }
6
+ import resultMatchesCellFilter from './resultMatchesCellFilter';
11
7
 
12
8
  const groupResults = (
13
9
  results: Result[] | undefined,
14
10
  query: string,
15
- cellFilter: Point[],
11
+ cellFilter: CellFilterEntry[],
16
12
  ): GroupedResults | undefined => {
17
13
  if (typeof results === 'undefined') {
18
14
  return results;
@@ -20,15 +16,11 @@ const groupResults = (
20
16
 
21
17
  const regExp = createRegExp(query);
22
18
 
23
- return results.reduce<GroupedResults>(
19
+ const { matching, other } = results.reduce<GroupedResults>(
24
20
  (groupedResults, result) => {
25
21
  const matchesQuery = () => regExp.test(result.word);
26
- const matchesCellFilter = () =>
27
- cellFilter.every(({ x, y }) => {
28
- return result.cells.some((cell) => cell.x === x && cell.y === y);
29
- });
30
22
 
31
- if (matchesCellFilter() && matchesQuery()) {
23
+ if (resultMatchesCellFilter(result, cellFilter) && matchesQuery()) {
32
24
  groupedResults.matching.push(result);
33
25
  } else {
34
26
  groupedResults.other.push(result);
@@ -38,6 +30,8 @@ const groupResults = (
38
30
  },
39
31
  { matching: [], other: [] },
40
32
  );
33
+
34
+ return { matching, other };
41
35
  };
42
36
 
43
37
  export default groupResults;
package/src/lib/index.ts CHANGED
@@ -32,7 +32,9 @@ export { default as isUpperCase } from './isUpperCase';
32
32
  export { default as memoize } from './memoize';
33
33
  export { default as noop } from './noop';
34
34
  export { default as numberComparator } from './numberComparator';
35
+ export { default as resultMatchesCellFilter } from './resultMatchesCellFilter';
35
36
  export { default as reverseComparator } from './reverseComparator';
37
+ export { default as sortGroupedResults } from './sortGroupedResults';
36
38
  export { default as sortResults } from './sortResults';
37
39
  export { default as unorderedArraysEqual } from './unorderedArraysEqual';
38
40
  export { default as zipCharactersAndTiles } from './zipCharactersAndTiles';
@@ -0,0 +1,23 @@
1
+ import { Result } from '@scrabble-solver/types';
2
+
3
+ import { CellFilterEntry } from 'types';
4
+
5
+ const resultMatchesCellFilter = (result: Result, cellFilter: CellFilterEntry[]) => {
6
+ const excludeFilters = cellFilter.filter((filter) => filter.type === 'exclude');
7
+ const matchesExcludeFilters = excludeFilters.every(({ x, y }) => {
8
+ return result.cells.every((cell) => cell.x !== x || cell.y !== y);
9
+ });
10
+
11
+ if (!matchesExcludeFilters) {
12
+ return false;
13
+ }
14
+
15
+ const includeFilter = cellFilter.filter((filter) => filter.type === 'include');
16
+ const matchesIncludeFilters = includeFilter.every(({ x, y }) => {
17
+ return result.cells.some((cell) => cell.x === x && cell.y === y);
18
+ });
19
+
20
+ return matchesExcludeFilters && matchesIncludeFilters;
21
+ };
22
+
23
+ export default resultMatchesCellFilter;
@@ -0,0 +1,22 @@
1
+ import { Locale } from '@scrabble-solver/types';
2
+
3
+ import { GroupedResults, Sort } from 'types';
4
+
5
+ import sortResults from './sortResults';
6
+
7
+ const sortGroupedResults = (
8
+ results: GroupedResults | undefined,
9
+ sort: Sort,
10
+ locale: Locale,
11
+ ): GroupedResults | undefined => {
12
+ if (typeof results === 'undefined') {
13
+ return results;
14
+ }
15
+
16
+ return {
17
+ matching: sortResults(results.matching, sort, locale) ?? [],
18
+ other: sortResults(results.other, sort, locale) ?? [],
19
+ };
20
+ };
21
+
22
+ export default sortGroupedResults;
@@ -6,7 +6,6 @@ export const BREAKPOINTS = {
6
6
  xl: 1400,
7
7
  };
8
8
 
9
- export const EASE_OUT_CUBIC = 'cubic-bezier(0.33, 1, 0.68, 1)'; // https://easings.net/#easeOutCubic
10
9
  export const TRANSITION = 'var(--transition)';
11
10
 
12
11
  export const GITHUB_PROJECT_URL = 'https://github.com/kamilmielnik/scrabble-solver';
@@ -128,11 +127,3 @@ export const RESULTS_HEADER_HEIGHT = RESULTS_ITEM_HEIGHT;
128
127
  export const SOLVER_COLUMN_WIDTH = 580;
129
128
 
130
129
  export const TEXT_INPUT_HEIGHT = 40;
131
-
132
- export const TILE_APPEAR_DURATION = 200;
133
-
134
- export const TILE_APPEAR_KEYFRAMES = [
135
- { transform: 'translateY(0)' },
136
- { transform: 'translateY(10%)', offset: 0.5 },
137
- { transform: 'translateY(0)' },
138
- ];
@@ -12,7 +12,7 @@ import { findWordDefinitions, solve, verify, visit } from 'sdk';
12
12
  import { initialize, reset } from './actions';
13
13
  import {
14
14
  selectBoard,
15
- selectCellIsFiltered,
15
+ selectCellFilter,
16
16
  selectCharacters,
17
17
  selectConfig,
18
18
  selectDictionary,
@@ -56,10 +56,10 @@ export function* rootSaga(): AnyGenerator {
56
56
  }
57
57
 
58
58
  function* onCellValueChange({ payload }: PayloadAction<{ value: string; x: number; y: number }>): AnyGenerator {
59
- const isFiltered = yield select((state) => selectCellIsFiltered(state, payload));
59
+ const filter = yield select((state) => selectCellFilter(state, payload));
60
60
 
61
- if (isFiltered) {
62
- yield put(cellFilterSlice.actions.toggle(payload));
61
+ if (filter) {
62
+ yield put(cellFilterSlice.actions.cancel(payload));
63
63
  }
64
64
 
65
65
  yield put(resultsSlice.actions.changeResultCandidate(null));