diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..1fa01c5 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,51 @@ +name: Deploy to GitHub Pages + +on: + push: + branches: + - trunk + - main + paths: + - 'src/**' + - 'public/**' + - 'index.html' + - 'vite.config.ts' + - 'package.json' + - 'pnpm-lock.yaml' + +permissions: + contents: write + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup pnpm + uses: pnpm/action-setup@v2 + with: + version: 10.12.4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build + run: pnpm run build + + - name: Deploy to gh-pages branch + if: github.event_name == 'push' && (github.ref == 'refs/heads/trunk' || github.ref == 'refs/heads/main') + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./dist + cname: false diff --git a/GITHUB_PAGES_SETUP.md b/GITHUB_PAGES_SETUP.md new file mode 100644 index 0000000..fe28581 --- /dev/null +++ b/GITHUB_PAGES_SETUP.md @@ -0,0 +1,120 @@ +# GitHub Pages Deployment Setup + +This project is configured to deploy to GitHub Pages automatically via GitHub Actions. + +## Prerequisites + +- Your repository must be accessible on GitHub +- You have push access to the repository + +## Automatic Deployment (GitHub Actions) + +The project includes a GitHub Actions workflow (`.github/workflows/deploy.yml`) that automatically deploys to GitHub Pages when you push to the `trunk` or `main` branch. + +### Initial Setup (REQUIRED) + +Follow these steps exactly to enable GitHub Pages: + +1. **Enable GitHub Pages** in your repository settings: + - Go to: https://github.com/pabrams/monkey-drill/settings/pages + - Under "Build and deployment" section + - Select the source: + - Choose "Deploy from a branch" + - Select branch: `gh-pages` (will be auto-created by Actions) + - Select folder: `/ (root)` + - Click "Save" + +2. **Push to trigger deployment**: + - Make a commit and push to `trunk` branch: + ```bash + git add . + git commit -m "Enable GitHub Pages deployment" + git push origin trunk + ``` + - GitHub Actions will automatically: + - Build the project with proper base path configuration + - Deploy the `dist` folder to the `gh-pages` branch + - GitHub Pages will serve from: `https://pabrams.github.io/monkey-drill/` + - Site should be live within 1-2 minutes + +### Accessing Your Deployed Site + +Once deployed, your site will be available at: +``` +https://.github.io/monkey-drill/ +``` + +## Local Testing + +To test the GitHub Pages build locally: + +```bash +# Build with GitHub Pages configuration +pnpm run build:gh-pages + +# Preview the build +pnpm run preview +``` + +This will build the app with the correct base path (`/monkey-drill/`) that GitHub Pages requires. + +## Manual Deployment (Optional) + +If you need to manually deploy without GitHub Actions, you can: + +1. Build the project: + ```bash + pnpm run build:gh-pages + ``` + +2. Push the `dist` folder to the `gh-pages` branch on GitHub (using a tool like `gh-pages` package) + +## Troubleshooting + +### Deployment Failed Error (404) +**Error Message**: "Failed to create deployment (status: 404)" + +**Solution**: GitHub Pages is not enabled yet. Follow these steps: +1. Go to: https://github.com/pabrams/monkey-drill/settings/pages +2. Under "Build and deployment" +3. Select source: "Deploy from a branch" +4. Choose branch: `gh-pages` and folder: `/ (root)` +5. Click "Save" +6. The workflow will automatically create the `gh-pages` branch on the next push +7. Re-run the failed workflow from the Actions tab + +### Site not updating after push +- Check the "Actions" tab: https://github.com/pabrams/monkey-drill/actions +- Click the "Deploy to GitHub Pages" workflow +- Check the build and deploy steps for errors +- If "build" succeeded but "deploy" failed, GitHub Pages is likely not enabled (see above) + +### Assets not loading (blank page, console errors like 404) +- Verify the base path is correct in `vite.config.ts` +- The build should output assets with `/monkey-drill/` prefix +- Check the browser console for 404 errors +- Verify the site is serving from `https://.github.io/monkey-drill/` (not just `/`) + +### 404 errors on page refresh +- This is expected behavior for single-page apps on GitHub Pages +- Routes like `/puzzle/123` will 404 on direct access +- Solution: Users should navigate through the app normally from the home page +- To fix: Configure `_redirects` or 404.html redirect (advanced) + +### Pages are showing old content +- GitHub Pages CDN may have cached old files +- Clear your browser cache (Ctrl+Shift+Delete on Windows/Linux, Cmd+Shift+Delete on Mac) +- Try accessing the site in an incognito/private window +- Wait 5-10 minutes for CDN to update + +### Repository is private +- GitHub Pages is only available on public repositories (for free) +- Make your repository public in Settings → Danger Zone → Change repository visibility + +## Configuration Details + +- **Base Path**: Configured in `vite.config.ts` - uses `/monkey-drill/` when `GH_PAGES=true` +- **Workflow File**: `.github/workflows/deploy.yml` +- **Node Version**: 20 (configurable in workflow) +- **Package Manager**: pnpm 10.12.4 +- **Deployment Branch**: `gh-pages` (created automatically by GitHub Actions) diff --git a/package.json b/package.json index ff7744b..8d343c7 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "jest-environment-jsdom": "^30.0.2", "jsdom": "^26.0.0", "typescript": "^5.8.3", - "vite": "^6.3.5", + "vite": "^6.4.1", "vitest": "^3.1.1" }, "packageManager": "pnpm@10.12.4+sha512.5ea8b0deed94ed68691c9bad4c955492705c5eeb8a87ef86bc62c74a26b037b08ff9570f108b2e4dbd1dd1a9186fea925e527f141c648e85af45631074680184" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f8db94..78afa25 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,7 +31,7 @@ importers: version: 19.1.0 react-chessboard: specifier: latest - version: 5.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 5.8.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-dom: specifier: ^19.1.0 version: 19.1.0(react@19.1.0) @@ -68,7 +68,7 @@ importers: version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': specifier: ^4.6.0 - version: 4.6.0(vite@6.3.5(@types/node@24.0.10)(terser@5.43.1)) + version: 4.6.0(vite@6.4.1(@types/node@24.0.10)(terser@5.43.1)) eslint: specifier: ^9.21.0 version: 9.30.1 @@ -97,8 +97,8 @@ importers: specifier: ^5.8.3 version: 5.8.3 vite: - specifier: ^6.3.5 - version: 6.3.5(@types/node@24.0.10)(terser@5.43.1) + specifier: ^6.4.1 + version: 6.4.1(@types/node@24.0.10)(terser@5.43.1) vitest: specifier: ^3.1.1 version: 3.2.4(@types/node@24.0.10)(jsdom@26.1.0)(terser@5.43.1) @@ -312,6 +312,10 @@ packages: resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} @@ -390,152 +394,158 @@ packages: '@emnapi/wasi-threads@1.0.2': resolution: {integrity: sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==} - '@esbuild/aix-ppc64@0.25.5': - resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.5': - resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.5': - resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.5': - resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.5': - resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.5': - resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==} + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.5': - resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.5': - resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==} + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.5': - resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.5': - resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.5': - resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.5': - resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.5': - resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.5': - resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.5': - resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.5': - resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.5': - resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==} + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.5': - resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==} + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.5': - resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==} + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.5': - resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==} + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.5': - resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==} + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.25.5': - resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==} + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.5': - resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.5': - resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.5': - resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==} + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -938,103 +948,113 @@ packages: '@rolldown/pluginutils@1.0.0-beta.19': resolution: {integrity: sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==} - '@rollup/rollup-android-arm-eabi@4.44.2': - resolution: {integrity: sha512-g0dF8P1e2QYPOj1gu7s/3LVP6kze9A7m6x0BZ9iTdXK8N5c2V7cpBKHV3/9A4Zd8xxavdhK0t4PnqjkqVmUc9Q==} + '@rollup/rollup-android-arm-eabi@4.52.5': + resolution: {integrity: sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.44.2': - resolution: {integrity: sha512-Yt5MKrOosSbSaAK5Y4J+vSiID57sOvpBNBR6K7xAaQvk3MkcNVV0f9fE20T+41WYN8hDn6SGFlFrKudtx4EoxA==} + '@rollup/rollup-android-arm64@4.52.5': + resolution: {integrity: sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.44.2': - resolution: {integrity: sha512-EsnFot9ZieM35YNA26nhbLTJBHD0jTwWpPwmRVDzjylQT6gkar+zenfb8mHxWpRrbn+WytRRjE0WKsfaxBkVUA==} + '@rollup/rollup-darwin-arm64@4.52.5': + resolution: {integrity: sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.44.2': - resolution: {integrity: sha512-dv/t1t1RkCvJdWWxQ2lWOO+b7cMsVw5YFaS04oHpZRWehI1h0fV1gF4wgGCTyQHHjJDfbNpwOi6PXEafRBBezw==} + '@rollup/rollup-darwin-x64@4.52.5': + resolution: {integrity: sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.44.2': - resolution: {integrity: sha512-W4tt4BLorKND4qeHElxDoim0+BsprFTwb+vriVQnFFtT/P6v/xO5I99xvYnVzKWrK6j7Hb0yp3x7V5LUbaeOMg==} + '@rollup/rollup-freebsd-arm64@4.52.5': + resolution: {integrity: sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.44.2': - resolution: {integrity: sha512-tdT1PHopokkuBVyHjvYehnIe20fxibxFCEhQP/96MDSOcyjM/shlTkZZLOufV3qO6/FQOSiJTBebhVc12JyPTA==} + '@rollup/rollup-freebsd-x64@4.52.5': + resolution: {integrity: sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.44.2': - resolution: {integrity: sha512-+xmiDGGaSfIIOXMzkhJ++Oa0Gwvl9oXUeIiwarsdRXSe27HUIvjbSIpPxvnNsRebsNdUo7uAiQVgBD1hVriwSQ==} + '@rollup/rollup-linux-arm-gnueabihf@4.52.5': + resolution: {integrity: sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.44.2': - resolution: {integrity: sha512-bDHvhzOfORk3wt8yxIra8N4k/N0MnKInCW5OGZaeDYa/hMrdPaJzo7CSkjKZqX4JFUWjUGm88lI6QJLCM7lDrA==} + '@rollup/rollup-linux-arm-musleabihf@4.52.5': + resolution: {integrity: sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.44.2': - resolution: {integrity: sha512-NMsDEsDiYghTbeZWEGnNi4F0hSbGnsuOG+VnNvxkKg0IGDvFh7UVpM/14mnMwxRxUf9AdAVJgHPvKXf6FpMB7A==} + '@rollup/rollup-linux-arm64-gnu@4.52.5': + resolution: {integrity: sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.44.2': - resolution: {integrity: sha512-lb5bxXnxXglVq+7imxykIp5xMq+idehfl+wOgiiix0191av84OqbjUED+PRC5OA8eFJYj5xAGcpAZ0pF2MnW+A==} + '@rollup/rollup-linux-arm64-musl@4.52.5': + resolution: {integrity: sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.44.2': - resolution: {integrity: sha512-Yl5Rdpf9pIc4GW1PmkUGHdMtbx0fBLE1//SxDmuf3X0dUC57+zMepow2LK0V21661cjXdTn8hO2tXDdAWAqE5g==} + '@rollup/rollup-linux-loong64-gnu@4.52.5': + resolution: {integrity: sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.44.2': - resolution: {integrity: sha512-03vUDH+w55s680YYryyr78jsO1RWU9ocRMaeV2vMniJJW/6HhoTBwyyiiTPVHNWLnhsnwcQ0oH3S9JSBEKuyqw==} + '@rollup/rollup-linux-ppc64-gnu@4.52.5': + resolution: {integrity: sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.44.2': - resolution: {integrity: sha512-iYtAqBg5eEMG4dEfVlkqo05xMOk6y/JXIToRca2bAWuqjrJYJlx/I7+Z+4hSrsWU8GdJDFPL4ktV3dy4yBSrzg==} + '@rollup/rollup-linux-riscv64-gnu@4.52.5': + resolution: {integrity: sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.44.2': - resolution: {integrity: sha512-e6vEbgaaqz2yEHqtkPXa28fFuBGmUJ0N2dOJK8YUfijejInt9gfCSA7YDdJ4nYlv67JfP3+PSWFX4IVw/xRIPg==} + '@rollup/rollup-linux-riscv64-musl@4.52.5': + resolution: {integrity: sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.44.2': - resolution: {integrity: sha512-evFOtkmVdY3udE+0QKrV5wBx7bKI0iHz5yEVx5WqDJkxp9YQefy4Mpx3RajIVcM6o7jxTvVd/qpC1IXUhGc1Mw==} + '@rollup/rollup-linux-s390x-gnu@4.52.5': + resolution: {integrity: sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.44.2': - resolution: {integrity: sha512-/bXb0bEsWMyEkIsUL2Yt5nFB5naLAwyOWMEviQfQY1x3l5WsLKgvZf66TM7UTfED6erckUVUJQ/jJ1FSpm3pRQ==} + '@rollup/rollup-linux-x64-gnu@4.52.5': + resolution: {integrity: sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.44.2': - resolution: {integrity: sha512-3D3OB1vSSBXmkGEZR27uiMRNiwN08/RVAcBKwhUYPaiZ8bcvdeEwWPvbnXvvXHY+A/7xluzcN+kaiOFNiOZwWg==} + '@rollup/rollup-linux-x64-musl@4.52.5': + resolution: {integrity: sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.44.2': - resolution: {integrity: sha512-VfU0fsMK+rwdK8mwODqYeM2hDrF2WiHaSmCBrS7gColkQft95/8tphyzv2EupVxn3iE0FI78wzffoULH1G+dkw==} + '@rollup/rollup-openharmony-arm64@4.52.5': + resolution: {integrity: sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.52.5': + resolution: {integrity: sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.44.2': - resolution: {integrity: sha512-+qMUrkbUurpE6DVRjiJCNGZBGo9xM4Y0FXU5cjgudWqIBWbcLkjE3XprJUsOFgC6xjBClwVa9k6O3A7K3vxb5Q==} + '@rollup/rollup-win32-ia32-msvc@4.52.5': + resolution: {integrity: sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.44.2': - resolution: {integrity: sha512-3+QZROYfJ25PDcxFF66UEk8jGWigHJeecZILvkPkyQN7oc5BvFo4YEXFkOs154j3FTMp9mn9Ky8RCOwastduEA==} + '@rollup/rollup-win32-x64-gnu@4.52.5': + resolution: {integrity: sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.52.5': + resolution: {integrity: sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==} cpu: [x64] os: [win32] @@ -1806,8 +1826,8 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} - esbuild@0.25.5: - resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} hasBin: true @@ -1945,8 +1965,9 @@ packages: fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} - fdir@6.4.6: - resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: @@ -2751,6 +2772,10 @@ packages: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + pirates@4.0.7: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} @@ -2809,8 +2834,8 @@ packages: resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} engines: {node: '>= 0.8'} - react-chessboard@5.0.0: - resolution: {integrity: sha512-v+Nxnm+hJ3vhBtCAFs+X9WtOEiOejcPK17kAospAtGwb++4jq3YphxTN1h7Ia7ngQThuD5Ecnvw0SawUgtfGkg==} + react-chessboard@5.8.3: + resolution: {integrity: sha512-F+HWCmJ+x3kOADsN+NVijVPYoS4Zw1MxSxMiWXo31Cy+I10zULH36LjGSoyg3Z6xt/C+1qdctPl5Kvm93BKI2g==} engines: {node: '>=20.11.0', pnpm: '>=9.4.0'} peerDependencies: react: ^19.0.0 @@ -2878,8 +2903,8 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} - rollup@4.44.2: - resolution: {integrity: sha512-PVoapzTwSEcelaWGth3uR66u7ZRo6qhPHc0f2uRO9fX6XDVNrIiGYS0Pj9+R8yIIYSD/mCx2b16Ws9itljKSPg==} + rollup@4.52.5: + resolution: {integrity: sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -3129,6 +3154,10 @@ packages: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + tinypool@1.1.1: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} @@ -3265,8 +3294,8 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - vite@6.3.5: - resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==} + vite@6.4.1: + resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: @@ -3707,6 +3736,8 @@ snapshots: '@babel/runtime@7.27.6': {} + '@babel/runtime@7.28.4': {} + '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 @@ -3796,79 +3827,82 @@ snapshots: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.25.5': + '@esbuild/aix-ppc64@0.25.12': optional: true - '@esbuild/android-arm64@0.25.5': + '@esbuild/android-arm64@0.25.12': optional: true - '@esbuild/android-arm@0.25.5': + '@esbuild/android-arm@0.25.12': optional: true - '@esbuild/android-x64@0.25.5': + '@esbuild/android-x64@0.25.12': optional: true - '@esbuild/darwin-arm64@0.25.5': + '@esbuild/darwin-arm64@0.25.12': optional: true - '@esbuild/darwin-x64@0.25.5': + '@esbuild/darwin-x64@0.25.12': optional: true - '@esbuild/freebsd-arm64@0.25.5': + '@esbuild/freebsd-arm64@0.25.12': optional: true - '@esbuild/freebsd-x64@0.25.5': + '@esbuild/freebsd-x64@0.25.12': optional: true - '@esbuild/linux-arm64@0.25.5': + '@esbuild/linux-arm64@0.25.12': optional: true - '@esbuild/linux-arm@0.25.5': + '@esbuild/linux-arm@0.25.12': optional: true - '@esbuild/linux-ia32@0.25.5': + '@esbuild/linux-ia32@0.25.12': optional: true - '@esbuild/linux-loong64@0.25.5': + '@esbuild/linux-loong64@0.25.12': optional: true - '@esbuild/linux-mips64el@0.25.5': + '@esbuild/linux-mips64el@0.25.12': optional: true - '@esbuild/linux-ppc64@0.25.5': + '@esbuild/linux-ppc64@0.25.12': optional: true - '@esbuild/linux-riscv64@0.25.5': + '@esbuild/linux-riscv64@0.25.12': optional: true - '@esbuild/linux-s390x@0.25.5': + '@esbuild/linux-s390x@0.25.12': optional: true - '@esbuild/linux-x64@0.25.5': + '@esbuild/linux-x64@0.25.12': optional: true - '@esbuild/netbsd-arm64@0.25.5': + '@esbuild/netbsd-arm64@0.25.12': optional: true - '@esbuild/netbsd-x64@0.25.5': + '@esbuild/netbsd-x64@0.25.12': optional: true - '@esbuild/openbsd-arm64@0.25.5': + '@esbuild/openbsd-arm64@0.25.12': optional: true - '@esbuild/openbsd-x64@0.25.5': + '@esbuild/openbsd-x64@0.25.12': optional: true - '@esbuild/sunos-x64@0.25.5': + '@esbuild/openharmony-arm64@0.25.12': optional: true - '@esbuild/win32-arm64@0.25.5': + '@esbuild/sunos-x64@0.25.12': optional: true - '@esbuild/win32-ia32@0.25.5': + '@esbuild/win32-arm64@0.25.12': optional: true - '@esbuild/win32-x64@0.25.5': + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': optional: true '@eslint-community/eslint-utils@4.7.0(eslint@9.30.1)': @@ -4393,64 +4427,70 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.19': {} - '@rollup/rollup-android-arm-eabi@4.44.2': + '@rollup/rollup-android-arm-eabi@4.52.5': + optional: true + + '@rollup/rollup-android-arm64@4.52.5': optional: true - '@rollup/rollup-android-arm64@4.44.2': + '@rollup/rollup-darwin-arm64@4.52.5': optional: true - '@rollup/rollup-darwin-arm64@4.44.2': + '@rollup/rollup-darwin-x64@4.52.5': optional: true - '@rollup/rollup-darwin-x64@4.44.2': + '@rollup/rollup-freebsd-arm64@4.52.5': optional: true - '@rollup/rollup-freebsd-arm64@4.44.2': + '@rollup/rollup-freebsd-x64@4.52.5': optional: true - '@rollup/rollup-freebsd-x64@4.44.2': + '@rollup/rollup-linux-arm-gnueabihf@4.52.5': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.44.2': + '@rollup/rollup-linux-arm-musleabihf@4.52.5': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.44.2': + '@rollup/rollup-linux-arm64-gnu@4.52.5': optional: true - '@rollup/rollup-linux-arm64-gnu@4.44.2': + '@rollup/rollup-linux-arm64-musl@4.52.5': optional: true - '@rollup/rollup-linux-arm64-musl@4.44.2': + '@rollup/rollup-linux-loong64-gnu@4.52.5': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.44.2': + '@rollup/rollup-linux-ppc64-gnu@4.52.5': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.44.2': + '@rollup/rollup-linux-riscv64-gnu@4.52.5': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.44.2': + '@rollup/rollup-linux-riscv64-musl@4.52.5': optional: true - '@rollup/rollup-linux-riscv64-musl@4.44.2': + '@rollup/rollup-linux-s390x-gnu@4.52.5': optional: true - '@rollup/rollup-linux-s390x-gnu@4.44.2': + '@rollup/rollup-linux-x64-gnu@4.52.5': optional: true - '@rollup/rollup-linux-x64-gnu@4.44.2': + '@rollup/rollup-linux-x64-musl@4.52.5': optional: true - '@rollup/rollup-linux-x64-musl@4.44.2': + '@rollup/rollup-openharmony-arm64@4.52.5': optional: true - '@rollup/rollup-win32-arm64-msvc@4.44.2': + '@rollup/rollup-win32-arm64-msvc@4.52.5': optional: true - '@rollup/rollup-win32-ia32-msvc@4.44.2': + '@rollup/rollup-win32-ia32-msvc@4.52.5': optional: true - '@rollup/rollup-win32-x64-msvc@4.44.2': + '@rollup/rollup-win32-x64-gnu@4.52.5': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.52.5': optional: true '@sinclair/typebox@0.34.37': {} @@ -4466,7 +4506,7 @@ snapshots: '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.27.1 - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 '@types/aria-query': 5.0.4 aria-query: 5.3.0 chalk: 4.1.2 @@ -4657,7 +4697,7 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.0': optional: true - '@vitejs/plugin-react@4.6.0(vite@6.3.5(@types/node@24.0.10)(terser@5.43.1))': + '@vitejs/plugin-react@4.6.0(vite@6.4.1(@types/node@24.0.10)(terser@5.43.1))': dependencies: '@babel/core': 7.28.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) @@ -4665,7 +4705,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.19 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.5(@types/node@24.0.10)(terser@5.43.1) + vite: 6.4.1(@types/node@24.0.10)(terser@5.43.1) transitivePeerDependencies: - supports-color @@ -4677,13 +4717,13 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@24.0.10)(terser@5.43.1))': + '@vitest/mocker@3.2.4(vite@6.4.1(@types/node@24.0.10)(terser@5.43.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.3.5(@types/node@24.0.10)(terser@5.43.1) + vite: 6.4.1(@types/node@24.0.10)(terser@5.43.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -5221,33 +5261,34 @@ snapshots: dependencies: es-errors: 1.3.0 - esbuild@0.25.5: + esbuild@0.25.12: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.5 - '@esbuild/android-arm': 0.25.5 - '@esbuild/android-arm64': 0.25.5 - '@esbuild/android-x64': 0.25.5 - '@esbuild/darwin-arm64': 0.25.5 - '@esbuild/darwin-x64': 0.25.5 - '@esbuild/freebsd-arm64': 0.25.5 - '@esbuild/freebsd-x64': 0.25.5 - '@esbuild/linux-arm': 0.25.5 - '@esbuild/linux-arm64': 0.25.5 - '@esbuild/linux-ia32': 0.25.5 - '@esbuild/linux-loong64': 0.25.5 - '@esbuild/linux-mips64el': 0.25.5 - '@esbuild/linux-ppc64': 0.25.5 - '@esbuild/linux-riscv64': 0.25.5 - '@esbuild/linux-s390x': 0.25.5 - '@esbuild/linux-x64': 0.25.5 - '@esbuild/netbsd-arm64': 0.25.5 - '@esbuild/netbsd-x64': 0.25.5 - '@esbuild/openbsd-arm64': 0.25.5 - '@esbuild/openbsd-x64': 0.25.5 - '@esbuild/sunos-x64': 0.25.5 - '@esbuild/win32-arm64': 0.25.5 - '@esbuild/win32-ia32': 0.25.5 - '@esbuild/win32-x64': 0.25.5 + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 escalade@3.2.0: {} @@ -5426,9 +5467,9 @@ snapshots: dependencies: bser: 2.1.1 - fdir@6.4.6(picomatch@4.0.2): + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: - picomatch: 4.0.2 + picomatch: 4.0.3 fflate@0.8.2: {} @@ -6015,7 +6056,7 @@ snapshots: chalk: 4.1.2 ci-info: 4.3.0 graceful-fs: 4.2.11 - picomatch: 4.0.2 + picomatch: 4.0.3 jest-validate@30.0.2: dependencies: @@ -6387,6 +6428,8 @@ snapshots: picomatch@4.0.2: {} + picomatch@4.0.3: {} + pirates@4.0.7: {} pkce-challenge@5.0.0: {} @@ -6443,7 +6486,7 @@ snapshots: iconv-lite: 0.6.3 unpipe: 1.0.0 - react-chessboard@5.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + react-chessboard@5.8.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@dnd-kit/core': 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@dnd-kit/modifiers': 9.0.0(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) @@ -6497,30 +6540,32 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 - rollup@4.44.2: + rollup@4.52.5: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.44.2 - '@rollup/rollup-android-arm64': 4.44.2 - '@rollup/rollup-darwin-arm64': 4.44.2 - '@rollup/rollup-darwin-x64': 4.44.2 - '@rollup/rollup-freebsd-arm64': 4.44.2 - '@rollup/rollup-freebsd-x64': 4.44.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.44.2 - '@rollup/rollup-linux-arm-musleabihf': 4.44.2 - '@rollup/rollup-linux-arm64-gnu': 4.44.2 - '@rollup/rollup-linux-arm64-musl': 4.44.2 - '@rollup/rollup-linux-loongarch64-gnu': 4.44.2 - '@rollup/rollup-linux-powerpc64le-gnu': 4.44.2 - '@rollup/rollup-linux-riscv64-gnu': 4.44.2 - '@rollup/rollup-linux-riscv64-musl': 4.44.2 - '@rollup/rollup-linux-s390x-gnu': 4.44.2 - '@rollup/rollup-linux-x64-gnu': 4.44.2 - '@rollup/rollup-linux-x64-musl': 4.44.2 - '@rollup/rollup-win32-arm64-msvc': 4.44.2 - '@rollup/rollup-win32-ia32-msvc': 4.44.2 - '@rollup/rollup-win32-x64-msvc': 4.44.2 + '@rollup/rollup-android-arm-eabi': 4.52.5 + '@rollup/rollup-android-arm64': 4.52.5 + '@rollup/rollup-darwin-arm64': 4.52.5 + '@rollup/rollup-darwin-x64': 4.52.5 + '@rollup/rollup-freebsd-arm64': 4.52.5 + '@rollup/rollup-freebsd-x64': 4.52.5 + '@rollup/rollup-linux-arm-gnueabihf': 4.52.5 + '@rollup/rollup-linux-arm-musleabihf': 4.52.5 + '@rollup/rollup-linux-arm64-gnu': 4.52.5 + '@rollup/rollup-linux-arm64-musl': 4.52.5 + '@rollup/rollup-linux-loong64-gnu': 4.52.5 + '@rollup/rollup-linux-ppc64-gnu': 4.52.5 + '@rollup/rollup-linux-riscv64-gnu': 4.52.5 + '@rollup/rollup-linux-riscv64-musl': 4.52.5 + '@rollup/rollup-linux-s390x-gnu': 4.52.5 + '@rollup/rollup-linux-x64-gnu': 4.52.5 + '@rollup/rollup-linux-x64-musl': 4.52.5 + '@rollup/rollup-openharmony-arm64': 4.52.5 + '@rollup/rollup-win32-arm64-msvc': 4.52.5 + '@rollup/rollup-win32-ia32-msvc': 4.52.5 + '@rollup/rollup-win32-x64-gnu': 4.52.5 + '@rollup/rollup-win32-x64-msvc': 4.52.5 fsevents: 2.3.3 router@2.2.0: @@ -6770,8 +6815,13 @@ snapshots: tinyglobby@0.2.14: dependencies: - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 tinypool@1.1.1: {} @@ -6912,7 +6962,7 @@ snapshots: debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.5(@types/node@24.0.10)(terser@5.43.1) + vite: 6.4.1(@types/node@24.0.10)(terser@5.43.1) transitivePeerDependencies: - '@types/node' - jiti @@ -6927,14 +6977,14 @@ snapshots: - tsx - yaml - vite@6.3.5(@types/node@24.0.10)(terser@5.43.1): + vite@6.4.1(@types/node@24.0.10)(terser@5.43.1): dependencies: - esbuild: 0.25.5 - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.44.2 - tinyglobby: 0.2.14 + rollup: 4.52.5 + tinyglobby: 0.2.15 optionalDependencies: '@types/node': 24.0.10 fsevents: 2.3.3 @@ -6944,7 +6994,7 @@ snapshots: dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.0.10)(terser@5.43.1)) + '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@24.0.10)(terser@5.43.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -6962,7 +7012,7 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@24.0.10)(terser@5.43.1) + vite: 6.4.1(@types/node@24.0.10)(terser@5.43.1) vite-node: 3.2.4(@types/node@24.0.10)(terser@5.43.1) why-is-node-running: 2.3.0 optionalDependencies: diff --git a/public/.nojekyll b/public/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/src/App.tsx b/src/App.tsx index 21280cf..57acdf4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,7 +5,8 @@ import { useChessGame } from './hooks/useChessGame'; import { useThemeContext } from './contexts/ThemeContext'; import { useArrows } from './hooks/useArrows'; import { useRating } from './hooks/useRating'; -import { useDrill, SOLVE_COMPLETION_DELAY_MS } from './hooks/useDrill'; +import { useDrill } from './hooks/useDrill'; +import { SOLVE_COMPLETION_DELAY_MS } from './hooks/usePuzzleQueue'; import { useMoveHandler } from './hooks/useMoveHandler'; import { usePuzzleResults } from './hooks/usePuzzleResults'; import Header from './components/Header'; diff --git a/src/hooks/useDrill.ts b/src/hooks/useDrill.ts index 2d1092c..afe8b25 100644 --- a/src/hooks/useDrill.ts +++ b/src/hooks/useDrill.ts @@ -1,13 +1,9 @@ -import { useReducer, useCallback, useEffect, useRef } from 'react'; -import { Square, Move } from 'chess.js'; +import { useCallback, useEffect } from 'react'; +import { Chess, Square } from 'chess.js'; import { LichessPuzzle } from '../types/lichess'; -import { UserColor } from '../types/drill'; import type { ChessGame } from './useChessGame'; -import { getLevelFromRating } from '../utils/ratingCalculation'; -import { sampleArray } from '../utils/arrayUtils'; -import { convertToLichessPuzzleFormat, type RawPuzzle } from '../utils/puzzleUtils'; - -export const SOLVE_COMPLETION_DELAY_MS = 1000; +import { usePuzzleLoader } from './usePuzzleLoader'; +import { usePuzzleQueue, SOLVE_COMPLETION_DELAY_MS } from './usePuzzleQueue'; interface UseDrillProps { chessGame: ChessGame; @@ -16,148 +12,96 @@ interface UseDrillProps { onPuzzleResult: () => void; } -interface DrillState { - userColor: UserColor; - puzzles: LichessPuzzle[]; - currentPuzzle: LichessPuzzle | null; -} - -type DrillAction = - | { type: 'INITIALIZE_COLOR'; payload: UserColor } - | { type: 'LOAD_PUZZLES'; payload: LichessPuzzle[] } - | { type: 'ADVANCE_PUZZLE'; payload: LichessPuzzle | null }; - -const initialState: DrillState = { - userColor: 'white', - puzzles: [], - currentPuzzle: null, -}; - -const drillReducer = (state: DrillState, action: DrillAction): DrillState => { - switch (action.type) { - case 'INITIALIZE_COLOR': - return { ...state, userColor: action.payload }; - case 'LOAD_PUZZLES': - return { ...state, puzzles: action.payload }; - case 'ADVANCE_PUZZLE': { - const [, ...remaining] = state.puzzles; - return { - ...state, - puzzles: remaining, - currentPuzzle: action.payload, - }; - } - default: - return state; - } -}; - -const selectRandomUserColor = (): UserColor => { - return Math.random() < 0.5 ? 'white' : 'black'; -}; - export const useDrill = ({ chessGame, rating, onResultRecorded, onPuzzleResult }: UseDrillProps) => { - const [state, dispatch] = useReducer(drillReducer, initialState); + // Load puzzles from JSON files + const { puzzles, userColor, loadPuzzles } = usePuzzleLoader({ rating }); - const initialRatingRef = useRef(rating); + // Manage which puzzle is current and advance through the queue + const { currentPuzzle, advanceToNextPuzzle } = usePuzzleQueue({ puzzles }); - // Load puzzles on mount + // Load initial puzzles on mount useEffect(() => { - const loadPuzzles = async () => { - const newUserColor = selectRandomUserColor(); - dispatch({ type: 'INITIALIZE_COLOR', payload: newUserColor }); - - try { - const playerLevel = getLevelFromRating(initialRatingRef.current); - const colorPrefix = newUserColor === 'white' ? 'w' : 'b'; - const puzzleFile = `/lichess_db_puzzle-${colorPrefix}-one-move-${playerLevel}.json`; - const response = await fetch(puzzleFile); - - if (!response.ok) { - throw new Error(`Failed to load puzzle file: ${response.status} ${response.statusText}`); - } - - const data = await response.json() as { puzzles: RawPuzzle[] }; - const sampled = sampleArray(data.puzzles, 200); - const converted = convertToLichessPuzzleFormat(sampled); - - dispatch({ type: 'LOAD_PUZZLES', payload: converted }); - if (converted.length > 0) { - dispatch({ type: 'ADVANCE_PUZZLE', payload: converted[0] }); - loadPuzzleOnBoard(converted[0]); - } - } catch (error) { - console.error('Error loading puzzles:', error); - } - }; - loadPuzzles(); - }, []); + }, [loadPuzzles]); + // Set up puzzle on board when current puzzle changes const loadPuzzleOnBoard = useCallback((puzzle: LichessPuzzle) => { - import('chess.js').then(({ Chess }) => { - const setupMove = (puzzle as any)._setupMove; - const fen = (puzzle as any)._fen; + const setupMove = (puzzle as any)._setupMove; + const fen = (puzzle as any)._fen; - if (!setupMove || !fen) return; + console.log('[useDrill] loadPuzzleOnBoard', puzzle.puzzle.id, 'setupMove:', setupMove, 'fen:', fen); - const success = chessGame.loadPgn(new Chess(fen).pgn()); - if (!success) return; + if (!setupMove || !fen) return; - setTimeout(() => { - const tempChess = new Chess(fen); - const from = setupMove.substring(0, 2); - const to = setupMove.substring(2, 4); - const promotion = setupMove.length > 4 ? setupMove.substring(4) : undefined; - - const move = tempChess.move({ from, to, promotion: promotion as any }); - if (move) { - chessGame.loadPgn(tempChess.pgn()); - } - }, 600); - }); - }, [chessGame]); - - const recordResultAndLoadNext = useCallback((success: boolean) => { - if (!state.currentPuzzle) return; - - onResultRecorded(success, state.currentPuzzle.puzzle.rating, state.currentPuzzle.puzzle.id); + const initialChess = new Chess(fen); + const success = chessGame.loadPgn(initialChess.pgn()); + console.log('[useDrill] loadPgn result:', success); + if (!success) return; + // Apply the setup move after a brief delay setTimeout(() => { - const [, ...remaining] = state.puzzles; - const next = remaining[0] ?? null; - dispatch({ type: 'ADVANCE_PUZZLE', payload: next }); - if (next) { - loadPuzzleOnBoard(next); + console.log('[useDrill] applying setup move after 600ms'); + const tempChess = new Chess(fen); + const from = setupMove.substring(0, 2); + const to = setupMove.substring(2, 4); + const promotion = setupMove.length > 4 ? setupMove.substring(4) : undefined; + + const move = tempChess.move({ from, to, promotion: promotion as any }); + if (move) { + chessGame.loadPgn(tempChess.pgn()); + console.log('[useDrill] setup move applied'); } - }, SOLVE_COMPLETION_DELAY_MS); - }, [state, onResultRecorded, loadPuzzleOnBoard]); + }, 600); + }, [chessGame]); + + // Set up board when puzzle loads + useEffect(() => { + if (currentPuzzle) { + console.log('[useDrill] currentPuzzle changed to', currentPuzzle.puzzle.id); + loadPuzzleOnBoard(currentPuzzle); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentPuzzle]); const handlePuzzleMove = useCallback((sourceSquare: Square, targetSquare: Square, promotion?: string) => { - if (!state.currentPuzzle) return null; + if (!currentPuzzle) return null; + console.log('[useDrill] makeMove', sourceSquare, 'to', targetSquare); const move = chessGame.makeMove(sourceSquare, targetSquare, promotion); if (!move) return move; - const expectedMove = state.currentPuzzle.puzzle.solution[0]; + const expectedMove = currentPuzzle.puzzle.solution[0]; const isCorrect = move.lan === expectedMove; + console.log('[useDrill] move lan:', move.lan, 'expected:', expectedMove, 'isCorrect:', isCorrect); if (!isCorrect) { + console.log('[useDrill] Move incorrect, undoing'); chessGame.undoLastMove(); - recordResultAndLoadNext(false); + // Record failure and advance after delay + setTimeout(() => { + console.log('[useDrill] Recording failure and advancing'); + onResultRecorded(false, currentPuzzle.puzzle.rating, currentPuzzle.puzzle.id); + advanceToNextPuzzle(); + }, SOLVE_COMPLETION_DELAY_MS); onPuzzleResult(); return null; } - recordResultAndLoadNext(true); + console.log('[useDrill] Move correct, will advance after', SOLVE_COMPLETION_DELAY_MS, 'ms'); + // Record success and advance after delay + setTimeout(() => { + console.log('[useDrill] Recording success and advancing'); + onResultRecorded(true, currentPuzzle.puzzle.rating, currentPuzzle.puzzle.id); + advanceToNextPuzzle(); + }, SOLVE_COMPLETION_DELAY_MS); onPuzzleResult(); return move; - }, [state.currentPuzzle, chessGame, onPuzzleResult, recordResultAndLoadNext]); + }, [currentPuzzle, chessGame, onResultRecorded, onPuzzleResult, advanceToNextPuzzle]); return { drillState: { - userColor: state.userColor, - currentPuzzle: state.currentPuzzle, + userColor, + currentPuzzle, }, handlePuzzleMove, }; diff --git a/src/hooks/usePuzzleLoader.ts b/src/hooks/usePuzzleLoader.ts new file mode 100644 index 0000000..9baafa3 --- /dev/null +++ b/src/hooks/usePuzzleLoader.ts @@ -0,0 +1,80 @@ +import { useState, useCallback, useRef } from 'react'; +import { LichessPuzzle } from '../types/lichess'; +import { UserColor } from '../types/drill'; +import { getLevelFromRating } from '../utils/ratingCalculation'; +import { sampleArray } from '../utils/arrayUtils'; +import { convertToLichessPuzzleFormat, type RawPuzzle } from '../utils/puzzleUtils'; + +interface UsePuzzleLoaderProps { + rating: number; +} + +interface LoaderState { + isLoading: boolean; + error: string | null; + puzzles: LichessPuzzle[]; + userColor: UserColor; +} + +export const usePuzzleLoader = ({ rating }: UsePuzzleLoaderProps) => { + const [state, setState] = useState({ + isLoading: true, + error: null, + puzzles: [], + userColor: 'white', + }); + + const initialRatingRef = useRef(rating); + const loadedRef = useRef(false); + + const selectRandomUserColor = useCallback((): UserColor => { + return Math.random() < 0.5 ? 'white' : 'black'; + }, []); + + const loadPuzzles = useCallback(async () => { + if (loadedRef.current) return; + loadedRef.current = true; + + try { + setState(prev => ({ ...prev, isLoading: true, error: null })); + + const userColor = selectRandomUserColor(); + const playerLevel = getLevelFromRating(initialRatingRef.current); + const colorPrefix = userColor === 'white' ? 'w' : 'b'; + const puzzleFile = `/lichess_db_puzzle-${colorPrefix}-one-move-${playerLevel}.json`; + + const response = await fetch(puzzleFile); + if (!response.ok) { + throw new Error(`Failed to load puzzle file: ${response.status} ${response.statusText}`); + } + + const data = await response.json() as { puzzles: RawPuzzle[] }; + const sampled = sampleArray(data.puzzles, 200); + const converted = convertToLichessPuzzleFormat(sampled); + + setState({ + isLoading: false, + error: null, + puzzles: converted, + userColor, + }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error loading puzzles'; + console.error('Error loading puzzles:', error); + setState(prev => ({ + ...prev, + isLoading: false, + error: errorMessage, + puzzles: [], + })); + } + }, [selectRandomUserColor]); + + return { + isLoading: state.isLoading, + error: state.error, + puzzles: state.puzzles, + userColor: state.userColor, + loadPuzzles, + }; +}; diff --git a/src/hooks/usePuzzleQueue.ts b/src/hooks/usePuzzleQueue.ts new file mode 100644 index 0000000..47ed79a --- /dev/null +++ b/src/hooks/usePuzzleQueue.ts @@ -0,0 +1,77 @@ +import { useReducer, useCallback, useRef } from 'react'; +import { LichessPuzzle } from '../types/lichess'; + +export const SOLVE_COMPLETION_DELAY_MS = 1000; + +interface PuzzleQueueState { + current: LichessPuzzle | null; + remaining: LichessPuzzle[]; +} + +type PuzzleQueueAction = + | { type: 'INITIALIZE'; payload: LichessPuzzle[] } + | { type: 'ADVANCE' }; + +const initialState: PuzzleQueueState = { + current: null, + remaining: [], +}; + +const puzzleQueueReducer = (state: PuzzleQueueState, action: PuzzleQueueAction): PuzzleQueueState => { + switch (action.type) { + case 'INITIALIZE': { + const puzzles = action.payload; + const newState = { + current: puzzles[0] ?? null, + remaining: puzzles.slice(1), + }; + console.log('[puzzleQueueReducer] INITIALIZE: setting current to', newState.current?.puzzle?.id, 'with', newState.remaining.length, 'remaining'); + return newState; + } + case 'ADVANCE': { + const next = state.remaining[0] ?? null; + const newRemaining = state.remaining.slice(1); + const newState = { + current: next, + remaining: newRemaining, + }; + console.log('[puzzleQueueReducer] ADVANCE: setting current to', newState.current?.puzzle?.id, 'with', newState.remaining.length, 'remaining'); + return newState; + } + default: + return state; + } +}; + +interface UsePuzzleQueueProps { + puzzles: LichessPuzzle[]; +} + +export const usePuzzleQueue = ({ puzzles }: UsePuzzleQueueProps) => { + const [state, dispatch] = useReducer(puzzleQueueReducer, initialState); + const initializedRef = useRef(false); + + // Initialize queue synchronously when puzzles are loaded + if (puzzles.length > 0 && !initializedRef.current) { + initializedRef.current = true; + console.log('[usePuzzleQueue] INITIALIZE with', puzzles.length, 'puzzles, first puzzle id:', puzzles[0]?.puzzle?.id); + dispatch({ type: 'INITIALIZE', payload: puzzles }); + } + + const advanceToNextPuzzle = useCallback(() => { + console.log('[usePuzzleQueue] ADVANCE called, state before:', { + currentId: state.current?.puzzle?.id, + remainingCount: state.remaining.length, + nextPuzzleId: state.remaining[0]?.puzzle?.id, + }); + dispatch({ type: 'ADVANCE' }); + }, []); + + console.log('[usePuzzleQueue] render: current:', state.current?.puzzle?.id, 'remaining:', state.remaining.length); + + return { + currentPuzzle: state.current, + remainingCount: state.remaining.length, + advanceToNextPuzzle, + }; +}; diff --git a/src/hooks/usePuzzleResults.ts b/src/hooks/usePuzzleResults.ts index 62f1b33..46d60e0 100644 --- a/src/hooks/usePuzzleResults.ts +++ b/src/hooks/usePuzzleResults.ts @@ -17,14 +17,20 @@ export const usePuzzleResults = ({ rating, onPointsAdded }: UsePuzzleResultsProp // Skip persisting on first render const isFirstRenderRef = useRef(true); + const ratingRef = useRef(rating); + useEffect(() => { if (isFirstRenderRef.current) { isFirstRenderRef.current = false; } }, []); + useEffect(() => { + ratingRef.current = rating; + }, [rating]); + const recordResult = useCallback((success: boolean, puzzleRating: number, puzzleId: string) => { - const ratingChange = calculateRatingChange(rating, puzzleRating, success); + const ratingChange = calculateRatingChange(ratingRef.current, puzzleRating, success); const attempt: PuzzleAttempt = { puzzleId, @@ -37,7 +43,7 @@ export const usePuzzleResults = ({ rating, onPointsAdded }: UsePuzzleResultsProp setAttempts(prev => [attempt, ...prev]); setLastResult(success); onPointsAdded(puzzleRating, success); - }, [rating, onPointsAdded, setAttempts]); + }, [onPointsAdded, setAttempts]); return { attempts, diff --git a/vite.config.ts b/vite.config.ts index 0acc542..36feb76 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -13,5 +13,5 @@ export default defineConfig({ usePolling: true, }, }, - base: '/', + base: '/monkey-drill/', });