diff --git a/package.json b/package.json index 1a5e7f6..702e1bb 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "build": "astro build", "preview": "astro preview", "lint": "eslint", - "test": "playwright test", + "test:e2e": "playwright test", + "test:unit": "vitest", "astro": "astro" }, "dependencies": { @@ -45,6 +46,7 @@ "playwright": "^1.48.0", "typescript": "5.8.3", "typescript-eslint": "^8.50.0", + "vitest": "^4.0.18", "wrangler": "^4.54.0" }, "pnpm": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 646b99e..141a10a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -106,6 +106,9 @@ importers: typescript-eslint: specifier: ^8.50.0 version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.8.3) + vitest: + specifier: ^4.0.18 + version: 4.0.18(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) wrangler: specifier: ^4.54.0 version: 4.61.0(@cloudflare/workers-types@4.20260129.0) @@ -1116,6 +1119,9 @@ packages: '@speed-highlight/core@1.2.14': resolution: {integrity: sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tailwindcss/node@4.1.18': resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} @@ -1214,9 +1220,15 @@ packages: '@tweenjs/tween.js@23.1.3': resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} @@ -1321,6 +1333,35 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@vitest/expect@4.0.18': + resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + + '@vitest/mocker@4.0.18': + resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.0.18': + resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + + '@vitest/runner@4.0.18': + resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + + '@vitest/snapshot@4.0.18': + resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + + '@vitest/spy@4.0.18': + resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + + '@vitest/utils@4.0.18': + resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + '@volar/kit@2.4.27': resolution: {integrity: sha512-ilZoQDMLzqmSsImJRWx4YiZ4FcvvPrPnFVmL6hSsIWB6Bn3qc7k88J9yP32dagrs5Y8EXIlvvD/mAFaiuEOACQ==} peerDependencies: @@ -1435,6 +1476,10 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + astring@1.9.0: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} hasBin: true @@ -1542,6 +1587,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -1924,6 +1973,10 @@ packages: eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} @@ -2807,6 +2860,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + ofetch@1.5.1: resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} @@ -3163,6 +3219,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + simple-code-frame@1.3.0: resolution: {integrity: sha512-MB4pQmETUBlNs62BBeRjIFGeuy/x6gGKh7+eRUemn1rCFhqo7K+4slPqsyizCbcbYLnaYqaoZ2FWsZ/jN06D8w==} @@ -3188,6 +3247,12 @@ packages: resolution: {integrity: sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==} engines: {node: '>=16'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -3277,6 +3342,9 @@ packages: tiny-inflate@1.0.3: resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -3285,6 +3353,10 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyrainbow@3.0.3: + resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -3551,6 +3623,40 @@ packages: vite: optional: true + vitest@4.0.18: + resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.0.18 + '@vitest/browser-preview': 4.0.18 + '@vitest/browser-webdriverio': 4.0.18 + '@vitest/ui': 4.0.18 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + volar-service-css@0.0.68: resolution: {integrity: sha512-lJSMh6f3QzZ1tdLOZOzovLX0xzAadPhx8EKwraDLPxBndLCYfoTvnNuiFFV8FARrpAlW5C0WkH+TstPaCxr00Q==} peerDependencies: @@ -3671,6 +3777,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + widest-line@5.0.0: resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} engines: {node: '>=18'} @@ -4713,6 +4824,8 @@ snapshots: '@speed-highlight/core@1.2.14': {} + '@standard-schema/spec@1.1.0': {} + '@tailwindcss/node@4.1.18': dependencies: '@jridgewell/remapping': 2.3.5 @@ -4788,10 +4901,17 @@ snapshots: '@tweenjs/tween.js@23.1.3': {} + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} + '@types/estree-jsx@1.0.5': dependencies: '@types/estree': 1.0.8 @@ -4930,6 +5050,45 @@ snapshots: '@ungap/structured-clone@1.3.0': {} + '@vitest/expect@4.0.18': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + chai: 6.2.2 + tinyrainbow: 3.0.3 + + '@vitest/mocker@4.0.18(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))': + dependencies: + '@vitest/spy': 4.0.18 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + + '@vitest/pretty-format@4.0.18': + dependencies: + tinyrainbow: 3.0.3 + + '@vitest/runner@4.0.18': + dependencies: + '@vitest/utils': 4.0.18 + pathe: 2.0.3 + + '@vitest/snapshot@4.0.18': + dependencies: + '@vitest/pretty-format': 4.0.18 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.0.18': {} + + '@vitest/utils@4.0.18': + dependencies: + '@vitest/pretty-format': 4.0.18 + tinyrainbow: 3.0.3 + '@volar/kit@2.4.27(typescript@5.8.3)': dependencies: '@volar/language-service': 2.4.27 @@ -5088,6 +5247,8 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 + assertion-error@2.0.1: {} + astring@1.9.0: {} astro-eslint-parser@1.2.2: @@ -5305,6 +5466,8 @@ snapshots: ccount@2.0.1: {} + chai@6.2.2: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -5851,6 +6014,8 @@ snapshots: eventemitter3@5.0.4: {} + expect-type@1.3.0: {} + exsolve@1.0.8: {} extend@3.0.2: {} @@ -7040,6 +7205,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + obug@2.1.1: {} + ofetch@1.5.1: dependencies: destr: 2.0.5 @@ -7559,6 +7726,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + simple-code-frame@1.3.0: dependencies: kolorist: 1.8.0 @@ -7575,6 +7744,10 @@ snapshots: stack-trace@1.0.0-pre2: {} + stackback@0.0.2: {} + + std-env@3.10.0: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -7691,6 +7864,8 @@ snapshots: tiny-inflate@1.0.3: {} + tinybench@2.9.0: {} + tinyexec@1.0.2: {} tinyglobby@0.2.15: @@ -7698,6 +7873,8 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinyrainbow@3.0.3: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -7924,6 +8101,43 @@ snapshots: optionalDependencies: vite: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + vitest@4.0.18(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2): + dependencies: + '@vitest/expect': 4.0.18 + '@vitest/mocker': 4.0.18(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) + '@vitest/pretty-format': 4.0.18 + '@vitest/runner': 4.0.18 + '@vitest/snapshot': 4.0.18 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + es-module-lexer: 1.7.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.7 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + volar-service-css@0.0.68(@volar/language-service@2.4.27): dependencies: vscode-css-languageservice: 6.3.9 @@ -8070,6 +8284,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + widest-line@5.0.0: dependencies: string-width: 7.2.0 diff --git a/public/images/sudoku-thumbnail.webp b/public/images/sudoku-thumbnail.webp new file mode 100644 index 0000000000000000000000000000000000000000..e8e22f468efbde4bc31aaa587674d9fffa7a7afe GIT binary patch literal 4228 zcmWIYbaSf_U|PquoT8x!y|`FIu=@6Su$yAgtZb97fzq9uy|5|@IzwnQPh4c%?Kd1lX|Ng(Ze)5_3 zwTW-kpRRsm{yq4nNxkos%t!49Uc3uj-X+hxSgCf&(G{y4V)!i>tIXIIS*J&`Em+c~ z^>iWIC3fW{N~bkzjSSMFzeS|V1d2-pWOaPePMEuurBpnv!hhMy5|&f^9aBUrtgGtW z>hxGZ*jd!VCSYNGm8A}c1pnHLH?>25l)f-eyDWL}=Jjh2*blt8@MgD@sSgg-@ZK_HW%H@_xpC9{HthQd-&iWRAMT&W`0McA2=!0w-I3f&)ZTZv-RAX-yeZBp+4GijvO?pH{khtwPHbMjUT_ZU zr}M93T`p88neyIe^z_T`jBhz*a_OYgU4z-D?XleKt;F}U>5`MX^MU4wI_5KWh0D8y?>UQp)LQ;9#MU@w zU%{+5cG59>%fZm$u)MUum_j00mYKL|a2CjZH;y%tZnHGgd~J1qPCnBY&=%cs)z z&)SprJE;3~#mnUi=O5*$Z&giCNr_A~)Z6}ot>@mFd95o0ZVL%8PP?}4yZd>w`9+`p zS^T+{#93aPG%Ci4cqV=vj-EH>A<@bG5WJo`-|Eb#Qt4C!MLav`* z)qmJJUs%+Bs;uK84bgjNieu{csb(tcTq!I4y5Kcq*j0_XTVEf{c^#Bt#6G#b{`|4% zM#ks*EZM5@)!T;9$dg6to&GsLG4(II+Gnw{G1D}k;go_j<9jdmd?-;*uS77+}@5%>;z*cefx!?Z_Fu%TAVjZjzA=QwS zq-S&YR{I*UkB_z*t++msHPrj_f{(jPD-OCb%NFD*A1F?K@xDTs$KCt%%=Oic@mvZI zU1T(ZUxl05^?eg@U_7~6;lWWZsZ!-*Q|nbk@>=xrpPaNz&M`RMeO+r~?Qj3~`$_8< z)1Rm&ocwl0PRpt#gxlfQDvlK@Ldzb`YH{4(y1zl?u4DE6>Pu`ZWbSP}sJI~YTH{x~ zj};4e`X1>#-npA;hj!D;-|4gT7TwA(3cGu_)J%>gx<@p|Y}<2p^QCKV>RnE{dsQzv zlRZ9{QN&sJaNpK{?>Bhgia)L7TDIV2>cXHE{vK=deS+KVr#bIl`m&ra*CWzgP3QKj zUlZ0489@rBy~XC>{NWkrAPh`MQD6gcbB&t(S;)8?EISDL^1E~Bj9)@nbM zFXu0~p6;?_dY)f=Q}(d;@_ebqUe-B>@4eJMcG3Bz@6DFI>l(|ht@Pkb;%RhZZZ&$* z*}~ly;%kwfInny-jx4P+KhrCAA6m0}uXN0*&3`uS{eI__tMfy#?zY#Tm11|ioi(Xj zWMA5bi>uWVJUi}j9=i7Yn9+rzqyC$gm(N&!u}U%%z9$* zSaZkzCw~>T2N?=iPVwBi%|}x(C}(QB^E^!reY5Cu{BLhh9<1}+zoPJ#{zkqB?^IUSPdDugEPngI<$~M$tJ<4GUhO;0*Id%c zkx&{^x9r@$Q|eQusrlFBRqwoG-qt!R{*umJo~y4@-emuH@JDm;ucSY|e(bFl?ZO82 zp(3+O4zVhnu#@z@uNw68-IDWfc>n_tzXZAKodXHTCd( zR{?`%mS!ijW_35NFLZst!g!g{KbYfqcr^dMb05w4>rZ{Z@#WWJ`3dJIvDxW%R;n+S zGHi5t`E{3f&tFBg+?)c-UbS7H?{s(9lzffqQ(No1#%-6PiGt9S;^2+(feWsB=Y4bR zGBjHBIrftC`}`@hGOo4i8(tNe8mW2uct8#>r|!P3a~KaTapQj5KJk(AYtMOe_-7XA zPtjBUy)&@KPj`!P&8hFDiBmh{V|IOe;XC1e<HTIV2)35Ct)hCHd2fNOT4YEG@e)HGMPJ7Cy#Lrk!^ZlGXtK9-uAvTtN zZc&!>pdHH2ZT}Jtu84Pr`QMZJSfqO6`+)_g-b~G0mutFK?*6~sXQ%Kd)Jg8$ur|y4 z_UoNpzw@uWtT5pamtG$qxUs}kTCA+$&!PzpzkQ_b)g#}8-1_1W_d!jfZ}x-tyVtzx zk)2+5m|;PG$f9R5#+w{PtWHWQoLqP=O`|=^r@>rX|MjEW^{z{e{I~Jc_Sb&7{5#DrHc13#Ssm~H zmLvAE+xMrtsm85a+h&$ME*HGK3O~USGWOYi-9Jt;xqft}so$@aodM ziOI+2izRgEPr9q_*leVGe(eKR-QRbwdBha!rf9Trw9oT=p)%EJWdln&d+nak*eTD> zR@bg%RB7Vdu{ZWG!}-f!yckl1PuKnM?-YC&cSkhfuEod72jLOP+c+Wwl^OcXtoWZq zIIub@F)WdNY{hU$eB;B%N8_E2SZ$2`wn1LN>R*rV)%}rW%1+f*A`KHYQ%oNmkxvUU zRI{7XV)K{(&20ORr3!&p;x5~ACCQh|NDHl&;}NVe zN7mi4IXQ3dmY5sv|8`=Y%JsuD%GbO&zE!+QYG$z5xn33q^_B|UnPt%{{q#Y0lAgVH-9+EUBx%@zmbJZ|1M5Z7X)l z-tmmJ>iXS(<*B-C1Fw6LG*|zY^uwtiyj&F0*kvZD@h>r9Sy#Bec2-Hzr6tyN=E4d` z&P?IZ`(JnJhshqjhAxYxd+U4+-v)_pIT%p3@Mqr~*04Lyq1shU(>12;UD$u|<@5U+ z{I{&;WfPuOaqLr3LiS$g&z(Y5$_YHISEJq@mF6Q49FJ}Th#-1BC8$LIZeDcQ4D+&Scv`X_dl@Sdw1 zE;=lV`g(7A!432NUW;2hwza7JWh)G7m6&I7rb)Q3^L<-!k9=2m#yM97qvYNNsgHNX zsW^S=llSb{^zr=E{K@}q#XOZWv{f}0gg=%)eOv14zcfbSl7PUZQnvwtPU2DaUrP~<9{~n#}e#U+i-;zeHMpIrVzVAnB zKh=Jbj|{%Pc>epRSEG!b*)I5QFu3IZZr09OnOnI8-wUYCE7^P9u%1Wq?6#avC6m@m ze?65Ry}8$>Br6c`nN`f6Ijf_wKJ zP9&}_-2N+2@JmqQ_w6b6{)Xv4xGt5HXojYrNfrL^RB6ZeQd^d5PjAkhbj-1HpZwW* zi&EeFygqO+`-khjm8E{IB~?AE43i71y?;(p($@9nyD?oq>DFO^gz{VLQ#wxWn)rL> z|KIgRhc&y)Z>YZ2f9BzPRr}+Sb^RTO|-`T9wyNMFT|tKqkIb0i38 z>{6dSn`?UL;mJ(1&mR=(D}TorZ71Tkz=Qkf_kse}wo9`n-ek>4GF@M$x$?^Zo z){nRq#_^iGnd)yjx7~lVE&SS?oLSwc6c=4v( - getGameValues(gameString) + getGameValues(gameString), ); const [currentHoveringValue, setCurrentHoveringValue] = useState(null); @@ -57,7 +57,7 @@ export function SudokuPage() { inputMode="numeric" value={gameString} onChange={(e) => { - const value = e.target.value.trim(); + const value = (e.target as HTMLInputElement).value.trim(); setGameString(value); }} /> @@ -101,7 +101,7 @@ function Game() { const { values } = useSudoku(); return ( -
+
{values.map((value, index) => ( ))} @@ -176,7 +176,7 @@ function Cell({ value, coord }: CellProps) { ]); const validValues = new Set([1, 2, 3, 4, 5, 6, 7, 8, 9]).difference( - invalidValues + invalidValues, ); return ( diff --git a/src/lib/lorem.ts b/src/lib/lorem.ts new file mode 100644 index 0000000..33701ac --- /dev/null +++ b/src/lib/lorem.ts @@ -0,0 +1,56 @@ +import { windowed } from "~/lib/utils/windowed.ts"; + +type WordGraph = Map; + +export function makeGraph(input: string): { + words: string[]; + graph: WordGraph; +} { + const allWords = input.split(/\s/g); + + const map: WordGraph = new Map(); + + const window = windowed(allWords, 2); + + for (const [w1, w2] of window) { + const lastNChars = w1.slice(-2); + const value = map.get(lastNChars); + if (value) { + value.push(w2); + } else { + map.set(lastNChars, [w2]); + } + } + + return { + graph: map, + words: [...new Set(allWords)], + }; +} + +export function makeLorem({ + words, + graph, + length, +}: { + words: string[]; + graph: WordGraph; + length: number; +}): string { + let current = words[Math.floor(Math.random() * words.length)]; + const picked = [current]; + + for (let i = 0; i < length; i++) { + const key = current.slice(-2); + const possible_words = graph.get(key)?.filter((w) => w !== current); + + const next = possible_words + ? possible_words[Math.floor(Math.random() * possible_words.length)] + : words[Math.floor(Math.random() * words.length)]; + + picked.push(next); + current = next; + } + + return picked.join(" "); +} diff --git a/src/lib/utils/windowed.ts b/src/lib/utils/windowed.ts new file mode 100644 index 0000000..eb64da5 --- /dev/null +++ b/src/lib/utils/windowed.ts @@ -0,0 +1,73 @@ +// https://github.com/toss/es-toolkit/blob/main/src/array/windowed.ts + +/** + * Options for the windowed function. + * + * @interface WindowedOptions + * @property {boolean} [partialWindows=false] - Whether to include partial windows at the end of the array. + */ +export interface WindowedOptions { + /** + * Whether to include partial windows at the end of the array. + * + * By default, `windowed` only includes full windows in the result, + * ignoring any leftover elements that can't form a full window. + * + * If `partialWindows` is true, the function will also include these smaller, partial windows at the end of the result. + */ + partialWindows?: boolean; +} + +/** + * Creates an array of sub-arrays (windows) from the input array, each of the specified size. + * The windows can overlap depending on the step size provided. + * + * By default, only full windows are included in the result, and any leftover elements that can't form a full window are ignored. + * + * If the `partialWindows` option is set to true in the options object, the function will also include partial windows at the end of the result. + * Partial windows are smaller sub-arrays created when there aren't enough elements left in the input array to form a full window. + * + * @template T + * @param {readonly T[]} arr - The input array to create windows from. + * @param {number} size - The size of each window. Must be a positive integer. + * @param {number} [step=1] - The step size between the start of each window. Must be a positive integer. + * @param {WindowedOptions} [options={}] - Options object to configure the behavior of the function. + * @param {boolean} [options.partialWindows=false] - Whether to include partial windows at the end of the array. + * @returns {T[][]} An array of windows (sub-arrays) created from the input array. + * @throws {Error} If the size or step is not a positive integer. + * + * @example + * windowed([1, 2, 3, 4], 2); + * // => [[1, 2], [2, 3], [3, 4]] + * + * @example + * windowed([1, 2, 3, 4, 5, 6], 3, 2); + * // => [[1, 2, 3], [3, 4, 5]] + * + * @example + * windowed([1, 2, 3, 4, 5, 6], 3, 2, { partialWindows: true }); + * // => [[1, 2, 3], [3, 4, 5], [5, 6]] + */ +export function windowed( + arr: readonly T[], + size: number, + step = 1, + { partialWindows = false }: WindowedOptions = {}, +): T[][] { + if (size <= 0 || !Number.isInteger(size)) { + throw new Error("Size must be a positive integer."); + } + + if (step <= 0 || !Number.isInteger(step)) { + throw new Error("Step must be a positive integer."); + } + + const result: T[][] = []; + const end = partialWindows ? arr.length : arr.length - size + 1; + + for (let i = 0; i < end; i += step) { + result.push(arr.slice(i, i + size)); + } + + return result; +} diff --git a/src/pages/index.astro b/src/pages/index.astro index 4350be2..880aa0a 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -16,7 +16,7 @@ import HeroImage from "../assets/hero.jpeg";
diff --git a/src/pages/links/index.astro b/src/pages/links/index.astro index fa1a588..c7dd8ae 100644 --- a/src/pages/links/index.astro +++ b/src/pages/links/index.astro @@ -6,7 +6,7 @@ import MainLayout from "~/layouts/main-layout.astro"; diff --git a/src/pages/posts/[id]/index.astro b/src/pages/posts/[id]/index.astro index dec44f0..2f167e1 100644 --- a/src/pages/posts/[id]/index.astro +++ b/src/pages/posts/[id]/index.astro @@ -28,7 +28,7 @@ const { Content } = await render(post);
diff --git a/src/pages/posts/index.astro b/src/pages/posts/index.astro index dfd889d..6719d06 100644 --- a/src/pages/posts/index.astro +++ b/src/pages/posts/index.astro @@ -4,14 +4,14 @@ import PostItem from "~/components/posts/post-item.astro"; import { getCollection } from "astro:content"; const allBlogPosts = await getCollection("blog").then((posts) => - posts.toSorted((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()) + posts.toSorted((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()), ); ---