scrabble-solver 2.18.0 → 2.18.2

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 (353) hide show
  1. package/.nx/workspace-data/d/server-process.json +2 -2
  2. package/.nx/workspace-data/file-map.json +1652 -1412
  3. package/.nx/workspace-data/lockfile-dependencies.hash +1 -1
  4. package/.nx/workspace-data/lockfile-nodes.hash +1 -1
  5. package/.nx/workspace-data/nx_files.nxt +0 -0
  6. package/.nx/workspace-data/parsed-lock-file.dependencies.json +184 -209
  7. package/.nx/workspace-data/parsed-lock-file.nodes.json +374 -419
  8. package/.nx/workspace-data/project-graph.json +669 -670
  9. package/.nx/workspace-data/source-maps.json +236 -0
  10. package/CLAUDE.md +13 -11
  11. package/README.md +2 -2
  12. package/bun.lock +115 -130
  13. package/e2e/app.spec.ts +1 -1
  14. package/e2e/bugs/398-digraphs-in-created-words.spec.ts +2 -2
  15. package/e2e/bugs/445-tile-text-selection.spec.ts +38 -0
  16. package/e2e/bugs/451-results-list-gap-hover.spec.ts +35 -0
  17. package/e2e/bugs/452-stale-translations-cache.spec.ts +32 -0
  18. package/e2e/features/427-highlight-created-words.spec.ts +86 -0
  19. package/e2e/features/430-highlight-unreachable-cells-results.spec.ts +75 -0
  20. package/e2e/features/91-hover-remaining-tile-highlights.spec.ts +22 -0
  21. package/e2e/features/result-candidate-picker.spec.ts +21 -0
  22. package/e2e/features/results-interactions.spec.ts +170 -0
  23. package/e2e/features/solver-paths.spec.ts +2 -2
  24. package/e2e/features/words-dictionary-search.spec.ts +36 -0
  25. package/e2e/features/words-empty-state.spec.ts +42 -0
  26. package/e2e/features/words-interactions.spec.ts +179 -0
  27. package/e2e/features/words-preview.spec.ts +140 -0
  28. package/e2e/features/words-table-filtering.spec.ts +22 -0
  29. package/e2e/features/words-table-sorting.spec.ts +33 -0
  30. package/e2e/lib/actions.ts +76 -6
  31. package/e2e/lib/selectors.ts +40 -3
  32. package/lerna.json +1 -1
  33. package/package.json +24 -44
  34. package/packages/configs/package.json +13 -4
  35. package/packages/constants/package.json +6 -2
  36. package/packages/dictionaries/package.json +12 -5
  37. package/packages/logger/package.json +7 -2
  38. package/packages/scrabble-solver/.next/BUILD_ID +1 -1
  39. package/packages/scrabble-solver/.next/build-manifest.json +15 -15
  40. package/packages/scrabble-solver/.next/cache/.previewinfo +1 -1
  41. package/packages/scrabble-solver/.next/cache/.rscinfo +1 -1
  42. package/packages/scrabble-solver/.next/cache/.tsbuildinfo +1 -1
  43. package/packages/scrabble-solver/.next/cache/webpack/client-production/0.pack +0 -0
  44. package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack +0 -0
  45. package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack.old +0 -0
  46. package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack +0 -0
  47. package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack.old +0 -0
  48. package/packages/scrabble-solver/.next/cache/webpack/server-production/0.pack +0 -0
  49. package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack +0 -0
  50. package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack.old +0 -0
  51. package/packages/scrabble-solver/.next/diagnostics/framework.json +1 -1
  52. package/packages/scrabble-solver/.next/dynamic-css-manifest.json +2 -2
  53. package/packages/scrabble-solver/.next/prerender-manifest.json +4 -4
  54. package/packages/scrabble-solver/.next/react-loadable-manifest.json +37 -37
  55. package/packages/scrabble-solver/.next/routes-manifest.json +1 -1
  56. package/packages/scrabble-solver/.next/server/chunks/309.js +1 -1
  57. package/packages/scrabble-solver/.next/server/chunks/344.js +1 -1
  58. package/packages/scrabble-solver/.next/server/chunks/371.js +1 -1
  59. package/packages/scrabble-solver/.next/server/chunks/392.js +1 -1
  60. package/packages/scrabble-solver/.next/server/chunks/465.js +1 -1
  61. package/packages/scrabble-solver/.next/server/chunks/546.js +1 -1
  62. package/packages/scrabble-solver/.next/server/chunks/641.js +1 -1
  63. package/packages/scrabble-solver/.next/server/chunks/67.js +1 -0
  64. package/packages/scrabble-solver/.next/server/chunks/712.js +1 -1
  65. package/packages/scrabble-solver/.next/server/chunks/846.js +1 -1
  66. package/packages/scrabble-solver/.next/server/chunks/886.js +1 -1
  67. package/packages/scrabble-solver/.next/server/chunks/942.js +1 -0
  68. package/packages/scrabble-solver/.next/server/chunks/953.js +1 -1
  69. package/packages/scrabble-solver/.next/server/chunks/961.js +1 -1
  70. package/packages/scrabble-solver/.next/server/dynamic-css-manifest.js +1 -1
  71. package/packages/scrabble-solver/.next/server/middleware-build-manifest.js +1 -1
  72. package/packages/scrabble-solver/.next/server/middleware-react-loadable-manifest.js +1 -1
  73. package/packages/scrabble-solver/.next/server/pages/404.html +3 -2
  74. package/packages/scrabble-solver/.next/server/pages/500.html +3 -2
  75. package/packages/scrabble-solver/.next/server/pages/_app.js +1 -1
  76. package/packages/scrabble-solver/.next/server/pages/_app.js.nft.json +1 -1
  77. package/packages/scrabble-solver/.next/server/pages/_document.js +2 -2
  78. package/packages/scrabble-solver/.next/server/pages/_document.js.nft.json +1 -1
  79. package/packages/scrabble-solver/.next/server/pages/_error.js +2 -2
  80. package/packages/scrabble-solver/.next/server/pages/_error.js.nft.json +1 -1
  81. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale]/[word].js +1 -1
  82. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale]/[word].js.nft.json +1 -1
  83. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale].js +1 -1
  84. package/packages/scrabble-solver/.next/server/pages/api/dictionary/[locale].js.nft.json +1 -1
  85. package/packages/scrabble-solver/.next/server/pages/api/solve.js +1 -1
  86. package/packages/scrabble-solver/.next/server/pages/api/solve.js.nft.json +1 -1
  87. package/packages/scrabble-solver/.next/server/pages/api/verify.js +1 -1
  88. package/packages/scrabble-solver/.next/server/pages/api/verify.js.nft.json +1 -1
  89. package/packages/scrabble-solver/.next/server/pages/index.html +3 -2
  90. package/packages/scrabble-solver/.next/server/pages/index.js +4 -4
  91. package/packages/scrabble-solver/.next/server/pages/index.js.nft.json +1 -1
  92. package/packages/scrabble-solver/.next/server/pages/index.json +1 -1
  93. package/packages/scrabble-solver/.next/server/pages/not-found.html +3 -2
  94. package/packages/scrabble-solver/.next/server/pages/not-found.js.nft.json +1 -1
  95. package/packages/scrabble-solver/.next/static/chunks/100.305bdf9c75b1758f.js +1 -0
  96. package/packages/scrabble-solver/.next/static/chunks/256.66ff9fb6d7db9ec7.js +1 -0
  97. package/packages/scrabble-solver/.next/static/chunks/{307.67f52f13e3d60d4e.js → 307.89e6100f58443679.js} +1 -1
  98. package/packages/scrabble-solver/.next/static/chunks/{363.01eed3233f92b6a4.js → 363.cec65b1f9668a75e.js} +1 -1
  99. package/packages/scrabble-solver/.next/static/chunks/{422.1559ae6756912ca5.js → 422.3e55b4c94e328701.js} +1 -1
  100. package/packages/scrabble-solver/.next/static/chunks/{663.d2c9ca36b26695b8.js → 663.bd2c45413fbca518.js} +1 -1
  101. package/packages/scrabble-solver/.next/static/chunks/693.677c88e0eb1340b5.js +1 -0
  102. package/packages/scrabble-solver/.next/static/chunks/{820.97b82f751ca49579.js → 820.823e9e885ebb66af.js} +1 -1
  103. package/packages/scrabble-solver/.next/static/chunks/827.c81b92b383a748c9.js +1 -0
  104. package/packages/scrabble-solver/.next/static/chunks/{865.a9b563a45848a83c.js → 865.59baba8296c9b116.js} +1 -1
  105. package/packages/scrabble-solver/.next/static/chunks/912.077603e4f0bc6cca.js +1 -0
  106. package/packages/scrabble-solver/.next/static/chunks/{939.19b33a3d3c351d73.js → 939.2ad86b50405c7897.js} +1 -1
  107. package/packages/scrabble-solver/.next/static/chunks/980.7ce17464fc56648a.js +1 -0
  108. package/packages/scrabble-solver/.next/static/chunks/{main-b92e08f3fc96dd8d.js → main-7e52da35a4beea0b.js} +1 -1
  109. package/packages/scrabble-solver/.next/static/chunks/pages/_app-f9ec81278ffa7b1a.js +1 -0
  110. package/packages/scrabble-solver/.next/static/chunks/pages/index-b5bf018100015277.js +1 -0
  111. package/packages/scrabble-solver/.next/static/chunks/pages/not-found-a59f9deeeff63d17.js +1 -0
  112. package/packages/scrabble-solver/.next/static/chunks/webpack-9f5c9d275348829b.js +1 -0
  113. package/packages/scrabble-solver/.next/static/css/65bcc9440e067113.css +1 -0
  114. package/packages/scrabble-solver/.next/static/css/{abc2425d3ba607bc.css → 92f85ce358a0a468.css} +2 -1
  115. package/packages/scrabble-solver/.next/static/css/a691348b788f8bc0.css +1 -0
  116. package/packages/scrabble-solver/.next/static/css/cd4d7f2458487399.css +1 -0
  117. package/packages/scrabble-solver/.next/static/{FL82obwzFHRzvV6z76I-U → nvGZh1gPuX98L07YmxlYD}/_buildManifest.js +1 -1
  118. package/packages/scrabble-solver/.next/trace +21 -21
  119. package/packages/scrabble-solver/.next/trace-build +1 -1
  120. package/packages/scrabble-solver/AGENTS.md +9 -0
  121. package/packages/scrabble-solver/CLAUDE.md +1 -0
  122. package/packages/scrabble-solver/package.json +22 -13
  123. package/packages/scrabble-solver/public/robots.txt +1 -0
  124. package/packages/scrabble-solver/public/service-worker.js +1 -1
  125. package/packages/scrabble-solver/public/site.webmanifest +14 -0
  126. package/packages/scrabble-solver/src/components/Board/Board.tsx +3 -1
  127. package/packages/scrabble-solver/src/components/Board/BoardPure.tsx +26 -20
  128. package/packages/scrabble-solver/src/components/Board/components/Cell/Cell.tsx +4 -4
  129. package/packages/scrabble-solver/src/components/Board/lib.test.ts +106 -0
  130. package/packages/scrabble-solver/src/components/Board/lib.ts +34 -3
  131. package/packages/scrabble-solver/src/components/Board/selectors.ts +17 -5
  132. package/packages/scrabble-solver/src/components/Button/Button.module.scss +1 -1
  133. package/packages/scrabble-solver/src/components/Modal/Modal.module.scss +0 -4
  134. package/packages/scrabble-solver/src/components/Results/Header.tsx +30 -5
  135. package/packages/scrabble-solver/src/components/Results/Result.tsx +45 -55
  136. package/packages/scrabble-solver/src/components/Results/Results.module.scss +0 -126
  137. package/packages/scrabble-solver/src/components/Results/Results.module.scss.d.ts +0 -10
  138. package/packages/scrabble-solver/src/components/Results/Results.tsx +25 -5
  139. package/packages/scrabble-solver/src/components/Solver/Solver.module.scss +4 -4
  140. package/packages/scrabble-solver/src/components/Solver/Solver.tsx +5 -3
  141. package/packages/scrabble-solver/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.module.scss +5 -1
  142. package/packages/scrabble-solver/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.module.scss.d.ts +1 -0
  143. package/packages/scrabble-solver/src/components/Solver/components/ResultCandidatePicker/ResultCandidatePicker.tsx +5 -2
  144. package/packages/scrabble-solver/src/components/Table/Cell.tsx +42 -0
  145. package/packages/scrabble-solver/src/components/Table/Header.tsx +11 -0
  146. package/packages/scrabble-solver/src/components/{Results → Table}/HeaderButton.tsx +22 -14
  147. package/packages/scrabble-solver/src/components/Table/Row.tsx +59 -0
  148. package/packages/scrabble-solver/src/components/{ResultsInput/ResultsInput.tsx → Table/Search.tsx} +9 -11
  149. package/packages/scrabble-solver/src/components/Table/Table.module.scss +147 -0
  150. package/packages/scrabble-solver/src/components/Table/Table.module.scss.d.ts +19 -0
  151. package/packages/scrabble-solver/src/components/Table/index.ts +5 -0
  152. package/packages/scrabble-solver/src/components/Tile/Tile.module.scss +2 -1
  153. package/packages/scrabble-solver/src/hooks/useColumns.ts +7 -5
  154. package/packages/scrabble-solver/src/hooks/useIsCompactLayout.ts +14 -0
  155. package/packages/scrabble-solver/src/i18n/languages/english.json +6 -1
  156. package/packages/scrabble-solver/src/i18n/languages/french.json +6 -1
  157. package/packages/scrabble-solver/src/i18n/languages/german.json +6 -1
  158. package/packages/scrabble-solver/src/i18n/languages/persian.json +6 -1
  159. package/packages/scrabble-solver/src/i18n/languages/polish.json +6 -1
  160. package/packages/scrabble-solver/src/i18n/languages/romanian.json +6 -1
  161. package/packages/scrabble-solver/src/i18n/languages/spanish.json +6 -1
  162. package/packages/scrabble-solver/src/i18n/languages/turkish.json +6 -1
  163. package/packages/scrabble-solver/src/icons/QuestionSquare.svg +5 -0
  164. package/packages/scrabble-solver/src/lib/createCoordinatesComparator.ts +23 -0
  165. package/packages/scrabble-solver/src/lib/createKeyComparator.ts +1 -4
  166. package/packages/scrabble-solver/src/lib/createSortComparator.ts +22 -0
  167. package/packages/scrabble-solver/src/lib/getCoordinates.test.ts +21 -0
  168. package/packages/scrabble-solver/src/lib/getCoordinates.ts +16 -0
  169. package/packages/scrabble-solver/src/lib/getNextSort.ts +12 -0
  170. package/packages/scrabble-solver/src/lib/waitForIdleOrFirstIntent.ts +19 -8
  171. package/packages/scrabble-solver/src/modals/RemainingTilesModal/RemainingTilesModal.tsx +3 -1
  172. package/packages/scrabble-solver/src/modals/RemainingTilesModal/components/Character/Character.tsx +8 -9
  173. package/packages/scrabble-solver/src/modals/ResultsModal/ResultsModal.tsx +14 -10
  174. package/packages/scrabble-solver/src/modals/WordsModal/WordsModal.module.scss +16 -27
  175. package/packages/scrabble-solver/src/modals/WordsModal/WordsModal.module.scss.d.ts +3 -6
  176. package/packages/scrabble-solver/src/modals/WordsModal/WordsModal.tsx +56 -42
  177. package/packages/scrabble-solver/src/modals/WordsModal/components/WordsTable/WordRow.tsx +92 -0
  178. package/packages/scrabble-solver/src/modals/WordsModal/components/WordsTable/WordsTable.module.scss +69 -0
  179. package/packages/scrabble-solver/src/modals/WordsModal/components/WordsTable/WordsTable.module.scss.d.ts +16 -0
  180. package/packages/scrabble-solver/src/modals/WordsModal/components/WordsTable/WordsTable.tsx +137 -0
  181. package/packages/scrabble-solver/src/modals/WordsModal/components/WordsTable/index.ts +1 -0
  182. package/packages/scrabble-solver/src/modals/WordsModal/components/index.ts +1 -0
  183. package/packages/scrabble-solver/src/pages/_app.tsx +40 -76
  184. package/packages/scrabble-solver/src/pages/_document.tsx +21 -19
  185. package/packages/scrabble-solver/src/pages/api/verify.ts +7 -6
  186. package/packages/scrabble-solver/src/pages/index.tsx +58 -6
  187. package/packages/scrabble-solver/src/pages/not-found.tsx +13 -2
  188. package/packages/scrabble-solver/src/parameters/index.ts +9 -0
  189. package/packages/scrabble-solver/src/sdk/verify.ts +3 -2
  190. package/packages/scrabble-solver/src/solver-worker/client.test.ts +10 -3
  191. package/packages/scrabble-solver/src/solver-worker/index.worker.ts +7 -7
  192. package/packages/scrabble-solver/src/solver-worker/messages.ts +3 -3
  193. package/packages/scrabble-solver/src/state/hoveredWord/index.ts +4 -0
  194. package/packages/scrabble-solver/src/state/hoveredWord/initialState.ts +5 -0
  195. package/packages/scrabble-solver/src/state/hoveredWord/selectors.ts +5 -0
  196. package/packages/scrabble-solver/src/state/hoveredWord/slice.ts +18 -0
  197. package/packages/scrabble-solver/src/state/hoveredWord/types.ts +5 -0
  198. package/packages/scrabble-solver/src/state/index.ts +1 -0
  199. package/packages/scrabble-solver/src/state/localStorage.ts +13 -2
  200. package/packages/scrabble-solver/src/state/results/lib.ts +9 -26
  201. package/packages/scrabble-solver/src/state/results/selectors.ts +9 -1
  202. package/packages/scrabble-solver/src/state/results/slice.ts +4 -16
  203. package/packages/scrabble-solver/src/state/sagas.ts +30 -5
  204. package/packages/scrabble-solver/src/state/selectors.ts +18 -1
  205. package/packages/scrabble-solver/src/state/store.ts +2 -0
  206. package/packages/scrabble-solver/src/state/types.ts +2 -0
  207. package/packages/scrabble-solver/src/state/verify/initialState.ts +7 -0
  208. package/packages/scrabble-solver/src/state/verify/lib.test.ts +55 -0
  209. package/packages/scrabble-solver/src/state/verify/lib.ts +49 -0
  210. package/packages/scrabble-solver/src/state/verify/selectors.ts +37 -0
  211. package/packages/scrabble-solver/src/state/verify/slice.ts +13 -2
  212. package/packages/scrabble-solver/src/state/verify/types.ts +6 -2
  213. package/packages/scrabble-solver/src/styles/_tokens.scss +2 -0
  214. package/packages/scrabble-solver/src/styles/mixins.scss +5 -0
  215. package/packages/scrabble-solver/src/styles/variables.module.scss +1 -0
  216. package/packages/scrabble-solver/src/styles/variables.module.scss.d.ts +1 -0
  217. package/packages/scrabble-solver/src/styles/variables.scss +1 -1
  218. package/packages/scrabble-solver/src/types/index.ts +24 -11
  219. package/packages/scrabble-solver/tsconfig.tsbuildinfo +1 -1
  220. package/packages/solver/package.json +14 -6
  221. package/packages/types/build/Bingo.d.ts +0 -3
  222. package/packages/types/build/Bingo.js +0 -14
  223. package/packages/types/build/Board.d.ts +3 -1
  224. package/packages/types/build/Board.js +5 -40
  225. package/packages/types/build/BoardJson.d.ts +0 -1
  226. package/packages/types/build/BoardJson.js +0 -19
  227. package/packages/types/build/BoardWord.d.ts +7 -0
  228. package/packages/types/build/BoardWord.js +2 -0
  229. package/packages/types/build/CellJson.d.ts +0 -1
  230. package/packages/types/build/CellJson.js +0 -11
  231. package/packages/types/build/Game.d.ts +0 -1
  232. package/packages/types/build/Game.js +1 -4
  233. package/packages/types/build/Locale.d.ts +0 -1
  234. package/packages/types/build/Locale.js +1 -4
  235. package/packages/types/build/Result.js +3 -3
  236. package/packages/types/build/TileJson.d.ts +0 -1
  237. package/packages/types/build/TileJson.js +0 -6
  238. package/packages/types/build/index.d.ts +9 -8
  239. package/packages/types/build/index.js +14 -17
  240. package/packages/types/build/lib/getBoardWords.d.ts +3 -0
  241. package/packages/types/build/lib/getBoardWords.js +10 -0
  242. package/packages/types/build/lib/getCells.d.ts +4 -0
  243. package/packages/types/build/lib/getCells.js +27 -0
  244. package/packages/types/build/lib/getCollidingWords.d.ts +3 -0
  245. package/packages/types/build/lib/getCollidingWords.js +19 -0
  246. package/packages/types/build/lib/getCollision.d.ts +3 -0
  247. package/packages/types/build/lib/getCollision.js +31 -0
  248. package/packages/types/build/lib/getCollisions.d.ts +3 -0
  249. package/packages/types/build/lib/getCollisions.js +17 -0
  250. package/packages/types/build/lib/getPlacedCell.d.ts +3 -0
  251. package/packages/types/build/lib/getPlacedCell.js +10 -0
  252. package/packages/types/build/lib/getWordThrough.d.ts +4 -0
  253. package/packages/types/build/lib/getWordThrough.js +23 -0
  254. package/packages/types/build/lib/getWordsInRows.d.ts +8 -0
  255. package/packages/types/build/lib/getWordsInRows.js +29 -0
  256. package/packages/types/build/lib/index.d.ts +5 -0
  257. package/packages/types/build/lib/index.js +21 -0
  258. package/packages/types/build/lib/isSameBoardWord.d.ts +2 -0
  259. package/packages/types/build/lib/isSameBoardWord.js +6 -0
  260. package/packages/types/build/lib/toBoardWord.d.ts +4 -0
  261. package/packages/types/build/lib/toBoardWord.js +11 -0
  262. package/packages/types/build/lib/transpose.d.ts +1 -0
  263. package/packages/types/build/lib/transpose.js +16 -0
  264. package/packages/types/build/type-guards/index.d.ts +10 -0
  265. package/packages/types/build/type-guards/index.js +23 -0
  266. package/packages/types/build/type-guards/isBingo.d.ts +2 -0
  267. package/packages/types/build/type-guards/isBingo.js +9 -0
  268. package/packages/types/build/type-guards/isBoardJson.d.ts +2 -0
  269. package/packages/types/build/type-guards/isBoardJson.js +21 -0
  270. package/packages/types/build/type-guards/isCellJson.d.ts +2 -0
  271. package/packages/types/build/type-guards/isCellJson.js +13 -0
  272. package/packages/types/build/type-guards/isGame.d.ts +2 -0
  273. package/packages/types/build/type-guards/isGame.js +7 -0
  274. package/packages/types/build/type-guards/isLocale.d.ts +2 -0
  275. package/packages/types/build/type-guards/isLocale.js +7 -0
  276. package/packages/types/build/type-guards/isMultiplierBingo.d.ts +2 -0
  277. package/packages/types/build/type-guards/isMultiplierBingo.js +8 -0
  278. package/packages/types/build/type-guards/isScoreBingo.d.ts +2 -0
  279. package/packages/types/build/type-guards/isScoreBingo.js +8 -0
  280. package/packages/types/build/type-guards/isTileJson.d.ts +2 -0
  281. package/packages/types/build/type-guards/isTileJson.js +8 -0
  282. package/packages/types/package.json +10 -3
  283. package/packages/types/src/Bingo.ts +0 -14
  284. package/packages/types/src/Board.test.ts +85 -0
  285. package/packages/types/src/Board.ts +8 -49
  286. package/packages/types/src/BoardJson.ts +1 -21
  287. package/packages/types/src/BoardWord.ts +8 -0
  288. package/packages/types/src/CellJson.ts +1 -12
  289. package/packages/types/src/Game.ts +0 -4
  290. package/packages/types/src/Locale.ts +0 -4
  291. package/packages/types/src/Result.ts +3 -3
  292. package/packages/types/src/TileJson.ts +0 -6
  293. package/packages/types/src/index.ts +20 -8
  294. package/packages/types/src/lib/getBoardWords.ts +11 -0
  295. package/packages/types/src/lib/getCells.test.ts +127 -0
  296. package/packages/types/src/lib/getCells.ts +31 -0
  297. package/packages/types/src/lib/getCollidingWords.ts +25 -0
  298. package/packages/types/src/lib/getCollision.ts +38 -0
  299. package/packages/types/src/lib/getCollisions.test.ts +82 -0
  300. package/packages/types/src/lib/getCollisions.ts +22 -0
  301. package/packages/types/src/lib/getPlacedCell.ts +9 -0
  302. package/packages/types/src/lib/getWordThrough.ts +30 -0
  303. package/packages/types/src/lib/getWordsInRows.ts +34 -0
  304. package/packages/types/src/lib/index.ts +5 -0
  305. package/packages/types/src/lib/isSameBoardWord.ts +5 -0
  306. package/packages/types/src/lib/toBoardWord.ts +12 -0
  307. package/packages/types/src/lib/transpose.ts +15 -0
  308. package/packages/types/src/type-guards/index.ts +10 -0
  309. package/packages/types/src/type-guards/isBingo.ts +8 -0
  310. package/packages/types/src/type-guards/isBoardJson.test.ts +33 -0
  311. package/packages/types/src/type-guards/isBoardJson.ts +23 -0
  312. package/packages/types/src/type-guards/isCellJson.ts +14 -0
  313. package/packages/types/src/type-guards/isGame.ts +5 -0
  314. package/packages/types/src/type-guards/isLocale.ts +5 -0
  315. package/packages/types/src/type-guards/isMultiplierBingo.ts +7 -0
  316. package/packages/types/src/type-guards/isScoreBingo.ts +7 -0
  317. package/packages/types/src/type-guards/isTileJson.ts +7 -0
  318. package/packages/word-definitions/package.json +9 -3
  319. package/packages/word-lists/package.json +11 -3
  320. package/playwright-report/index.html +1 -1
  321. package/screencast.gif +0 -0
  322. package/packages/scrabble-solver/.next/server/chunks/316.js +0 -1
  323. package/packages/scrabble-solver/.next/server/chunks/916.js +0 -1
  324. package/packages/scrabble-solver/.next/static/chunks/106.e414e88694add27f.js +0 -1
  325. package/packages/scrabble-solver/.next/static/chunks/127.125d896a19061d71.js +0 -1
  326. package/packages/scrabble-solver/.next/static/chunks/18.8b91e137d239e15c.js +0 -1
  327. package/packages/scrabble-solver/.next/static/chunks/583.7a46807be14c377e.js +0 -1
  328. package/packages/scrabble-solver/.next/static/chunks/711.ba7b8f900dfd40d0.js +0 -1
  329. package/packages/scrabble-solver/.next/static/chunks/912.ff8037ba9d4db5c8.js +0 -1
  330. package/packages/scrabble-solver/.next/static/chunks/pages/_app-ba5d628e81a15461.js +0 -1
  331. package/packages/scrabble-solver/.next/static/chunks/pages/index-5f9fa6ac3cd3d97b.js +0 -1
  332. package/packages/scrabble-solver/.next/static/chunks/pages/not-found-23d61fd3ed791cad.js +0 -1
  333. package/packages/scrabble-solver/.next/static/chunks/webpack-e34c3ae12275e978.js +0 -1
  334. package/packages/scrabble-solver/.next/static/css/b74cd7de82dabcdd.css +0 -1
  335. package/packages/scrabble-solver/.next/static/css/c403ce739a9c0d83.css +0 -1
  336. package/packages/scrabble-solver/.next/static/css/fbfdce70731d0d2b.css +0 -1
  337. package/packages/scrabble-solver/src/components/Results/Cell.tsx +0 -32
  338. package/packages/scrabble-solver/src/components/ResultsInput/ResultsInput.module.scss +0 -12
  339. package/packages/scrabble-solver/src/components/ResultsInput/ResultsInput.module.scss.d.ts +0 -7
  340. package/packages/scrabble-solver/src/components/ResultsInput/index.ts +0 -1
  341. package/packages/scrabble-solver/src/components/SeoMessage/SeoMessage.tsx +0 -18
  342. package/packages/scrabble-solver/src/components/SeoMessage/index.ts +0 -1
  343. package/packages/types/build/readResultCells.d.ts +0 -5
  344. package/packages/types/build/readResultCells.js +0 -75
  345. package/packages/types/src/readResultCells.test.ts +0 -140
  346. package/packages/types/src/readResultCells.ts +0 -90
  347. /package/packages/scrabble-solver/.next/static/{FL82obwzFHRzvV6z76I-U → nvGZh1gPuX98L07YmxlYD}/_ssgManifest.js +0 -0
  348. /package/packages/types/build/{isError.d.ts → type-guards/isError.d.ts} +0 -0
  349. /package/packages/types/build/{isError.js → type-guards/isError.js} +0 -0
  350. /package/packages/types/build/{isObject.d.ts → type-guards/isObject.d.ts} +0 -0
  351. /package/packages/types/build/{isObject.js → type-guards/isObject.js} +0 -0
  352. /package/packages/types/src/{isError.ts → type-guards/isError.ts} +0 -0
  353. /package/packages/types/src/{isObject.ts → type-guards/isObject.ts} +0 -0
