@scrabble-solver/scrabble-solver 2.11.1 → 2.11.3

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 (127) 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/next-server.js.nft.json +1 -1
  6. package/.next/cache/webpack/client-production/0.pack +0 -0
  7. package/.next/cache/webpack/client-production/index.pack +0 -0
  8. package/.next/cache/webpack/edge-server-production/0.pack +0 -0
  9. package/.next/cache/webpack/edge-server-production/index.pack +0 -0
  10. package/.next/cache/webpack/server-production/0.pack +0 -0
  11. package/.next/cache/webpack/server-production/index.pack +0 -0
  12. package/.next/next-server.js.nft.json +1 -1
  13. package/.next/prerender-manifest.json +1 -1
  14. package/.next/routes-manifest.json +1 -1
  15. package/.next/server/chunks/210.js +109 -0
  16. package/.next/server/chunks/277.js +380 -258
  17. package/.next/server/chunks/44.js +47 -0
  18. package/.next/server/chunks/987.js +91 -0
  19. package/.next/server/middleware-build-manifest.js +1 -1
  20. package/.next/server/pages/404.html +2 -2
  21. package/.next/server/pages/404.js.nft.json +1 -1
  22. package/.next/server/pages/500.html +1 -1
  23. package/.next/server/pages/_app.js +1 -73
  24. package/.next/server/pages/_app.js.nft.json +1 -1
  25. package/.next/server/pages/_document.js.nft.json +1 -1
  26. package/.next/server/pages/api/dictionary/[locale]/[word].js +3 -4
  27. package/.next/server/pages/api/dictionary/[locale]/[word].js.nft.json +1 -1
  28. package/.next/server/pages/api/dictionary/[locale].js +3 -4
  29. package/.next/server/pages/api/dictionary/[locale].js.nft.json +1 -1
  30. package/.next/server/pages/api/solve.js +38 -13
  31. package/.next/server/pages/api/solve.js.nft.json +1 -1
  32. package/.next/server/pages/api/verify.js +5 -5
  33. package/.next/server/pages/api/verify.js.nft.json +1 -1
  34. package/.next/server/pages/api/visit.js +3 -4
  35. package/.next/server/pages/api/visit.js.nft.json +1 -1
  36. package/.next/server/pages/index.html +1 -1
  37. package/.next/server/pages/index.js +141 -237
  38. package/.next/server/pages/index.js.nft.json +1 -1
  39. package/.next/server/pages/index.json +1 -1
  40. package/.next/static/{esK8DG-6aS5V7QFRtR3YE → USLkKOoHbITebIEHkMGX_}/_buildManifest.js +1 -1
  41. package/.next/static/chunks/pages/_app-21c83ddb81fc09d0.js +28 -0
  42. package/.next/static/chunks/pages/index-0858deea02b2a417.js +1 -0
  43. package/.next/static/css/885da289cec275b3.css +1 -0
  44. package/.next/static/css/ea1c8134fe9a143e.css +2 -0
  45. package/.next/trace +53 -53
  46. package/package.json +9 -9
  47. package/src/api/index.ts +1 -0
  48. package/src/api/isCellValid.ts +3 -2
  49. package/src/api/isCharacterValid.ts +13 -0
  50. package/src/components/Board/components/Cell/Cell.module.scss +10 -2
  51. package/src/components/Board/hooks/useGrid.ts +1 -2
  52. package/src/components/Board/lib/getPositionInGrid.ts +1 -1
  53. package/src/components/Button/Button.module.scss +14 -1
  54. package/src/components/LogoSplashScreen/LogoSplashScreen.module.scss +4 -1
  55. package/src/components/Modal/Modal.module.scss +21 -1
  56. package/src/components/Modal/Modal.tsx +4 -1
  57. package/src/components/NotFound/NotFound.module.scss +13 -4
  58. package/src/components/NotFound/NotFound.tsx +4 -7
  59. package/src/components/Rack/Rack.tsx +3 -1
  60. package/src/components/Results/HeaderButton.tsx +5 -12
  61. package/src/components/Results/Result.tsx +5 -3
  62. package/src/components/Results/Results.module.scss +13 -1
  63. package/src/components/Results/Results.tsx +29 -43
  64. package/src/components/Results/types.ts +1 -1
  65. package/src/components/Solver/Solver.module.scss +4 -0
  66. package/src/components/Solver/Solver.tsx +9 -12
  67. package/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.tsx +5 -6
  68. package/src/components/Tile/Tile.module.scss +53 -79
  69. package/src/components/Tile/Tile.tsx +1 -3
  70. package/src/components/Tile/TilePure.tsx +4 -14
  71. package/src/hooks/useAppLayout.ts +3 -1
  72. package/src/i18n/constants.ts +65 -0
  73. package/src/i18n/de.json +1 -1
  74. package/src/i18n/en.json +1 -1
  75. package/src/i18n/es.json +1 -1
  76. package/src/i18n/fa.json +1 -1
  77. package/src/i18n/fr.json +1 -1
  78. package/src/i18n/i18n.module.scss +27 -0
  79. package/src/i18n/pl.json +1 -1
  80. package/src/icons/DashCircleFill.svg +1 -0
  81. package/src/icons/EyeFill.svg +5 -0
  82. package/src/icons/index.ts +3 -2
  83. package/src/lib/createRegExp.ts +13 -0
  84. package/src/lib/groupResults.ts +38 -0
  85. package/src/lib/guessLocale.ts +22 -0
  86. package/src/lib/index.ts +4 -1
  87. package/src/lib/sortResults.ts +6 -10
  88. package/src/modals/DictionaryModal/DictionaryModal.module.scss +0 -1
  89. package/src/modals/MenuModal/MenuModal.module.scss +23 -0
  90. package/src/modals/MenuModal/MenuModal.tsx +8 -2
  91. package/src/modals/RemainingTilesModal/RemainingTilesModal.tsx +4 -1
  92. package/src/modals/RemainingTilesModal/components/Character/Character.module.scss +8 -0
  93. package/src/modals/ResultsModal/ResultsModal.module.scss +2 -3
  94. package/src/modals/ResultsModal/ResultsModal.tsx +47 -11
  95. package/src/modals/SettingsModal/components/LocaleSetting/LocaleSetting.module.scss +3 -44
  96. package/src/modals/SettingsModal/components/LocaleSetting/LocaleSetting.tsx +4 -2
  97. package/src/pages/api/dictionary/[locale]/[word].ts +0 -1
  98. package/src/pages/api/dictionary/[locale]/index.ts +0 -1
  99. package/src/pages/api/solve.ts +3 -3
  100. package/src/pages/api/verify.ts +0 -1
  101. package/src/pages/api/visit.ts +0 -1
  102. package/src/pages/index.tsx +13 -15
  103. package/src/parameters/index.ts +2 -2
  104. package/src/state/sagas.ts +1 -0
  105. package/src/state/selectors.ts +37 -37
  106. package/src/state/slices/boardInitialState.ts +3 -1
  107. package/src/state/slices/cellFilterInitialState.ts +3 -3
  108. package/src/state/slices/cellFilterSlice.ts +3 -1
  109. package/src/state/slices/dictionaryInitialState.ts +2 -2
  110. package/src/state/slices/rackInitialState.ts +3 -1
  111. package/src/state/slices/resultsInitialState.ts +11 -4
  112. package/src/state/slices/settingsInitialState.ts +10 -21
  113. package/src/state/slices/solveInitialState.ts +2 -2
  114. package/src/state/slices/solveSlice.ts +2 -0
  115. package/src/state/slices/verifyInitialState.ts +13 -4
  116. package/src/styles/mixins.scss +5 -1
  117. package/src/styles/variables.scss +13 -0
  118. package/src/types/index.ts +11 -2
  119. package/.next/server/chunks/417.js +0 -221
  120. package/.next/server/chunks/664.js +0 -621
  121. package/.next/static/chunks/pages/_app-270526803bc274eb.js +0 -28
  122. package/.next/static/chunks/pages/index-c6e7754ccf3532df.js +0 -1
  123. package/.next/static/css/ad39b36eab07e613.css +0 -1
  124. package/.next/static/css/e5803e581e4c0451.css +0 -2
  125. package/src/components/Board/types/index.ts +0 -4
  126. package/src/modals/SettingsModal/components/LocaleSetting/options.ts +0 -68
  127. /package/.next/static/{esK8DG-6aS5V7QFRtR3YE → USLkKOoHbITebIEHkMGX_}/_ssgManifest.js +0 -0
