scrabble-solver 2.18.0 → 2.18.1

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 (74) hide show
  1. package/.nx/workspace-data/d/server-process.json +2 -2
  2. package/.nx/workspace-data/file-map.json +1481 -1485
  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/project-graph.json +71 -28
  7. package/.nx/workspace-data/source-maps.json +172 -0
  8. package/CLAUDE.md +1 -1
  9. package/bun.lock +29 -29
  10. package/e2e/app.spec.ts +1 -1
  11. package/e2e/features/solver-paths.spec.ts +2 -2
  12. package/lerna.json +1 -1
  13. package/package.json +18 -37
  14. package/packages/configs/package.json +13 -4
  15. package/packages/constants/package.json +6 -2
  16. package/packages/dictionaries/package.json +12 -5
  17. package/packages/logger/package.json +7 -2
  18. package/packages/scrabble-solver/.next/BUILD_ID +1 -1
  19. package/packages/scrabble-solver/.next/build-manifest.json +10 -10
  20. package/packages/scrabble-solver/.next/cache/.previewinfo +1 -1
  21. package/packages/scrabble-solver/.next/cache/.rscinfo +1 -1
  22. package/packages/scrabble-solver/.next/cache/.tsbuildinfo +1 -1
  23. package/packages/scrabble-solver/.next/cache/webpack/client-production/0.pack +0 -0
  24. package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack +0 -0
  25. package/packages/scrabble-solver/.next/cache/webpack/client-production/index.pack.old +0 -0
  26. package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack +0 -0
  27. package/packages/scrabble-solver/.next/cache/webpack/edge-server-production/index.pack.old +0 -0
  28. package/packages/scrabble-solver/.next/cache/webpack/server-production/0.pack +0 -0
  29. package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack +0 -0
  30. package/packages/scrabble-solver/.next/cache/webpack/server-production/index.pack.old +0 -0
  31. package/packages/scrabble-solver/.next/prerender-manifest.json +4 -4
  32. package/packages/scrabble-solver/.next/routes-manifest.json +1 -1
  33. package/packages/scrabble-solver/.next/server/chunks/712.js +1 -1
  34. package/packages/scrabble-solver/.next/server/middleware-build-manifest.js +1 -1
  35. package/packages/scrabble-solver/.next/server/pages/404.html +3 -2
  36. package/packages/scrabble-solver/.next/server/pages/500.html +3 -2
  37. package/packages/scrabble-solver/.next/server/pages/_document.js +1 -1
  38. package/packages/scrabble-solver/.next/server/pages/_error.js +2 -2
  39. package/packages/scrabble-solver/.next/server/pages/index.html +3 -2
  40. package/packages/scrabble-solver/.next/server/pages/index.js +3 -3
  41. package/packages/scrabble-solver/.next/server/pages/index.json +1 -1
  42. package/packages/scrabble-solver/.next/server/pages/not-found.html +3 -2
  43. package/packages/scrabble-solver/.next/server/pages-manifest.json +1 -1
  44. package/packages/scrabble-solver/.next/static/{FL82obwzFHRzvV6z76I-U → 5KOWzYqfY1Z2rjrzptr8D}/_buildManifest.js +1 -1
  45. package/packages/scrabble-solver/.next/static/chunks/pages/_app-2e607dcd4135f340.js +1 -0
  46. package/packages/scrabble-solver/.next/static/chunks/pages/index-e0087247c995328d.js +1 -0
  47. package/packages/scrabble-solver/.next/static/chunks/pages/not-found-a59f9deeeff63d17.js +1 -0
  48. package/packages/scrabble-solver/.next/static/chunks/{webpack-e34c3ae12275e978.js → webpack-cfb34d175e081981.js} +1 -1
  49. package/packages/scrabble-solver/.next/static/css/{abc2425d3ba607bc.css → 4dbcacc1122ac6b9.css} +1 -0
  50. package/packages/scrabble-solver/.next/trace +21 -21
  51. package/packages/scrabble-solver/.next/trace-build +1 -1
  52. package/packages/scrabble-solver/package.json +17 -10
  53. package/packages/scrabble-solver/public/robots.txt +1 -0
  54. package/packages/scrabble-solver/public/service-worker.js +1 -1
  55. package/packages/scrabble-solver/public/site.webmanifest +14 -0
  56. package/packages/scrabble-solver/src/lib/waitForIdleOrFirstIntent.ts +19 -8
  57. package/packages/scrabble-solver/src/pages/_app.tsx +40 -76
  58. package/packages/scrabble-solver/src/pages/_document.tsx +21 -19
  59. package/packages/scrabble-solver/src/pages/index.tsx +30 -3
  60. package/packages/scrabble-solver/src/pages/not-found.tsx +13 -2
  61. package/packages/scrabble-solver/src/parameters/index.ts +7 -0
  62. package/packages/scrabble-solver/src/state/sagas.ts +4 -4
  63. package/packages/scrabble-solver/tsconfig.tsbuildinfo +1 -1
  64. package/packages/solver/package.json +14 -6
  65. package/packages/types/package.json +10 -3
  66. package/packages/word-definitions/package.json +9 -3
  67. package/packages/word-lists/package.json +11 -3
  68. package/playwright-report/index.html +1 -1
  69. package/packages/scrabble-solver/.next/static/chunks/pages/_app-ba5d628e81a15461.js +0 -1
  70. package/packages/scrabble-solver/.next/static/chunks/pages/index-5f9fa6ac3cd3d97b.js +0 -1
  71. package/packages/scrabble-solver/.next/static/chunks/pages/not-found-23d61fd3ed791cad.js +0 -1
  72. package/packages/scrabble-solver/src/components/SeoMessage/SeoMessage.tsx +0 -18
  73. package/packages/scrabble-solver/src/components/SeoMessage/index.ts +0 -1
  74. /package/packages/scrabble-solver/.next/static/{FL82obwzFHRzvV6z76I-U → 5KOWzYqfY1Z2rjrzptr8D}/_ssgManifest.js +0 -0