@@ -0,0 +1,137 @@
1
+ import { isSameBoardWord } from '@scrabble-solver/types';
2
+ import classNames from 'classnames';
3
+ import { type FunctionComponent, useCallback, useEffect, useMemo } from 'react';
4
+ import { useDispatch } from 'react-redux';
5
+ import { List } from 'react-window';
6
+
7
+ import { EmptyState } from '@/components/EmptyState';
8
+ import { Header, HeaderButton, Search } from '@/components/Table';
9
+ import { useIsCompactLayout } from '@/hooks/useIsCompactLayout';
10
+ import { useIsTouchDevice } from '@/hooks/useIsTouchDevice';
11
+ import { LOCALE_FEATURES } from '@/i18n/constants';
12
+ import GeoAlt from '@/icons/GeoAlt.svg';
13
+ import QuestionSquare from '@/icons/QuestionSquare.svg';
14
+ import { RESULTS_ITEM_HEIGHT, RESULTS_OVERSCAN_COUNT } from '@/parameters';
15
+ import {
16
+ hoveredWordSlice,
17
+ selectHoveredWord,
18
+ selectLocale,
19
+ selectProcessedWords,
20
+ selectWordsQuery,
21
+ selectWordsSort,
22
+ useTranslate,
23
+ useTypedSelector,
24
+ verifySlice,
25
+ } from '@/state';
26
+ import { WordColumnId } from '@/types';
27
+
28
+ import { WordRow, type WordRowData } from './WordRow';
29
+ import styles from './WordsTable.module.scss';
30
+
31
+ interface Props {
32
+ className?: string;
33
+ isOpen: boolean;
34
+ onPreview: () => void;
35
+ }
36
+
37
+ export const WordsTable: FunctionComponent<Props> = ({ className, isOpen, onPreview }) => {
38
+ const dispatch = useDispatch();
39
+ const translate = useTranslate();
40
+ const isTouchDevice = useIsTouchDevice();
41
+ const isCompactLayout = useIsCompactLayout();
42
+ const highlightFollowsPointer = !isTouchDevice && !isCompactLayout;
43
+ const locale = useTypedSelector(selectLocale);
44
+ const { direction } = LOCALE_FEATURES[locale];
45
+ const words = useTypedSelector(selectProcessedWords);
46
+ const query = useTypedSelector(selectWordsQuery);
47
+ const sort = useTypedSelector(selectWordsSort);
48
+ const hoveredWord = useTypedSelector(selectHoveredWord);
49
+ const highlightedIndex = hoveredWord ? words.findIndex((word) => isSameBoardWord(word, hoveredWord)) : -1;
50
+ const rowProps = useMemo<WordRowData>(
51
+ () => ({ highlightFollowsPointer, highlightedIndex, isTouchDevice, words, onPreview }),
52
+ [highlightFollowsPointer, highlightedIndex, isTouchDevice, words, onPreview],
53
+ );
54
+
55
+ const handleSort = useCallback(
56
+ (columnId: WordColumnId) => {
57
+ dispatch(verifySlice.actions.sort(columnId));
58
+ },
59
+ [dispatch],
60
+ );
61
+
62
+ const handleQueryChange = useCallback(
63
+ (newQuery: string) => {
64
+ dispatch(verifySlice.actions.changeQuery(newQuery));
65
+ },
66
+ [dispatch],
67
+ );
68
+
69
+ const handleMouseLeave = useCallback(() => {
70
+ dispatch(hoveredWordSlice.actions.clear());
71
+ }, [dispatch]);
72
+
73
+ useEffect(() => {
74
+ if (isOpen && isCompactLayout && words.length > 0 && highlightedIndex === -1) {
75
+ dispatch(hoveredWordSlice.actions.set(words[0]));
76
+ }
77
+ }, [dispatch, highlightedIndex, isOpen, isCompactLayout, words]);
78
+
79
+ return (
80
+ <div
81
+ aria-label={translate('words')}
82
+ className={classNames(styles.words, className)}
83
+ data-testid="words"
84
+ role="region"
85
+ >
86
+ <Header>
87
+ <HeaderButton
88
+ className={styles.coordinates}
89
+ Icon={GeoAlt}
90
+ id={WordColumnId.Coordinates}
91
+ sort={sort}
92
+ translationKey="settings.showCoordinates"
93
+ onSort={handleSort}
94
+ />
95
+
96
+ <HeaderButton id={WordColumnId.Word} primary sort={sort} translationKey="common.word" onSort={handleSort} />
97
+
98
+ <HeaderButton
99
+ className={styles.stat}
100
+ Icon={QuestionSquare}
101
+ id={WordColumnId.Validity}
102
+ sort={sort}
103
+ translationKey="words.validity"
104
+ onSort={handleSort}
105
+ />
106
+ </Header>
107
+
108
+ <div aria-live="polite" className={styles.content}>
109
+ {words.length === 0 && <EmptyState variant="info">{translate('words.empty-state.no-words')}</EmptyState>}
110
+
111
+ {words.length > 0 && (
112
+ <div className={styles.listContainer}>
113
+ <List
114
+ className={styles.list}
115
+ dir={direction}
116
+ overscanCount={RESULTS_OVERSCAN_COUNT}
117
+ rowComponent={WordRow}
118
+ rowCount={words.length}
119
+ rowHeight={RESULTS_ITEM_HEIGHT}
120
+ rowProps={rowProps}
121
+ onMouseLeave={highlightFollowsPointer ? handleMouseLeave : undefined}
122
+ />
123
+ </div>
124
+ )}
125
+ </div>
126
+
127
+ {words.length > 0 && (
128
+ <Search
129
+ className={styles.input}
130
+ placeholder={translate('words.input.placeholder')}
131
+ value={query}
132
+ onChange={handleQueryChange}
133
+ />
134
+ )}
135
+ </div>
136
+ );
137
+ };
@@ -0,0 +1 @@
1
+ export { WordsTable } from './WordsTable';
@@ -0,0 +1 @@
1
+ export { WordsTable } from './WordsTable';
@@ -4,85 +4,49 @@ import Head from 'next/head';
4
4
  import { type FunctionComponent } from 'react';
