diff --git a/.forgejo/workflows/docker-build.yml b/.forgejo/workflows/docker-build.yml new file mode 100644 index 0000000..5ed2a43 --- /dev/null +++ b/.forgejo/workflows/docker-build.yml @@ -0,0 +1,32 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - main # Ou la branche sur laquelle vous pushez habituellement + +jobs: + build: + runs-on: alpine # Utilisation de votre runner + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Build Docker image + run: | + docker build -t git.wilmoredynamics.com/ab/ab01:${GITHUB_SHA::8} . + # Tag the image with 'latest' + docker tag git.wilmoredynamics.com/ab/ab01:${GITHUB_SHA::8} git.wilmoredynamics.com/ab/ab01:latest + + - name: Log in to Forgejo Container Registry + uses: docker/login-action@v2 + with: + registry: git.wilmoredynamics.com + username: AB # Votre nom d'utilisateur ou organisation Forgejo + password: ${{ secrets.FORGEJO_TOKEN }} # Votre jeton d'accès personnel Forgejo + + - name: Push Docker image to Forgejo Container Registry + run: | + docker push git.wilmoredynamics.com/ab/ab01:${GITHUB_SHA::8} + docker push git.wilmoredynamics.com/ab/ab01:latest \ No newline at end of file diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..956caad --- /dev/null +++ b/dockerfile @@ -0,0 +1,66 @@ +# syntax=docker.io/docker/dockerfile:1 + +FROM node:18-alpine AS base + +# Install dependencies only when needed +FROM base AS deps +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Install dependencies based on the preferred package manager +COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./ +RUN \ + if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ + elif [ -f package-lock.json ]; then npm ci; \ + elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \ + else echo "Lockfile not found." && exit 1; \ + fi + + +# Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Next.js collects completely anonymous telemetry data about general usage. +# Learn more here: https://nextjs.org/telemetry +# Uncomment the following line in case you want to disable telemetry during the build. +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN \ + if [ -f yarn.lock ]; then yarn run build; \ + elif [ -f package-lock.json ]; then npm run build; \ + elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \ + else echo "Lockfile not found." && exit 1; \ + fi + +# Production image, copy all the files and run next +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production +# Uncomment the following line in case you want to disable telemetry during runtime. +# ENV NEXT_TELEMETRY_DISABLED=1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT=3000 + +# server.js is created by next build from the standalone output +# https://nextjs.org/docs/pages/api-reference/config/next-config-js/output +ENV HOSTNAME="0.0.0.0" +CMD ["node", "server.js"] \ No newline at end of file diff --git a/fdr.md b/fdr.md new file mode 100644 index 0000000..45ba824 --- /dev/null +++ b/fdr.md @@ -0,0 +1,54 @@ +Absolument ! Voici la feuille de route condensée, sans la configuration du projet, la phase de tests ni le déploiement : + +**Phase 1 : Conception et Planification** + +1. **Définir la portée de l'application :** + * **Jeux :** Mots croisés, Sudoku, Binero. + * **Niveaux de difficulté :** Facile, Moyen, Difficile (et potentiellement Expert). Définir les critères pour chaque niveau de chaque jeu. + * **Fonctionnalités :** Score par partie, progression locale (sauvegarde de l'état du jeu et des meilleurs scores). + * **Public cible :** Tout public, avec une interface intuitive et accessible. + * **Design :** Inspiration Dieter Rams (minimalisme, clarté, fonctionnalité) et Apple (simplicité, attention aux détails, typographie soignée). + +2. **Choisir les technologies :** + * Next.js, TypeScript, Material UI. + +3. **Conception de l'interface utilisateur (UI) et de l'expérience utilisateur (UX) :** + * **Principes de design :** Minimalisme, clarté, fonctionnalité, cohérence, attention aux détails. + * **Wireframes :** Schémas simples des écrans clés (accueil, sélection du jeu, interface de chaque jeu). + * **Maquettes (Mockups) :** Designs détaillés basés sur Material UI, personnalisés pour un style épuré (couleurs neutres, typographie élégante, espaces blancs). + +4. **Planification de la structure du projet :** + * Organisation des dossiers (`pages`, `components`, `styles`, `utils`, `types`). + +**Phase 2 : Développement des Jeux** + +1. **Développement du jeu de Mots Croisés :** + * **Logique du jeu :** Gestion des niveaux de difficulté (taille de la grille, nombre de mots, complexité des définitions), génération (ou utilisation de grilles prédéfinies), vérification de la solution, calcul du score. + * **Interface utilisateur (avec Material UI) :** Affichage de la grille, des définitions, interaction pour remplir les cases. + +2. **Développement du jeu de Sudoku :** + * **Logique du jeu :** Gestion des niveaux de difficulté (nombre de cases pré-remplies), génération de grilles valides, vérification de la solution, calcul du score. + * **Interface utilisateur (avec Material UI) :** Affichage de la grille, interaction pour entrer les chiffres. + +3. **Développement du jeu de Binero :** + * **Logique du jeu :** Gestion des niveaux de difficulté (taille de la grille, nombre de cases initialement remplies), génération de grilles valides, vérification de la solution, calcul du score. + * **Interface utilisateur (avec Material UI) :** Affichage de la grille, interaction pour placer des 0 ou des 1. + +**Phase 3 : Développement des Fonctionnalités Additionnelles** + +* **Système de score :** Implémenter la logique de calcul du score pour chaque jeu. +* **Progression locale :** Utiliser `localStorage` pour sauvegarder l'état des parties en cours et les meilleurs scores. + +**Phase 4 : Conception et Développement de l'Interface Utilisateur (UI)** + +* **Thème Material UI :** Personnalisation pour refléter le style Dieter Rams et Apple (couleurs, typographie, espacements). +* **Composants Material UI :** Utilisation judicieuse et minimaliste des composants. +* **Mise en page :** Création d'interfaces claires et organisées avec les outils de Material UI. +* **Typographie :** Choix d'une police élégante et lisible. +* **Icônes :** Utilisation parcimonieuse et significative des icônes Material UI. + +**Phase 5 : Maintenance et Améliorations** + +* Surveillance de l'application, correction des bugs, ajout de nouvelles fonctionnalités. + +Cette feuille de route se concentre sur les étapes essentielles de conception et de développement de votre application. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e0764b0..39442c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,10 @@ "name": "game", "version": "0.1.0", "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@mui/icons-material": "^7.0.2", + "@mui/material": "^7.0.2", "next": "15.3.0", "react": "^19.0.0", "react-dom": "^19.0.0" @@ -37,6 +41,139 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", + "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", + "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.27.0", + "@babel/parser": "^7.27.0", + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@emnapi/core": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.1.tgz", @@ -70,6 +207,152 @@ "tslib": "^2.4.0" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", + "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz", + "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.0.tgz", @@ -174,6 +457,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/js": { "version": "9.24.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz", @@ -664,6 +960,287 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.0.2.tgz", + "integrity": "sha512-TfeFU9TgN1N06hyb/pV/63FfO34nijZRMqgHk0TJ3gkl4Fbd+wZ73+ZtOd7jag6hMmzO9HSrBc6Vdn591nhkAg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.0.2.tgz", + "integrity": "sha512-Bo57PFLOqXOqPNrXjd8AhzH5s6TCsNUQbvnQ0VKZ8D+lIlteqKnrk/O1luMJUc/BXONK7BfIdTdc7qOnXYbMdw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^7.0.2", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.0.2.tgz", + "integrity": "sha512-rjJlJ13+3LdLfobRplkXbjIFEIkn6LgpetgU/Cs3Xd8qINCCQK9qXQIjjQ6P0FXFTPFzEVMj0VgBR1mN+FhOcA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.0", + "@mui/core-downloads-tracker": "^7.0.2", + "@mui/system": "^7.0.2", + "@mui/types": "^7.4.1", + "@mui/utils": "^7.0.2", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^19.1.0", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^7.0.2", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.0.2.tgz", + "integrity": "sha512-6lt8heDC9wN8YaRqEdhqnm0cFCv08AMf4IlttFvOVn7ZdKd81PNpD/rEtPGLLwQAFyyKSxBG4/2XCgpbcdNKiA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.0", + "@mui/utils": "^7.0.2", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.0.2.tgz", + "integrity": "sha512-11Bt4YdHGlh7sB8P75S9mRCUxTlgv7HGbr0UKz6m6Z9KLeiw1Bm9y/t3iqLLVMvSHYB6zL8X8X+LmfTE++gyBw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.0", + "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.0.2.tgz", + "integrity": "sha512-yFUraAWYWuKIISPPEVPSQ1NLeqmTT4qiQ+ktmyS8LO/KwHxB+NNVOacEZaIofh5x1NxY8rzphvU5X2heRZ/RDA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.0", + "@mui/private-theming": "^7.0.2", + "@mui/styled-engine": "^7.0.2", + "@mui/types": "^7.4.1", + "@mui/utils": "^7.0.2", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.1.tgz", + "integrity": "sha512-gUL8IIAI52CRXP/MixT1tJKt3SI6tVv4U/9soFsTtAsHzaJQptZ42ffdHZV3niX1ei0aUgMvOxBBN0KYqdG39g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.0" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.0.2.tgz", + "integrity": "sha512-72gcuQjPzhj/MLmPHLCgZjy2VjOH4KniR/4qRtXTTXIEwbkgcN+Y5W/rC90rWtMmZbjt9svZev/z+QHUI4j74w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.0", + "@mui/types": "^7.4.1", + "@types/prop-types": "^15.7.14", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.8", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.8.tgz", @@ -869,6 +1446,16 @@ "node": ">=12.4.0" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1177,11 +1764,22 @@ "undici-types": "~6.19.2" } }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.1.1", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.1.tgz", "integrity": "sha512-ePapxDL7qrgqSF67s0h9m412d9DbXyC1n59O2st+9rjuuamWsZuD2w55rqY12CbzsZ7uVXb5Nw0gEp9Z8MMutQ==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -1197,6 +1795,15 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.29.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.1.tgz", @@ -1944,6 +2551,21 @@ "node": ">= 0.4" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2040,7 +2662,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2089,6 +2710,15 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -2141,6 +2771,28 @@ "dev": true, "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2160,7 +2812,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -2228,7 +2879,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2308,6 +2958,16 @@ "node": ">=0.10.0" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2344,6 +3004,15 @@ "node": ">=10.13.0" } }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-abstract": { "version": "1.23.9", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", @@ -2522,7 +3191,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -3044,6 +3712,12 @@ "node": ">=8" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3102,7 +3776,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3223,16 +3896,12 @@ } }, "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "license": "MIT", "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=4" } }, "node_modules/globalthis": { @@ -3364,7 +4033,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -3373,6 +4041,21 @@ "node": ">= 0.4" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3387,7 +4070,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -3444,11 +4126,10 @@ } }, "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT", - "optional": true + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" }, "node_modules/is-async-function": { "version": "2.1.1", @@ -3530,7 +4211,6 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -3866,7 +4546,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -3882,6 +4561,18 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -3889,6 +4580,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -4215,6 +4912,12 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4242,7 +4945,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -4312,7 +5014,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -4426,7 +5127,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4617,7 +5317,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -4626,6 +5325,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4650,9 +5367,17 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4725,7 +5450,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -4733,6 +5457,12 @@ "react-is": "^16.13.1" } }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4786,12 +5516,27 @@ } }, "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", + "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==", "license": "MIT" }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -4815,6 +5560,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -4840,7 +5591,6 @@ "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -4861,7 +5611,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -5185,6 +5934,22 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT", + "optional": true + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5368,6 +6133,12 @@ } } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -5385,7 +6156,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5785,6 +6555,15 @@ "node": ">=0.10.0" } }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 7a856df..1651b2b 100644 --- a/package.json +++ b/package.json @@ -9,19 +9,23 @@ "lint": "next lint" }, "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@mui/icons-material": "^7.0.2", + "@mui/material": "^7.0.2", + "next": "15.3.0", "react": "^19.0.0", - "react-dom": "^19.0.0", - "next": "15.3.0" + "react-dom": "^19.0.0" }, "devDependencies": { - "typescript": "^5", + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "@tailwindcss/postcss": "^4", - "tailwindcss": "^4", "eslint": "^9", "eslint-config-next": "15.3.0", - "@eslint/eslintrc": "^3" + "tailwindcss": "^4", + "typescript": "^5" } } diff --git a/src/app/binero/page.tsx b/src/app/binero/page.tsx new file mode 100644 index 0000000..b3ecfd2 --- /dev/null +++ b/src/app/binero/page.tsx @@ -0,0 +1,775 @@ +'use client'; + +import { Box, Container, Typography, Button, useTheme, ButtonGroup, Alert, Snackbar, IconButton, Tooltip, Paper, Divider, Dialog, DialogTitle, DialogContent, LinearProgress, Badge } from '@mui/material'; +import { BineroGrid } from '@/components/games/binero/BineroGrid'; +import { Timer } from '@/components/games/sudoku/Timer'; +import { generateBinero, validateGrid, isGridComplete, calculateScore } from '@/components/games/binero/bineroLogic'; +import { BineroRules } from '@/components/games/binero/BineroRules'; +import { useState, useCallback, useEffect, useMemo } from 'react'; +import { Help, Refresh, CheckCircle, Timer as TimerIcon, EmojiEvents, Undo, Save, Settings } from '@mui/icons-material'; +import { useLocalStorage } from '@/hooks/useLocalStorage'; + +type CellValue = 0 | 1 | null; +type Grid = CellValue[][]; + +interface GameState { + grid: Grid; + difficulty: 'easy' | 'medium' | 'hard'; + isPlaying: boolean; + score: number; + size: number; + mistakes: number; + gridSize: { rows: number; cols: number }; +} + +const DIFFICULTY_MULTIPLIER = { + easy: 1, + medium: 1.5, + hard: 2, +}; + +const GRID_SIZES = [ + { rows: 6, cols: 6, label: '6×6' }, + { rows: 8, cols: 8, label: '8×8' }, + { rows: 10, cols: 10, label: '10×10' }, + { rows: 12, cols: 12, label: '12×12' }, + { rows: 14, cols: 14, label: '14×14' }, +]; + +export default function BineroPage() { + const theme = useTheme(); + const [showRules, setShowRules] = useState(false); + const [showHints, setShowHints] = useState(false); + const [showSettings, setShowSettings] = useState(false); + const [saveCounter, setSaveCounter] = useState(0); + const [bestScores, setBestScores] = useLocalStorage>('bineroBestScores', { + easy: 0, + medium: 0, + hard: 0, + }); + const [savedGames, setSavedGames] = useLocalStorage>('bineroSavedGames', {}); + const [history, setHistory] = useState([]); + const [historyIndex, setHistoryIndex] = useState(-1); + + const [gameState, setGameState] = useState(() => ({ + grid: generateBinero(8, 'easy'), + difficulty: 'easy', + isPlaying: false, + score: 0, + size: 8, + mistakes: 0, + gridSize: { rows: 8, cols: 8 }, + })); + + const [message, setMessage] = useState<{ text: string; severity: 'success' | 'error' | 'info' }>({ + text: '', + severity: 'info', + }); + + const memoizedGrid = useMemo(() => gameState.grid, [gameState.grid]); + + const gridCompletion = useMemo(() => { + const totalCells = gameState.grid.length * gameState.grid.length; + const filledCells = gameState.grid.flat().filter(cell => cell !== null).length; + return Math.round((filledCells / totalCells) * 100); + }, [gameState.grid]); + + const updateBestScore = useCallback((score: number) => { + setBestScores(prev => ({ + ...prev, + [gameState.difficulty]: Math.max(score, prev[gameState.difficulty] || 0), + })); + }, [gameState.difficulty, setBestScores]); + + useEffect(() => { + if (!gameState.isPlaying && gameState.score > 0) { + updateBestScore(gameState.score); + } + }, [gameState.isPlaying, gameState.score, updateBestScore]); + + const handleCellChange = useCallback((row: number, col: number, value: 0 | 1 | null) => { + setGameState(prev => { + const newGrid = prev.grid.map((r, i) => + r.map((c, j) => (i === row && j === col ? value : c)) + ); + + setHistory(prevHistory => { + const newHistory = prevHistory.slice(0, historyIndex + 1); + newHistory.push(JSON.parse(JSON.stringify(newGrid))); + return newHistory; + }); + setHistoryIndex(prev => prev + 1); + + return { ...prev, grid: newGrid }; + }); + }, [historyIndex]); + + const handleGameComplete = useCallback(() => { + setGameState(prev => ({ ...prev, isPlaying: false })); + setMessage({ text: 'Félicitations ! Vous avez terminé le Binero !', severity: 'success' }); + }, []); + + const handleNewGame = useCallback((newDifficulty: GameState['difficulty'], newSize?: { rows: number; cols: number }) => { + const size = newSize || gameState.gridSize; + + setTimeout(() => { + const newGrid = generateBinero(size.rows, newDifficulty); + + setGameState(prev => ({ + ...prev, + grid: newGrid, + difficulty: newDifficulty, + isPlaying: true, + score: 0, + mistakes: 0, + gridSize: size, + })); + + setMessage({ text: `Nouvelle partie en difficulté ${newDifficulty} (${size.rows}×${size.cols})`, severity: 'info' }); + }, 0); + }, [gameState.gridSize]); + + const handleCheck = useCallback(() => { + const isComplete = isGridComplete(gameState.grid); + + if (!isComplete) { + setMessage({ + text: 'La grille n\'est pas complète. Continuez à remplir les cases vides.', + severity: 'info' + }); + return; + } + + // Vérifier les règles une par une pour donner des messages plus précis + let errorMessage = ''; + + // Vérifier le nombre égal de 0 et 1 dans chaque ligne et colonne + for (let i = 0; i < gameState.grid.length; i++) { + const row = gameState.grid[i]; + const col = gameState.grid.map(r => r[i]); + + const rowZeros = row.filter(cell => cell === 0).length; + const rowOnes = row.filter(cell => cell === 1).length; + const colZeros = col.filter(cell => cell === 0).length; + const colOnes = col.filter(cell => cell === 1).length; + + if (rowZeros !== rowOnes) { + errorMessage = `La ligne ${i+1} n'a pas un nombre égal de 0 et de 1.`; + break; + } + + if (colZeros !== colOnes) { + errorMessage = `La colonne ${i+1} n'a pas un nombre égal de 0 et de 1.`; + break; + } + } + + // Vérifier les chiffres consécutifs + if (!errorMessage) { + for (let i = 0; i < gameState.grid.length; i++) { + const row = gameState.grid[i]; + const col = gameState.grid.map(r => r[i]); + + for (let j = 0; j < gameState.grid.length - 2; j++) { + if (row[j] !== null && row[j] === row[j+1] && row[j] === row[j+2]) { + errorMessage = `La ligne ${i+1} contient trois ${row[j]} consécutifs.`; + break; + } + + if (col[j] !== null && col[j] === col[j+1] && col[j] === col[j+2]) { + errorMessage = `La colonne ${i+1} contient trois ${col[j]} consécutifs.`; + break; + } + } + + if (errorMessage) break; + } + } + + // Vérifier les lignes/colonnes identiques uniquement en mode difficile + if (!errorMessage && gameState.difficulty === 'hard') { + const rows = gameState.grid.map(row => row.join(',')); + const cols = gameState.grid[0].map((_, colIndex) => + gameState.grid.map(row => row[colIndex]).join(',') + ); + + // Vérifier les lignes identiques + for (let i = 0; i < rows.length; i++) { + for (let j = i + 1; j < rows.length; j++) { + if (rows[i] === rows[j]) { + errorMessage = `Les lignes ${i+1} et ${j+1} sont identiques.`; + break; + } + } + if (errorMessage) break; + } + + // Vérifier les colonnes identiques + if (!errorMessage) { + for (let i = 0; i < cols.length; i++) { + for (let j = i + 1; j < cols.length; j++) { + if (cols[i] === cols[j]) { + errorMessage = `Les colonnes ${i+1} et ${j+1} sont identiques.`; + break; + } + } + if (errorMessage) break; + } + } + } + + if (errorMessage) { + setGameState(prev => ({ ...prev, mistakes: prev.mistakes + 1 })); + setMessage({ + text: `Erreur : ${errorMessage}`, + severity: 'error' + }); + } else { + setMessage({ + text: 'Félicitations ! Votre grille est valide et complète.', + severity: 'success' + }); + handleGameComplete(); + } + }, [gameState.grid, gameState.difficulty, handleGameComplete]); + + const handleTimeUpdate = useCallback((seconds: number) => { + if (!gameState.isPlaying) return; + + const currentScore = calculateScore( + gameState.difficulty, + seconds, + gameState.mistakes, + gameState.gridSize + ); + + setGameState(prev => ({ ...prev, score: currentScore })); + }, [gameState.difficulty, gameState.mistakes, gameState.gridSize, gameState.isPlaying]); + + const handleCellClick = useCallback((row: number, col: number) => { + const currentValue = gameState.grid[row][col]; + let newValue: 0 | 1 | null = null; + + if (currentValue === null) { + newValue = 0; + } else if (currentValue === 0) { + newValue = 1; + } + + handleCellChange(row, col, newValue); + }, [gameState.grid, handleCellChange]); + + useEffect(() => { + if (isGridComplete(gameState.grid) && validateGrid(gameState.grid, gameState.difficulty)) { + handleGameComplete(); + } + }, [gameState.grid, gameState.difficulty, handleGameComplete]); + + const handleUndo = useCallback(() => { + if (historyIndex > 0) { + setHistoryIndex(prev => prev - 1); + setGameState(prev => ({ + ...prev, + grid: JSON.parse(JSON.stringify(history[historyIndex - 1])) + })); + } + }, [history, historyIndex]); + + const handleSaveGame = useCallback(() => { + const saveKey = `binero_${gameState.difficulty}_${gameState.gridSize.rows}x${gameState.gridSize.cols}_${saveCounter}`; + setSaveCounter(prev => prev + 1); + setSavedGames(prev => ({ + ...prev, + [saveKey]: gameState + })); + setMessage({ text: 'Partie sauvegardée avec succès !', severity: 'success' }); + }, [gameState, setSavedGames, saveCounter]); + + const handleLoadGame = useCallback((savedGame: GameState) => { + setGameState(savedGame); + setHistory([JSON.parse(JSON.stringify(savedGame.grid))]); + setHistoryIndex(0); + setMessage({ text: 'Partie chargée avec succès !', severity: 'success' }); + setShowSettings(false); + }, []); + + return ( + + + + + + Binero + + + + setShowRules(!showRules)} color="primary"> + + + + + setShowSettings(!showSettings)} color="primary"> + + + + + handleNewGame(gameState.difficulty)} color="primary"> + + + + + + + + + + + + + + + + Score: {gameState.score} + + + + + + + + Progression: {gridCompletion}% + + + Meilleur score ({gameState.difficulty}): {bestScores[gameState.difficulty] || 0} + + + + + + + + + + + + + + + + + + {GRID_SIZES.map((size) => ( + + ))} + + + + + + + + + + + + + + + + + + + + + + + + + + setShowRules(false)} + maxWidth="sm" + fullWidth + > + Règles du Binero + + + + + + setShowSettings(false)} + maxWidth="sm" + fullWidth + > + Paramètres et parties sauvegardées + + Parties sauvegardées + {Object.keys(savedGames).length === 0 ? ( + + Aucune partie sauvegardée. + + ) : ( + + {Object.entries(savedGames).map(([key, game]) => ( + + + + {game.difficulty.charAt(0).toUpperCase() + game.difficulty.slice(1)} ({game.gridSize.rows}×{game.gridSize.cols}) + + + Score: {game.score} | Progression: {Math.round((game.grid.flat().filter(cell => cell !== null).length / (game.gridSize.rows * game.gridSize.cols)) * 100)}% + + + + + ))} + + )} + + + + setMessage({ text: '', severity: 'info' })} + anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} + > + + {message.text} + + + + + ); +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7fa87e..0df502b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,9 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; +import { ColorModeProvider } from '@/context/ColorModeContext'; +import { Navbar } from '@/components/Navbar'; +import { CssBaseline } from '@mui/material'; const geistSans = Geist({ variable: "--font-geist-sans", @@ -13,21 +16,23 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Logica - Jeux de Logique", + description: "Collection de jeux de logique et de réflexion", }; export default function RootLayout({ children, -}: Readonly<{ +}: { children: React.ReactNode; -}>) { +}) { return ( - - - {children} + + + + + + {children} + ); diff --git a/src/app/page.tsx b/src/app/page.tsx index e68abe6..08b33fa 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,103 +1,112 @@ -import Image from "next/image"; +'use client'; + +import { Box, Container, Typography, Grid, Card, CardContent, CardActions, Button, useTheme } from '@mui/material'; +import { Extension, Grid3x3, GridOn } from '@mui/icons-material'; +import Link from 'next/link'; export default function Home() { - return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - src/app/page.tsx - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
+ const theme = useTheme(); - -
- -
+ {games.map((game) => ( + + + + {game.icon} + + {game.title} + + {game.description} + + + + + + + ))} + + + ); } diff --git a/src/app/sudoku/page.tsx b/src/app/sudoku/page.tsx new file mode 100644 index 0000000..f033c1a --- /dev/null +++ b/src/app/sudoku/page.tsx @@ -0,0 +1,197 @@ +'use client'; + +import { Box, Container, Typography, Button, useTheme, ButtonGroup, Alert, Snackbar } from '@mui/material'; +import { SudokuGrid } from '@/components/games/sudoku/SudokuGrid'; +import { Timer } from '@/components/games/sudoku/Timer'; +import { generateSudoku, validateGrid, isGridComplete } from '@/components/games/sudoku/sudokuLogic'; +import { useState, useCallback, useEffect } from 'react'; + +interface GameState { + grid: (number | null)[][]; + difficulty: 'easy' | 'medium' | 'hard'; + isPlaying: boolean; + score: number; + bestScores: Record; +} + +const DIFFICULTY_MULTIPLIER = { + easy: 1, + medium: 2, + hard: 3, +}; + +const calculateScore = (difficulty: 'easy' | 'medium' | 'hard', timeInSeconds: number): number => { + const baseScore = 1000; + const timeMultiplier = Math.max(0, 1 - timeInSeconds / 3600); // 1 heure max + return Math.round(baseScore * DIFFICULTY_MULTIPLIER[difficulty] * timeMultiplier); +}; + +export default function SudokuPage() { + const theme = useTheme(); + const [gameState, setGameState] = useState(() => { + const savedState = localStorage.getItem('sudokuGameState'); + const savedBestScores = localStorage.getItem('sudokuBestScores'); + + return { + grid: generateSudoku('easy'), + difficulty: 'easy', + isPlaying: false, + score: 0, + bestScores: savedBestScores ? JSON.parse(savedBestScores) : { easy: 0, medium: 0, hard: 0 }, + }; + }); + + const [message, setMessage] = useState<{ text: string; severity: 'success' | 'error' | 'info' }>({ text: '', severity: 'info' }); + + useEffect(() => { + localStorage.setItem('sudokuBestScores', JSON.stringify(gameState.bestScores)); + }, [gameState.bestScores]); + + const handleCellChange = (row: number, col: number, value: number | null) => { + const newGrid = gameState.grid.map((r, i) => + r.map((c, j) => (i === row && j === col ? value : c)) + ); + + setGameState(prev => ({ ...prev, grid: newGrid })); + + if (isGridComplete(newGrid)) { + handleGameComplete(); + } + }; + + const handleGameComplete = () => { + setGameState(prev => ({ ...prev, isPlaying: false })); + setMessage({ text: 'Félicitations ! Vous avez terminé le Sudoku !', severity: 'success' }); + }; + + const handleNewGame = useCallback((newDifficulty: GameState['difficulty']) => { + setGameState(prev => ({ + ...prev, + grid: generateSudoku(newDifficulty), + difficulty: newDifficulty, + isPlaying: true, + score: 0, + })); + setMessage({ text: `Nouvelle partie en difficulté ${newDifficulty}`, severity: 'info' }); + }, []); + + const handleCheck = () => { + if (validateGrid(gameState.grid)) { + setMessage({ text: 'La grille est valide, continuez !', severity: 'success' }); + } else { + setMessage({ text: 'Il y a des erreurs dans la grille', severity: 'error' }); + } + }; + + const handleTimeUpdate = (seconds: number) => { + const currentScore = calculateScore(gameState.difficulty, seconds); + setGameState(prev => ({ ...prev, score: currentScore })); + }; + + const updateBestScore = useCallback((score: number) => { + setGameState(prev => ({ + ...prev, + bestScores: { + ...prev.bestScores, + [prev.difficulty]: Math.max(score, prev.bestScores[prev.difficulty] || 0), + }, + })); + }, []); + + useEffect(() => { + if (!gameState.isPlaying && gameState.score > 0) { + updateBestScore(gameState.score); + } + }, [gameState.isPlaying, gameState.score, updateBestScore]); + + return ( + + + + Sudoku + + + + + + + + Score: {gameState.score} + + + + Meilleur score ({gameState.difficulty}): {gameState.bestScores[gameState.difficulty] || 0} + + + + + + + + + + + + + + + + + + + + + setMessage({ text: '', severity: 'info' })} + anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} + > + + {message.text} + + + + ); +} \ No newline at end of file diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx new file mode 100644 index 0000000..cc4915a --- /dev/null +++ b/src/components/Navbar.tsx @@ -0,0 +1,93 @@ +'use client'; + +import { AppBar, Toolbar, Typography, IconButton, useTheme, Box, Drawer, List, ListItem, ListItemIcon, ListItemText } from '@mui/material'; +import { Brightness4, Brightness7, Menu, Extension, Grid3x3, GridOn } from '@mui/icons-material'; +import { useContext, useState } from 'react'; +import Link from 'next/link'; +import { ColorModeContext } from '@/context/ColorModeContext'; + +export const Navbar = () => { + const theme = useTheme(); + const colorMode = useContext(ColorModeContext); + const [drawerOpen, setDrawerOpen] = useState(false); + + const menuItems = [ + { title: 'Mots Croisés', icon: , href: '/mots-croises' }, + { title: 'Sudoku', icon: , href: '/sudoku' }, + { title: 'Binero', icon: , href: '/binero' }, + ]; + + return ( + <> + + + setDrawerOpen(true)} + > + + + + + LOGICA + + + + {menuItems.map((item) => ( + + {item.title} + + ))} + + + + {theme.palette.mode === 'dark' ? : } + + + + + setDrawerOpen(false)} + > + + {menuItems.map((item) => ( + setDrawerOpen(false)} + sx={{ textDecoration: 'none', color: 'inherit' }} + > + + {item.icon} + + + + ))} + + + + ); +}; \ No newline at end of file diff --git a/src/components/games/binero/BineroGrid.tsx b/src/components/games/binero/BineroGrid.tsx new file mode 100644 index 0000000..cc7e7ff --- /dev/null +++ b/src/components/games/binero/BineroGrid.tsx @@ -0,0 +1,161 @@ +'use client'; + +import { Box, Paper, Typography, useTheme } from '@mui/material'; +import { memo, useCallback, useMemo } from 'react'; + +type CellValue = 0 | 1 | null; +type Grid = CellValue[][]; + +interface BineroGridProps { + grid: Grid; + onCellClick: (row: number, col: number) => void; + showHints?: boolean; + difficulty: 'easy' | 'medium' | 'hard'; +} + +// Composant de cellule mémorisé pour éviter les re-rendus inutiles +const BineroCell = memo(({ + value, + isConflict, + showHints, + onClick, + theme +}: { + value: CellValue; + isConflict: boolean; + showHints: boolean; + onClick: () => void; + theme: any; +}) => ( + + + {value === null ? '·' : value} + + +)); + +BineroCell.displayName = 'BineroCell'; + +export const BineroGrid = memo(({ grid, onCellClick, showHints = false, difficulty }: BineroGridProps) => { + const theme = useTheme(); + const size = grid.length; + + // Vérifie si une cellule est en conflit avec les règles + const isCellInConflict = useCallback((row: number, col: number): boolean => { + if (grid[row][col] === null) return false; + + const value = grid[row][col]; + const rowValues = grid[row]; + const colValues = grid.map((r: CellValue[]) => r[col]); + + // Vérifier les chiffres consécutifs horizontalement + if (col >= 2 && rowValues[col - 1] === value && rowValues[col - 2] === value) return true; + if (col <= size - 3 && rowValues[col + 1] === value && rowValues[col + 2] === value) return true; + if (col > 0 && col < size - 1 && rowValues[col - 1] === value && rowValues[col + 1] === value) return true; + + // Vérifier les chiffres consécutifs verticalement + if (row >= 2 && colValues[row - 1] === value && colValues[row - 2] === value) return true; + if (row <= size - 3 && colValues[row + 1] === value && colValues[row + 2] === value) return true; + if (row > 0 && row < size - 1 && colValues[row - 1] === value && colValues[row + 1] === value) return true; + + // Vérifier le nombre de 0 et 1 dans la ligne + const rowZeros = rowValues.filter((cell: CellValue) => cell === 0).length; + const rowOnes = rowValues.filter((cell: CellValue) => cell === 1).length; + const maxAllowed = Math.ceil(size / 2); + if (rowZeros > maxAllowed || rowOnes > maxAllowed) return true; + + // Vérifier le nombre de 0 et 1 dans la colonne + const colZeros = colValues.filter((cell: CellValue) => cell === 0).length; + const colOnes = colValues.filter((cell: CellValue) => cell === 1).length; + if (colZeros > maxAllowed || colOnes > maxAllowed) return true; + + // Vérifier les lignes/colonnes identiques uniquement en mode difficile + if (difficulty === 'hard') { + const rowStr = rowValues.join(','); + const colStr = colValues.join(','); + + // Vérifier si cette ligne est identique à une autre ligne + for (let i = 0; i < size; i++) { + if (i !== row) { + const otherRowStr = grid[i].join(','); + if (rowStr === otherRowStr) return true; + } + } + + // Vérifier si cette colonne est identique à une autre colonne + for (let i = 0; i < size; i++) { + if (i !== col) { + const otherColStr = grid.map((r: CellValue[]) => r[i]).join(','); + if (colStr === otherColStr) return true; + } + } + } + + return false; + }, [grid, size, difficulty]); + + // Mémoriser la grille pour éviter les recalculs inutiles + const gridStyle = useMemo(() => ({ + display: 'grid', + gridTemplateColumns: `repeat(${size}, 1fr)`, + gap: 1, + justifyContent: 'center', + }), [size]); + + return ( + + + {grid.map((row: CellValue[], rowIndex: number) => + row.map((cell: CellValue, colIndex: number) => ( + onCellClick(rowIndex, colIndex)} + theme={theme} + /> + )) + )} + + + ); +}); + +BineroGrid.displayName = 'BineroGrid'; \ No newline at end of file diff --git a/src/components/games/binero/BineroRules.tsx b/src/components/games/binero/BineroRules.tsx new file mode 100644 index 0000000..0447f5b --- /dev/null +++ b/src/components/games/binero/BineroRules.tsx @@ -0,0 +1,30 @@ +'use client'; + +import { Box, Typography, List, ListItem, ListItemIcon, ListItemText, Paper, useTheme } from '@mui/material'; +import { Check, Close, Info } from '@mui/icons-material'; + +export const BineroRules = () => { + const theme = useTheme(); + + return ( + + + Le Binero est un jeu de logique où vous devez remplir une grille avec des 0 et des 1 en respectant trois règles : + + + + Chaque ligne et chaque colonne doit contenir un nombre égal de 0 et de 1. + + + Il ne peut pas y avoir plus de deux chiffres identiques côte à côte (horizontalement ou verticalement). + + + Deux lignes ou deux colonnes ne peuvent pas être identiques. + + + + Cliquez sur une case pour alterner entre 0, 1 et vide. Double-cliquez pour effacer la valeur. + + + ); +}; \ No newline at end of file diff --git a/src/components/games/binero/bineroLogic.ts b/src/components/games/binero/bineroLogic.ts new file mode 100644 index 0000000..3c7378b --- /dev/null +++ b/src/components/games/binero/bineroLogic.ts @@ -0,0 +1,194 @@ +type CellValue = 0 | 1 | null; +type Grid = CellValue[][]; + +interface GridSize { + rows: number; + cols: number; +} + +// Vérifie si une ligne ou colonne a un nombre égal de 0 et 1 +const hasEqualZerosAndOnes = (line: CellValue[]): boolean => { + // Ne vérifier que si la ligne est complète + if (line.some(cell => cell === null)) { + return true; // Si la ligne n'est pas complète, on considère qu'elle est valide + } + + const zeros = line.filter(cell => cell === 0).length; + const ones = line.filter(cell => cell === 1).length; + return zeros === ones; +}; + +// Vérifie s'il y a plus de deux chiffres identiques consécutifs +const hasMoreThanTwoConsecutive = (line: CellValue[]): boolean => { + // Ne vérifier que si la ligne est complète + if (line.some(cell => cell === null)) { + return false; // Si la ligne n'est pas complète, on ne vérifie pas cette règle + } + + for (let i = 0; i < line.length - 2; i++) { + // Vérifier trois cellules consécutives + if (line[i] !== null && line[i] === line[i + 1] && line[i] === line[i + 2]) { + return true; + } + } + return false; +}; + +// Vérifie si deux lignes ou colonnes sont identiques +const hasDuplicateLines = (grid: Grid): boolean => { + // Ne vérifier que si la grille est complète + if (grid.some(row => row.some(cell => cell === null))) { + return false; // Si la grille n'est pas complète, on ne vérifie pas cette règle + } + + // Convertir les lignes et colonnes en chaînes de caractères pour la comparaison + const rows = grid.map(row => row.join(',')); + const cols = grid[0].map((_, colIndex) => + grid.map(row => row[colIndex]).join(',') + ); + + // Vérifier si le nombre de lignes/colonnes uniques est égal au nombre total + // Si ce n'est pas le cas, il y a des doublons + return new Set(rows).size !== rows.length || new Set(cols).size !== cols.length; +}; + +// Génère une grille valide selon le niveau de difficulté +export const generateBinero = (size: number, difficulty: 'easy' | 'medium' | 'hard'): Grid => { + const grid: Grid = Array(size).fill(null).map(() => Array(size).fill(null)); + + // Nombre de cases pré-remplies selon la difficulté + const filledCells = { + easy: Math.floor(size * size * 0.3), + medium: Math.floor(size * size * 0.2), + hard: Math.floor(size * size * 0.1), + }[difficulty]; + + // Fonction pour vérifier si une valeur peut être placée + const canPlaceValue = (row: number, col: number, value: 0 | 1): boolean => { + // Vérifier les chiffres consécutifs horizontalement + if (col >= 2 && grid[row][col - 1] === value && grid[row][col - 2] === value) return false; + if (col <= size - 3 && grid[row][col + 1] === value && grid[row][col + 2] === value) return false; + if (col > 0 && col < size - 1 && grid[row][col - 1] === value && grid[row][col + 1] === value) return false; + + // Vérifier les chiffres consécutifs verticalement + if (row >= 2 && grid[row - 1][col] === value && grid[row - 2][col] === value) return false; + if (row <= size - 3 && grid[row + 1][col] === value && grid[row + 2][col] === value) return false; + if (row > 0 && row < size - 1 && grid[row - 1][col] === value && grid[row + 1][col] === value) return false; + + // Vérifier le nombre de 0 et 1 dans la ligne + const rowZeros = grid[row].filter(cell => cell === 0).length + (value === 0 ? 1 : 0); + const rowOnes = grid[row].filter(cell => cell === 1).length + (value === 1 ? 1 : 0); + if (rowZeros > size / 2 || rowOnes > size / 2) return false; + + // Vérifier le nombre de 0 et 1 dans la colonne + const colZeros = grid.map(r => r[col]).filter(cell => cell === 0).length + (value === 0 ? 1 : 0); + const colOnes = grid.map(r => r[col]).filter(cell => cell === 1).length + (value === 1 ? 1 : 0); + if (colZeros > size / 2 || colOnes > size / 2) return false; + + return true; + }; + + // Fonction pour résoudre la grille + const solve = (row: number = 0, col: number = 0): boolean => { + if (col === size) { + col = 0; + row++; + } + if (row === size) return true; + + if (grid[row][col] !== null) { + return solve(row, col + 1); + } + + const values: (0 | 1)[] = Math.random() < 0.5 ? [0, 1] : [1, 0]; + for (const value of values) { + if (canPlaceValue(row, col, value)) { + grid[row][col] = value; + if (solve(row, col + 1)) return true; + grid[row][col] = null; + } + } + + return false; + }; + + // Générer une solution valide + solve(); + + // Retirer des cellules aléatoirement selon la difficulté + const cellsToKeep = size * size - filledCells; + let removed = 0; + + while (removed < filledCells) { + const row = Math.floor(Math.random() * size); + const col = Math.floor(Math.random() * size); + if (grid[row][col] !== null) { + grid[row][col] = null; + removed++; + } + } + + return grid; +}; + +// Vérifie si la grille est valide selon toutes les règles +export const validateGrid = (grid: Grid, difficulty: 'easy' | 'medium' | 'hard'): boolean => { + // Vérifier si la grille est complète + if (!isGridComplete(grid)) { + return false; + } + + // Vérifier chaque ligne et colonne + for (let i = 0; i < grid.length; i++) { + const row = grid[i]; + const col = grid.map(r => r[i]); + + // Vérifier le nombre égal de 0 et 1 + if (!hasEqualZerosAndOnes(row) || !hasEqualZerosAndOnes(col)) { + return false; + } + + // Vérifier les chiffres consécutifs + if (hasMoreThanTwoConsecutive(row) || hasMoreThanTwoConsecutive(col)) { + return false; + } + } + + // Vérifier les lignes/colonnes identiques uniquement en mode difficile + if (difficulty === 'hard' && hasDuplicateLines(grid)) { + return false; + } + + return true; +}; + +// Vérifie si la grille est complète +export const isGridComplete = (grid: Grid): boolean => { + return grid.every(row => row.every(cell => cell !== null)); +}; + +// Calcule le score en fonction du temps et des erreurs +export const calculateScore = ( + difficulty: 'easy' | 'medium' | 'hard', + timeInSeconds: number, + mistakes: number, + gridSize: GridSize +): number => { + const baseScore = 1000; + const sizeMultiplier = (gridSize.rows * gridSize.cols) / 64; // 8x8 comme référence + const timeMultiplier = Math.max(0, 1 - timeInSeconds / 1800); // 30 minutes max + const mistakePenalty = Math.max(0, 1 - (mistakes * 0.1)); // -10% par erreur + const difficultyMultiplier = { + easy: 1, + medium: 1.5, + hard: 2, + }[difficulty]; + + return Math.round( + baseScore * + sizeMultiplier * + difficultyMultiplier * + timeMultiplier * + mistakePenalty + ); +}; \ No newline at end of file diff --git a/src/components/games/sudoku/SudokuGrid.tsx b/src/components/games/sudoku/SudokuGrid.tsx new file mode 100644 index 0000000..9d84917 --- /dev/null +++ b/src/components/games/sudoku/SudokuGrid.tsx @@ -0,0 +1,76 @@ +'use client'; + +import { Box, TextField, useTheme } from '@mui/material'; +import { useState } from 'react'; + +interface SudokuGridProps { + initialGrid: (number | null)[][]; + onCellChange: (row: number, col: number, value: number | null) => void; +} + +export const SudokuGrid = ({ initialGrid, onCellChange }: SudokuGridProps) => { + const theme = useTheme(); + const [grid, setGrid] = useState(initialGrid); + + const handleChange = (row: number, col: number, value: string) => { + const numValue = value === '' ? null : parseInt(value); + if (numValue === null || (numValue >= 1 && numValue <= 9)) { + const newGrid = grid.map((r, i) => + r.map((c, j) => (i === row && j === col ? numValue : c)) + ); + setGrid(newGrid); + onCellChange(row, col, numValue); + } + }; + + return ( + div': { + aspectRatio: '1/1', + }, + }} + > + {grid.map((row, rowIndex) => + row.map((cell, colIndex) => ( + handleChange(rowIndex, colIndex, e.target.value)} + inputProps={{ + maxLength: 1, + style: { + padding: 0, + textAlign: 'center', + fontSize: '1.2rem', + height: '100%', + }, + }} + sx={{ + '& .MuiOutlinedInput-root': { + height: '100%', + backgroundColor: theme.palette.background.paper, + borderRadius: 0, + borderRight: (colIndex + 1) % 3 === 0 ? 2 : 1, + borderBottom: (rowIndex + 1) % 3 === 0 ? 2 : 1, + borderColor: theme.palette.divider, + '&:hover': { + borderColor: theme.palette.primary.main, + }, + '&.Mui-focused': { + borderColor: theme.palette.primary.main, + }, + }, + }} + /> + )) + )} + + ); +}; \ No newline at end of file diff --git a/src/components/games/sudoku/Timer.tsx b/src/components/games/sudoku/Timer.tsx new file mode 100644 index 0000000..1f2fac6 --- /dev/null +++ b/src/components/games/sudoku/Timer.tsx @@ -0,0 +1,72 @@ +'use client'; + +import { useEffect, useRef, useState, useCallback } from 'react'; +import { Typography } from '@mui/material'; + +interface TimerProps { + isRunning: boolean; + onTimeUpdate?: (seconds: number) => void; +} + +export const Timer = ({ isRunning, onTimeUpdate }: TimerProps) => { + const [seconds, setSeconds] = useState(0); + const intervalRef = useRef(); + const lastUpdateRef = useRef(0); + const isFirstRender = useRef(true); + const secondsRef = useRef(0); + + const updateTimer = useCallback(() => { + const now = Date.now(); + const delta = Math.floor((now - lastUpdateRef.current) / 1000); + if (delta > 0) { + lastUpdateRef.current = now; + secondsRef.current += delta; + setSeconds(secondsRef.current); + } + }, []); + + useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } + + if (isRunning) { + lastUpdateRef.current = Date.now(); + intervalRef.current = setInterval(updateTimer, 1000); + } else { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + } + + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + }, [isRunning, updateTimer]); + + useEffect(() => { + if (onTimeUpdate) { + onTimeUpdate(secondsRef.current); + } + }, [seconds, onTimeUpdate]); + + const formatTime = (totalSeconds: number) => { + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + if (hours > 0) { + return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + } + return `${minutes}:${seconds.toString().padStart(2, '0')}`; + }; + + return ( + + {formatTime(seconds)} + + ); +}; \ No newline at end of file diff --git a/src/components/games/sudoku/sudokuLogic.ts b/src/components/games/sudoku/sudokuLogic.ts new file mode 100644 index 0000000..32a95b8 --- /dev/null +++ b/src/components/games/sudoku/sudokuLogic.ts @@ -0,0 +1,148 @@ +type Grid = (number | null)[][]; + +// Vérifie si un nombre peut être placé dans une cellule +const isValid = (grid: Grid, row: number, col: number, num: number): boolean => { + // Vérifie la ligne + for (let x = 0; x < 9; x++) { + if (grid[row][x] === num) return false; + } + + // Vérifie la colonne + for (let x = 0; x < 9; x++) { + if (grid[x][col] === num) return false; + } + + // Vérifie le bloc 3x3 + const startRow = Math.floor(row / 3) * 3; + const startCol = Math.floor(col / 3) * 3; + for (let i = 0; i < 3; i++) { + for (let j = 0; j < 3; j++) { + if (grid[i + startRow][j + startCol] === num) return false; + } + } + + return true; +}; + +// Résout une grille de Sudoku +const solveSudoku = (grid: Grid): boolean => { + let row = 0; + let col = 0; + let isEmpty = false; + + // Trouve une cellule vide + for (let i = 0; i < 9; i++) { + for (let j = 0; j < 9; j++) { + if (grid[i][j] === null) { + row = i; + col = j; + isEmpty = true; + break; + } + } + if (isEmpty) break; + } + + // Si toutes les cellules sont remplies, la grille est résolue + if (!isEmpty) return true; + + // Essaie les chiffres 1-9 + for (let num = 1; num <= 9; num++) { + if (isValid(grid, row, col, num)) { + grid[row][col] = num; + if (solveSudoku(grid)) return true; + grid[row][col] = null; + } + } + + return false; +}; + +// Génère une nouvelle grille de Sudoku +export const generateSudoku = (difficulty: 'easy' | 'medium' | 'hard'): Grid => { + // Crée une grille vide + const grid: Grid = Array(9).fill(null).map(() => Array(9).fill(null)); + + // Remplit quelques cellules aléatoirement + for (let i = 0; i < 3; i++) { + const num = Math.floor(Math.random() * 9) + 1; + const row = Math.floor(Math.random() * 9); + const col = Math.floor(Math.random() * 9); + if (isValid(grid, row, col, num)) { + grid[row][col] = num; + } + } + + // Résout la grille + solveSudoku(grid); + + // Détermine le nombre de cellules à retirer selon la difficulté + const cellsToRemove = { + easy: 40, + medium: 50, + hard: 60, + }[difficulty]; + + // Retire des cellules aléatoirement + let removed = 0; + while (removed < cellsToRemove) { + const row = Math.floor(Math.random() * 9); + const col = Math.floor(Math.random() * 9); + if (grid[row][col] !== null) { + grid[row][col] = null; + removed++; + } + } + + return grid; +}; + +// Vérifie si la grille est valide +export const validateGrid = (grid: Grid): boolean => { + // Vérifie chaque ligne + for (let row = 0; row < 9; row++) { + const nums = new Set(); + for (let col = 0; col < 9; col++) { + const num = grid[row][col]; + if (num !== null) { + if (nums.has(num)) return false; + nums.add(num); + } + } + } + + // Vérifie chaque colonne + for (let col = 0; col < 9; col++) { + const nums = new Set(); + for (let row = 0; row < 9; row++) { + const num = grid[row][col]; + if (num !== null) { + if (nums.has(num)) return false; + nums.add(num); + } + } + } + + // Vérifie chaque bloc 3x3 + for (let block = 0; block < 9; block++) { + const nums = new Set(); + const startRow = Math.floor(block / 3) * 3; + const startCol = (block % 3) * 3; + for (let i = 0; i < 3; i++) { + for (let j = 0; j < 3; j++) { + const num = grid[startRow + i][startCol + j]; + if (num !== null) { + if (nums.has(num)) return false; + nums.add(num); + } + } + } + } + + return true; +}; + +// Vérifie si la grille est complète +export const isGridComplete = (grid: Grid): boolean => { + return grid.every(row => row.every(cell => cell !== null)) && validateGrid(grid); +}; \ No newline at end of file diff --git a/src/context/ColorModeContext.tsx b/src/context/ColorModeContext.tsx new file mode 100644 index 0000000..45eb1fb --- /dev/null +++ b/src/context/ColorModeContext.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { createContext, useState, useMemo, ReactNode, useEffect } from 'react'; +import { ThemeProvider, PaletteMode } from '@mui/material'; +import { getTheme } from '@/theme/theme'; + +interface ColorModeContextType { + toggleColorMode: () => void; + mode: PaletteMode; +} + +export const ColorModeContext = createContext({ + toggleColorMode: () => {}, + mode: 'light', +}); + +interface ColorModeProviderProps { + children: ReactNode; +} + +export const ColorModeProvider = ({ children }: ColorModeProviderProps) => { + const [mode, setMode] = useState('light'); + + // Charger le thème depuis localStorage au montage + useEffect(() => { + const savedMode = localStorage.getItem('themeMode') as PaletteMode; + if (savedMode) { + setMode(savedMode); + } else { + // Détecter le thème système par défaut + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + setMode(prefersDark ? 'dark' : 'light'); + } + }, []); + + const colorMode = useMemo( + () => ({ + toggleColorMode: () => { + setMode((prevMode) => { + const newMode = prevMode === 'light' ? 'dark' : 'light'; + localStorage.setItem('themeMode', newMode); + return newMode; + }); + }, + mode, + }), + [mode] + ); + + const theme = useMemo(() => getTheme(mode), [mode]); + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts new file mode 100644 index 0000000..49bace2 --- /dev/null +++ b/src/hooks/useLocalStorage.ts @@ -0,0 +1,40 @@ +import { useState, useEffect } from 'react'; + +export function useLocalStorage(key: string, initialValue: T) { + // État pour stocker notre valeur + // Passe la fonction d'initialisation à useState pour que la logique ne s'exécute qu'une seule fois + const [storedValue, setStoredValue] = useState(() => { + if (typeof window === 'undefined') { + return initialValue; + } + + try { + const item = window.localStorage.getItem(key); + return item ? JSON.parse(item) : initialValue; + } catch (error) { + console.warn(`Error reading localStorage key "${key}":`, error); + return initialValue; + } + }); + + // Retourne une version enveloppée de la fonction setState de useState qui ... + // ... persiste la nouvelle valeur dans localStorage. + const setValue = (value: T | ((val: T) => T)) => { + try { + // Permet à la valeur d'être une fonction pour que nous ayons la même API que useState + const valueToStore = value instanceof Function ? value(storedValue) : value; + + // Sauvegarde l'état + setStoredValue(valueToStore); + + // Sauvegarde dans localStorage + if (typeof window !== 'undefined') { + window.localStorage.setItem(key, JSON.stringify(valueToStore)); + } + } catch (error) { + console.warn(`Error setting localStorage key "${key}":`, error); + } + }; + + return [storedValue, setValue] as const; +} \ No newline at end of file diff --git a/src/theme/theme.ts b/src/theme/theme.ts new file mode 100644 index 0000000..80e78c9 --- /dev/null +++ b/src/theme/theme.ts @@ -0,0 +1,50 @@ +import { createTheme, PaletteMode } from '@mui/material'; + +export const getTheme = (mode: PaletteMode) => { + return createTheme({ + palette: { + mode, + primary: { + main: mode === 'light' ? '#2C3E50' : '#3498DB', + contrastText: '#fff', + }, + secondary: { + main: mode === 'light' ? '#E74C3C' : '#E74C3C', + }, + background: { + default: mode === 'light' ? '#F5F6FA' : '#1A1A2E', + paper: mode === 'light' ? '#FFFFFF' : '#16213E', + }, + }, + typography: { + fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif', + h1: { + fontSize: '2.5rem', + fontWeight: 600, + }, + h2: { + fontSize: '2rem', + fontWeight: 500, + }, + body1: { + fontSize: '1rem', + lineHeight: 1.5, + }, + }, + components: { + MuiAppBar: { + styleOverrides: { + root: { + backgroundColor: mode === 'light' ? '#FFFFFF' : '#16213E', + color: mode === 'light' ? '#2C3E50' : '#FFFFFF', + boxShadow: 'none', + borderBottom: `1px solid ${mode === 'light' ? '#E0E0E0' : '#2C3E50'}`, + }, + }, + }, + }, + shape: { + borderRadius: 8, + }, + }); +}; \ No newline at end of file