@@ -1 +1 @@
1
- [{"name":"run-typescript","duration":158286,"timestamp":6510951452,"id":13,"parentId":1,"tags":{},"startTime":1786870651390,"traceId":"9b40d23f6be05912"},{"name":"run-webpack","duration":6341877,"timestamp":6511112797,"id":15,"parentId":1,"tags":{},"startTime":1786870651551,"traceId":"9b40d23f6be05912"},{"name":"static-check","duration":174428,"timestamp":6517506175,"id":2023,"parentId":1,"tags":{},"startTime":1786870657944,"traceId":"9b40d23f6be05912"},{"name":"static-generation","duration":239693,"timestamp":6517690173,"id":2041,"parentId":1,"tags":{},"startTime":1786870658128,"traceId":"9b40d23f6be05912"},{"name":"collect-build-traces","duration":4445976,"timestamp":6517680881,"id":2038,"parentId":1,"tags":{},"startTime":1786870658119,"traceId":"9b40d23f6be05912"},{"name":"telemetry-flush","duration":18,"timestamp":6522128075,"id":2048,"parentId":1,"tags":{},"startTime":1786870662566,"traceId":"9b40d23f6be05912"},{"name":"next-build","duration":11409192,"timestamp":6510718905,"id":1,"tags":{"buildMode":"default","version":"16.3.1","bundler":"webpack","has-custom-webpack-config":"true","use-build-worker":"false"},"startTime":1786870651157,"traceId":"9b40d23f6be05912"}]
1
+ [{"name":"run-typescript","duration":167919,"timestamp":15741743485,"id":13,"parentId":1,"tags":{},"startTime":1786879882182,"traceId":"09ff88835bed4614"},{"name":"run-webpack","duration":6553601,"timestamp":15741913668,"id":15,"parentId":1,"tags":{},"startTime":1786879882352,"traceId":"09ff88835bed4614"},{"name":"static-check","duration":184131,"timestamp":15748541724,"id":2019,"parentId":1,"tags":{},"startTime":1786879888980,"traceId":"09ff88835bed4614"},{"name":"static-generation","duration":249410,"timestamp":15748736408,"id":2037,"parentId":1,"tags":{},"startTime":1786879889175,"traceId":"09ff88835bed4614"},{"name":"collect-build-traces","duration":4410264,"timestamp":15748726538,"id":2034,"parentId":1,"tags":{},"startTime":1786879889165,"traceId":"09ff88835bed4614"},{"name":"telemetry-flush","duration":21,"timestamp":15753138117,"id":2044,"parentId":1,"tags":{},"startTime":1786879893576,"traceId":"09ff88835bed4614"},{"name":"next-build","duration":11637473,"timestamp":15741500668,"id":1,"tags":{"buildMode":"default","version":"16.3.1","bundler":"webpack","has-custom-webpack-config":"true","use-build-worker":"false"},"startTime":1786879881939,"traceId":"09ff88835bed4614"}]
@@ -1,7 +1,14 @@
1
1
  {
2
2
  "name": "@scrabble-solver/scrabble-solver",
3
- "version": "2.18.0",
4
- "description": "Scrabble Solver 2 - App",
3
+ "version": "2.18.1",
4
+ "description": "Scrabble Solver web app: free word finder and board solver for Scrabble, Super Scrabble, Literaki, and more.",
5
+ "keywords": [
6
+ "scrabble",
7
+ "word-finder",
8
+ "solver",
9
+ "nextjs",
10
+ "react"
11
+ ],
5
12
  "repository": {
6
13
  "type": "git",
7
14
  "url": "git+https://github.com/kamilmielnik/scrabble-solver.git"
@@ -11,7 +18,7 @@
11
18
  "email": "kamil@kamilmielnik.com",
12
19
  "url": "https://kamilmielnik.com"
13
20
  },
14
- "license": "CC BY-NC-ND 4.0",
21
+ "license": "CC-BY-NC-ND-4.0",
15
22
  "bugs": {
16
23
  "url": "https://github.com/kamilmielnik/scrabble-solver/issues"
17
24
  },
@@ -48,13 +55,13 @@
48
55
  "@floating-ui/react": "^0.27.20",
49
56
  "@kamilmielnik/gaddag": "^2.0.0",
50
57
  "@reduxjs/toolkit": "^2.12.0",
51
- "@scrabble-solver/configs": "^2.18.0",
52
- "@scrabble-solver/constants": "^2.18.0",
53
- "@scrabble-solver/dictionaries": "^2.18.0",
54
- "@scrabble-solver/logger": "^2.18.0",
55
- "@scrabble-solver/solver": "^2.18.0",
56
- "@scrabble-solver/types": "^2.18.0",
57
- "@scrabble-solver/word-definitions": "^2.18.0",
58
+ "@scrabble-solver/configs": "^2.18.1",
59
+ "@scrabble-solver/constants": "^2.18.1",
60
+ "@scrabble-solver/dictionaries": "^2.18.1",
61
+ "@scrabble-solver/logger": "^2.18.1",
62
+ "@scrabble-solver/solver": "^2.18.1",
63
+ "@scrabble-solver/types": "^2.18.1",
64
+ "@scrabble-solver/word-definitions": "^2.18.1",
58
65
  "classnames": "^2.5.1",
59
66
  "env-cmd": "^11.0.0",
60
67
  "include-media": "^2.0.0",
@@ -1,4 +1,5 @@
1
1
  User-agent: *
2
2
  Allow: /
3
+ Disallow: /api/
3
4
 
4
5
  Sitemap: https://scrabble-solver.org/sitemap.xml
@@ -1,2 +1,2 @@
1
1
  (()=>{"use strict";let e,t,s;var a,r={303:()=>{try{self["workbox:strategies:7.4.0"]&&_()}catch(e){}},448:()=>{try{self["workbox:routing:7.4.0"]&&_()}catch(e){}},693:()=>{try{self["workbox:core:7.4.0"]&&_()}catch(e){}},802:()=>{try{self["workbox:precaching:7.4.0"]&&_()}catch(e){}}},i={};function n(e){var t=i[e];if(void 0!==t)return t.exports;var s=i[e]={exports:{}},a=!0;try{r[e](s,s.exports,n),a=!1}finally{a&&delete i[e]}return s.exports}n(693);class l extends Error{constructor(e,t){super(((e,...t)=>{let s=e;return t.length>0&&(s+=` :: ${JSON.stringify(t)}`),s})(e,t)),this.name=e,this.details=t}}let c={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:"u">typeof registration?registration.scope:""},h=e=>[c.prefix,e,c.suffix].filter(e=>e&&e.length>0).join("-"),o=e=>e||h(c.precache);function u(e,t){let s=t();return e.waitUntil(s),s}n(802);class f{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:e,state:t})=>{t&&(t.originalRequest=e)},this.cachedResponseWillBeUsed=async({event:e,state:t,cachedResponse:s})=>{if("install"===e.type&&t&&t.originalRequest&&t.originalRequest instanceof Request){let e=t.originalRequest.url;s?this.notUpdatedURLs.push(e):this.updatedURLs.push(e)}return s}}}class d{constructor({precacheController:e}){this.cacheKeyWillBeUsed=async({request:e,params:t})=>{let s=(null==t?void 0:t.cacheKey)||this._precacheController.getCacheKeyForURL(e.url);return s?new Request(s,{headers:e.headers}):e},this._precacheController=e}}async function p(t,s){let a=null;if(t.url&&(a=new URL(t.url).origin),a!==self.location.origin)throw new l("cross-origin-copy-response",{origin:a});let r=t.clone(),i={headers:new Headers(r.headers),status:r.status,statusText:r.statusText},n=s?s(i):i,c=!function(){if(void 0===e){let t=new Response("");if("body"in t)try{new Response(t.body),e=!0}catch(t){e=!1}e=!1}return e}()?await r.blob():r.body;return new Response(c,n)}function w(e,t){let s=new URL(e);for(let e of t)s.searchParams.delete(e);return s.href}async function y(e,t,s,a){let r=w(t.url,s);if(t.url===r)return e.match(t,a);let i=Object.assign(Object.assign({},a),{ignoreSearch:!0});for(let n of(await e.keys(t,i)))if(r===w(n.url,s))return e.match(n,a)}class g{constructor(){this.promise=new Promise((e,t)=>{this.resolve=e,this.reject=t})}}let m=new Set;async function R(){for(let e of m)await e()}function C(e){return"string"==typeof e?new Request(e):e}n(303);class b{constructor(e,t){for(const s of(this._cacheKeys={},Object.assign(this,t),this.event=t.event,this._strategy=e,this._handlerDeferred=new g,this._extendLifetimePromises=[],this._plugins=[...e.plugins],this._pluginStateMap=new Map,this._plugins))this._pluginStateMap.set(s,{});this.event.waitUntil(this._handlerDeferred.promise)}async fetch(e){let{event:t}=this,s=C(e);if("navigate"===s.mode&&t instanceof FetchEvent&&t.preloadResponse){let e=await t.preloadResponse;if(e)return e}let a=this.hasCallback("fetchDidFail")?s.clone():null;try{for(let e of this.iterateCallbacks("requestWillFetch"))s=await e({request:s.clone(),event:t})}catch(e){if(e instanceof Error)throw new l("plugin-error-request-will-fetch",{thrownErrorMessage:e.message})}let r=s.clone();try{let e;for(let a of(e=await fetch(s,"navigate"===s.mode?void 0:this._strategy.fetchOptions),this.iterateCallbacks("fetchDidSucceed")))e=await a({event:t,request:r,response:e});return e}catch(e){throw a&&await this.runCallbacks("fetchDidFail",{error:e,event:t,originalRequest:a.clone(),request:r.clone()}),e}}async fetchAndCachePut(e){let t=await this.fetch(e),s=t.clone();return this.waitUntil(this.cachePut(e,s)),t}async cacheMatch(e){let t,s=C(e),{cacheName:a,matchOptions:r}=this._strategy,i=await this.getCacheKey(s,"read"),n=Object.assign(Object.assign({},r),{cacheName:a});for(let e of(t=await caches.match(i,n),this.iterateCallbacks("cachedResponseWillBeUsed")))t=await e({cacheName:a,matchOptions:r,cachedResponse:t,request:i,event:this.event})||void 0;return t}async cachePut(e,t){let s=C(e);await new Promise(e=>setTimeout(e,0));let a=await this.getCacheKey(s,"write");if(!t)throw new l("cache-put-with-no-response",{url:new URL(String(a.url),location.href).href.replace(RegExp(`^${location.origin}`),"")});let r=await this._ensureResponseSafeToCache(t);if(!r)return!1;let{cacheName:i,matchOptions:n}=this._strategy,c=await self.caches.open(i),h=this.hasCallback("cacheDidUpdate"),o=h?await y(c,a.clone(),["__WB_REVISION__"],n):null;try{await c.put(a,h?r.clone():r)}catch(e){if(e instanceof Error)throw"QuotaExceededError"===e.name&&await R(),e}for(let e of this.iterateCallbacks("cacheDidUpdate"))await e({cacheName:i,oldResponse:o,newResponse:r.clone(),request:a,event:this.event});return!0}async getCacheKey(e,t){let s=`${e.url} | ${t}`;if(!this._cacheKeys[s]){let a=e;for(let e of this.iterateCallbacks("cacheKeyWillBeUsed"))a=C(await e({mode:t,request:a,event:this.event,params:this.params}));this._cacheKeys[s]=a}return this._cacheKeys[s]}hasCallback(e){for(let t of this._strategy.plugins)if(e in t)return!0;return!1}async runCallbacks(e,t){for(let s of this.iterateCallbacks(e))await s(t)}*iterateCallbacks(e){for(let t of this._strategy.plugins)if("function"==typeof t[e]){let s=this._pluginStateMap.get(t),a=a=>{let r=Object.assign(Object.assign({},a),{state:s});return t[e](r)};yield a}}waitUntil(e){return this._extendLifetimePromises.push(e),e}async doneWaiting(){for(;this._extendLifetimePromises.length;){let e=this._extendLifetimePromises.splice(0),t=(await Promise.allSettled(e)).find(e=>"rejected"===e.status);if(t)throw t.reason}}destroy(){this._handlerDeferred.resolve(null)}async _ensureResponseSafeToCache(e){let t=e,s=!1;for(let e of this.iterateCallbacks("cacheWillUpdate"))if(t=await e({request:this.request,response:t,event:this.event})||void 0,s=!0,!t)break;return!s&&t&&200!==t.status&&(t=void 0),t}}class v{constructor(e={}){this.cacheName=(e=>e||h(c.runtime))(e.cacheName),this.plugins=e.plugins||[],this.fetchOptions=e.fetchOptions,this.matchOptions=e.matchOptions}handle(e){let[t]=this.handleAll(e);return t}handleAll(e){e instanceof FetchEvent&&(e={event:e,request:e.request});let t=e.event,s="string"==typeof e.request?new Request(e.request):e.request,a=new b(this,{event:t,request:s,params:"params"in e?e.params:void 0}),r=this._getResponse(a,s,t),i=this._awaitComplete(r,a,s,t);return[r,i]}async _getResponse(e,t,s){let a;await e.runCallbacks("handlerWillStart",{event:s,request:t});try{if(!(a=await this._handle(t,e))||"error"===a.type)throw new l("no-response",{url:t.url})}catch(r){if(r instanceof Error){for(let i of e.iterateCallbacks("handlerDidError"))if(a=await i({error:r,event:s,request:t}))break}if(a);else throw r}for(let r of e.iterateCallbacks("handlerWillRespond"))a=await r({event:s,request:t,response:a});return a}async _awaitComplete(e,t,s,a){let r,i;try{r=await e}catch(e){}try{await t.runCallbacks("handlerDidRespond",{event:a,request:s,response:r}),await t.doneWaiting()}catch(e){e instanceof Error&&(i=e)}if(await t.runCallbacks("handlerDidComplete",{event:a,request:s,response:r,error:i}),t.destroy(),i)throw i}}class L extends v{constructor(e={}){e.cacheName=o(e.cacheName),super(e),this._fallbackToNetwork=!1!==e.fallbackToNetwork,this.plugins.push(L.copyRedirectedCacheableResponsesPlugin)}async _handle(e,t){let s=await t.cacheMatch(e);return s||(t.event&&"install"===t.event.type?await this._handleInstall(e,t):await this._handleFetch(e,t))}async _handleFetch(e,t){let s,a=t.params||{};if(this._fallbackToNetwork){let r=a.integrity,i=e.integrity,n=!i||i===r;s=await t.fetch(new Request(e,{integrity:"no-cors"!==e.mode?i||r:void 0})),r&&n&&"no-cors"!==e.mode&&(this._useDefaultCacheabilityPluginIfNeeded(),await t.cachePut(e,s.clone()))}else throw new l("missing-precache-entry",{cacheName:this.cacheName,url:e.url});return s}async _handleInstall(e,t){this._useDefaultCacheabilityPluginIfNeeded();let s=await t.fetch(e);if(!await t.cachePut(e,s.clone()))throw new l("bad-precaching-response",{url:e.url,status:s.status});return s}_useDefaultCacheabilityPluginIfNeeded(){let e=null,t=0;for(let[s,a]of this.plugins.entries())a!==L.copyRedirectedCacheableResponsesPlugin&&(a===L.defaultPrecacheCacheabilityPlugin&&(e=s),a.cacheWillUpdate&&t++);0===t?this.plugins.push(L.defaultPrecacheCacheabilityPlugin):t>1&&null!==e&&this.plugins.splice(e,1)}}L.defaultPrecacheCacheabilityPlugin={cacheWillUpdate:async({response:e})=>!e||e.status>=400?null:e},L.copyRedirectedCacheableResponsesPlugin={cacheWillUpdate:async({response:e})=>e.redirected?await p(e):e};class U{constructor({cacheName:e,plugins:t=[],fallbackToNetwork:s=!0}={}){this._urlsToCacheKeys=new Map,this._urlsToCacheModes=new Map,this._cacheKeysToIntegrities=new Map,this._strategy=new L({cacheName:o(e),plugins:[...t,new d({precacheController:this})],fallbackToNetwork:s}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this._strategy}precache(e){this.addToCacheList(e),this._installAndActiveListenersAdded||(self.addEventListener("install",this.install),self.addEventListener("activate",this.activate),this._installAndActiveListenersAdded=!0)}addToCacheList(e){let t=[];for(let s of e){"string"==typeof s?t.push(s):s&&void 0===s.revision&&t.push(s.url);let{cacheKey:e,url:a}=function(e){if(!e)throw new l("add-to-cache-list-unexpected-type",{entry:e});if("string"==typeof e){let t=new URL(e,location.href);return{cacheKey:t.href,url:t.href}}let{revision:t,url:s}=e;if(!s)throw new l("add-to-cache-list-unexpected-type",{entry:e});if(!t){let e=new URL(s,location.href);return{cacheKey:e.href,url:e.href}}let a=new URL(s,location.href),r=new URL(s,location.href);return a.searchParams.set("__WB_REVISION__",t),{cacheKey:a.href,url:r.href}}(s),r="string"!=typeof s&&s.revision?"reload":"default";if(this._urlsToCacheKeys.has(a)&&this._urlsToCacheKeys.get(a)!==e)throw new l("add-to-cache-list-conflicting-entries",{firstEntry:this._urlsToCacheKeys.get(a),secondEntry:e});if("string"!=typeof s&&s.integrity){if(this._cacheKeysToIntegrities.has(e)&&this._cacheKeysToIntegrities.get(e)!==s.integrity)throw new l("add-to-cache-list-conflicting-integrities",{url:a});this._cacheKeysToIntegrities.set(e,s.integrity)}this._urlsToCacheKeys.set(a,e),this._urlsToCacheModes.set(a,r),t.length>0&&console.warn(`Workbox is precaching URLs without revision info: ${t.join(", ")}
2
- This is generally NOT safe. Learn more at https://bit.ly/wb-precache`)}}install(e){return u(e,async()=>{let t=new f;for(let[s,a]of(this.strategy.plugins.push(t),this._urlsToCacheKeys)){let t=this._cacheKeysToIntegrities.get(a),r=this._urlsToCacheModes.get(s),i=new Request(s,{integrity:t,cache:r,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:a},request:i,event:e}))}let{updatedURLs:s,notUpdatedURLs:a}=t;return{updatedURLs:s,notUpdatedURLs:a}})}activate(e){return u(e,async()=>{let e=await self.caches.open(this.strategy.cacheName),t=await e.keys(),s=new Set(this._urlsToCacheKeys.values()),a=[];for(let r of t)s.has(r.url)||(await e.delete(r),a.push(r.url));return{deletedURLs:a}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){let t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){let t=e instanceof Request?e.url:e,s=this.getCacheKeyForURL(t);if(s)return(await self.caches.open(this.strategy.cacheName)).match(s)}createHandlerBoundToURL(e){let t=this.getCacheKeyForURL(e);if(!t)throw new l("non-precached-url",{url:e});return s=>(s.request=new Request(e),s.params=Object.assign({cacheKey:t},s.params),this.strategy.handle(s))}}let k=()=>(t||(t=new U),t);n(448);let T=e=>e&&"object"==typeof e?e:{handle:e};class K{constructor(e,t,s="GET"){this.handler=T(t),this.match=e,this.method=s}setCatchHandler(e){this.catchHandler=T(e)}}class x extends K{constructor(e,t,s){super(({url:t})=>{let s=e.exec(t.href);if(s)return t.origin!==location.origin&&0!==s.index?void 0:s.slice(1)},t,s)}}class P{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener("fetch",e=>{let{request:t}=e,s=this.handleRequest({request:t,event:e});s&&e.respondWith(s)})}addCacheListener(){self.addEventListener("message",e=>{if(e.data&&"CACHE_URLS"===e.data.type){let{payload:t}=e.data,s=Promise.all(t.urlsToCache.map(t=>{"string"==typeof t&&(t=[t]);let s=new Request(...t);return this.handleRequest({request:s,event:e})}));e.waitUntil(s),e.ports&&e.ports[0]&&s.then(()=>e.ports[0].postMessage(!0))}})}handleRequest({request:e,event:t}){let s,a=new URL(e.url,location.href);if(!a.protocol.startsWith("http"))return;let r=a.origin===location.origin,{params:i,route:n}=this.findMatchingRoute({event:t,request:e,sameOrigin:r,url:a}),l=n&&n.handler,c=e.method;if(!l&&this._defaultHandlerMap.has(c)&&(l=this._defaultHandlerMap.get(c)),!l)return;try{s=l.handle({url:a,request:e,event:t,params:i})}catch(e){s=Promise.reject(e)}let h=n&&n.catchHandler;return s instanceof Promise&&(this._catchHandler||h)&&(s=s.catch(async s=>{if(h)try{return await h.handle({url:a,request:e,event:t,params:i})}catch(e){e instanceof Error&&(s=e)}if(this._catchHandler)return this._catchHandler.handle({url:a,request:e,event:t});throw s})),s}findMatchingRoute({url:e,sameOrigin:t,request:s,event:a}){for(let r of this._routes.get(s.method)||[]){let i,n=r.match({url:e,sameOrigin:t,request:s,event:a});if(n)return Array.isArray(i=n)&&0===i.length||n.constructor===Object&&0===Object.keys(n).length?i=void 0:"boolean"==typeof n&&(i=void 0),{route:r,params:i}}return{}}setDefaultHandler(e,t="GET"){this._defaultHandlerMap.set(t,T(e))}setCatchHandler(e){this._catchHandler=T(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new l("unregister-route-but-not-found-with-method",{method:e.method});let t=this._routes.get(e.method).indexOf(e);if(t>-1)this._routes.get(e.method).splice(t,1);else throw new l("unregister-route-route-not-registered")}}function q(e,t,a){let r;if("string"==typeof e){let s=new URL(e,location.href);r=new K(({url:e})=>e.href===s.href,t,a)}else if(e instanceof RegExp)r=new x(e,t,a);else if("function"==typeof e)r=new K(e,t,a);else if(e instanceof K)r=e;else throw new l("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});return(!s&&((s=new P).addFetchListener(),s.addCacheListener()),s).registerRoute(r),r}class E extends K{constructor(e,t){super(({request:s})=>{let a=e.getURLsToCacheKeys();for(let r of function*(e,{ignoreURLParametersMatching:t=[/^utm_/,/^fbclid$/],directoryIndex:s="index.html",cleanURLs:a=!0,urlManipulation:r}={}){let i=new URL(e,location.href);i.hash="",yield i.href;let n=function(e,t=[]){for(let s of[...e.searchParams.keys()])t.some(e=>e.test(s))&&e.searchParams.delete(s);return e}(i,t);if(yield n.href,s&&n.pathname.endsWith("/")){let e=new URL(n.href);e.pathname+=s,yield e.href}if(a){let e=new URL(n.href);e.pathname+=".html",yield e.href}if(r)for(let e of r({url:i}))yield e.href}(s.url,t)){let t=a.get(r);if(t){let s=e.getIntegrityForCacheKey(t);return{cacheKey:t,integrity:s}}}},e.strategy)}}let M="-precache-",N=async(e,t=M)=>{let s=(await self.caches.keys()).filter(s=>s.includes(t)&&s.includes(self.registration.scope)&&s!==e);return await Promise.all(s.map(e=>self.caches.delete(e))),s};class W extends K{constructor(e,{allowlist:t=[/./],denylist:s=[]}={}){super(e=>this._match(e),e),this._allowlist=t,this._denylist=s}_match({url:e,request:t}){if(t&&"navigate"!==t.mode)return!1;let s=e.pathname+e.search;for(let e of this._denylist)if(e.test(s))return!1;return!!this._allowlist.some(e=>e.test(s))}}let O="app-shell";async function I(){let e=await caches.open(O);await e.add("/")}async function S(e){let{request:t}=e;try{return await fetch(t)}catch(s){let e=await caches.open(O),t=await e.match("/");if(!t)throw s;return t}}self.addEventListener("install",e=>{e.waitUntil(I()),self.skipWaiting()}),self.addEventListener("activate",()=>{self.clients.claim()}),self.addEventListener("activate",e=>{let t=o();e.waitUntil(N(t).then(e=>{}))}),a=[{'revision':'0707b4bb1472b9e3965e35b76761e790','url':'/_next/build-manifest.json'},{'revision':'82cbcbcce8a0a8cb21c5e4efa1de5e4c','url':'/_next/dynamic-css-manifest.json'},{'revision':'1da1766c9299366bb973f535ec69ff90','url':'/_next/react-loadable-manifest.json'},{'revision':'271476b8fdb65f6d540f92faa6bca0be','url':'/_next/server/dynamic-css-manifest.js'},{'revision':'74461776ec2e686577afaa0b23cd248a','url':'/_next/server/middleware-build-manifest.js'},{'revision':'72c6f757460ce938d861f3c2673b3bab','url':'/_next/server/middleware-react-loadable-manifest.js'},{'revision':'9fce7989bff5d35b01e177447faca50d','url':'/_next/server/next-font-manifest.js'},{'revision':'d51420cd4aa5d37d6719849cf36d0d6f','url':'/_next/server/next-font-manifest.json'},{'revision':'fb88ebaa5db2efc73edc730122dfc471','url':'/_next/static/FL82obwzFHRzvV6z76I-U/_buildManifest.js'},{'revision':'b6652df95db52feb4daf4eca35380933','url':'/_next/static/FL82obwzFHRzvV6z76I-U/_ssgManifest.js'},{'revision':null,'url':'/_next/static/chunks/101-829ab8a8d310e7a0.js'},{'revision':null,'url':'/_next/static/chunks/106.e414e88694add27f.js'},{'revision':null,'url':'/_next/static/chunks/127.125d896a19061d71.js'},{'revision':null,'url':'/_next/static/chunks/18.8b91e137d239e15c.js'},{'revision':null,'url':'/_next/static/chunks/277.6785dfaf22fdb0b7.js'},{'revision':null,'url':'/_next/static/chunks/307.67f52f13e3d60d4e.js'},{'revision':null,'url':'/_next/static/chunks/341.79535ba85bf1986f.js'},{'revision':null,'url':'/_next/static/chunks/363.01eed3233f92b6a4.js'},{'revision':null,'url':'/_next/static/chunks/422.1559ae6756912ca5.js'},{'revision':null,'url':'/_next/static/chunks/541.dcd9e8b804f88da5.js'},{'revision':null,'url':'/_next/static/chunks/583.7a46807be14c377e.js'},{'revision':null,'url':'/_next/static/chunks/631.98d42abad0a60af6.js'},{'revision':null,'url':'/_next/static/chunks/663.d2c9ca36b26695b8.js'},{'revision':null,'url':'/_next/static/chunks/711.ba7b8f900dfd40d0.js'},{'revision':null,'url':'/_next/static/chunks/744.b06c9a3de4a1d877.js'},{'revision':null,'url':'/_next/static/chunks/820.97b82f751ca49579.js'},{'revision':null,'url':'/_next/static/chunks/865.a9b563a45848a83c.js'},{'revision':null,'url':'/_next/static/chunks/912.ff8037ba9d4db5c8.js'},{'revision':null,'url':'/_next/static/chunks/939.19b33a3d3c351d73.js'},{'revision':null,'url':'/_next/static/chunks/990.61296e5870b91f11.js'},{'revision':null,'url':'/_next/static/chunks/framework-626cbd48ac5aca57.js'},{'revision':null,'url':'/_next/static/chunks/main-b92e08f3fc96dd8d.js'},{'revision':null,'url':'/_next/static/chunks/pages/_app-ba5d628e81a15461.js'},{'revision':null,'url':'/_next/static/chunks/pages/_error-f0290a98eb3ed0ec.js'},{'revision':null,'url':'/_next/static/chunks/pages/index-5f9fa6ac3cd3d97b.js'},{'revision':null,'url':'/_next/static/chunks/pages/not-found-23d61fd3ed791cad.js'},{'revision':'846118c33b2c0e922d7b3a7676f81f6f','url':'/_next/static/chunks/polyfills-42372ed130431b0a.js'},{'revision':null,'url':'/_next/static/chunks/webpack-e34c3ae12275e978.js'}],k().precache(a),q(new E(k(),void 0)),q(new W(S,{denylist:[/^\/api\//]}))})();
2
+ This is generally NOT safe. Learn more at https://bit.ly/wb-precache`)}}install(e){return u(e,async()=>{let t=new f;for(let[s,a]of(this.strategy.plugins.push(t),this._urlsToCacheKeys)){let t=this._cacheKeysToIntegrities.get(a),r=this._urlsToCacheModes.get(s),i=new Request(s,{integrity:t,cache:r,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:a},request:i,event:e}))}let{updatedURLs:s,notUpdatedURLs:a}=t;return{updatedURLs:s,notUpdatedURLs:a}})}activate(e){return u(e,async()=>{let e=await self.caches.open(this.strategy.cacheName),t=await e.keys(),s=new Set(this._urlsToCacheKeys.values()),a=[];for(let r of t)s.has(r.url)||(await e.delete(r),a.push(r.url));return{deletedURLs:a}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){let t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){let t=e instanceof Request?e.url:e,s=this.getCacheKeyForURL(t);if(s)return(await self.caches.open(this.strategy.cacheName)).match(s)}createHandlerBoundToURL(e){let t=this.getCacheKeyForURL(e);if(!t)throw new l("non-precached-url",{url:e});return s=>(s.request=new Request(e),s.params=Object.assign({cacheKey:t},s.params),this.strategy.handle(s))}}let k=()=>(t||(t=new U),t);n(448);let T=e=>e&&"object"==typeof e?e:{handle:e};class K{constructor(e,t,s="GET"){this.handler=T(t),this.match=e,this.method=s}setCatchHandler(e){this.catchHandler=T(e)}}class x extends K{constructor(e,t,s){super(({url:t})=>{let s=e.exec(t.href);if(s)return t.origin!==location.origin&&0!==s.index?void 0:s.slice(1)},t,s)}}class P{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener("fetch",e=>{let{request:t}=e,s=this.handleRequest({request:t,event:e});s&&e.respondWith(s)})}addCacheListener(){self.addEventListener("message",e=>{if(e.data&&"CACHE_URLS"===e.data.type){let{payload:t}=e.data,s=Promise.all(t.urlsToCache.map(t=>{"string"==typeof t&&(t=[t]);let s=new Request(...t);return this.handleRequest({request:s,event:e})}));e.waitUntil(s),e.ports&&e.ports[0]&&s.then(()=>e.ports[0].postMessage(!0))}})}handleRequest({request:e,event:t}){let s,a=new URL(e.url,location.href);if(!a.protocol.startsWith("http"))return;let r=a.origin===location.origin,{params:i,route:n}=this.findMatchingRoute({event:t,request:e,sameOrigin:r,url:a}),l=n&&n.handler,c=e.method;if(!l&&this._defaultHandlerMap.has(c)&&(l=this._defaultHandlerMap.get(c)),!l)return;try{s=l.handle({url:a,request:e,event:t,params:i})}catch(e){s=Promise.reject(e)}let h=n&&n.catchHandler;return s instanceof Promise&&(this._catchHandler||h)&&(s=s.catch(async s=>{if(h)try{return await h.handle({url:a,request:e,event:t,params:i})}catch(e){e instanceof Error&&(s=e)}if(this._catchHandler)return this._catchHandler.handle({url:a,request:e,event:t});throw s})),s}findMatchingRoute({url:e,sameOrigin:t,request:s,event:a}){for(let r of this._routes.get(s.method)||[]){let i,n=r.match({url:e,sameOrigin:t,request:s,event:a});if(n)return Array.isArray(i=n)&&0===i.length||n.constructor===Object&&0===Object.keys(n).length?i=void 0:"boolean"==typeof n&&(i=void 0),{route:r,params:i}}return{}}setDefaultHandler(e,t="GET"){this._defaultHandlerMap.set(t,T(e))}setCatchHandler(e){this._catchHandler=T(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new l("unregister-route-but-not-found-with-method",{method:e.method});let t=this._routes.get(e.method).indexOf(e);if(t>-1)this._routes.get(e.method).splice(t,1);else throw new l("unregister-route-route-not-registered")}}function q(e,t,a){let r;if("string"==typeof e){let s=new URL(e,location.href);r=new K(({url:e})=>e.href===s.href,t,a)}else if(e instanceof RegExp)r=new x(e,t,a);else if("function"==typeof e)r=new K(e,t,a);else if(e instanceof K)r=e;else throw new l("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});return(!s&&((s=new P).addFetchListener(),s.addCacheListener()),s).registerRoute(r),r}class E extends K{constructor(e,t){super(({request:s})=>{let a=e.getURLsToCacheKeys();for(let r of function*(e,{ignoreURLParametersMatching:t=[/^utm_/,/^fbclid$/],directoryIndex:s="index.html",cleanURLs:a=!0,urlManipulation:r}={}){let i=new URL(e,location.href);i.hash="",yield i.href;let n=function(e,t=[]){for(let s of[...e.searchParams.keys()])t.some(e=>e.test(s))&&e.searchParams.delete(s);return e}(i,t);if(yield n.href,s&&n.pathname.endsWith("/")){let e=new URL(n.href);e.pathname+=s,yield e.href}if(a){let e=new URL(n.href);e.pathname+=".html",yield e.href}if(r)for(let e of r({url:i}))yield e.href}(s.url,t)){let t=a.get(r);if(t){let s=e.getIntegrityForCacheKey(t);return{cacheKey:t,integrity:s}}}},e.strategy)}}let M="-precache-",N=async(e,t=M)=>{let s=(await self.caches.keys()).filter(s=>s.includes(t)&&s.includes(self.registration.scope)&&s!==e);return await Promise.all(s.map(e=>self.caches.delete(e))),s};class W extends K{constructor(e,{allowlist:t=[/./],denylist:s=[]}={}){super(e=>this._match(e),e),this._allowlist=t,this._denylist=s}_match({url:e,request:t}){if(t&&"navigate"!==t.mode)return!1;let s=e.pathname+e.search;for(let e of this._denylist)if(e.test(s))return!1;return!!this._allowlist.some(e=>e.test(s))}}let O="app-shell";async function I(){let e=await caches.open(O);await e.add("/")}async function S(e){let{request:t}=e;try{return await fetch(t)}catch(s){let e=await caches.open(O),t=await e.match("/");if(!t)throw s;return t}}self.addEventListener("install",e=>{e.waitUntil(I()),self.skipWaiting()}),self.addEventListener("activate",()=>{self.clients.claim()}),self.addEventListener("activate",e=>{let t=o();e.waitUntil(N(t).then(e=>{}))}),a=[{'revision':'8486f920bd3e2ac450039aafcb1d8f76','url':'/_next/build-manifest.json'},{'revision':'82cbcbcce8a0a8cb21c5e4efa1de5e4c','url':'/_next/dynamic-css-manifest.json'},{'revision':'1da1766c9299366bb973f535ec69ff90','url':'/_next/react-loadable-manifest.json'},{'revision':'271476b8fdb65f6d540f92faa6bca0be','url':'/_next/server/dynamic-css-manifest.js'},{'revision':'260f621e04297bcecd94427e45d7405b','url':'/_next/server/middleware-build-manifest.js'},{'revision':'72c6f757460ce938d861f3c2673b3bab','url':'/_next/server/middleware-react-loadable-manifest.js'},{'revision':'9fce7989bff5d35b01e177447faca50d','url':'/_next/server/next-font-manifest.js'},{'revision':'d51420cd4aa5d37d6719849cf36d0d6f','url':'/_next/server/next-font-manifest.json'},{'revision':'d09f2baf8f9ee346bc1af7cbc794463f','url':'/_next/static/5KOWzYqfY1Z2rjrzptr8D/_buildManifest.js'},{'revision':'b6652df95db52feb4daf4eca35380933','url':'/_next/static/5KOWzYqfY1Z2rjrzptr8D/_ssgManifest.js'},{'revision':null,'url':'/_next/static/chunks/101-829ab8a8d310e7a0.js'},{'revision':null,'url':'/_next/static/chunks/106.e414e88694add27f.js'},{'revision':null,'url':'/_next/static/chunks/127.125d896a19061d71.js'},{'revision':null,'url':'/_next/static/chunks/18.8b91e137d239e15c.js'},{'revision':null,'url':'/_next/static/chunks/277.6785dfaf22fdb0b7.js'},{'revision':null,'url':'/_next/static/chunks/307.67f52f13e3d60d4e.js'},{'revision':null,'url':'/_next/static/chunks/341.79535ba85bf1986f.js'},{'revision':null,'url':'/_next/static/chunks/363.01eed3233f92b6a4.js'},{'revision':null,'url':'/_next/static/chunks/422.1559ae6756912ca5.js'},{'revision':null,'url':'/_next/static/chunks/541.dcd9e8b804f88da5.js'},{'revision':null,'url':'/_next/static/chunks/583.7a46807be14c377e.js'},{'revision':null,'url':'/_next/static/chunks/631.98d42abad0a60af6.js'},{'revision':null,'url':'/_next/static/chunks/663.d2c9ca36b26695b8.js'},{'revision':null,'url':'/_next/static/chunks/711.ba7b8f900dfd40d0.js'},{'revision':null,'url':'/_next/static/chunks/744.b06c9a3de4a1d877.js'},{'revision':null,'url':'/_next/static/chunks/820.97b82f751ca49579.js'},{'revision':null,'url':'/_next/static/chunks/865.a9b563a45848a83c.js'},{'revision':null,'url':'/_next/static/chunks/912.ff8037ba9d4db5c8.js'},{'revision':null,'url':'/_next/static/chunks/939.19b33a3d3c351d73.js'},{'revision':null,'url':'/_next/static/chunks/990.61296e5870b91f11.js'},{'revision':null,'url':'/_next/static/chunks/framework-626cbd48ac5aca57.js'},{'revision':null,'url':'/_next/static/chunks/main-b92e08f3fc96dd8d.js'},{'revision':null,'url':'/_next/static/chunks/pages/_app-2e607dcd4135f340.js'},{'revision':null,'url':'/_next/static/chunks/pages/_error-f0290a98eb3ed0ec.js'},{'revision':null,'url':'/_next/static/chunks/pages/index-e0087247c995328d.js'},{'revision':null,'url':'/_next/static/chunks/pages/not-found-a59f9deeeff63d17.js'},{'revision':'846118c33b2c0e922d7b3a7676f81f6f','url':'/_next/static/chunks/polyfills-42372ed130431b0a.js'},{'revision':null,'url':'/_next/static/chunks/webpack-cfb34d175e081981.js'}],k().precache(a),q(new E(k(),void 0)),q(new W(S,{denylist:[/^\/api\//]}))})();
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "Scrabble Solver",
3
+ "short_name": "Scrabble Solver",
4
+ "description": "Free Scrabble word finder and board solver.",
5
+ "start_url": "/",
6
+ "display": "standalone",
7
+ "background_color": "#f4f4f4",
8
+ "theme_color": "#efe3ae",
9
+ "icons": [
10
+ { "src": "/icons/favicon-196x196.png", "sizes": "196x196", "type": "image/png" },
11
+ { "src": "/icons/favicon-128.png", "sizes": "128x128", "type": "image/png" },
12
+ { "src": "/icons/favicon.svg", "sizes": "any", "type": "image/svg+xml" }
13
+ ]
14
+ }
@@ -1,21 +1,32 @@
1
1
  export function waitForIdleOrFirstIntent(): Promise<void> {
2
2
  return new Promise((resolve) => {
3
- const settle = () => {
4
- window.removeEventListener('pointerdown', settle);
5
- window.removeEventListener('keydown', settle);
6
- resolve();
7
- };
8
-
9
- window.addEventListener('pointerdown', settle, { once: true });
10
- window.addEventListener('keydown', settle, { once: true });
3
+ const settle = onFirstIntent(resolve);
11
4
  whenIdle(settle);
12
5
  });
13
6
  }
14
7
 
8
+ export function waitForFirstIntent(): Promise<void> {
9
+ return new Promise((resolve) => {
10
+ onFirstIntent(resolve);
11
+ });
12
+ }
13
+
15
14
  export function waitForIdle(): Promise<void> {
16
15
  return new Promise((resolve) => whenIdle(resolve));
17
16
  }
18
17
 
18
+ function onFirstIntent(callback: () => void): () => void {
19
+ const settle = () => {
20
+ window.removeEventListener('pointerdown', settle);
21
+ window.removeEventListener('keydown', settle);
22
+ callback();
23
+ };
24
+
25
+ window.addEventListener('pointerdown', settle, { once: true });
26
+ window.addEventListener('keydown', settle, { once: true });
27
+ return settle;
28
+ }
29
+
19
30
  function whenIdle(callback: () => void): void {
20
31
  const requestIdle = () => {
21
32
  if ('requestIdleCallback' in window) {
@@ -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>
@@ -18,7 +18,7 @@ import { useLanguage } from '@/hooks/useLanguage';
18
18
  import { useLocalStorage } from '@/hooks/useLocalStorage';
19
19
  import { LOCALE_FEATURES } from '@/i18n/constants';
20
20
  import { schedulePreloadModals } from '@/modals/preload';
21
- import { CONFIG_PENDING_CLASS } from '@/parameters';
21
+ import { CONFIG_PENDING_CLASS, GITHUB_PROJECT_URL, NPM_PACKAGE_URL, SITE_DESCRIPTION, SITE_URL } from '@/parameters';
22
22
  import { registerServiceWorker } from '@/serviceWorkerManager';
23
23
  import { initialize, reset, selectConfig, selectIsHydrated, selectLocale, useTypedSelector } from '@/state';
24
24
 
@@ -49,6 +49,30 @@ const WordsModal = dynamic(() => import('@/modals/WordsModal').then((module) =>
49
49
  ssr: false,
50
50
  });
51
51
 
52
+ const WEB_APPLICATION_JSON_LD = JSON.stringify({
53
+ '@context': 'https://schema.org',
54
+ '@type': 'WebApplication',
55
+ name: 'Scrabble Solver',
56
+ alternateName: 'Scrabble Solver 2',
57
+ url: `${SITE_URL}/`,
58
+ description: SITE_DESCRIPTION,
59
+ applicationCategory: 'GameApplication',
60
+ operatingSystem: 'Any',
61
+ browserRequirements: 'Requires JavaScript',
62
+ image: `${SITE_URL}/og.png`,
63
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
64
+ inLanguage: ['en-US', 'en-GB', 'de-DE', 'es-ES', 'fa-IR', 'fr-FR', 'pl-PL', 'ro-RO', 'tr-TR'],
65
+ featureList: [
66
+ 'Finds the highest-scoring words for a given board and rack',
67
+ 'Supports Scrabble, Super Scrabble, Scrabble Duel, Letter League, Crossplay, Literaki, and Kelimelik',
68
+ 'Dictionaries and word definitions in 8 languages',
69
+ 'Works offline once a dictionary is cached',
70
+ 'Free, open source, no ads, no sign-up',
71
+ ],
72
+ sameAs: [GITHUB_PROJECT_URL, NPM_PACKAGE_URL],
73
+ author: { '@type': 'Person', name: 'Kamil Mielnik', url: 'https://kamilmielnik.com' },
74
+ });
75
+
52
76
  interface Props {
53
77
  version: string;
54
78
  }
@@ -149,11 +173,11 @@ const Index: FunctionComponent<Props> = ({ version }) => {
149
173
  <>
150
174
  <nav className={styles.nav}>
151
175
  <div className={styles.navContent}>
152
- <div className={styles.navLogo}>
176
+ <h1 className={styles.navLogo}>
153
177
  <a className={styles.logoContainer} href="/" title={version}>
154
178
  <Logo className={styles.logo} />
155
179
  </a>
156
- </div>
180
+ </h1>
157
181
 
158
182
  <NavButtons
159
183
  onClear={handleClear}
@@ -170,6 +194,9 @@ const Index: FunctionComponent<Props> = ({ version }) => {
170
194
  <Solver className={styles.solver} onShowResults={handleShowResults} />
171
195
  </main>
172
196
 
197
+ {/* eslint-disable-next-line react/no-danger */}
198
+ <script dangerouslySetInnerHTML={{ __html: WEB_APPLICATION_JSON_LD }} type="application/ld+json" />
199
+
173
200
  {mountedModals.menu && (
174
201
  <MenuModal
175
202
  isOpen={modals.menu}
@@ -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;
@@ -10,6 +10,13 @@ export const BREAKPOINTS = {
10
10
 
11
11
  export const GITHUB_PROJECT_URL = 'https://github.com/kamilmielnik/scrabble-solver';
12
12
 
13
+ export const NPM_PACKAGE_URL = 'https://www.npmjs.com/package/scrabble-solver';
14
+
15
+ export const SITE_URL = 'https://scrabble-solver.org';
16
+
17
+ export const SITE_DESCRIPTION =
18
+ '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.';
19
+
13
20
  // Must match the class name in global.scss
14
21
  export const CONFIG_PENDING_CLASS = 'config-pending';
15
22
 
@@ -11,7 +11,7 @@ import { call, delay, put, select, spawn, takeEvery, takeLatest } from 'redux-sa
11
11
  import { LOCALE_FEATURES } from '@/i18n/constants';
12
12
  import { loadTranslations } from '@/i18n/i18n';
13
13
  import { memoize } from '@/lib/memoize';
14
- import { waitForIdleOrFirstIntent } from '@/lib/waitForIdleOrFirstIntent';
14
+ import { waitForFirstIntent, waitForIdleOrFirstIntent } from '@/lib/waitForIdleOrFirstIntent';
15
15
  import { findWordDefinitions, solve, verify, visit } from '@/sdk';
16
16
  import { prefetchDictionary } from '@/solver-worker';
17
17
 
@@ -139,7 +139,7 @@ function* onInitialize({ payload }: PayloadAction<{ version: string }>): AnyGene
139
139
  const board = yield select(selectBoard);
140
140
  const locale = yield select(selectLocale);
141
141
 
142
- yield spawn(prefetchDictionaryWhenIdle);
142
+ yield spawn(prefetchDictionaryOnFirstIntent);
143
143
  yield spawn(loadLocaleTranslations, locale);
144
144
  yield spawn(preloadTranslationsWhenIdle);
145
145
  yield spawn(visitWhenIdle);
@@ -202,8 +202,8 @@ function* visitWhenIdle(): AnyGenerator {
202
202
  yield call(visit);
203
203
  }
204
204
 
205
- function* prefetchDictionaryWhenIdle(): AnyGenerator {
206
- yield call(waitForIdleOrFirstIntent);
205
+ function* prefetchDictionaryOnFirstIntent(): AnyGenerator {
206
+ yield call(waitForFirstIntent);
207
207
  const locale = yield select(selectLocale);
208
208
  yield call(prefetchDictionary, locale);
209
209
  }