5
5
  import { Provider } from 'react-redux';
6
6
 
7
- import { SeoMessage } from '@/components/SeoMessage';
7
+ import { SITE_DESCRIPTION, SITE_URL } from '@/parameters';
8
8
  import { store } from '@/state';
9
9
 
10
10
  import '../styles/global.scss';
11
11
 
12
- const DESCRIPTION =
13
- 'Scrabble Solver 2 - Free and open-source analysis tool for Scrabble, Scrabble Duel, Super Scrabble, Letter League, Crossplay, Literaki and Kelimelik. Quickly find top scoring words using given letters and board state. Available in English, French, German, Persian, Polish, Romanian, Spanish, and Turkish.';
14
-
15
- const KEYWORDS = [
16
- 'Scrabble Solver',
17
- 'Scrabble',
18
- 'Scrabble Duel',
19
- 'Solver',
20
- 'Super Scrabble',
21
- 'Kelimelik',
22
- 'Letter League',
23
- 'Crossplay',
24
- 'NYT Crossplay',
25
- 'Literaki',
26
- 'Board',
27
- 'Open-source',
28
- 'Open',
29
- 'Source',
30
- 'Word',
31
- 'Finder',
32
- 'Cheating',
33
- 'Word',
34
- 'English',
35
- 'Français',
36
- 'French',
37
- 'Deutsch',
38
- 'German',
39
- 'Polski',
40
- 'Polish',
41
- 'فارسی',
42
- 'Farsi',
43
- 'Español',
44
- 'Spanish',
45
- 'Română',
46
- 'Romanian',
47
- 'Türkçe',
48
- 'Turkish',
49
- 'SOWPODS',
50
- 'TWL06',
51
- 'SJP',
52
- 'FISE-2017',
53
- 'FISE-2',
54
- 'CNRTL',
55
- 'Kamil Mielnik',
56
- ].join(',');
57
-
58
- const App: FunctionComponent<AppProps> = ({ Component, pageProps }) => (
59
- <>
60
- <Head>
61
- <title>Scrabble Solver 2</title>
62
- <meta charSet="utf-8" />
63
- <meta name="author" content="Kamil Mielnik" />
64
- <meta name="description" content={DESCRIPTION} />
65
- <meta name="keywords" content={KEYWORDS} />
66
- <meta name="robots" content="index, follow, notranslate, noimageindex" />
67
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
68
- <meta property="og:title" content={DESCRIPTION} />
69
- <meta property="og:type" content="website" />
70
- <meta property="og:url" content="https://scrabble-solver.org" />
71
- <meta property="og:image" content="https://scrabble-solver.org/og.png" />
72
- <meta property="og:image:type" content="image/png" />
73
- <meta property="og:image:width" content="1200" />
74
- <meta property="og:image:height" content="630" />
75
- <meta property="og:image:alt" content={DESCRIPTION} />
76
- </Head>
77
-
78
- <Provider store={store}>
79
- <SeoMessage />
80
-
81
- <FloatingDelayGroup delay={0}>
82
- <Component {...pageProps} />
83
- </FloatingDelayGroup>
84
- </Provider>
85
- </>
86
- );
12
+ const TITLE = 'Scrabble Solver 2 - Board Solver & Word Finder';
13
+
14
+ const App: FunctionComponent<AppProps> = ({ Component, pageProps, router }) => {
15
+ const isIndexable = router.pathname === '/';
16
+
17
+ return (
18
+ <>
19
+ <Head>
20
+ <title>{TITLE}</title>
21
+ <meta charSet="utf-8" />
22
+ <meta name="author" content="Kamil Mielnik" />
23
+ <meta name="description" content={SITE_DESCRIPTION} />
24
+ {isIndexable ? <link rel="canonical" href={`${SITE_URL}/`} /> : <meta name="robots" content="noindex" />}
25
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
26
+ <meta property="og:site_name" content="Scrabble Solver" />
27
+ <meta property="og:title" content={TITLE} />
28
+ <meta property="og:description" content={SITE_DESCRIPTION} />
29
+ <meta property="og:type" content="website" />
30
+ <meta property="og:url" content={`${SITE_URL}/`} />
31
+ <meta property="og:locale" content="en_US" />
32
+ <meta property="og:image" content={`${SITE_URL}/og.png`} />
33
+ <meta property="og:image:type" content="image/png" />
34
+ <meta property="og:image:width" content="1200" />
35
+ <meta property="og:image:height" content="630" />
36
+ <meta property="og:image:alt" content="Scrabble tiles spelling out Scrabble Solver 2" />
37
+ <meta name="twitter:card" content="summary_large_image" />
38
+ <meta name="twitter:title" content={TITLE} />
39
+ <meta name="twitter:description" content={SITE_DESCRIPTION} />
40
+ <meta name="twitter:image" content={`${SITE_URL}/og.png`} />
41
+ </Head>
42
+
43
+ <Provider store={store}>
44
+ <FloatingDelayGroup delay={0}>
45
+ <Component {...pageProps} />
46
+ </FloatingDelayGroup>
47
+ </Provider>
48
+ </>
49
+ );
50
+ };
87
51
 