@@ -11,9 +11,8 @@ import {
11
11
  selectAreResultsOutdated,
12
12
  selectConfig,
13
13
  selectResultCandidate,
14
+ selectResults,
14
15
  selectSolveError,
15
- selectSortedFilteredResults,
16
- selectSortedResults,
17
16
  solveSlice,
18
17
  useTranslate,
19
18
  useTypedSelector,
@@ -41,18 +40,16 @@ const Solver: FunctionComponent<Props> = ({ className, height, width, onShowResu
41
40
  const dispatch = useDispatch();
42
41
  const translate = useTranslate();
43
42
  const isTouchDevice = useIsTouchDevice();
44
- const { componentsSpacing, isBoardFullWidth, showColumn, showCompactControls, showFloatingSolveButton } =
43
+ const { columnWidth, componentsSpacing, isBoardFullWidth, showColumn, showCompactControls, showFloatingSolveButton } =
45
44
  useAppLayout();
46
- const [bottomContainerRef, { height: bottomContainerHeight }] = useMeasure<HTMLDivElement>();
47
- const [columnRef, { width: columnWidth }] = useMeasure<HTMLDivElement>();
48
- const maxBoardWidth = width - columnWidth - (showColumn ? componentsSpacing : 0) - 2 * componentsSpacing;
49
- const maxBoardHeight = isBoardFullWidth ? Number.POSITIVE_INFINITY : Math.max(height - bottomContainerHeight, 0);
50
45
  const config = useTypedSelector(selectConfig);
51
46
  const error = useTypedSelector(selectSolveError);
52
47
  const isOutdated = useTypedSelector(selectAreResultsOutdated);
53
48
  const resultCandidate = useTypedSelector(selectResultCandidate);
54
- const allResults = useTypedSelector(selectSortedResults);
55
- const results = useTypedSelector(selectSortedFilteredResults);
49
+ const results = useTypedSelector(selectResults);
50
+ const [bottomContainerRef, { height: bottomContainerHeight }] = useMeasure<HTMLDivElement>();
51
+ const maxBoardWidth = width - columnWidth - (showColumn ? componentsSpacing : 0) - 2 * componentsSpacing;
52
+ const maxBoardHeight = isBoardFullWidth ? Number.POSITIVE_INFINITY : Math.max(height - bottomContainerHeight, 0);
56
53
  const [bestResult] = results || [];
57
54
  const cellWidth = (maxBoardWidth - (config.boardWidth + 1) * BORDER_WIDTH) / config.boardWidth;
58
55
  const cellHeight = (maxBoardHeight - (config.boardHeight + 1) * BORDER_WIDTH) / config.boardHeight;
@@ -117,8 +114,8 @@ const Solver: FunctionComponent<Props> = ({ className, height, width, onShowResu
117
114
  <input className={styles.submitInput} tabIndex={-1} type="submit" />
118
115
  </form>
119
116
 
120
- <div className={styles.column} ref={columnRef}>
121
- <Results callbacks={callbacks} />
117
+ <div className={styles.column}>
118
+ <Results callbacks={callbacks} className={styles.results} />
122
119
 
123
120
  <div className={styles.dictionaryContainer}>
124
121
  <Dictionary className={styles.dictionary} />
@@ -145,7 +142,7 @@ const Solver: FunctionComponent<Props> = ({ className, height, width, onShowResu
145
142
  </Alert>
146
143
  )}
147
144
 
148
- {allResults && allResults.length === 0 && !isOutdated && (
145
+ {results && results.length === 0 && !isOutdated && (
149
146
  <Alert className={styles.emptyState} variant="warning">
150
147
  {translate('results.empty-state.no-results')}
151
148
  </Alert>
@@ -10,7 +10,7 @@ import {
10
10
  selectIsLoading,
11
11
  selectLocale,
12
12
  selectResultCandidate,
13
- selectSortedResults,
13
+ selectResults,
14
14
  useTranslate,
15
15
  useTypedSelector,
16
16
  } from 'state';
@@ -31,13 +31,12 @@ const ResultCandidatePicker: FunctionComponent<Props> = ({ className, onResultCl
31
31
  const locale = useTypedSelector(selectLocale);
32
32
  const isLoading = useTypedSelector(selectIsLoading);
33
33
  const isOutdated = useTypedSelector(selectAreResultsOutdated);
34
- const sortedResults = useTypedSelector(selectSortedResults);
35
- const results = sortedResults || [];
34
+ const results = useTypedSelector(selectResults);
36
35
  const resultCandidate = useTypedSelector(selectResultCandidate);
37
- const index = resultCandidate ? results.findIndex((result) => result.id === resultCandidate.id) : -1;
36
+ const index = resultCandidate && results ? results.findIndex((result) => result.id === resultCandidate.id) : -1;
38
37
  const disabled = isOutdated || !resultCandidate;
39
- const isPreviousDisabled = index <= 0 || disabled;
40
- const isNextDisabled = index >= results.length - 1 || disabled;
38
+ const isPreviousDisabled = !results || index <= 0 || disabled;
39
+ const isNextDisabled = !results || index >= results.length - 1 || disabled;
41
40
  const bothEnabled = !isPreviousDisabled && !isNextDisabled;
42
41
  const { showFloatingSolveButton } = useAppLayout();
43
42
 
@@ -4,8 +4,17 @@
4
4
  --background-color: transparent;
5
5
 
6
6
  position: relative;
7
- transition: var(--transition);
7
+ display: flex;
8
+ align-items: center;
9
+ justify-content: center;
10
+ padding: 0;
11
+ background-color: var(--background-color);
8
12
  border-radius: var(--border--radius);
13
+ font-weight: bold;
14
+ text-transform: uppercase;
15
+ text-align: center;
16
+ transition: var(--transition);
17
+ user-select: none;
9
18
 
10
19
  &.points1 {
11
20
  --background-color: var(--color--yellow);
@@ -24,13 +33,29 @@
24
33
  --background-color: var(--color--red);
25
34
  }
26
35
 
36
+ &.raised {
37
+ box-shadow: var(--box-shadow--raised);
38
+
39
+ @include media('<xs') {
40
+ box-shadow: var(--box-shadow--raised--subtle);
41
+ }
42
+ }
43
+
27
44
  &.blank {
28
45
  --background-color: var(--color--white);
29
46
 
30
- .placeholder,
31
- .character {
32
- color: var(--color--foreground);
33
- }
47
+ color: var(--color--foreground);
48
+ }
49
+
50
+ &.empty {
51
+ color: var(--color--inactive);
52
+ }
53
+
54
+ &.invalid {
55
+ --background-color: var(--color--foreground);
56
+ --shadow--color: var(--box-shadow--color--inverse);
57
+
58
+ color: var(--color--white);
34
59
  }
35
60
 
36
61
  &.highlighted {
@@ -38,85 +63,28 @@
38
63
 
39
64
  color: var(--color--primary--opposite);
40
65
 
41
- .placeholder,
42
- .character {
43
- color: inherit;
44
- }
45
-
46
66
  .points {
47
67
  color: inherit;
48
68
  }
49
69
  }
50
-
51
- &:not(.disabled) {
52
- .input::selection {
53
- --background--color: transparent;
54
- }
55
- }
56
- }
57
-
58
- .input,
59
- .character,
60
- .placeholder {
61
- padding: 0;
62
- font-weight: bold;
63
- text-transform: uppercase;
64
- text-align: center;
65
- caret-color: transparent;
66
- box-sizing: border-box;
67
70
  }
68
71
 
69
72
  .input {
70
- width: 100%;
71
- height: 100%;
72
- background-color: transparent;
73
- color: transparent;
74
- border: none;
75
- font-size: 16px; // prevent iOS from automatically zooming in on focus
76
- }
77
-
78
- .character,
79
- .placeholder {
80
73
  position: absolute;
81
74
  top: 0;
82
75
  right: 0;
83
76
  bottom: 0;
84
77
  left: 0;
85
- display: flex;
86
- align-items: center;
87
- justify-content: center;
88
- background-color: var(--background-color);
89
- border-radius: inherit;
90
- transition: var(--transition);
91
- pointer-events: none;
92
- user-select: none;
93
-
94
- .empty & {
95
- color: var(--color--inactive);
96
- }
97
-
98
- .raised & {
99
- --shadow--size: 2px;
100
- --shadow--blur: 2px;
101
- --shadow--spread: -1px;
102
- --shadow--color: rgba(34, 34, 34, 0.8);
103
-
104
- box-shadow: inset calc(-1 * var(--shadow--size)) calc(-1 * var(--shadow--size)) var(--shadow--blur)
105
- var(--shadow--spread) var(--shadow--color);
106
-
107
- @include media('<xs') {
108
- --shadow--size: 1px;
109
- --shadow--spread: 0;
110
- --shadow--blur: 1px;
111
- }
112
- }
113
- }
114
-
115
- .character {
116
- opacity: 1;
78
+ width: 100%;
79
+ height: 100%;
80
+ border: none;
81
+ background-color: transparent;
82
+ color: transparent;
83
+ caret-color: transparent;
84
+ font-size: 16px; // prevent iOS from automatically zooming in on focus
117
85
 
118
- .empty & {
119
- opacity: 0;
86
+ &::selection {
87
+ --background--color: transparent;
120
88
  }
121
89
  }
122
90
 
@@ -145,23 +113,29 @@
145
113
  }
146
114
 
147
115
  .alert {
148
- $size: 30%;
116
+ --size: 30%;
149
117
 
150
118
  position: absolute;
151
- width: $size;
152
- height: $size;
153
- background-color: var(--color--error--opposite);
119
+ width: var(--size);
120
+ height: var(--size);
121
+ background: radial-gradient(
122
+ var(--color--error--opposite),
123
+ var(--color--error--opposite) 85%,
124
+ transparent 85%,
125
+ transparent
126
+ );
154
127
  color: var(--color--error);
128
+ pointer-events: none;
155
129
 
156
130
  [dir='ltr'] & {
157
131
  top: 0;
158
132
  right: 0;
159
- border-top-right-radius: inherit;
133
+ border-bottom-right-radius: inherit;
160
134
  }
161
135
 
162
136
  [dir='rtl'] & {
163
- bottom: 0;
164
- right: 0;
165
- border-bottom-right-radius: inherit;
137
+ top: 0;
138
+ left: 0;
139
+ border-top-left-radius: inherit;
166
140
  }
167
141
  }
@@ -59,9 +59,8 @@ const Tile: FunctionComponent<Props> = ({
59
59
  }) => {
60
60
  const locale = useTypedSelector(selectLocale);
61
61
  const { animateTile, showTilePoints } = useAppLayout();
62
- const { pointsFontSize, tileFontSize, tileSize } = getTileSizes(size);
62
+ const { pointsFontSize, tileSize } = getTileSizes(size);
63
63
  const style = useMemo(() => ({ height: tileSize, width: tileSize }), [tileSize]);
64
- const characterStyle = useMemo(() => ({ fontSize: tileFontSize }), [tileFontSize]);
65
64
  const pointsStyle = useMemo(() => ({ fontSize: pointsFontSize }), [pointsFontSize]);
66
65
  const ref = useRef<HTMLInputElement>(null);
67
66
  const mergedRef = useMergeRefs(inputRef ? [ref, inputRef] : [ref]);
@@ -98,7 +97,6 @@ const Tile: FunctionComponent<Props> = ({
98
97
  autoFocus={autoFocus}
99
98
  canShowPoints={canShowPoints}
100
99
  character={character}
101
- characterStyle={characterStyle}
102
100
  className={className}
103
101
  disabled={disabled}
104
102
  highlighted={highlighted}
@@ -18,7 +18,6 @@ interface Props {
18
18
  autoFocus?: boolean;
19
19
  canShowPoints?: boolean;
20
20
  character?: string;
21
- characterStyle?: CSSProperties;
22
21
  className?: string;
23
22
  disabled?: boolean;
24
23
  highlighted?: boolean;
@@ -42,7 +41,6 @@ const TilePure: FunctionComponent<Props> = ({
42
41
  autoFocus,
43
42
  canShowPoints,
44
43
  character,
45
- characterStyle,
46
44
  className,
47
45
  disabled,
48
46
  highlighted,
@@ -63,18 +61,20 @@ const TilePure: FunctionComponent<Props> = ({
63
61
  <div
64
62
  className={classNames(styles.tile, className, {
65
63
  [styles.blank]: isBlank,
66
- [styles.disabled]: disabled,
67
64
  [styles.empty]: !character,
65
+ [styles.invalid]: !isValid,
68
66
  [styles.highlighted]: highlighted,
69
- [styles.raised]: raised,
70
67
  [styles.points1]: points === 1,
71
68
  [styles.points2]: points === 2,
72
69
  [styles.points3]: points === 3,
73
70
  [styles.points4]: points === 4,
74
71
  [styles.points5]: typeof points === 'number' && points >= 5,
72
+ [styles.raised]: raised,
75
73
  })}
76
74
  style={style}
77
75
  >
76
+ {character || placeholder}
77
+
78
78
  <input
79
79
  aria-label={ariaLabel}
80
80
  autoCapitalize="none"
@@ -92,16 +92,6 @@ const TilePure: FunctionComponent<Props> = ({
92
92
  onKeyDown={onKeyDown}
93
93
  />
94
94
 
95
- {placeholder && (
96
- <div className={styles.placeholder} style={characterStyle} tabIndex={-1}>
97
- {placeholder}
98
- </div>
99
- )}
100
-
101
- <div className={styles.character} style={characterStyle} tabIndex={-1}>
102
- {character}
103
- </div>
104
-
105
95
  {canShowPoints && (
106
96
  <span className={styles.points} style={pointsStyle}>
107
97
  {pointsFormatted}
@@ -1,4 +1,4 @@
1
- import { COMPONENTS_SPACING, COMPONENTS_SPACING_SMALL } from 'parameters';
1
+ import { COMPONENTS_SPACING, COMPONENTS_SPACING_SMALL, SOLVER_COLUMN_WIDTH } from 'parameters';
2
2
 
3
3
  import useIsTouchDevice from './useIsTouchDevice';
4
4
  import useMediaQuery from './useMediaQuery';
@@ -14,8 +14,10 @@ const useAppLayout = () => {
14
14
 
15
15
  return {
16
16
  animateTile: !isLessThanXs,
17
+ columnWidth: showColumn ? SOLVER_COLUMN_WIDTH : 0,
17
18
  componentsSpacing: isLessThanXl ? COMPONENTS_SPACING_SMALL : COMPONENTS_SPACING,
18
19
  isBoardFullWidth: isLessThanM,
20
+ isModalFullWidth: isLessThanS,
19
21
  showColumn,
20
22
  showCompactControls: !showColumn,
21
23
  showFloatingSolveButton: isTouchDevice,
@@ -1,4 +1,9 @@
1
1
  import { Locale } from '@scrabble-solver/types';
2
+ import { FunctionComponent, SVGAttributes } from 'react';
3
+
4
+ import { FlagDe, FlagEs, FlagFa, FlagFr, FlagGb, FlagPl, FlagUs } from 'icons';
5
+
6
+ import styles from './i18n.module.scss';
2
7
 
3
8
  interface LocaleFeatures {
4
9
  direction: 'ltr' | 'rtl';
@@ -51,3 +56,63 @@ export const LOCALE_FEATURES: Record<Locale, LocaleFeatures> = {
51
56
  vowels: true,
52
57
  },
53
58
  };
59
+
60
+ interface Flag {
61
+ className: string;
62
+ Icon: FunctionComponent<SVGAttributes<SVGElement>>;
63
+ label: string;
64
+ name: string;
65
+ value: Locale;
66
+ }
67
+
68
+ export const LOCALE_FLAGS: Record<Locale, Flag> = {
69
+ [Locale.EN_GB]: {
70
+ className: styles.gb,
71
+ Icon: FlagGb,
72
+ label: 'English (GB)',
73
+ name: 'English (GB)',
74
+ value: Locale.EN_GB,
75
+ },
76
+ [Locale.EN_US]: {
77
+ className: styles.us,
78
+ Icon: FlagUs,
79
+ label: 'English (US)',
80
+ name: 'English (US)',
81
+ value: Locale.EN_US,
82
+ },
83
+ [Locale.FA_IR]: {
84
+ className: styles.fa,
85
+ Icon: FlagFa,
86
+ label: 'فارسی',
87
+ name: 'Persian',
88
+ value: Locale.FA_IR,
89
+ },
90
+ [Locale.FR_FR]: {
91
+ className: styles.fr,
92
+ Icon: FlagFr,
93
+ label: 'Français',
94
+ name: 'French',
95
+ value: Locale.FR_FR,
96
+ },
97
+ [Locale.DE_DE]: {
98
+ className: styles.de,
99
+ Icon: FlagDe,
100
+ label: 'Deutsch',
101
+ name: 'German',
102
+ value: Locale.DE_DE,
103
+ },
104
+ [Locale.PL_PL]: {
105
+ className: styles.pl,
106
+ Icon: FlagPl,
107
+ label: 'Polski',
108
+ name: 'Polish',
109
+ value: Locale.PL_PL,
110
+ },
111
+ [Locale.ES_ES]: {
112
+ className: styles.es,
113
+ Icon: FlagEs,
114
+ label: 'Español',
115
+ name: 'Spanish',
116
+ value: Locale.ES_ES,
117
+ },
118
+ };
package/src/i18n/de.json CHANGED
@@ -45,12 +45,12 @@
45
45
  "rack.tile.location": "Ablage: Stein ({{index}})",
46
46
  "remaining-tiles": "Restliche Steine",
47
47
  "results": "Ergebnisse",
48
- "results.empty-state.no-filtered-results": "Keine Ergebnisse für diese Anfrage.",
49
48
  "results.empty-state.no-results": "Keine Ergebnisse - kein Wort konnte generiert werden.",
50
49
  "results.empty-state.outdated": "Ergebnisse sind alt. Klicken zum Aktualisieren.",
51
50
  "results.empty-state.uninitialized": "Wörter die aus deinen Buchstaben generiert wurden erscheinen hier.",
52
51
  "results.input.placeholder": "Suchergebnisse... (RegExp)",
53
52
  "results.insert": "Hinzufügen",
53
+ "results.preview": "Vorschau",
54
54
  "results.solve": "Lösen",
55
55
  "settings": "Einstellungen",
56
56
  "settings.autoGroupTiles": "Restliche Steine gruppieren",
package/src/i18n/en.json CHANGED
@@ -45,12 +45,12 @@
45
45
  "rack.tile.location": "Rack: tile ({{index}})",
46
46
  "remaining-tiles": "Remaining tiles",
47
47
  "results": "Results",
48
- "results.empty-state.no-filtered-results": "No result matches this query.",
49
48
  "results.empty-state.no-results": "No results - unable to generate any words.",
50
49
  "results.empty-state.outdated": "Results are outdated. Click below to update.",
51
50
  "results.empty-state.uninitialized": "Words generated from your letters will be shown here.",
52
51
  "results.input.placeholder": "Search results... (RegExp)",
53
52
  "results.insert": "Insert",
53
+ "results.preview": "Preview",
54
54
  "results.solve": "Solve",
55
55
  "settings": "Settings",
56
56
  "settings.autoGroupTiles": "Group remaining tiles",
package/src/i18n/es.json CHANGED
@@ -45,12 +45,12 @@
45
45
  "rack.tile.location": "Estante: espacio ({{index}})",
46
46
  "remaining-tiles": "Casillas restantes",
47
47
  "results": "Resultados",
48
- "results.empty-state.no-filtered-results": "Ningún resultado coincide con esta consulta.",
49
48
  "results.empty-state.no-results": "No hay resultados; no se pueden generar palabras",
50
49
  "results.empty-state.outdated": "Los resultados están desactualizados. Haga clic a continuación para actualizar.",
51
50
  "results.empty-state.uninitialized": "Aquí se mostrarán las palabras generadas a partir de sus letras.",
52
51
  "results.input.placeholder": "Busque una solución... (RegExp)",
53
52
  "results.insert": "Insertar",
53
+ "results.preview": "Vista previa",
54
54
  "results.solve": "Resolver",
55
55
  "settings": "Configuración",
56
56
  "settings.autoGroupTiles": "Agrupar casillas restantes",
package/src/i18n/fa.json CHANGED
@@ -45,12 +45,12 @@
45
45
  "rack.tile.location": "({{index}}) کاشی: طاقچه",
46
46
  "remaining-tiles": "کاشی های باقی مانده",
47
47
  "results": "نتایج",
48
- "results.empty-state.no-filtered-results": "پاسخی یافت نشد.",
49
48
  "results.empty-state.no-results": "کلمه قابل استفاده پیدا نشد.",
50
49
  "results.empty-state.outdated": "نتایج به روز نیستند، برای بروز رسانی کلیک کنید.",
51
50
  "results.empty-state.uninitialized": "کلمات تولید شده از حروف شما اینجا نمایش داده خواهد شد.",
52
51
  "results.input.placeholder": "جستجو در نتایج (RegExp)",
53
52
  "results.insert": "وارد کردن",
53
+ "results.preview": "پیش نمایش",
54
54
  "results.solve": "حل کن",
55
55
  "settings": "تنظیمات",
56
56
  "settings.autoGroupTiles": "کاشی های باقی مانده ی طاقچه را کنار هم قرار بده",
package/src/i18n/fr.json CHANGED
@@ -45,12 +45,12 @@
45
45
  "rack.tile.location": "Chevalet: la case ({{index}})",
46
46
  "remaining-tiles": "Cases restantes",
47
47
  "results": "Résultats",
48
- "results.empty-state.no-filtered-results": "Aucun résultat ne correspond à cette requête",
49
48
  "results.empty-state.no-results": "Pas de résultats - impossible de générer des mots.",
50
49
  "results.empty-state.outdated": "Les résultats sont dépassé. Cliquer ci-dessous pour mettre à jour.",
51
50
  "results.empty-state.uninitialized": "Les mots générés à partir de vos lettres seront affichés ici.",
52
51
  "results.input.placeholder": "Rechercher les résultats... (RegExp)",
53
52
  "results.insert": "Inserer",
53
+ "results.preview": "Prévisualisation",
54
54
  "results.solve": "Résoudre",
55
55
  "settings": "Options",
56
56
  "settings.autoGroupTiles": "Grouper les cases restantes",
@@ -0,0 +1,27 @@
1
+ .de {
2
+ --aspect-ratio: var(--flag--de--aspect-ratio);
3
+ }
4
+
5
+ .es {
6
+ --aspect-ratio: var(--flag--es--aspect-ratio);
7
+ }
8
+
9
+ .fa {
10
+ --aspect-ratio: var(--flag--fa--aspect-ratio);
11
+ }
12
+
13
+ .fr {
14
+ --aspect-ratio: var(--flag--fr--aspect-ratio);
15
+ }
16
+
17
+ .gb {
18
+ --aspect-ratio: var(--flag--gb--aspect-ratio);
19
+ }
20
+
21
+ .pl {
22
+ --aspect-ratio: var(--flag--pl--aspect-ratio);
23
+ }
24
+
25
+ .us {
26
+ --aspect-ratio: var(--flag--us--aspect-ratio);
27
+ }
package/src/i18n/pl.json CHANGED
@@ -45,12 +45,12 @@
45
45
  "rack.tile.location": "Stojak: płytka ({{index}})",
46
46
  "remaining-tiles": "Pozostałe płytki",
47
47
  "results": "Wyniki",
48
- "results.empty-state.no-filtered-results": "Żaden wynik nie pasuje do tej kwerendy.",
49
48
  "results.empty-state.no-results": "Brak wyników - nie można wygenerować żadnego słowa.",
50
49
  "results.empty-state.outdated": "Wyniki są nieaktualne. Kliknij poniżej, aby zaktualizować.",
51
50
  "results.empty-state.uninitialized": "Tu zostaną wyświetlone słowa wygenerowane z Twoich liter.",
52
51
  "results.input.placeholder": "Szukaj rozwiązania... (RegExp)",
53
52
  "results.insert": "Wstaw",
53
+ "results.preview": "Podgląd",
54
54
  "results.solve": "Rozwiąż",
55
55
  "settings": "Opcje",
56
56
  "settings.autoGroupTiles": "Grupuj pozostałe płytki",
@@ -1,3 +1,4 @@
1
+ <!-- https://icons.getbootstrap.com/icons/dash-circle-fill/ -->
1
2
  <svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
2
3
  <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM4.5 7.5a.5.5 0 0 0 0 1h7a.5.5 0 0 0 0-1h-7z" fill="currentColor" />
3
4
  </svg>
@@ -0,0 +1,5 @@
1
+ <!-- https://icons.getbootstrap.com/icons/eye-fill/ -->
2
+ <svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
3
+ <path d="M10.5 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z" fill="currentColor" />
4
+ <path d="M0 8s3-5.5 8-5.5S16 8 16 8s-3 5.5-8 5.5S0 8 0 8zm8 3.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z" fill="currentColor" />
5
+ </svg>
@@ -18,15 +18,16 @@ export { default as DashCircleFill } from './DashCircleFill.svg';
18
18
  export { default as Eraser } from './Eraser.svg';
19
19
  export { default as ExclamationSquareFill } from './ExclamationSquareFill.svg';
20
20
  export { default as ExclamationTriangleFill } from './ExclamationTriangleFill.svg';
21
+ export { default as EyeFill } from './EyeFill.svg';
21
22
  export { default as Flag } from './Flag.svg';
22
- export { default as FlagFill } from './FlagFill.svg';
23
+ export { default as FlagDe } from './FlagDe.svg';
23
24
  export { default as FlagEs } from './FlagEs.svg';
24
25
  export { default as FlagFa } from './FlagFa.svg';
26
+ export { default as FlagFill } from './FlagFill.svg';
25
27
  export { default as FlagFr } from './FlagFr.svg';
26
28
  export { default as FlagGb } from './FlagGb.svg';
27
29
  export { default as FlagPl } from './FlagPl.svg';
28
30
  export { default as FlagUs } from './FlagUs.svg';
29
- export { default as FlagDe } from './FlagDe.svg';
30
31
  export { default as Github } from './Github.svg';
31
32
  export { default as InfoCircleFill } from './InfoCircleFill.svg';
32
33
  export { default as Keyboard } from './Keyboard.svg';
@@ -0,0 +1,13 @@
1
+ const createRegExp = (input: string): RegExp => {
2
+ if (input.trim().length === 0) {
3
+ return /.*/;
4
+ }
5
+
6
+ try {
7
+ return new RegExp(input, 'gi');
8
+ } catch {
9
+ return /.*/;
10
+ }
11
+ };
12
+
13
+ export default createRegExp;
@@ -0,0 +1,38 @@
1
+ import { Result } from '@scrabble-solver/types';
2
+
3
+ import { Point } from 'types';
4
+
5
+ import createRegExp from './createRegExp';
6
+
7
+ interface GroupedResults {
8
+ matching: Result[];
9
+ other: Result[];
10
+ }
11
+
12
+ const groupResults = (
13
+ results: Result[] | undefined,
14
+ query: string,
15
+ cellFilter: Point[],
16
+ ): GroupedResults | undefined => {
17
+ if (typeof results === 'undefined') {
18
+ return results;
19
+ }
20
+
21
+ return results.reduce<GroupedResults>(
22
+ ({ matching, other }, result) => {
23
+ const matchesQuery = createRegExp(query).test(result.word);
24
+ const matchesCellFilter = cellFilter.every(({ x, y }) => {
25
+ return result.cells.some((cell) => cell.x === x && cell.y === y);
26
+ });
27
+ const isMatching = matchesQuery && matchesCellFilter;
28
+
29
+ return {
30
+ matching: isMatching ? [...matching, result] : matching,
31
+ other: isMatching ? other : [...other, result],
32
+ };
33
+ },
34
+ { matching: [], other: [] },
35
+ );
36
+ };
37
+
38
+ export default groupResults;
@@ -0,0 +1,22 @@
1
+ import { Locale } from '@scrabble-solver/types';
2
+
3
+ const guessLocale = (): Locale => {
4
+ if (!globalThis.navigator) {
5
+ return Locale.EN_US;
6
+ }
7
+
8
+ const locales = Object.values(Locale);
9
+ const exactMatch = locales.find((locale) => globalThis.navigator.language === locale);
10
+
11
+ if (exactMatch) {
12
+ return exactMatch;
13
+ }
14
+
15
+ const partialMatch = locales.find((locale) => {
16
+ return globalThis.navigator.language === locale.substring(0, 2);
17
+ });
18
+
19
+ return partialMatch || Locale.EN_US;
20
+ };
21
+
22
+ export default guessLocale;