88
52
  export default App;
@@ -70,27 +70,29 @@ export default class MyDocument extends Document {
70
70
  <InlineCssHead>
71
71
  {/* eslint-disable-next-line react/no-danger */}
72
72
  <script dangerouslySetInnerHTML={{ __html: preHydrationScript }} />
73
- <link rel="apple-touch-icon-precomposed" sizes="57x57" href="icons/apple-touch-icon-57x57.png" />
74
- <link rel="apple-touch-icon-precomposed" sizes="114x114" href="icons/apple-touch-icon-114x114.png" />
75
- <link rel="apple-touch-icon-precomposed" sizes="72x72" href="icons/apple-touch-icon-72x72.png" />
76
- <link rel="apple-touch-icon-precomposed" sizes="144x144" href="icons/apple-touch-icon-144x144.png" />
77
- <link rel="apple-touch-icon-precomposed" sizes="60x60" href="icons/apple-touch-icon-60x60.png" />
78
- <link rel="apple-touch-icon-precomposed" sizes="120x120" href="icons/apple-touch-icon-120x120.png" />
79
- <link rel="apple-touch-icon-precomposed" sizes="76x76" href="icons/apple-touch-icon-76x76.png" />
80
- <link rel="apple-touch-icon-precomposed" sizes="152x152" href="icons/apple-touch-icon-152x152.png" />
81
- <link rel="icon" type="image/png" href="icons/favicon-196x196.png" sizes="196x196" />
82
- <link rel="icon" type="image/png" href="icons/favicon-96x96.png" sizes="96x96" />
83
- <link rel="icon" type="image/png" href="icons/favicon-32x32.png" sizes="32x32" />
84
- <link rel="icon" type="image/png" href="icons/favicon-16x16.png" sizes="16x16" />
85
- <link rel="icon" type="image/png" href="icons/favicon-128.png" sizes="128x128" />
86
- <link rel="icon" href="icons/favicon.svg" />
73
+ <link rel="apple-touch-icon-precomposed" sizes="57x57" href="/icons/apple-touch-icon-57x57.png" />
74
+ <link rel="apple-touch-icon-precomposed" sizes="114x114" href="/icons/apple-touch-icon-114x114.png" />
75
+ <link rel="apple-touch-icon-precomposed" sizes="72x72" href="/icons/apple-touch-icon-72x72.png" />
76
+ <link rel="apple-touch-icon-precomposed" sizes="144x144" href="/icons/apple-touch-icon-144x144.png" />
77
+ <link rel="apple-touch-icon-precomposed" sizes="60x60" href="/icons/apple-touch-icon-60x60.png" />
78
+ <link rel="apple-touch-icon-precomposed" sizes="120x120" href="/icons/apple-touch-icon-120x120.png" />
79
+ <link rel="apple-touch-icon-precomposed" sizes="76x76" href="/icons/apple-touch-icon-76x76.png" />
80
+ <link rel="apple-touch-icon-precomposed" sizes="152x152" href="/icons/apple-touch-icon-152x152.png" />
81
+ <link rel="icon" type="image/png" href="/icons/favicon-196x196.png" sizes="196x196" />
82
+ <link rel="icon" type="image/png" href="/icons/favicon-96x96.png" sizes="96x96" />
83
+ <link rel="icon" type="image/png" href="/icons/favicon-32x32.png" sizes="32x32" />
84
+ <link rel="icon" type="image/png" href="/icons/favicon-16x16.png" sizes="16x16" />
85
+ <link rel="icon" type="image/png" href="/icons/favicon-128.png" sizes="128x128" />
86
+ <link rel="icon" href="/icons/favicon.svg" />
87
+ <link rel="manifest" href="/site.webmanifest" />
87
88
  <meta name="application-name" content="Scrabble Solver" />
89
+ <meta name="theme-color" content="#EFE3AE" />
88
90
  <meta name="msapplication-TileColor" content="#EFE3AE" />
89
- <meta name="msapplication-TileImage" content="icons/mstile-144x144.png" />
90
- <meta name="msapplication-square70x70logo" content="icons/mstile-70x70.png" />
91
- <meta name="msapplication-square150x150logo" content="icons/mstile-150x150.png" />
92
- <meta name="msapplication-wide310x150logo" content="icons/mstile-310x150.png" />
93
- <meta name="msapplication-square310x310logo" content="icons/mstile-310x310.png" />
91
+ <meta name="msapplication-TileImage" content="/icons/mstile-144x144.png" />
92
+ <meta name="msapplication-square70x70logo" content="/icons/mstile-70x70.png" />
93
+ <meta name="msapplication-square150x150logo" content="/icons/mstile-150x150.png" />
94
+ <meta name="msapplication-wide310x150logo" content="/icons/mstile-310x150.png" />
95
+ <meta name="msapplication-square310x310logo" content="/icons/mstile-310x310.png" />
94
96
  </InlineCssHead>
95
97
 
96
98
  <body>
@@ -5,6 +5,7 @@ import { Board, type Config, type Game, type Locale, isBoardJson, isGame, isLoca
5
5
  import { type NextApiRequest, type NextApiResponse } from 'next';
6
6
 
7
7
  import { getServerLoggingData, isBoardValid } from '@/api';
8
+ import { type VerifiedWord } from '@/types';
8
9
 
9
10
  interface RequestData {
10
11
  board: Board;
@@ -31,15 +32,15 @@ const verify = async (request: NextApiRequest, response: NextApiResponse): Promi
31
32
  });
32
33
 
33
34
  const gaddag = await dictionaries.get(locale);
34
- const words = board.getWords().sort((a, b) => a.localeCompare(b, locale));
35
- const invalidWords: string[] = [];
36
- const validWords: string[] = [];
35
+ const words = board.getWords().sort((a, b) => a.word.localeCompare(b.word, locale));
36
+ const invalidWords: VerifiedWord[] = [];
37
+ const validWords: VerifiedWord[] = [];
37
38
 
38
39
  for (const word of words) {
39
- if (gaddag.has(word)) {
40
- validWords.push(word);
40
+ if (gaddag.has(word.word)) {
41
+ validWords.push({ ...word, isValid: true });
41
42
  } else {
42
- invalidWords.push(word);
43
+ invalidWords.push({ ...word, isValid: false });
43
44
  }
44
45
  }
45
46
 
@@ -6,7 +6,7 @@ import { execSync } from 'child_process';
6
6
  import fs from 'fs';
7
7
  import dynamic from 'next/dynamic';
8
8
  import path from 'path';
9
- import { type FunctionComponent, useCallback, useEffect, useState } from 'react';
9
+ import { type FunctionComponent, useCallback, useEffect, useRef, useState } from 'react';
10
10
  import { useDispatch } from 'react-redux';
11
11
 
12
12
  import { Logo } from '@/components/Logo';
@@ -14,13 +14,23 @@ import { NavButtons } from '@/components/NavButtons';
14
14
  import { Solver } from '@/components/Solver';
15
15
  import { useDirection } from '@/hooks/useDirection';
16
16
  import { useEffectOnce } from '@/hooks/useEffectOnce';
17
+ import { useIsCompactLayout } from '@/hooks/useIsCompactLayout';
17
18
  import { useLanguage } from '@/hooks/useLanguage';
18
19
  import { useLocalStorage } from '@/hooks/useLocalStorage';
19
20
  import { LOCALE_FEATURES } from '@/i18n/constants';
20
21
  import { schedulePreloadModals } from '@/modals/preload';
21
- import { CONFIG_PENDING_CLASS } from '@/parameters';
22
+ import { CONFIG_PENDING_CLASS, GITHUB_PROJECT_URL, NPM_PACKAGE_URL, SITE_DESCRIPTION, SITE_URL } from '@/parameters';
22
23
  import { registerServiceWorker } from '@/serviceWorkerManager';
23
- import { initialize, reset, selectConfig, selectIsHydrated, selectLocale, useTypedSelector } from '@/state';
24
+ import {
25
+ hoveredWordSlice,
26
+ initialize,
27
+ reset,
28
+ resultsSlice,
29
+ selectConfig,
30
+ selectIsHydrated,
31
+ selectLocale,
32
+ useTypedSelector,
33
+ } from '@/state';
24
34
 
25
35
  import styles from './index.module.scss';
26
36
 
@@ -49,6 +59,30 @@ const WordsModal = dynamic(() => import('@/modals/WordsModal').then((module) =>
49
59
  ssr: false,
50
60
  });
51
61
 
62
+ const WEB_APPLICATION_JSON_LD = JSON.stringify({
63
+ '@context': 'https://schema.org',
64
+ '@type': 'WebApplication',
65
+ name: 'Scrabble Solver',
66
+ alternateName: 'Scrabble Solver 2',
67
+ url: `${SITE_URL}/`,
68
+ description: SITE_DESCRIPTION,
69
+ applicationCategory: 'GameApplication',
70
+ operatingSystem: 'Any',
71
+ browserRequirements: 'Requires JavaScript',
72
+ image: `${SITE_URL}/og.png`,
73
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
74
+ inLanguage: ['en-US', 'en-GB', 'de-DE', 'es-ES', 'fa-IR', 'fr-FR', 'pl-PL', 'ro-RO', 'tr-TR'],
75
+ featureList: [
76
+ 'Finds the highest-scoring words for a given board and rack',
77
+ 'Supports Scrabble, Super Scrabble, Scrabble Duel, Letter League, Crossplay, Literaki, and Kelimelik',
78
+ 'Dictionaries and word definitions in 8 languages',
79
+ 'Works offline once a dictionary is cached',
80
+ 'Free, open source, no ads, no sign-up',
81
+ ],
82
+ sameAs: [GITHUB_PROJECT_URL, NPM_PACKAGE_URL],
83
+ author: { '@type': 'Person', name: 'Kamil Mielnik', url: 'https://kamilmielnik.com' },
84
+ });
85
+
52
86
  interface Props {
53
87
  version: string;
54
88
  }
@@ -60,6 +94,8 @@ const Index: FunctionComponent<Props> = ({ version }) => {
60
94
  const config = useTypedSelector(selectConfig);
61
95
  const locale = useTypedSelector(selectLocale);
62
96
  const isHydrated = useTypedSelector(selectIsHydrated);
97
+ const isCompactLayout = useIsCompactLayout();
98
+ const previousIsCompactLayout = useRef(isCompactLayout);
63
99
  const [modals, setModals] = useState<Record<Modal, boolean>>({
64
100
  dictionary: false,
65
101
  keyMap: false,
@@ -107,6 +143,7 @@ const Index: FunctionComponent<Props> = ({ version }) => {
107
143
  const handleCloseResults = useCallback(() => patchModals({ results: false }), [patchModals]);
108
144
  const handleCloseSettings = useCallback(() => patchModals({ settings: false }), [patchModals]);
109
145
  const handleCloseWords = useCallback(() => patchModals({ words: false }), [patchModals]);
146
+ const handlePreviewWord = useCallback(() => patchModals({ menu: false, words: false }), [patchModals]);
110
147
 
111
148
  useDirection(LOCALE_FEATURES[locale].direction);
112
149
  useLanguage(locale);
@@ -135,6 +172,16 @@ const Index: FunctionComponent<Props> = ({ version }) => {
135
172
  }
136
173
  }, [isHydrated]);
137
174
 
175
+ useEffect(() => {
176
+ if (previousIsCompactLayout.current === isCompactLayout) {
177
+ return;
178
+ }
179
+
180
+ previousIsCompactLayout.current = isCompactLayout;
181
+ dispatch(resultsSlice.actions.changeResultCandidate(null));
182
+ dispatch(hoveredWordSlice.actions.clear());
183
+ }, [dispatch, isCompactLayout]);
184
+
138
185
  useEffectOnce(() => {
139
186
  if (process.env.NODE_ENV === 'production') {
140
187
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
@@ -149,11 +196,11 @@ const Index: FunctionComponent<Props> = ({ version }) => {
149
196
  <>
150
197
  <nav className={styles.nav}>
151
198
  <div className={styles.navContent}>
152
- <div className={styles.navLogo}>
199
+ <h1 className={styles.navLogo}>
153
200
  <a className={styles.logoContainer} href="/" title={version}>
154
201
  <Logo className={styles.logo} />
155
202
  </a>
156
- </div>
203
+ </h1>
157
204
 
158
205
  <NavButtons
159
206
  onClear={handleClear}
@@ -170,6 +217,9 @@ const Index: FunctionComponent<Props> = ({ version }) => {
170
217
  <Solver className={styles.solver} onShowResults={handleShowResults} />
171
218
  </main>
172
219
 
220
+ {/* eslint-disable-next-line react/no-danger */}
221
+ <script dangerouslySetInnerHTML={{ __html: WEB_APPLICATION_JSON_LD }} type="application/ld+json" />
222
+
173
223
  {mountedModals.menu && (
174
224
  <MenuModal
175
225
  isOpen={modals.menu}
@@ -185,7 +235,9 @@ const Index: FunctionComponent<Props> = ({ version }) => {
185
235
 
186
236
  {mountedModals.keyMap && <KeyMapModal isOpen={modals.keyMap} onClose={handleCloseKeyMap} />}
187
237
 
188
- {mountedModals.words && <WordsModal isOpen={modals.words} onClose={handleCloseWords} />}
238
+ {mountedModals.words && (
239
+ <WordsModal isOpen={modals.words} onClose={handleCloseWords} onPreview={handlePreviewWord} />
240
+ )}
189
241
 
190
242
  {config.supportsRemainingTiles && mountedModals.remainingTiles && (
191
243
  <RemainingTilesModal isOpen={modals.remainingTiles} onClose={handleCloseRemainingTiles} />
@@ -1,5 +1,16 @@
1
- 'use-client';
1
+ import Head from 'next/head';
2
+ import { type FunctionComponent } from 'react';
2
3
 
3
4
  import { NotFound } from '@/components/NotFound';
4
5
 
5
- export default NotFound;
6
+ const NotFoundPage: FunctionComponent = () => (
7
+ <>
8
+ <Head>
9
+ <title>Page not found - Scrabble Solver</title>
10
+ </Head>
11
+
12
+ <NotFound />
13
+ </>
14
+ );
15
+
16
+ export default NotFoundPage;
@@ -8,8 +8,17 @@ export const BREAKPOINTS = {
8
8
  xl: Number(variables.breakpointXl),
9
9
  };
10
10
 
11
+ export const BREAKPOINT_HEIGHT_L = Number(variables.breakpointHeightL);
12
+
11
13
  export const GITHUB_PROJECT_URL = 'https://github.com/kamilmielnik/scrabble-solver';
12
14
 
15
+ export const NPM_PACKAGE_URL = 'https://www.npmjs.com/package/scrabble-solver';
16
+
17
+ export const SITE_URL = 'https://scrabble-solver.org';
18
+
19
+ export const SITE_DESCRIPTION =
20
+ 'Free, open-source board solver and word finder for Scrabble, Letter League, and Literaki. Enter your letters and board to get the highest-scoring words.';
21
+
13
22
  // Must match the class name in global.scss
14
23
  export const CONFIG_PENDING_CLASS = 'config-pending';
15
24
 
@@ -1,6 +1,7 @@
1
1
  import { type BoardJson, type Game, type Locale } from '@scrabble-solver/types';
2
2
 
3
3
  import { verifyLocally } from '@/solver-worker';
4
+ import { type VerifiedWord } from '@/types';
4
5
 
5
6
  import { fetchJson } from './fetchJson';
6
7
 
@@ -11,8 +12,8 @@ interface Payload {
11
12
  }
12
13
 
13
14
  interface Response {
14
- invalidWords: string[];
15
- validWords: string[];
15
+ invalidWords: VerifiedWord[];
16
+ validWords: VerifiedWord[];
16
17
  }
17
18
 
18
19
  export const verify = async ({ board, game, locale }: Payload): Promise<Response> => {
@@ -2,7 +2,7 @@ import { type ResultJson } from '@scrabble-solver/types';
2
2
  import { afterAll, beforeEach, describe, expect, it } from 'bun:test';
3
3
 
4
4
  import type * as clientModule from './client';
5
- import { type SolverWorkerRequest, type SolverWorkerResponse } from './messages';
5
+ import { type SolverWorkerRequest, type SolverWorkerResponse, type VerifyResult } from './messages';
6
6
 
7
7
  type Listener = (event: { data: SolverWorkerResponse }) => void;
8
8
 
@@ -102,9 +102,9 @@ describe('solver worker client', () => {
102
102
 
103
103
  it('resolves a verify with the worker words', async () => {
104
104
  const verify = verifyLocally(verifyPayload);
105
- respond({ data: { invalidWords: ['zz'], validWords: ['ab'] }, id: lastRequestId(), outcome: 'answered' });
105
+ respond({ data: createVerifyResult(), id: lastRequestId(), outcome: 'answered' });
106
106
 
107
- expect(await verify).toEqual({ invalidWords: ['zz'], validWords: ['ab'] });
107
+ expect(await verify).toEqual(createVerifyResult());
108
108
  });
109
109
 
110
110
  it('does not wait for a prefetch to answer', () => {
@@ -149,3 +149,10 @@ function lastRequestId(): number {
149
149
  function createResults(word: string): ResultJson[] {
150
150
  return [{ blankIndices: [], id: 0, isHorizontal: true, points: 1, tiles: word.split(''), x: 0, y: 0 }];
151
151
  }
152
+
153
+ function createVerifyResult(): VerifyResult {
154
+ return {
155
+ invalidWords: [{ direction: 'horizontal', isValid: false, word: 'zz', x: 0, y: 0 }],
156
+ validWords: [{ direction: 'vertical', isValid: true, word: 'ab', x: 3, y: 5 }],
157
+ };
158
+ }
@@ -5,7 +5,7 @@ import { BLANK } from '@scrabble-solver/constants';
5
5
  import { solve } from '@scrabble-solver/solver';
6
6
  import { Board, type Locale, Tile } from '@scrabble-solver/types';
7
7
 
8
- import { type SolveRequestPayload, type VerifyRequestPayload } from '@/types';
8
+ import { type SolveRequestPayload, type VerifiedWord, type VerifyRequestPayload } from '@/types';
9
9
 
10
10
  import { revalidateDictionary } from './dictionaries';
11
11
  import { getGaddag } from './getGaddag';
@@ -64,15 +64,15 @@ async function handleVerify(
64
64
  }
65
65
 
66
66
  const board = Board.fromJson(boardJson);
67
- const words = board.getWords().sort((a, b) => a.localeCompare(b, locale));
68
- const invalidWords: string[] = [];
69
- const validWords: string[] = [];
67
+ const words = board.getWords().sort((a, b) => a.word.localeCompare(b.word, locale));
68
+ const invalidWords: VerifiedWord[] = [];
69
+ const validWords: VerifiedWord[] = [];
70
70
 
71
71
  for (const word of words) {
72
- if (gaddag.has(word)) {
73
- validWords.push(word);
72
+ if (gaddag.has(word.word)) {
73
+ validWords.push({ ...word, isValid: true });
74
74
  } else {
75
- invalidWords.push(word);
75
+ invalidWords.push({ ...word, isValid: false });
76
76
  }
77
77
  }
78
78
 
@@ -1,10 +1,10 @@
1
1
  import { type Locale, type ResultJson } from '@scrabble-solver/types';
2
2
 
3
- import { type SolveRequestPayload, type VerifyRequestPayload } from '@/types';
3
+ import { type SolveRequestPayload, type VerifiedWord, type VerifyRequestPayload } from '@/types';
4
4
 
5
5
  export interface VerifyResult {
6
- invalidWords: string[];
7
- validWords: string[];
6
+ invalidWords: VerifiedWord[];
7
+ validWords: VerifiedWord[];
8
8
  }
9
9
 
10
10
  export type SolverWorkerRequest =
@@ -0,0 +1,4 @@
1
+ export * from './initialState';
2
+ export * from './selectors';
3
+ export * from './slice';
4
+ export * from './types';
@@ -0,0 +1,5 @@
1
+ import type { HoveredWordState } from './types';
2
+
3
+ export const hoveredWordInitialState: HoveredWordState = {
4
+ word: null,
5
+ };
@@ -0,0 +1,5 @@
1
+ import type { VerifiedWord } from '@/types';
2
+
3
+ import type { RootState } from '../types';
4
+
5
+ export const selectHoveredWord = (state: RootState): VerifiedWord | null => state.hoveredWord.word;