diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml deleted file mode 100644 index c148849..0000000 --- a/.forgejo/workflows/ci.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: CI - -# Lance lint + typecheck + tests + build sur push/PR. -# -# Workflow dormant tant qu'aucun runner Forgejo n'est enregistré. -# Pour activer : -# 1) Sur git.cosmolan.fr, générer un token runner : -# Admin → Actions → Runners → Create new Runner Token -# (ou pour ce repo seul : Settings → Actions → Runners → Create) -# 2) Sur la machine d'exécution : -# wget https://codeberg.org/forgejo/runner/releases/download/v6.7.0/forgejo-runner-6.7.0-linux-amd64 -# chmod +x forgejo-runner-6.7.0-linux-amd64 -# ./forgejo-runner-6.7.0-linux-amd64 register \ -# --instance https://git.cosmolan.fr \ -# --token \ -# --name karbe-ci \ -# --labels "ubuntu-latest:docker://node:20" -# 3) Démarrer : -# ./forgejo-runner-6.7.0-linux-amd64 daemon - -on: - push: - branches: [main] - pull_request: - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: "20" - cache: "npm" - - - name: Install dependencies - run: npm ci --no-audit --no-fund - - - name: Generate Prisma client - run: npx prisma generate - - - name: Lint - run: npm run lint - - - name: Typecheck - run: npm run typecheck - - - name: Test - run: npm test - - - name: Build (smoke) - run: npm run build - env: - # Stubs nécessaires au build statique — pas de connexion réelle. - DATABASE_URL: "postgresql://stub:stub@localhost:5432/stub?schema=public" - NEXTAUTH_SECRET: "ci-secret-not-for-production" - AUTH_SECRET: "ci-secret-not-for-production" - NEXT_PUBLIC_SITE_URL: "https://example.invalid" diff --git a/package-lock.json b/package-lock.json index 7d8475d..f80d6c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,23 +10,14 @@ "hasInstallScript": true, "dependencies": { "@aws-sdk/client-s3": "^3.1056.0", - "@aws-sdk/s3-request-presigner": "^3.1058.0", - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^8.0.0", - "@dnd-kit/utilities": "^3.2.2", "@prisma/adapter-pg": "^7.8.0", "@prisma/client": "^7.8.0", - "@types/leaflet": "^1.9.21", "bcryptjs": "^3.0.3", - "leaflet": "^1.9.4", "next": "16.2.6", "next-auth": "^5.0.0-beta.31", "pg": "^8.21.0", "react": "19.2.4", "react-dom": "19.2.4", - "react-leaflet": "^5.0.0", - "resend": "^4.8.0", - "sharp": "^0.34.5", "stripe": "^18.3.0" }, "devDependencies": { @@ -34,14 +25,12 @@ "@types/node": "^20.19.41", "@types/react": "^19.2.15", "@types/react-dom": "^19.2.3", - "@vitest/coverage-v8": "^3.2.4", "dotenv": "^17.4.2", "eslint": "^9.39.4", "eslint-config-next": "^16.2.6", "prisma": "^7.8.0", "tailwindcss": "^4", - "typescript": "^5.9.3", - "vitest": "^3.2.4" + "typescript": "^5.9.3" } }, "node_modules/@alloc/quick-lru": { @@ -57,20 +46,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@auth/core": { "version": "0.41.2", "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.2.tgz", @@ -514,23 +489,6 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/s3-request-presigner": { - "version": "3.1058.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1058.0.tgz", - "integrity": "sha512-IRgNfn8U3zfsZ0JkpmwjS59R/XyHMHxpuwW6HVuJhik+FsbClhNkujEO0w1WqJvXrF4FX+7qIAwUrvlwNvaZ7Q==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.15", - "@aws-sdk/signature-v4-multi-region": "^3.996.30", - "@aws-sdk/types": "^3.973.9", - "@smithy/core": "^3.24.5", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@aws-sdk/signature-v4-multi-region": { "version": "3.996.30", "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.30.tgz", @@ -747,7 +705,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -757,7 +715,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -791,7 +749,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.7" @@ -841,7 +799,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.29.7", @@ -851,69 +809,6 @@ "node": ">=6.9.0" } }, - "node_modules/@bcoe/v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@dnd-kit/accessibility": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", - "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/core": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", - "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", - "license": "MIT", - "dependencies": { - "@dnd-kit/accessibility": "^3.1.1", - "@dnd-kit/utilities": "^3.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/sortable": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-8.0.0.tgz", - "integrity": "sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==", - "license": "MIT", - "dependencies": { - "@dnd-kit/utilities": "^3.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "@dnd-kit/core": "^6.1.0", - "react": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/utilities": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", - "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, "node_modules/@electric-sql/pglite": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz", @@ -977,448 +872,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -1647,6 +1100,7 @@ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "license": "MIT", + "optional": true, "engines": { "node": ">=18" } @@ -2107,34 +1561,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", - "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -2424,17 +1850,6 @@ "url": "https://github.com/sponsors/panva" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@prisma/adapter-pg": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.8.0.tgz", @@ -2816,424 +2231,6 @@ } } }, - "node_modules/@react-email/render": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.1.2.tgz", - "integrity": "sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw==", - "license": "MIT", - "dependencies": { - "html-to-text": "^9.0.5", - "prettier": "^3.5.3", - "react-promise-suspense": "^0.3.4" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "react": "^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" - } - }, - "node_modules/@react-leaflet/core": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz", - "integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==", - "license": "Hippocratic-2.1", - "peerDependencies": { - "leaflet": "^1.9.0", - "react": "^19.0.0", - "react-dom": "^19.0.0" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", - "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", - "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", - "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", - "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", - "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", - "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", - "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", - "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", - "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", - "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", - "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", - "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", - "cpu": [ - "loong64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", - "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", - "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", - "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", - "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", - "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", - "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", - "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", - "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", - "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", - "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", - "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", - "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", - "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -3241,19 +2238,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@selderee/plugin-htmlparser2": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", - "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", - "license": "MIT", - "dependencies": { - "domhandler": "^5.0.3", - "selderee": "^0.11.0" - }, - "funding": { - "url": "https://ko-fi.com/killymxi" - } - }, "node_modules/@smithy/core": { "version": "3.24.5", "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.5.tgz", @@ -3672,24 +2656,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -3697,12 +2663,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/geojson": { - "version": "7946.0.16", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", - "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", - "license": "MIT" - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -3717,15 +2677,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/leaflet": { - "version": "1.9.21", - "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz", - "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==", - "license": "MIT", - "dependencies": { - "@types/geojson": "*" - } - }, "node_modules/@types/node": { "version": "20.19.41", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", @@ -3760,7 +2711,7 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -4374,155 +3325,6 @@ "win32" ] }, - "node_modules/@vitest/coverage-v8": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", - "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "@bcoe/v8-coverage": "^1.0.2", - "ast-v8-to-istanbul": "^0.3.3", - "debug": "^4.4.1", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.6", - "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.17", - "magicast": "^0.3.5", - "std-env": "^3.9.0", - "test-exclude": "^7.0.1", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@vitest/browser": "3.2.4", - "vitest": "3.2.4" - }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - } - } - }, - "node_modules/@vitest/expect": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "3.2.4", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", - "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "3.2.4", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", - "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -4563,19 +3365,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -4769,16 +3558,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -4786,25 +3565,6 @@ "dev": true, "license": "MIT" }, - "node_modules/ast-v8-to-istanbul": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", - "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.31", - "estree-walker": "^3.0.3", - "js-tokens": "^10.0.0" - } - }, - "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", - "dev": true, - "license": "MIT" - }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -4989,16 +3749,6 @@ } } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -5077,23 +3827,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -5124,16 +3857,6 @@ "pnpm": ">=8" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, "node_modules/chokidar": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", @@ -5298,16 +4021,6 @@ } } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -5315,15 +4028,6 @@ "dev": true, "license": "MIT" }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/deepmerge-ts": { "version": "7.1.5", "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", @@ -5398,6 +4102,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -5416,61 +4121,6 @@ "node": ">=0.10.0" } }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, "node_modules/dotenv": { "version": "17.4.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", @@ -5498,13 +4148,6 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/effect": { "version": "3.20.0", "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz", @@ -5554,18 +4197,6 @@ "node": ">=10.13.0" } }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/env-paths": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", @@ -5694,13 +4325,6 @@ "node": ">= 0.4" } }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, - "license": "MIT" - }, "node_modules/es-object-atoms": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", @@ -5760,48 +4384,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@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" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -6221,16 +4803,6 @@ "node": ">=4.0" } }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -6241,16 +4813,6 @@ "node": ">=0.10.0" } }, - "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/exsolve": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", @@ -6493,21 +5055,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -6663,28 +5210,6 @@ "giget": "dist/cli.mjs" } }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -6698,32 +5223,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -6906,48 +5405,6 @@ "node": ">=16.9.0" } }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/html-to-text": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", - "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==", - "license": "MIT", - "dependencies": { - "@selderee/plugin-htmlparser2": "^0.11.0", - "deepmerge": "^4.3.1", - "dom-serializer": "^2.0.0", - "htmlparser2": "^8.0.2", - "selderee": "^0.11.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" - } - }, "node_modules/http-status-codes": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", @@ -7208,16 +5665,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -7470,60 +5917,6 @@ "devOptional": true, "license": "ISC" }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -7542,22 +5935,6 @@ "node": ">= 0.4" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", @@ -7690,21 +6067,6 @@ "node": ">=0.10" } }, - "node_modules/leac": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", - "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==", - "license": "MIT", - "funding": { - "url": "https://ko-fi.com/killymxi" - } - }, - "node_modules/leaflet": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", - "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", - "license": "BSD-2-Clause" - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -8023,13 +6385,6 @@ "loose-envify": "cli.js" } }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -8066,47 +6421,6 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/magicast": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", - "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -8163,16 +6477,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -8598,13 +6902,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -8618,19 +6915,6 @@ "node": ">=6" } }, - "node_modules/parseley": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", - "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", - "license": "MIT", - "dependencies": { - "leac": "^0.6.0", - "peberminta": "^0.9.0" - }, - "funding": { - "url": "https://ko-fi.com/killymxi" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -8673,30 +6957,6 @@ "dev": true, "license": "MIT" }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -8704,25 +6964,6 @@ "devOptional": true, "license": "MIT" }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, - "node_modules/peberminta": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", - "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", - "license": "MIT", - "funding": { - "url": "https://ko-fi.com/killymxi" - } - }, "node_modules/perfect-debounce": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", @@ -8980,21 +7221,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prettier": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", - "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, "node_modules/prisma": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.8.0.tgz", @@ -9162,35 +7388,6 @@ "dev": true, "license": "MIT" }, - "node_modules/react-leaflet": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz", - "integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==", - "license": "Hippocratic-2.1", - "dependencies": { - "@react-leaflet/core": "^3.0.0" - }, - "peerDependencies": { - "leaflet": "^1.9.0", - "react": "^19.0.0", - "react-dom": "^19.0.0" - } - }, - "node_modules/react-promise-suspense": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz", - "integrity": "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^2.0.1" - } - }, - "node_modules/react-promise-suspense/node_modules/fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", - "license": "MIT" - }, "node_modules/readdirp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", @@ -9269,18 +7466,6 @@ "node": ">=0.10.0" } }, - "node_modules/resend": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/resend/-/resend-4.8.0.tgz", - "integrity": "sha512-R8eBOFQDO6dzRTDmaMEdpqrkmgSjPpVXt4nGfWsZdYOet0kqra0xgbvTES6HmCriZEXbmGk3e0DiGIaLFTFSHA==", - "license": "MIT", - "dependencies": { - "@react-email/render": "1.1.2" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/resolve": { "version": "2.0.0-next.7", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", @@ -9346,58 +7531,6 @@ "node": ">=0.10.0" } }, - "node_modules/rollup": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", - "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.4", - "@rollup/rollup-android-arm64": "4.60.4", - "@rollup/rollup-darwin-arm64": "4.60.4", - "@rollup/rollup-darwin-x64": "4.60.4", - "@rollup/rollup-freebsd-arm64": "4.60.4", - "@rollup/rollup-freebsd-x64": "4.60.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", - "@rollup/rollup-linux-arm-musleabihf": "4.60.4", - "@rollup/rollup-linux-arm64-gnu": "4.60.4", - "@rollup/rollup-linux-arm64-musl": "4.60.4", - "@rollup/rollup-linux-loong64-gnu": "4.60.4", - "@rollup/rollup-linux-loong64-musl": "4.60.4", - "@rollup/rollup-linux-ppc64-gnu": "4.60.4", - "@rollup/rollup-linux-ppc64-musl": "4.60.4", - "@rollup/rollup-linux-riscv64-gnu": "4.60.4", - "@rollup/rollup-linux-riscv64-musl": "4.60.4", - "@rollup/rollup-linux-s390x-gnu": "4.60.4", - "@rollup/rollup-linux-x64-gnu": "4.60.4", - "@rollup/rollup-linux-x64-musl": "4.60.4", - "@rollup/rollup-openbsd-x64": "4.60.4", - "@rollup/rollup-openharmony-arm64": "4.60.4", - "@rollup/rollup-win32-arm64-msvc": "4.60.4", - "@rollup/rollup-win32-ia32-msvc": "4.60.4", - "@rollup/rollup-win32-x64-gnu": "4.60.4", - "@rollup/rollup-win32-x64-msvc": "4.60.4", - "fsevents": "~2.3.2" - } - }, - "node_modules/rollup/node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -9490,18 +7623,6 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, - "node_modules/selderee": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", - "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", - "license": "MIT", - "dependencies": { - "parseley": "^0.12.0" - }, - "funding": { - "url": "https://ko-fi.com/killymxi" - } - }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -9573,6 +7694,7 @@ "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, "license": "Apache-2.0", + "optional": true, "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", @@ -9616,6 +7738,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", "license": "ISC", + "optional": true, "bin": { "semver": "bin/semver.js" }, @@ -9718,13 +7841,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -9773,13 +7889,6 @@ "dev": true, "license": "MIT" }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, "node_modules/std-env": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", @@ -9801,70 +7910,6 @@ "node": ">= 0.4" } }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -9978,46 +8023,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -10041,26 +8046,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", - "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, "node_modules/stripe": { "version": "18.5.0", "resolved": "https://registry.npmjs.org/stripe/-/stripe-18.5.0.tgz", @@ -10163,74 +8148,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/test-exclude": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", - "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^10.4.1", - "minimatch": "^10.2.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/test-exclude/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -10279,36 +8196,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", - "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -10615,221 +8502,6 @@ } } }, - "node_modules/vite": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", - "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/vitest": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", - "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@vitest/pretty-format": "^3.2.4", - "@vitest/runner": "3.2.4", - "@vitest/snapshot": "3.2.4", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", - "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.4", - "@vitest/ui": "3.2.4", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/debug": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -10935,23 +8607,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -10962,101 +8617,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/xml-naming": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", diff --git a/package.json b/package.json index e0a10f1..4b2d77d 100644 --- a/package.json +++ b/package.json @@ -7,30 +7,18 @@ "build": "next build", "start": "next start", "lint": "eslint", - "postinstall": "prisma generate", - "test": "vitest run", - "test:watch": "vitest", - "typecheck": "tsc --noEmit" + "postinstall": "prisma generate" }, "dependencies": { "@aws-sdk/client-s3": "^3.1056.0", - "@aws-sdk/s3-request-presigner": "^3.1058.0", - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^8.0.0", - "@dnd-kit/utilities": "^3.2.2", "@prisma/adapter-pg": "^7.8.0", "@prisma/client": "^7.8.0", - "@types/leaflet": "^1.9.21", "bcryptjs": "^3.0.3", - "leaflet": "^1.9.4", "next": "16.2.6", "next-auth": "^5.0.0-beta.31", "pg": "^8.21.0", "react": "19.2.4", "react-dom": "19.2.4", - "react-leaflet": "^5.0.0", - "resend": "^4.8.0", - "sharp": "^0.34.5", "stripe": "^18.3.0" }, "devDependencies": { @@ -38,13 +26,11 @@ "@types/node": "^20.19.41", "@types/react": "^19.2.15", "@types/react-dom": "^19.2.3", - "@vitest/coverage-v8": "^3.2.4", "dotenv": "^17.4.2", "eslint": "^9.39.4", "eslint-config-next": "^16.2.6", "prisma": "^7.8.0", "tailwindcss": "^4", - "typescript": "^5.9.3", - "vitest": "^3.2.4" + "typescript": "^5.9.3" } } diff --git a/prisma/migrations/20260601060000_password_reset_token/migration.sql b/prisma/migrations/20260601060000_password_reset_token/migration.sql deleted file mode 100644 index 50033de..0000000 --- a/prisma/migrations/20260601060000_password_reset_token/migration.sql +++ /dev/null @@ -1,9 +0,0 @@ -CREATE TABLE "PasswordResetToken" ( - "tokenHash" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "expiresAt" TIMESTAMP(3) NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT "PasswordResetToken_pkey" PRIMARY KEY ("tokenHash") -); -CREATE INDEX "PasswordResetToken_userId_idx" ON "PasswordResetToken"("userId"); -CREATE INDEX "PasswordResetToken_expiresAt_idx" ON "PasswordResetToken"("expiresAt"); diff --git a/prisma/migrations/20260601150000_carbet_nightly_price/migration.sql b/prisma/migrations/20260601150000_carbet_nightly_price/migration.sql deleted file mode 100644 index e53b6bf..0000000 --- a/prisma/migrations/20260601150000_carbet_nightly_price/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE "Carbet" ADD COLUMN "nightlyPrice" DECIMAL(10,2) NOT NULL DEFAULT 0; -UPDATE "Carbet" SET "nightlyPrice" = 80 WHERE "nightlyPrice" = 0; diff --git a/prisma/migrations/20260602030000_operational_criteria/migration.sql b/prisma/migrations/20260602030000_operational_criteria/migration.sql deleted file mode 100644 index 5bdca5f..0000000 --- a/prisma/migrations/20260602030000_operational_criteria/migration.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE TYPE "RoadAccess" AS ENUM ('NONE', 'DRY_SEASON_ONLY', 'ALL_YEAR'); -CREATE TYPE "Electricity" AS ENUM ('NONE', 'SOLAR', 'GENERATOR_READY', 'EDF'); - -ALTER TABLE "Carbet" ADD COLUMN "roadAccess" "RoadAccess"; -ALTER TABLE "Carbet" ADD COLUMN "electricity" "Electricity"; -ALTER TABLE "Carbet" ADD COLUMN "gsmAtCarbet" BOOLEAN NOT NULL DEFAULT false; -ALTER TABLE "Carbet" ADD COLUMN "gsmExitDistanceKm" DECIMAL(4,2); - --- Seed des 6 carbets démo avec valeurs réalistes -UPDATE "Carbet" SET "roadAccess" = 'NONE', "electricity" = 'SOLAR', "gsmAtCarbet" = false, "gsmExitDistanceKm" = 1.5 WHERE id = 'demo-carbet-awara'; -UPDATE "Carbet" SET "roadAccess" = 'ALL_YEAR', "electricity" = 'EDF', "gsmAtCarbet" = true WHERE id = 'demo-carbet-kourou'; -UPDATE "Carbet" SET "roadAccess" = 'ALL_YEAR', "electricity" = 'EDF', "gsmAtCarbet" = true WHERE id = 'demo-carbet-mahury'; -UPDATE "Carbet" SET "roadAccess" = 'NONE', "electricity" = 'GENERATOR_READY', "gsmAtCarbet" = false, "gsmExitDistanceKm" = 4.0 WHERE id = 'demo-carbet-maripa'; -UPDATE "Carbet" SET "roadAccess" = 'DRY_SEASON_ONLY', "electricity" = 'SOLAR', "gsmAtCarbet" = false, "gsmExitDistanceKm" = 0.5 WHERE id = 'demo-carbet-paripou'; -UPDATE "Carbet" SET "roadAccess" = 'ALL_YEAR', "electricity" = 'EDF', "gsmAtCarbet" = true WHERE id = 'demo-carbet-wapa'; diff --git a/prisma/migrations/20260602100000_favorite/migration.sql b/prisma/migrations/20260602100000_favorite/migration.sql deleted file mode 100644 index 8abf012..0000000 --- a/prisma/migrations/20260602100000_favorite/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -CREATE TABLE "Favorite" ( - "userId" TEXT NOT NULL, - "carbetId" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT "Favorite_pkey" PRIMARY KEY ("userId", "carbetId") -); -CREATE INDEX "Favorite_userId_idx" ON "Favorite"("userId"); -CREATE INDEX "Favorite_carbetId_idx" ON "Favorite"("carbetId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0636340..caa7314 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -124,13 +124,6 @@ model Carbet { // Détails d'accès route pour ROAD_AND_RIVER (GPS, distance, type de piste). roadAccessNote String? capacity Int - // 4 critères opérationnels dealbreakers (dispo en filtres + badges UI) - roadAccess RoadAccess? - electricity Electricity? - gsmAtCarbet Boolean @default(false) - gsmExitDistanceKm Decimal? @db.Decimal(4, 2) - // Prix par nuit pour le carbet entier (toute capacité). En euros. - nightlyPrice Decimal @db.Decimal(10, 2) @default(0) // Contraintes séjour (plugin min-stay). null = pas de contrainte. minStayNights Int? maxStayNights Int? @@ -366,36 +359,3 @@ model Translation { @@id([key, lang]) @@index([lang]) } - -model PasswordResetToken { - tokenHash String @id - userId String - expiresAt DateTime - createdAt DateTime @default(now()) - - @@index([userId]) - @@index([expiresAt]) -} - -model Favorite { - userId String - carbetId String - createdAt DateTime @default(now()) - - @@id([userId, carbetId]) - @@index([userId]) - @@index([carbetId]) -} - -enum RoadAccess { - NONE - DRY_SEASON_ONLY - ALL_YEAR -} - -enum Electricity { - NONE - SOLAR - GENERATOR_READY - EDF -} diff --git a/public/icons/apple-touch-icon.png b/public/icons/apple-touch-icon.png deleted file mode 100644 index a185b67..0000000 Binary files a/public/icons/apple-touch-icon.png and /dev/null differ diff --git a/public/icons/favicon-32.png b/public/icons/favicon-32.png deleted file mode 100644 index c062acf..0000000 Binary files a/public/icons/favicon-32.png and /dev/null differ diff --git a/public/icons/icon-192-maskable.png b/public/icons/icon-192-maskable.png deleted file mode 100644 index e80f811..0000000 Binary files a/public/icons/icon-192-maskable.png and /dev/null differ diff --git a/public/icons/icon-192.png b/public/icons/icon-192.png deleted file mode 100644 index cb0fd13..0000000 Binary files a/public/icons/icon-192.png and /dev/null differ diff --git a/public/icons/icon-512-maskable.png b/public/icons/icon-512-maskable.png deleted file mode 100644 index 5041e00..0000000 Binary files a/public/icons/icon-512-maskable.png and /dev/null differ diff --git a/public/icons/icon-512.png b/public/icons/icon-512.png deleted file mode 100644 index abb04bf..0000000 Binary files a/public/icons/icon-512.png and /dev/null differ diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest deleted file mode 100644 index 2f32e8d..0000000 --- a/public/manifest.webmanifest +++ /dev/null @@ -1,60 +0,0 @@ -{ - "name": "Karbé — carbets fluviaux de Guyane", - "short_name": "Karbé", - "description": "Au fil de l'eau : louez des carbets le long des fleuves de Guyane.", - "start_url": "/decouvrir", - "id": "/decouvrir", - "scope": "/", - "display": "standalone", - "orientation": "portrait", - "background_color": "#000000", - "theme_color": "#059669", - "lang": "fr", - "categories": ["travel", "lifestyle"], - "icons": [ - { - "src": "/icons/icon-192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "any" - }, - { - "src": "/icons/icon-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "any" - }, - { - "src": "/icons/icon-192-maskable.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "/icons/icon-512-maskable.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } - ], - "shortcuts": [ - { - "name": "Au fil de l'eau", - "short_name": "Découvrir", - "url": "/decouvrir", - "icons": [{ "src": "/icons/icon-192.png", "sizes": "192x192" }] - }, - { - "name": "Mes favoris", - "short_name": "Favoris", - "url": "/mes-favoris", - "icons": [{ "src": "/icons/icon-192.png", "sizes": "192x192" }] - }, - { - "name": "Mon compte", - "short_name": "Compte", - "url": "/mon-compte", - "icons": [{ "src": "/icons/icon-192.png", "sizes": "192x192" }] - } - ] -} diff --git a/scripts/backup-postgres.sh b/scripts/backup-postgres.sh deleted file mode 100755 index abe63d4..0000000 --- a/scripts/backup-postgres.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash -# -# Backup nightly du PostgreSQL Karbé vers MinIO. -# Lancé par un systemd timer (karbe-backup.timer). -# -# Rétention 30 jours côté MinIO (s'appuyer sur une lifecycle policy ou un -# nettoyage côté `mc rm` planifié — TODO si on veut être propre). - -set -euo pipefail - -STAMP=$(date -u +%Y%m%d-%H%M%S) -DUMP_DIR=/tmp/karbe-backup -DUMP_FILE="$DUMP_DIR/karbe-${STAMP}.sql.gz" -BUCKET_DEST="karbe-backups/postgres/karbe-${STAMP}.sql.gz" - -mkdir -p "$DUMP_DIR" - -# Dump compressé depuis le conteneur postgres -docker compose -f /home/ubuntu/karbe/docker-compose.prod.yml \ - -f /home/ubuntu/karbe/docker-compose.override.yml \ - exec -T postgres pg_dump -U karbe -d karbe \ - | gzip > "$DUMP_FILE" - -SIZE=$(stat -c %s "$DUMP_FILE") -echo "[$(date -u +%FT%TZ)] dump created size=${SIZE}B path=${DUMP_FILE}" - -# Push vers MinIO via mc Docker -docker run --rm --network karbe-net \ - --entrypoint /bin/sh \ - -v "$DUMP_DIR:/dump" \ - -e MINIO_ROOT_USER \ - -e MINIO_ROOT_PASSWORD \ - minio/mc:latest -c " - mc alias set karbe http://minio:9000 \"\$MINIO_ROOT_USER\" \"\$MINIO_ROOT_PASSWORD\" >/dev/null 2>&1 && \ - mc mb karbe/karbe-backups --ignore-existing >/dev/null 2>&1 && \ - mc cp /dump/karbe-${STAMP}.sql.gz karbe/${BUCKET_DEST} - " - -echo "[$(date -u +%FT%TZ)] uploaded to karbe/${BUCKET_DEST}" - -# Nettoyage local -rm -f "$DUMP_FILE" - -# Rétention : supprime les backups > 30 jours dans MinIO -docker run --rm --network karbe-net \ - --entrypoint /bin/sh \ - -e MINIO_ROOT_USER \ - -e MINIO_ROOT_PASSWORD \ - minio/mc:latest -c " - mc alias set karbe http://minio:9000 \"\$MINIO_ROOT_USER\" \"\$MINIO_ROOT_PASSWORD\" >/dev/null 2>&1 && \ - mc rm --recursive --force --older-than 30d karbe/karbe-backups/ 2>/dev/null || true - " - -echo "[$(date -u +%FT%TZ)] retention sweep done (>30d removed)" diff --git a/src/app/accueil/page.tsx b/src/app/accueil/page.tsx deleted file mode 100644 index 513e1ac..0000000 --- a/src/app/accueil/page.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import Link from "next/link"; -import { IfPluginEnabled } from "@/components/IfPluginEnabled"; -import { HeroSection } from "@/components/landing/HeroSection"; -import { ExperiencesSection } from "@/components/landing/ExperiencesSection"; -import { HowItWorksSection } from "@/components/landing/HowItWorksSection"; -import { CESection } from "@/components/landing/CESection"; -import { TestimonialsSection } from "@/components/landing/TestimonialsSection"; -import { LandingFooter } from "@/components/landing/Footer"; - -export const metadata = { title: "Accueil — Karbé" }; - -/** - * Landing « marketing » historique (hero + sections + footer riche). Conservée - * à /accueil après la promotion de /decouvrir comme nouvelle page d'index. - */ -export default function LandingPage() { - return ( - <> - -
-

- Karbé — carbets fluviaux de Guyane -

-

- La marketplace pour louer des carbets le long des fleuves de Guyane. -

-
- - Au fil de l'eau - - - Catalogue - -
-
- - } - > - -
- - - - - - - - - - ); -} diff --git a/src/app/admin/bookings/actions.ts b/src/app/admin/bookings/actions.ts index ca9e401..a8726d4 100644 --- a/src/app/admin/bookings/actions.ts +++ b/src/app/admin/bookings/actions.ts @@ -6,7 +6,6 @@ import { BookingStatus, PaymentStatus, UserRole } from "@/generated/prisma/enums import { requireRole } from "@/lib/authorization"; import { prisma } from "@/lib/prisma"; import { recordAudit } from "@/lib/admin/audit"; -import { sendBookingConfirmed, sendBookingRefunded } from "@/lib/email"; async function audit(event: string, target: string, actor: string | null, details: Record) { await recordAudit({ scope: "admin.bookings", event, target, actorEmail: actor, details }); @@ -32,32 +31,11 @@ export async function updateBookingStatusAction(id: string, status: string) { return { ok: false as const, error: "Statut invalide" }; } const session = await auth(); - const before = await prisma.booking.findUnique({ - where: { id }, - select: { status: true }, - }); - const updated = await prisma.booking.update({ + await prisma.booking.update({ where: { id }, data: { status: status as BookingStatus }, - include: { - tenant: { select: { email: true, firstName: true } }, - carbet: { select: { title: true } }, - }, }); await audit("booking.status.update", id, session?.user?.email ?? null, { status }); - if ( - before?.status !== BookingStatus.CONFIRMED && - updated.status === BookingStatus.CONFIRMED - ) { - sendBookingConfirmed( - updated.tenant.email, - updated.tenant.firstName, - updated.id, - updated.carbet.title, - updated.startDate, - updated.endDate, - ).catch(() => {}); - } revalidatePath("/admin/bookings"); revalidatePath(`/admin/bookings/${id}`); return { ok: true as const }; @@ -82,26 +60,14 @@ export async function updateBookingPaymentAction(id: string, paymentStatus: stri export async function refundBookingAction(id: string) { await requireRole([UserRole.ADMIN]); const session = await auth(); - const updated = await prisma.booking.update({ + await prisma.booking.update({ where: { id }, data: { paymentStatus: PaymentStatus.REFUNDED, status: BookingStatus.CANCELLED, }, - include: { - tenant: { select: { email: true, firstName: true } }, - carbet: { select: { title: true } }, - }, }); await audit("booking.refund", id, session?.user?.email ?? null, {}); - sendBookingRefunded( - updated.tenant.email, - updated.tenant.firstName, - updated.id, - updated.carbet.title, - updated.amount.toString(), - updated.currency, - ).catch(() => {}); revalidatePath("/admin/bookings"); revalidatePath(`/admin/bookings/${id}`); return { ok: true as const }; diff --git a/src/app/admin/carbets/[id]/_components/MediaManager.tsx b/src/app/admin/carbets/[id]/_components/MediaManager.tsx index ab91606..47947da 100644 --- a/src/app/admin/carbets/[id]/_components/MediaManager.tsx +++ b/src/app/admin/carbets/[id]/_components/MediaManager.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useTransition } from "react"; +import Image from "next/image"; import { addMediaAction, removeMediaAction, reorderMediaAction } from "../../actions"; import { FormField, inputCls, selectCls } from "@/components/admin/FormField"; @@ -124,7 +125,7 @@ export function MediaManager({ carbetId, media: initial }: { carbetId: string; m - {/* Le serveur calcule un s3Key déterministe à partir de l'URL si vide. */} + {error ?
{error}
: null}
-
- ) : null} - - - - {datesSelected ? ( -
-
- - {nightlyPrice.toFixed(0)} € × {nights} nuit{nights > 1 ? "s" : ""} - - {(nightlyPrice * nights).toFixed(2)} € -
-
- Total - {total.toFixed(2)} € -
-
- ) : null} - - {datesSelected && !nightsOk ? ( -
- Séjour entre {minN} et {maxN} nuits requis. -
- ) : null} - - {error ? ( -
{error}
- ) : null} - - - - {!isAuthenticated ? ( -

- Pas encore de compte ?{" "} - - Créer un compte - -

- ) : null} - -

- {stripeEnabled - ? "Vous serez redirigé vers Stripe pour le paiement sécurisé." - : "Le créneau est bloqué dès l'envoi. Statut « En attente » jusqu'à confirmation."} -

- - ); -} diff --git a/src/app/carbets/_components/carbet-card.tsx b/src/app/carbets/_components/carbet-card.tsx index f32cc8e..9a6a53b 100644 --- a/src/app/carbets/_components/carbet-card.tsx +++ b/src/app/carbets/_components/carbet-card.tsx @@ -3,9 +3,7 @@ import Link from "next/link"; import type { CarbetSearchResult } from "@/lib/carbet-search"; import { formatPirogueDuration, truncate } from "@/lib/format"; import { formatAverageRating } from "@/lib/reviews"; -import { buildSrcSet } from "@/lib/image-variants"; import { AccessTypeBadge } from "@/components/AccessTypeBadge"; -import { OperationalBadges } from "@/components/OperationalBadges"; import { StayConstraints } from "@/components/StayConstraints"; import { StarRating } from "./star-rating"; @@ -16,14 +14,13 @@ export function CarbetCard({ carbet }: { carbet: CarbetSearchResult }) {
{carbet.coverUrl ? ( + // Use a plain here — uploaded media URLs come from MinIO/S3 and + // don't go through next/image's optimizer in this environment. // eslint-disable-next-line @next/next/no-img-element {`Photo ) : ( @@ -42,18 +39,9 @@ export function CarbetCard({ carbet }: { carbet: CarbetSearchResult }) {

- Fleuve {carbet.river} + Fleuve {carbet.river} · {carbet.capacity} voyageur + {carbet.capacity > 1 ? "s" : ""}

-
- -
(null); - - const close = useCallback(() => setActive(null), []); - const next = useCallback(() => { - setActive((i) => (i === null ? null : (i + 1) % media.length)); - }, [media.length]); - const prev = useCallback(() => { - setActive((i) => (i === null ? null : (i - 1 + media.length) % media.length)); - }, [media.length]); - - useEffect(() => { - if (active === null) return; - function onKey(e: KeyboardEvent) { - if (e.key === "Escape") close(); - else if (e.key === "ArrowRight") next(); - else if (e.key === "ArrowLeft") prev(); - } - window.addEventListener("keydown", onKey); - document.body.style.overflow = "hidden"; - return () => { - window.removeEventListener("keydown", onKey); - document.body.style.overflow = ""; - }; - }, [active, close, next, prev]); - if (media.length === 0) { return (
@@ -49,159 +17,57 @@ export function CarbetGallery({ title, media }: Props) { ); } - const cover = media[0]; - const rest = media.slice(1); - const current = active === null ? null : media[active]; + const [cover, ...rest] = media; return ( - <> -
- +
+
+ {cover.type === MediaType.VIDEO ? ( +
- {rest.length > 0 ? ( -
    - {rest.map((item, idx) => ( -
  • - -
  • - ))} -
- ) : null} -
- - {current ? ( -
- - - {media.length > 1 ? ( - <> - - - - ) : null} - -
e.stopPropagation()} - > - {current.type === MediaType.VIDEO ? ( -
- -
- {active! + 1} / {media.length} -
-
+ {rest.length > 0 ? ( +
    + {rest.map((item) => ( +
  • + {item.type === MediaType.VIDEO ? ( +
  • + ))} +
) : null} - +
); } diff --git a/src/app/carbets/_components/carbet-map-inner.tsx b/src/app/carbets/_components/carbet-map-inner.tsx deleted file mode 100644 index 63d1ab6..0000000 --- a/src/app/carbets/_components/carbet-map-inner.tsx +++ /dev/null @@ -1,74 +0,0 @@ -"use client"; - -import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet"; -import L from "leaflet"; - -import "leaflet/dist/leaflet.css"; - -// Fix icône Leaflet (les paths par défaut pointent vers un CDN qui n'existe plus). -// On utilise un SVG inline en data URL. -const ICON = L.divIcon({ - className: "karbe-leaflet-marker", - html: ` -
- - - - -
- `, - iconSize: [32, 40], - iconAnchor: [16, 40], - popupAnchor: [0, -36], -}); - -type Props = { - latitude: number; - longitude: number; - title: string; - river: string; - embarkPoint: string; -}; - -export function CarbetMapInner({ latitude, longitude, title, river, embarkPoint }: Props) { - const position: [number, number] = [latitude, longitude]; - - return ( -
- - - - - {title} -
- Fleuve {river} -
- Embarquement : {embarkPoint} -
- - Ouvrir dans OpenStreetMap ↗ - -
-
-
-
- ); -} diff --git a/src/app/carbets/_components/carbet-map.tsx b/src/app/carbets/_components/carbet-map.tsx deleted file mode 100644 index 31b9718..0000000 --- a/src/app/carbets/_components/carbet-map.tsx +++ /dev/null @@ -1,31 +0,0 @@ -"use client"; - -/** - * Carte interactive sur la fiche carbet — Leaflet + OpenStreetMap. - * - * Chargée dynamiquement (ssr:false) car Leaflet manipule window. - */ - -import dynamic from "next/dynamic"; - -const CarbetMapInner = dynamic( - () => import("./carbet-map-inner").then((m) => m.CarbetMapInner), - { - ssr: false, - loading: () => ( -
- ), - }, -); - -type Props = { - latitude: number; - longitude: number; - title: string; - river: string; - embarkPoint: string; -}; - -export function CarbetMap(props: Props) { - return ; -} diff --git a/src/app/carbets/_components/catalog-map-inner.tsx b/src/app/carbets/_components/catalog-map-inner.tsx deleted file mode 100644 index 1abac02..0000000 --- a/src/app/carbets/_components/catalog-map-inner.tsx +++ /dev/null @@ -1,113 +0,0 @@ -"use client"; - -import { useMemo } from "react"; -import Link from "next/link"; -import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet"; -import L, { LatLngBoundsExpression } from "leaflet"; - -import "leaflet/dist/leaflet.css"; - -import type { CatalogMapPoint } from "./catalog-map"; - -const ICON = L.divIcon({ - className: "karbe-catalog-marker", - html: ` -
- - - - -
- `, - iconSize: [28, 36], - iconAnchor: [14, 36], - popupAnchor: [0, -32], -}); - -export function CatalogMapInner({ points }: { points: CatalogMapPoint[] }) { - const bounds = useMemo(() => { - if (points.length === 0) { - // Centre par défaut sur la Guyane (Cayenne). - return [ - [3.5, -54.5], - [5.5, -52.0], - ]; - } - const lats = points.map((p) => p.latitude); - const lngs = points.map((p) => p.longitude); - const minLat = Math.min(...lats); - const maxLat = Math.max(...lats); - const minLng = Math.min(...lngs); - const maxLng = Math.max(...lngs); - // Padding 0.1° - return [ - [minLat - 0.1, minLng - 0.1], - [maxLat + 0.1, maxLng + 0.1], - ]; - }, [points]); - - return ( -
- - - {points.map((p) => ( - - -
- {p.coverUrl ? ( - // eslint-disable-next-line @next/next/no-img-element - {p.title} - ) : null} - {p.title} -
- - Fleuve {p.river} - -
- - {Number(p.nightlyPrice).toFixed(0)} € - - / nuit -
- - Voir la fiche → - -
-
-
- ))} -
-
- ); -} diff --git a/src/app/carbets/_components/catalog-map.tsx b/src/app/carbets/_components/catalog-map.tsx deleted file mode 100644 index 5f65463..0000000 --- a/src/app/carbets/_components/catalog-map.tsx +++ /dev/null @@ -1,29 +0,0 @@ -"use client"; - -import dynamic from "next/dynamic"; - -const CatalogMapInner = dynamic( - () => import("./catalog-map-inner").then((m) => m.CatalogMapInner), - { - ssr: false, - loading: () => ( -
- ), - }, -); - -export type CatalogMapPoint = { - id: string; - slug: string; - title: string; - river: string; - nightlyPrice: string; - latitude: number; - longitude: number; - coverUrl: string | null; -}; - -export function CatalogMap({ points }: { points: CatalogMapPoint[] }) { - if (points.length === 0) return null; - return ; -} diff --git a/src/app/carbets/_components/mini-calendar.tsx b/src/app/carbets/_components/mini-calendar.tsx deleted file mode 100644 index bdcbb0d..0000000 --- a/src/app/carbets/_components/mini-calendar.tsx +++ /dev/null @@ -1,186 +0,0 @@ -"use client"; - -import { useMemo, useState } from "react"; - -type Props = { - startDate: string | null; - endDate: string | null; - blockedDates: Set; - onChange: (start: string | null, end: string | null) => void; -}; - -const MONTH_LABEL = [ - "Janvier", "Février", "Mars", "Avril", "Mai", "Juin", - "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre", -]; -const DOW_LABEL = ["L", "M", "M", "J", "V", "S", "D"]; - -function isoDay(d: Date): string { - return d.toISOString().slice(0, 10); -} - -function startOfMonth(d: Date): Date { - return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1)); -} - -function addMonths(d: Date, n: number): Date { - return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + n, 1)); -} - -/** Génère la grille du mois : 6 semaines × 7 jours, en commençant un lundi. */ -function monthGrid(monthStart: Date): (Date | null)[] { - const year = monthStart.getUTCFullYear(); - const month = monthStart.getUTCMonth(); - // Premier jour du mois — décale pour que la semaine commence un lundi (0=L, 6=D) - const firstDay = new Date(Date.UTC(year, month, 1)); - const firstDow = (firstDay.getUTCDay() + 6) % 7; - const lastDay = new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); - const cells: (Date | null)[] = []; - for (let i = 0; i < firstDow; i++) cells.push(null); - for (let d = 1; d <= lastDay; d++) { - cells.push(new Date(Date.UTC(year, month, d))); - } - while (cells.length % 7 !== 0) cells.push(null); - // Toujours 6 lignes pour éviter le saut de hauteur - while (cells.length < 42) cells.push(null); - return cells; -} - -export function MiniCalendar({ startDate, endDate, blockedDates, onChange }: Props) { - const today = useMemo(() => { - const d = new Date(); - return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())); - }, []); - - const [viewMonth, setViewMonth] = useState(() => { - const ref = startDate ? new Date(startDate + "T00:00:00Z") : today; - return startOfMonth(ref); - }); - - const cells = useMemo(() => monthGrid(viewMonth), [viewMonth]); - - const startISO = startDate; - const endISO = endDate; - - function onClick(day: Date) { - const iso = isoDay(day); - if (day.getTime() < today.getTime()) return; - if (blockedDates.has(iso)) return; - - // Aucune sélection ou les deux déjà posées → reset + nouvelle start - if (!startISO || (startISO && endISO)) { - onChange(iso, null); - return; - } - // Une seule (start) déjà sélectionnée - if (iso === startISO) { - onChange(null, null); - return; - } - if (iso < startISO) { - onChange(iso, null); - return; - } - // Vérifie qu'aucun jour intermédiaire n'est bloqué - const startMs = new Date(startISO + "T00:00:00Z").getTime(); - const endMs = day.getTime(); - for (let t = startMs; t < endMs; t += 86_400_000) { - const d = new Date(t).toISOString().slice(0, 10); - if (blockedDates.has(d)) { - // Tombe sur un jour bloqué → on resélectionne start - onChange(iso, null); - return; - } - } - onChange(startISO, iso); - } - - const canGoBack = viewMonth > startOfMonth(today); - - return ( -
-
- - - {MONTH_LABEL[viewMonth.getUTCMonth()]} {viewMonth.getUTCFullYear()} - - -
- -
- {DOW_LABEL.map((d, i) => ( -
- {d} -
- ))} -
- -
- {cells.map((cell, i) => { - if (!cell) return
; - const iso = isoDay(cell); - const isPast = cell.getTime() < today.getTime(); - const isBlocked = blockedDates.has(iso); - const isStart = iso === startISO; - const isEnd = iso === endISO; - const inRange = startISO && endISO && iso > startISO && iso < endISO; - const isToday = iso === isoDay(today); - const disabled = isPast || isBlocked; - - let cls = - "relative h-7 rounded text-xs flex items-center justify-center transition"; - if (disabled) { - cls += " text-zinc-300 cursor-not-allowed"; - if (isBlocked && !isPast) cls += " line-through"; - } else if (isStart || isEnd) { - cls += " bg-emerald-600 text-white font-semibold cursor-pointer"; - } else if (inRange) { - cls += " bg-emerald-100 text-emerald-900 cursor-pointer"; - } else { - cls += " text-zinc-800 hover:bg-zinc-100 cursor-pointer"; - if (isToday) cls += " ring-1 ring-zinc-400"; - } - - return ( - - ); - })} -
- -

- {!startISO - ? "Choisissez votre date d'arrivée." - : !endISO - ? "Choisissez votre date de départ." - : ""} -

-
- ); -} diff --git a/src/app/carbets/_components/search-filters.tsx b/src/app/carbets/_components/search-filters.tsx index 13454f4..999f66c 100644 --- a/src/app/carbets/_components/search-filters.tsx +++ b/src/app/carbets/_components/search-filters.tsx @@ -1,8 +1,6 @@ import Link from "next/link"; import type { CarbetSearchFilters } from "@/lib/carbet-search"; -import { AMENITY_CATALOG } from "@/lib/amenities"; -import { Electricity, RoadAccess } from "@/generated/prisma/enums"; type SearchFiltersProps = { filters: CarbetSearchFilters; @@ -63,165 +61,18 @@ export function SearchFilters({ filters, rivers }: SearchFiltersProps) { - - - - -
- Accès route -
- {[ - { value: RoadAccess.ALL_YEAR, label: "🛣️ Route toute saison" }, - { value: RoadAccess.DRY_SEASON_ONLY, label: "🟠 Route saison sèche" }, - { value: RoadAccess.NONE, label: "🛶 Pirogue uniquement" }, - ].map((opt) => { - const checked = (filters.roadAccess ?? []).includes(opt.value); - return ( - - ); - })} -
-
- -
- Électricité -
- {[ - { value: Electricity.EDF, label: "⚡ EDF / raccordé" }, - { value: Electricity.GENERATOR_READY, label: "🔌 Préinstall groupe" }, - { value: Electricity.SOLAR, label: "☀️ Solaire" }, - { value: Electricity.NONE, label: "🕯️ Aucune" }, - ].map((opt) => { - const checked = (filters.electricity ?? []).includes(opt.value); - return ( - - ); - })} -
-
- - - -
- Équipements souhaités -
- {AMENITY_CATALOG.map((a) => { - const checked = (filters.amenities ?? []).includes(a.key); - return ( - - ); - })} -
-
-
-
- Profils de séjour -
-
    - {SEARCH_PROFILES.map((p) => ( -
  • - - {p.emoji} - {p.label} - -
  • - ))} -
-
- ); -} diff --git a/src/app/carbets/page.tsx b/src/app/carbets/page.tsx index 512c79f..a49ed1b 100644 --- a/src/app/carbets/page.tsx +++ b/src/app/carbets/page.tsx @@ -8,9 +8,7 @@ import { } from "@/lib/carbet-search"; import { CarbetCard } from "./_components/carbet-card"; -import { CatalogMap } from "./_components/catalog-map"; import { SearchFilters } from "./_components/search-filters"; -import { SearchProfiles } from "./_components/search-profiles"; export const metadata: Metadata = { title: "Rechercher un carbet", @@ -58,7 +56,6 @@ export default async function CarbetsSearchPage({

-
@@ -75,20 +72,6 @@ export default async function CarbetsSearchPage({ {results.length} carbet{results.length > 1 ? "s" : ""} trouvé {results.length > 1 ? "s" : ""}.

-
- ({ - id: c.id, - slug: c.slug, - title: c.title, - river: c.river, - nightlyPrice: c.nightlyPrice, - latitude: c.latitude, - longitude: c.longitude, - coverUrl: c.coverUrl, - }))} - /> -
    {results.map((carbet) => (
  • diff --git a/src/app/connexion/page.tsx b/src/app/connexion/page.tsx index 5ac18e4..032a91b 100644 --- a/src/app/connexion/page.tsx +++ b/src/app/connexion/page.tsx @@ -1,16 +1,11 @@ -import Link from "next/link"; import { redirect } from "next/navigation"; import { auth, signIn } from "@/auth"; -type Props = { searchParams: Promise<{ next?: string }> }; - -export default async function SignInPage({ searchParams }: Props) { +export default async function SignInPage() { const session = await auth(); - const sp = await searchParams; - const next = sp.next && sp.next.startsWith("/") ? sp.next : "/"; if (session?.user?.id) { - redirect(next); + redirect("/"); } return ( @@ -53,20 +48,6 @@ export default async function SignInPage({ searchParams }: Props) { > Se connecter -

    - - Mot de passe oublié ? - -

    -

    - Pas encore de compte ?{" "} - - Créer un compte - -

    ); diff --git a/src/app/decouvrir/_components/ReelSlide.tsx b/src/app/decouvrir/_components/ReelSlide.tsx deleted file mode 100644 index 7c1b4e7..0000000 --- a/src/app/decouvrir/_components/ReelSlide.tsx +++ /dev/null @@ -1,403 +0,0 @@ -"use client"; - -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import Link from "next/link"; - -import type { ReelCarbet } from "@/lib/reels"; -import { buildSrcSet } from "@/lib/image-variants"; - -type Props = { - carbet: ReelCarbet; - isActive: boolean; - shouldPreload: boolean; - isFavorite: boolean; - onToggleFavorite: () => void; -}; - -const SWIPE_THRESHOLD_RATIO = 0.18; // % de la largeur pour valider le swipe -const VELOCITY_THRESHOLD = 0.4; // px/ms — un flick rapide même court valide - -export function ReelSlide({ carbet, isActive, shouldPreload, isFavorite, onToggleFavorite }: Props) { - const [mediaIndex, setMediaIndex] = useState(0); - const [muted, setMuted] = useState(true); - const [dragX, setDragX] = useState(0); - const [transitioning, setTransitioning] = useState(false); - const [containerWidth, setContainerWidth] = useState(0); - const containerRef = useRef(null); - const videoRefs = useRef>(new Map()); - const drag = useRef<{ - startX: number; - startY: number; - startTime: number; - locked: "horizontal" | "vertical" | null; - } | null>(null); - - const total = carbet.media.length; - const current = carbet.media[mediaIndex]; - - const goTo = useCallback( - (next: number, animated = true) => { - const clamped = ((next % total) + total) % total; - setTransitioning(animated); - setMediaIndex(clamped); - setDragX(0); - }, - [total], - ); - - const nextMedia = useCallback(() => goTo(mediaIndex + 1), [goTo, mediaIndex]); - const prevMedia = useCallback(() => goTo(mediaIndex - 1), [goTo, mediaIndex]); - - // Suit la largeur du container pour les calculs de seuils / progress - useEffect(() => { - const el = containerRef.current; - if (!el) return; - const update = () => setContainerWidth(el.offsetWidth || window.innerWidth); - update(); - const ro = new ResizeObserver(update); - ro.observe(el); - window.addEventListener("resize", update); - return () => { - ro.disconnect(); - window.removeEventListener("resize", update); - }; - }, []); - - // Auto-play/pause vidéos selon média actif - useEffect(() => { - videoRefs.current.forEach((video, idx) => { - if (idx === mediaIndex && isActive && carbet.media[idx]?.type === "VIDEO") { - video.play().catch(() => {}); - } else { - video.pause(); - } - }); - }, [isActive, mediaIndex, carbet.media]); - - // Reset au changement de slide carbet (différé pour éviter cascading renders) - useEffect(() => { - if (isActive) return; - queueMicrotask(() => goTo(0, false)); - }, [isActive, goTo]); - - // Navigation clavier ← → - useEffect(() => { - if (!isActive) return; - function onKey(e: KeyboardEvent) { - const tag = (e.target as HTMLElement | null)?.tagName?.toLowerCase(); - if (tag === "input" || tag === "textarea") return; - if (e.key === "ArrowRight" || e.key === "l") { - e.preventDefault(); - nextMedia(); - } else if (e.key === "ArrowLeft" || e.key === "h") { - e.preventDefault(); - prevMedia(); - } - } - window.addEventListener("keydown", onKey); - return () => window.removeEventListener("keydown", onKey); - }, [isActive, nextMedia, prevMedia]); - - function onTouchStart(e: React.TouchEvent) { - const t = e.touches[0]; - drag.current = { - startX: t.clientX, - startY: t.clientY, - startTime: Date.now(), - locked: null, - }; - setTransitioning(false); - } - - function onTouchMove(e: React.TouchEvent) { - if (!drag.current) return; - const t = e.touches[0]; - const dx = t.clientX - drag.current.startX; - const dy = t.clientY - drag.current.startY; - - // Première détection : verrouille l'axe (horizontal ou vertical) - if (drag.current.locked === null) { - if (Math.abs(dx) < 6 && Math.abs(dy) < 6) return; // trop petit, attend - drag.current.locked = Math.abs(dx) > Math.abs(dy) ? "horizontal" : "vertical"; - } - - if (drag.current.locked !== "horizontal") return; - // Empêche le scroll vertical pendant un swipe horizontal - e.stopPropagation(); - if (e.cancelable) e.preventDefault(); - - // Résistance aux bords : si on swipe gauche sur le 1er ou droite sur le dernier, - // on glisse moins (effet rubber-band) - let effective = dx; - if (total <= 1) { - effective = dx * 0.2; - } else if (mediaIndex === 0 && dx > 0) { - effective = dx * 0.35; - } else if (mediaIndex === total - 1 && dx < 0) { - effective = dx * 0.35; - } - setDragX(effective); - } - - function onTouchEnd() { - if (!drag.current) return; - const wasHorizontal = drag.current.locked === "horizontal"; - const elapsed = Date.now() - drag.current.startTime; - const width = containerWidth || window.innerWidth; - const velocity = Math.abs(dragX) / Math.max(1, elapsed); // px/ms - drag.current = null; - - if (!wasHorizontal) { - setDragX(0); - return; - } - - const distance = Math.abs(dragX); - const isFlick = velocity > VELOCITY_THRESHOLD && distance > 20; - const isSlow = distance > width * SWIPE_THRESHOLD_RATIO; - const shouldChange = (isFlick || isSlow) && total > 1; - - if (shouldChange) { - if (dragX < 0 && mediaIndex < total - 1) { - goTo(mediaIndex + 1); - } else if (dragX > 0 && mediaIndex > 0) { - goTo(mediaIndex - 1); - } else { - // Bord : retour à 0 - setTransitioning(true); - setDragX(0); - } - } else { - setTransitioning(true); - setDragX(0); - } - } - - // Préchargement intelligent : current, current ± 1 - const preloadIndexes = useMemo(() => { - const s = new Set(); - s.add(mediaIndex); - if (mediaIndex > 0) s.add(mediaIndex - 1); - if (mediaIndex < total - 1) s.add(mediaIndex + 1); - return s; - }, [mediaIndex, total]); - - const share = useCallback(async () => { - const url = `${window.location.origin}/carbets/${carbet.slug}`; - const title = carbet.title; - if (navigator.share) { - navigator.share({ title, url }).catch(() => {}); - } else { - navigator.clipboard?.writeText(url).catch(() => {}); - } - }, [carbet.slug, carbet.title]); - - if (!current) return null; - - const offsetPct = -mediaIndex * 100; - - return ( -
    - {/* Track : tous les médias en ligne, transformX selon index + drag */} -
    setTransitioning(false)} - > - {carbet.media.map((m, idx) => { - const visible = preloadIndexes.has(idx) || shouldPreload; - return ( -
    - {m.type === "VIDEO" ? ( -
    - ); - })} -
    - - {/* Voile dégradé en bas pour lisibilité */} -
    - - {/* Indicateurs progression médias (sticks en haut) */} - {total > 1 ? ( -
    - {carbet.media.map((_, i) => { - const isActiveStick = i === mediaIndex; - const wasSeen = i < mediaIndex; - // Progression visuelle pendant le drag (preview du swipe) - const progress = isActiveStick && Math.abs(dragX) > 0 && containerWidth > 0 - ? Math.min(1, Math.abs(dragX) / containerWidth) - : 0; - return ( - - 0 ? { width: `${progress * 100}%` } : undefined} - /> - - ); - })} -
    - ) : null} - - {/* Zones tap horizontales (50/50) sur desktop */} - - - - - {current.type === "VIDEO" ? ( - - ) : null} -
    - - {/* Bloc info bas + CTAs */} -
    -
    -

    {carbet.title}

    - {carbet.averageRating !== null ? ( - - ★ {carbet.averageRating.toFixed(1)} ({carbet.reviewCount}) - - ) : null} -
    -
    - 📍 {carbet.river} - · - 👥 jusqu'à {carbet.capacity} - · - {Number(carbet.nightlyPrice).toFixed(0)} € / nuit -
    -
    - - Voir la fiche - - - Réserver - -
    -
    -
    - ); -} diff --git a/src/app/decouvrir/_components/ReelsViewer.tsx b/src/app/decouvrir/_components/ReelsViewer.tsx deleted file mode 100644 index aec37ce..0000000 --- a/src/app/decouvrir/_components/ReelsViewer.tsx +++ /dev/null @@ -1,158 +0,0 @@ -"use client"; - -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; - -import type { ReelCarbet } from "@/lib/reels"; - -import { ReelSlide } from "./ReelSlide"; - -type Props = { - carbets: ReelCarbet[]; - initialFavoriteIds: string[]; - isAuthenticated: boolean; -}; - -export function ReelsViewer({ carbets, initialFavoriteIds, isAuthenticated }: Props) { - const router = useRouter(); - const containerRef = useRef(null); - const slideRefs = useRef<(HTMLDivElement | null)[]>([]); - const [activeIndex, setActiveIndex] = useState(0); - const [favorites, setFavorites] = useState>(new Set(initialFavoriteIds)); - - // Détection du carbet actif via IntersectionObserver - useEffect(() => { - const observer = new IntersectionObserver( - (entries) => { - const visible = entries.filter((e) => e.isIntersecting); - if (visible.length === 0) return; - const best = visible.reduce((a, b) => (a.intersectionRatio > b.intersectionRatio ? a : b)); - const idx = slideRefs.current.findIndex((el) => el === best.target); - if (idx !== -1) setActiveIndex(idx); - }, - { root: containerRef.current, threshold: [0.55, 0.85] }, - ); - slideRefs.current.forEach((el) => el && observer.observe(el)); - return () => observer.disconnect(); - }, [carbets.length]); - - // Navigation clavier ↑↓ - useEffect(() => { - function onKey(e: KeyboardEvent) { - const tag = (e.target as HTMLElement | null)?.tagName?.toLowerCase(); - if (tag === "input" || tag === "textarea") return; - if (e.key === "ArrowDown" || e.key === "j") { - e.preventDefault(); - const next = Math.min(activeIndex + 1, carbets.length - 1); - slideRefs.current[next]?.scrollIntoView({ behavior: "smooth", block: "start" }); - } else if (e.key === "ArrowUp" || e.key === "k") { - e.preventDefault(); - const prev = Math.max(activeIndex - 1, 0); - slideRefs.current[prev]?.scrollIntoView({ behavior: "smooth", block: "start" }); - } - } - window.addEventListener("keydown", onKey); - return () => window.removeEventListener("keydown", onKey); - }, [activeIndex, carbets.length]); - - const toggleFavorite = useCallback( - async (carbetId: string) => { - if (!isAuthenticated) { - router.push(`/connexion?next=${encodeURIComponent("/decouvrir")}`); - return; - } - const isFav = favorites.has(carbetId); - // Optimistic update - setFavorites((prev) => { - const next = new Set(prev); - if (isFav) next.delete(carbetId); - else next.add(carbetId); - return next; - }); - const method = isFav ? "DELETE" : "POST"; - const res = await fetch("/api/favorites", { - method, - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ carbetId }), - }); - if (!res.ok) { - // Rollback - setFavorites((prev) => { - const next = new Set(prev); - if (isFav) next.add(carbetId); - else next.delete(carbetId); - return next; - }); - } - }, - [favorites, isAuthenticated, router], - ); - - // Préchargement N+1 et N-1 médias (un peu d'AGGRESSIVE prefetch) - const preloadIndexes = useMemo( - () => [activeIndex - 1, activeIndex, activeIndex + 1].filter((i) => i >= 0 && i < carbets.length), - [activeIndex, carbets.length], - ); - - return ( -
    - {/* Bouton retour catalogue */} - - ← Catalogue - - - {/* Compteur */} -
    - {activeIndex + 1} / {carbets.length} -
    - - {/* Logo Karbé en surimpression haut centre */} - - Karbé - - -
    - {carbets.map((c, idx) => ( -
    { - slideRefs.current[idx] = el; - }} - className="h-full snap-start snap-always" - style={{ scrollSnapAlign: "start" }} - > - toggleFavorite(c.id)} - /> -
    - ))} -
    -
    - ); -} diff --git a/src/app/decouvrir/page.tsx b/src/app/decouvrir/page.tsx deleted file mode 100644 index ed232bf..0000000 --- a/src/app/decouvrir/page.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import Link from "next/link"; - -import { auth } from "@/auth"; -import { prisma } from "@/lib/prisma"; -import { listReelCarbets } from "@/lib/reels"; - -import { ReelsViewer } from "./_components/ReelsViewer"; - -export const dynamic = "force-dynamic"; - -export const metadata = { - title: "Au fil de l'eau", - description: "Découvrez les carbets de Guyane façon Reels — swipez pour explorer.", -}; - -export default async function DecouvrirPage() { - const session = await auth(); - const userId = session?.user?.id ?? null; - const [carbets, favoriteIds] = await Promise.all([ - listReelCarbets({ take: 30 }), - userId - ? prisma.favorite.findMany({ where: { userId }, select: { carbetId: true } }).then((r) => r.map((x) => x.carbetId)) - : Promise.resolve([] as string[]), - ]); - - if (carbets.length === 0) { - return ( -
    -

    Au fil de l'eau

    -

    - Pas encore assez de carbets avec des photos pour démarrer le mode immersif. -

    - - Voir le catalogue - -
    - ); - } - - return ( - - ); -} diff --git a/src/app/espace-hote/_components/BookingDecision.tsx b/src/app/espace-hote/_components/BookingDecision.tsx deleted file mode 100644 index 9380ea2..0000000 --- a/src/app/espace-hote/_components/BookingDecision.tsx +++ /dev/null @@ -1,77 +0,0 @@ -"use client"; - -import { useState, useTransition } from "react"; -import { useRouter } from "next/navigation"; - -import { confirmBookingAsHost, rejectBookingAsHost } from "../actions"; - -export function BookingDecision({ bookingId }: { bookingId: string }) { - const router = useRouter(); - const [pending, startTransition] = useTransition(); - const [confirmReject, setConfirmReject] = useState(false); - const [error, setError] = useState(null); - - function accept() { - setError(null); - startTransition(async () => { - const res = await confirmBookingAsHost(bookingId); - if (res && res.ok === false) setError(res.error); - router.refresh(); - }); - } - function reject() { - setError(null); - startTransition(async () => { - const res = await rejectBookingAsHost(bookingId); - if (res && res.ok === false) setError(res.error); - setConfirmReject(false); - router.refresh(); - }); - } - - return ( -
    - {confirmReject ? ( -
    - Refuser ? - - -
    - ) : ( - <> - - - - )} - {error ? {error} : null} -
    - ); -} diff --git a/src/app/espace-hote/actions.ts b/src/app/espace-hote/actions.ts deleted file mode 100644 index fa9208c..0000000 --- a/src/app/espace-hote/actions.ts +++ /dev/null @@ -1,75 +0,0 @@ -"use server"; - -import { revalidatePath } from "next/cache"; - -import { auth } from "@/auth"; -import { BookingStatus, UserRole } from "@/generated/prisma/enums"; -import { prisma } from "@/lib/prisma"; -import { recordAudit } from "@/lib/admin/audit"; -import { sendBookingConfirmed } from "@/lib/email"; - -async function requireBookingOwnership(bookingId: string) { - const session = await auth(); - if (!session?.user?.id) throw new Error("Non authentifié"); - const booking = await prisma.booking.findUnique({ - where: { id: bookingId }, - include: { - carbet: { select: { ownerId: true, title: true } }, - tenant: { select: { email: true, firstName: true } }, - }, - }); - if (!booking) throw new Error("Réservation introuvable"); - const isAdmin = session.user.role === UserRole.ADMIN; - if (!isAdmin && booking.carbet.ownerId !== session.user.id) { - throw new Error("Accès refusé"); - } - return { session, booking }; -} - -export async function confirmBookingAsHost(bookingId: string) { - const { session, booking } = await requireBookingOwnership(bookingId); - if (booking.status !== BookingStatus.PENDING) { - return { ok: false as const, error: "Cette réservation ne peut plus être confirmée." }; - } - const updated = await prisma.booking.update({ - where: { id: bookingId }, - data: { status: BookingStatus.CONFIRMED }, - }); - await recordAudit({ - scope: "host.bookings", - event: "confirm", - target: bookingId, - actorEmail: session.user.email ?? null, - details: {}, - }); - sendBookingConfirmed( - booking.tenant.email, - booking.tenant.firstName, - bookingId, - booking.carbet.title, - updated.startDate, - updated.endDate, - ).catch(() => {}); - revalidatePath("/espace-hote"); - return { ok: true as const }; -} - -export async function rejectBookingAsHost(bookingId: string) { - const { session, booking } = await requireBookingOwnership(bookingId); - if (booking.status !== BookingStatus.PENDING) { - return { ok: false as const, error: "Cette réservation ne peut plus être refusée." }; - } - await prisma.booking.update({ - where: { id: bookingId }, - data: { status: BookingStatus.CANCELLED }, - }); - await recordAudit({ - scope: "host.bookings", - event: "reject", - target: bookingId, - actorEmail: session.user.email ?? null, - details: {}, - }); - revalidatePath("/espace-hote"); - return { ok: true as const }; -} diff --git a/src/app/espace-hote/carbets/[carbetId]/page.tsx b/src/app/espace-hote/carbets/[carbetId]/page.tsx index 2b8b069..93768b1 100644 --- a/src/app/espace-hote/carbets/[carbetId]/page.tsx +++ b/src/app/espace-hote/carbets/[carbetId]/page.tsx @@ -3,10 +3,11 @@ import { notFound } from "next/navigation"; import { canManageCarbet, requireOwnerSession } from "@/lib/carbet-access"; import { prisma } from "@/lib/prisma"; -import { MediaUploader } from "@/components/MediaUploader"; +import { isStorageConfigured } from "@/lib/storage"; import { updateCarbet } from "../actions"; import { CarbetForm } from "../_components/carbet-form"; +import { MediaManager } from "../_components/media-manager"; export default async function EditCarbetPage({ params, @@ -35,7 +36,7 @@ export default async function EditCarbetPage({ status: true, media: { orderBy: { sortOrder: "asc" }, - select: { id: true, type: true, s3Url: true, s3Key: true, sortOrder: true }, + select: { id: true, type: true, s3Url: true, sortOrder: true }, }, amenities: { select: { amenity: { select: { key: true } } } }, }, @@ -79,10 +80,14 @@ export default async function EditCarbetPage({

    Médias

    - Déposez photos et vidéos courtes, réorganisez par glisser-déposer. - Le premier média sert de cover sur le catalogue et la home. + Le premier média sert de photo de couverture. Réordonnez avec les + flèches.

    - +
    diff --git a/src/app/espace-hote/page.tsx b/src/app/espace-hote/page.tsx index 32f3d03..d412d73 100644 --- a/src/app/espace-hote/page.tsx +++ b/src/app/espace-hote/page.tsx @@ -1,287 +1,25 @@ import Link from "next/link"; -import { auth } from "@/auth"; import { requireRole } from "@/lib/authorization"; -import { BookingStatus, UserRole } from "@/generated/prisma/enums"; -import { - getHostKpis, - listHostCarbets, - listHostRecentBookings, - isScopeAdmin, -} from "@/lib/host-dashboard"; -import { BookingDecision } from "./_components/BookingDecision"; - -export const dynamic = "force-dynamic"; - -const STATUS_TONES: Record = { - PENDING: "bg-sky-100 text-sky-800 ring-sky-300", - CONFIRMED: "bg-emerald-100 text-emerald-800 ring-emerald-300", - CANCELLED: "bg-rose-100 text-rose-700 ring-rose-300", - COMPLETED: "bg-zinc-100 text-zinc-700 ring-zinc-300", - SUCCEEDED: "bg-emerald-100 text-emerald-800 ring-emerald-300", - REFUNDED: "bg-amber-100 text-amber-800 ring-amber-300", - FAILED: "bg-rose-100 text-rose-700 ring-rose-300", - AUTHORIZED: "bg-indigo-100 text-indigo-800 ring-indigo-300", - DRAFT: "bg-zinc-100 text-zinc-700 ring-zinc-300", - PUBLISHED: "bg-emerald-100 text-emerald-800 ring-emerald-300", - ARCHIVED: "bg-amber-100 text-amber-800 ring-amber-300", -}; - -const STATUS_LABEL: Record = { - PENDING: "En attente", - CONFIRMED: "Confirmée", - CANCELLED: "Annulée", - COMPLETED: "Terminée", - SUCCEEDED: "Payé", - REFUNDED: "Remboursé", - FAILED: "Échec", - AUTHORIZED: "Autorisé", - DRAFT: "Brouillon", - PUBLISHED: "Publié", - ARCHIVED: "Archivé", -}; - -function Badge({ value }: { value: string }) { - const tone = STATUS_TONES[value] ?? STATUS_TONES.PENDING; - return ( - - {STATUS_LABEL[value] ?? value} - - ); -} - -function fmtEur(amount: string, currency: string): string { - const n = Number(amount); - return n.toLocaleString("fr-FR", { style: "currency", currency: currency || "EUR" }); -} - -const dateFmt = new Intl.DateTimeFormat("fr-FR", { - day: "2-digit", - month: "short", - year: "2-digit", -}); - -export default async function HostDashboardPage() { - await requireRole([UserRole.OWNER, UserRole.ADMIN]); - const session = await auth(); - const userId = session!.user.id; - const isAdmin = isScopeAdmin(session?.user?.role); - const scope = { ownerId: userId, isAdmin }; - - const [kpis, recent, carbets] = await Promise.all([ - getHostKpis(scope), - listHostRecentBookings(scope, 12), - listHostCarbets(scope), - ]); - - const pendingBookings = recent.filter((b) => b.status === BookingStatus.PENDING); +export default async function HostPage() { + const session = await requireRole(["OWNER", "ADMIN"]); return ( -
    -
    -
    -

    Espace hôte

    -

    - Bienvenue {session?.user?.name || session?.user?.email}.{" "} - {isAdmin ? "Vue globale (admin)." : "Vue limitée à vos carbets."} -

    -
    -
    - - + Nouveau carbet - - - Tous mes carbets - -
    -
    +
    +

    Espace hôte

    +

    + Accès autorisé pour {session.user.email} ({session.user.role}). +

    -
    - - - - 0 ? "warn" : "neutral"} - /> - - -
    - - {kpis.nextArrival ? ( -
    -
    Prochaine arrivée
    -
    - {kpis.nextArrival.tenantName} · {kpis.nextArrival.carbetTitle} -
    -
    - {dateFmt.format(kpis.nextArrival.startDate)} -
    -
    - ) : null} - - {pendingBookings.length > 0 ? ( -
    -

    - Demandes en attente ({pendingBookings.length}) -

    -
      - {pendingBookings.map((b) => ( -
    • -
      -
      - {b.tenantName} — {b.carbetTitle} -
      -
      - {dateFmt.format(b.startDate)} → {dateFmt.format(b.endDate)} ·{" "} - {b.guestCount} pers · {fmtEur(b.amount, b.currency)} -
      -
      - -
    • - ))} -
    -
    - ) : null} - -
    -

    - Mes carbets ({carbets.length}) -

    - {carbets.length === 0 ? ( -
    - Aucun carbet pour l'instant.{" "} - - Créer mon premier carbet - -
    - ) : ( -
    - - - - - - - - - - - - - - - {carbets.map((c) => ( - - - - - - - - - - - ))} - -
    TitreFleuve€/nuitCap.MédiasRésasAvisStatut
    - - {c.title} - -
    - /{c.slug} -
    -
    {c.river} - {Number(c.nightlyPrice).toFixed(0)} - {c.capacity} - {c._count.media} - - {c._count.bookings} - - {c._count.reviews} - - -
    -
    - )} -
    - - {recent.length > 0 ? ( -
    -

    - Activité récente -

    -
    - - - - - - - - - - - - - {recent.map((b) => ( - - - - - - - - - ))} - -
    CarbetLocataireSéjourMontantRésaPaiement
    {b.carbetTitle}{b.tenantName} - {dateFmt.format(b.startDate)} → {dateFmt.format(b.endDate)} - - {fmtEur(b.amount, b.currency)} - - - - -
    -
    -
    - ) : null} +
    + + Gérer mes carbets + +
    ); } - -function Kpi({ - label, - value, - tone = "neutral", -}: { - label: string; - value: string; - tone?: "neutral" | "warn"; -}) { - return ( -
    -
    {label}
    -
    - {value} -
    -
    - ); -} diff --git a/src/app/inscription/_components/SignupForm.tsx b/src/app/inscription/_components/SignupForm.tsx deleted file mode 100644 index 2ffd914..0000000 --- a/src/app/inscription/_components/SignupForm.tsx +++ /dev/null @@ -1,149 +0,0 @@ -"use client"; - -import { useState, useTransition } from "react"; -import { useRouter } from "next/navigation"; -import { signIn } from "next-auth/react"; - -type Props = { next: string }; - -export function SignupForm({ next }: Props) { - const router = useRouter(); - const [pending, startTransition] = useTransition(); - const [error, setError] = useState(null); - const [role, setRole] = useState<"TOURIST" | "OWNER">("TOURIST"); - - function onSubmit(formData: FormData) { - setError(null); - const email = (formData.get("email") as string | null)?.trim() ?? ""; - const password = (formData.get("password") as string | null) ?? ""; - const firstName = (formData.get("firstName") as string | null)?.trim() ?? ""; - const lastName = (formData.get("lastName") as string | null)?.trim() ?? ""; - const phone = (formData.get("phone") as string | null)?.trim() ?? ""; - - if (password.length < 8) { - setError("Le mot de passe doit faire au moins 8 caractères."); - return; - } - - startTransition(async () => { - const res = await fetch("/api/signup", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email, password, firstName, lastName, phone: phone || null, role }), - }); - const json = await res.json().catch(() => ({})); - if (!res.ok) { - setError(json?.error || `Erreur ${res.status}`); - return; - } - const result = await signIn("credentials", { - email, - password, - redirect: false, - }); - if (result?.error) { - setError("Compte créé mais connexion impossible. Essayez la page de connexion."); - return; - } - router.push(next); - router.refresh(); - }); - } - - const inputCls = - "w-full rounded-md border border-zinc-300 px-3 py-2 text-sm focus:border-zinc-900 focus:outline-none"; - - return ( -
    -
    -
    - - -
    - - - - - - - -
    - Type de compte -
    - - -
    -
    - - {error ? ( -
    {error}
    - ) : null} - - -
    -
    - ); -} diff --git a/src/app/inscription/page.tsx b/src/app/inscription/page.tsx deleted file mode 100644 index 35e871e..0000000 --- a/src/app/inscription/page.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { redirect } from "next/navigation"; -import Link from "next/link"; - -import { auth } from "@/auth"; -import { SignupForm } from "./_components/SignupForm"; - -export const dynamic = "force-dynamic"; - -type PageProps = { - searchParams: Promise<{ next?: string }>; -}; - -export default async function SignupPage({ searchParams }: PageProps) { - const session = await auth(); - const sp = await searchParams; - const next = sp.next && sp.next.startsWith("/") ? sp.next : "/"; - if (session?.user?.id) redirect(next); - - return ( -
    -
    -
    -

    Créer un compte

    -

    - Un compte vous permet de réserver un séjour ou, en tant qu'hôte, de publier votre carbet. -

    -
    - - - -

    - Déjà un compte ?{" "} - - Se connecter - -

    -
    -
    - ); -} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1e1dc82..30c807f 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,7 +4,6 @@ import "./globals.css"; import { PluginProvider } from "@/lib/plugins/client"; import { getEnabledPluginKeys, syncPluginsFromRegistry } from "@/lib/plugins/server"; import { SeasonBanner } from "@/components/SeasonBanner"; -import { SiteHeaderGuard } from "@/components/SiteHeaderGuard"; import { LocaleProvider } from "@/lib/i18n/client"; import { dict, getLocale } from "@/lib/i18n/server"; @@ -52,21 +51,6 @@ export const metadata: Metadata = { }, description: "Karbé, la marketplace de location de carbets fluviaux de Guyane.", - manifest: "/manifest.webmanifest", - applicationName: "Karbé", - appleWebApp: { - capable: true, - statusBarStyle: "black-translucent", - title: "Karbé", - }, - icons: { - icon: [ - { url: "/icons/favicon-32.png", sizes: "32x32", type: "image/png" }, - { url: "/icons/icon-192.png", sizes: "192x192", type: "image/png" }, - { url: "/icons/icon-512.png", sizes: "512x512", type: "image/png" }, - ], - apple: "/icons/apple-touch-icon.png", - }, openGraph: { type: "website", siteName: "Karbé", @@ -77,13 +61,6 @@ export const metadata: Metadata = { }, }; -export const viewport = { - themeColor: "#059669", - width: "device-width", - initialScale: 1, - viewportFit: "cover" as const, -}; - export default async function RootLayout({ children, }: Readonly<{ @@ -125,7 +102,6 @@ export default async function RootLayout({ - {children} diff --git a/src/app/mes-favoris/page.tsx b/src/app/mes-favoris/page.tsx deleted file mode 100644 index 5887400..0000000 --- a/src/app/mes-favoris/page.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { redirect } from "next/navigation"; -import Link from "next/link"; - -import { auth } from "@/auth"; -import { listFavoriteCarbets } from "@/lib/reels"; -import { buildSrcSet } from "@/lib/image-variants"; - -export const dynamic = "force-dynamic"; - -export const metadata = { title: "Mes favoris" }; - -export default async function MyFavoritesPage() { - const session = await auth(); - if (!session?.user?.id) redirect("/connexion?next=/mes-favoris"); - - const carbets = await listFavoriteCarbets(session.user.id); - - return ( -
    -

    Mes favoris

    -

    - {carbets.length === 0 - ? "Aucun favori pour l'instant — ajoutez des carbets depuis le mode Au fil de l'eau ou les fiches." - : `${carbets.length} carbet${carbets.length > 1 ? "s" : ""} sauvegardé${carbets.length > 1 ? "s" : ""}.`} -

    - - {carbets.length === 0 ? ( -
    - - Découvrir des carbets - -
    - ) : ( -
      - {carbets.map((c) => ( -
    • - - {c.media[0] ? ( - // eslint-disable-next-line @next/next/no-img-element - {c.title} - ) : ( -
      - )} -
      -

      {c.title}

      -

      - {c.river} · {Number(c.nightlyPrice).toFixed(0)} € / nuit -

      -
      - -
    • - ))} -
    - )} -
    - ); -} diff --git a/src/app/mon-compte/_components/DangerZone.tsx b/src/app/mon-compte/_components/DangerZone.tsx deleted file mode 100644 index 0671134..0000000 --- a/src/app/mon-compte/_components/DangerZone.tsx +++ /dev/null @@ -1,67 +0,0 @@ -"use client"; - -import { useState, useTransition } from "react"; - -import { deleteAccountAction } from "../actions"; - -export function DangerZone() { - const [pending, startTransition] = useTransition(); - const [step, setStep] = useState<"idle" | "confirm">("idle"); - const [typed, setTyped] = useState(""); - - function deleteAccount() { - startTransition(async () => { - await deleteAccountAction(); - }); - } - - return ( -
    -

    - La suppression anonymise votre compte (nom, email, téléphone effacés). Vos réservations - passées restent en base pour les obligations comptables, mais ne sont plus rattachées à - des données personnelles identifiantes. -

    - {step === "idle" ? ( - - ) : ( -
    -

    - Pour confirmer, saisissez SUPPRIMER ci-dessous. -

    - setTyped(e.target.value)} - className="w-full rounded-md border border-rose-300 px-3 py-1.5 text-sm focus:border-rose-500 focus:outline-none" - disabled={pending} - /> -
    - - -
    -
    - )} -
    - ); -} diff --git a/src/app/mon-compte/_components/PasswordForm.tsx b/src/app/mon-compte/_components/PasswordForm.tsx deleted file mode 100644 index b809b87..0000000 --- a/src/app/mon-compte/_components/PasswordForm.tsx +++ /dev/null @@ -1,69 +0,0 @@ -"use client"; - -import { useRef, useState, useTransition } from "react"; - -import { changePasswordAction } from "../actions"; - -export function PasswordForm() { - const formRef = useRef(null); - const [pending, startTransition] = useTransition(); - const [error, setError] = useState(null); - const [success, setSuccess] = useState(null); - - function onSubmit(fd: FormData) { - setError(null); - setSuccess(null); - const next = (fd.get("next") as string | null) ?? ""; - const confirm = (fd.get("confirm") as string | null) ?? ""; - if (next !== confirm) { - setError("Les deux nouveaux mots de passe ne correspondent pas."); - return; - } - startTransition(async () => { - const res = await changePasswordAction(fd); - if (res && res.ok === false) setError(res.error); - else { - setSuccess("Mot de passe mis à jour."); - formRef.current?.reset(); - } - }); - } - - const inputCls = - "mt-0.5 w-full rounded-md border border-zinc-300 px-3 py-2 text-sm focus:border-zinc-900 focus:outline-none"; - - return ( -
    -
    - -
    - - -
    - - {error ? ( -
    {error}
    - ) : null} - {success ? ( -
    {success}
    - ) : null} - - -
    -
    - ); -} diff --git a/src/app/mon-compte/_components/ProfileForm.tsx b/src/app/mon-compte/_components/ProfileForm.tsx deleted file mode 100644 index 23eac80..0000000 --- a/src/app/mon-compte/_components/ProfileForm.tsx +++ /dev/null @@ -1,88 +0,0 @@ -"use client"; - -import { useState, useTransition } from "react"; -import { useRouter } from "next/navigation"; - -import { updateProfileAction } from "../actions"; - -type Props = { - initial: { firstName: string; lastName: string; phone: string | null }; -}; - -export function ProfileForm({ initial }: Props) { - const router = useRouter(); - const [pending, startTransition] = useTransition(); - const [error, setError] = useState(null); - const [success, setSuccess] = useState(null); - - function onSubmit(fd: FormData) { - setError(null); - setSuccess(null); - startTransition(async () => { - const res = await updateProfileAction(fd); - if (res && res.ok === false) setError(res.error); - else { - setSuccess("Profil enregistré."); - router.refresh(); - } - }); - } - - const inputCls = - "mt-0.5 w-full rounded-md border border-zinc-300 px-3 py-2 text-sm focus:border-zinc-900 focus:outline-none"; - - return ( -
    -
    -
    - - -
    - - - {error ? ( -
    {error}
    - ) : null} - {success ? ( -
    {success}
    - ) : null} - - -
    -
    - ); -} diff --git a/src/app/mon-compte/actions.ts b/src/app/mon-compte/actions.ts deleted file mode 100644 index c002a49..0000000 --- a/src/app/mon-compte/actions.ts +++ /dev/null @@ -1,115 +0,0 @@ -"use server"; - -import { revalidatePath } from "next/cache"; -import { redirect } from "next/navigation"; -import { z } from "zod"; - -import { auth, signOut } from "@/auth"; -import { prisma } from "@/lib/prisma"; -import { hashPassword, verifyPassword } from "@/lib/password"; -import { recordAudit } from "@/lib/admin/audit"; - -const profileSchema = z.object({ - firstName: z.string().trim().min(1).max(100), - lastName: z.string().trim().min(1).max(100), - phone: z.string().trim().max(40).optional().nullable(), -}); - -const passwordSchema = z - .object({ - current: z.string().min(1), - next: z.string().min(8).max(200), - }) - .refine((d) => d.current !== d.next, { message: "Le nouveau mdp doit être différent de l'ancien." }); - -async function requireSelf() { - const session = await auth(); - if (!session?.user?.id) throw new Error("Non authentifié"); - return session; -} - -export async function updateProfileAction(fd: FormData) { - const session = await requireSelf(); - const parsed = profileSchema.safeParse({ - firstName: fd.get("firstName"), - lastName: fd.get("lastName"), - phone: ((fd.get("phone") as string | null) ?? "").trim() || null, - }); - if (!parsed.success) { - return { ok: false as const, error: parsed.error.issues.map((i) => i.message).join(" · ") }; - } - await prisma.user.update({ - where: { id: session.user.id }, - data: parsed.data, - }); - await recordAudit({ - scope: "public.profile", - event: "profile.update", - target: session.user.id, - actorEmail: session.user.email ?? null, - details: parsed.data, - }); - revalidatePath("/mon-compte"); - return { ok: true as const }; -} - -export async function changePasswordAction(fd: FormData) { - const session = await requireSelf(); - const parsed = passwordSchema.safeParse({ - current: fd.get("current"), - next: fd.get("next"), - }); - if (!parsed.success) { - return { ok: false as const, error: parsed.error.issues.map((i) => i.message).join(" · ") }; - } - const user = await prisma.user.findUnique({ - where: { id: session.user.id }, - select: { passwordHash: true }, - }); - if (!user) return { ok: false as const, error: "Utilisateur introuvable." }; - const ok = await verifyPassword(parsed.data.current, user.passwordHash); - if (!ok) return { ok: false as const, error: "Mot de passe actuel incorrect." }; - await prisma.user.update({ - where: { id: session.user.id }, - data: { passwordHash: await hashPassword(parsed.data.next) }, - }); - await recordAudit({ - scope: "public.profile", - event: "password.change", - target: session.user.id, - actorEmail: session.user.email ?? null, - details: {}, - }); - return { ok: true as const }; -} - -/** Supprime le compte + cascade : bookings restent en DB (anonymisées) pour les obligations comptables. */ -export async function deleteAccountAction() { - const session = await requireSelf(); - const userId = session.user.id; - const email = session.user.email ?? ""; - - // Anonymise plutôt que supprimer pour préserver l'historique comptable des bookings. - const anon = `anonymise-${userId}@karbe.invalid`; - await prisma.user.update({ - where: { id: userId }, - data: { - email: anon, - firstName: "Compte", - lastName: "supprimé", - phone: null, - passwordHash: "", // verrouille le login (bcrypt.compare retournera toujours false) - isActive: false, - }, - }); - await prisma.passwordResetToken.deleteMany({ where: { userId } }); - await recordAudit({ - scope: "public.profile", - event: "account.delete", - target: userId, - actorEmail: email, - details: { anonymisedTo: anon }, - }); - await signOut({ redirect: false }); - redirect("/?account=deleted"); -} diff --git a/src/app/mon-compte/page.tsx b/src/app/mon-compte/page.tsx deleted file mode 100644 index 982b78a..0000000 --- a/src/app/mon-compte/page.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { redirect } from "next/navigation"; - -import { auth } from "@/auth"; -import { prisma } from "@/lib/prisma"; - -import { ProfileForm } from "./_components/ProfileForm"; -import { PasswordForm } from "./_components/PasswordForm"; -import { DangerZone } from "./_components/DangerZone"; - -export const dynamic = "force-dynamic"; - -export default async function MyAccountPage() { - const session = await auth(); - if (!session?.user?.id) redirect("/connexion?next=/mon-compte"); - - const user = await prisma.user.findUnique({ - where: { id: session.user.id }, - select: { email: true, firstName: true, lastName: true, phone: true, role: true, createdAt: true }, - }); - if (!user) redirect("/connexion"); - - const dateFmt = new Intl.DateTimeFormat("fr-FR", { day: "2-digit", month: "long", year: "numeric" }); - - return ( -
    -
    -

    Mon compte

    -

    - Connecté avec {user.email} · inscrit le {dateFmt.format(user.createdAt)} -

    -
    - -
    -

    Identité

    - -
    - -
    -

    - Sécurité -

    - -
    - -
    -

    - Mes données (RGPD) -

    -

    - Téléchargez l'intégralité des données associées à votre compte au format JSON, - conformément à l'article 20 du RGPD (droit à la portabilité). -

    - - Télécharger mes données - -
    - -
    -

    - Zone dangereuse -

    - -
    -
    - ); -} diff --git a/src/app/mot-de-passe-oublie/[token]/_components/ResetForm.tsx b/src/app/mot-de-passe-oublie/[token]/_components/ResetForm.tsx deleted file mode 100644 index e77c3e7..0000000 --- a/src/app/mot-de-passe-oublie/[token]/_components/ResetForm.tsx +++ /dev/null @@ -1,75 +0,0 @@ -"use client"; - -import { useState, useTransition } from "react"; -import { useRouter } from "next/navigation"; - -export function ResetForm({ token }: { token: string }) { - const router = useRouter(); - const [pending, startTransition] = useTransition(); - const [error, setError] = useState(null); - - function onSubmit(fd: FormData) { - setError(null); - const password = (fd.get("password") as string | null) ?? ""; - const confirm = (fd.get("confirm") as string | null) ?? ""; - if (password.length < 8) { - setError("Mot de passe trop court (8 caractères min)."); - return; - } - if (password !== confirm) { - setError("Les deux mots de passe ne correspondent pas."); - return; - } - startTransition(async () => { - const res = await fetch("/api/password/reset", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ token, password }), - }); - const json = await res.json().catch(() => ({})); - if (!res.ok) { - setError(json?.error || `Erreur ${res.status}`); - return; - } - router.push("/connexion?reset=ok"); - }); - } - - return ( -
    -
    - - - {error ? ( -
    - {error} -
    - ) : null} - -
    -
    - ); -} diff --git a/src/app/mot-de-passe-oublie/[token]/page.tsx b/src/app/mot-de-passe-oublie/[token]/page.tsx deleted file mode 100644 index 80893ac..0000000 --- a/src/app/mot-de-passe-oublie/[token]/page.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import Link from "next/link"; - -import { ResetForm } from "./_components/ResetForm"; - -export const dynamic = "force-dynamic"; - -type PageProps = { params: Promise<{ token: string }> }; - -export default async function ResetPage({ params }: PageProps) { - const { token } = await params; - - return ( -
    -
    -
    -

    Nouveau mot de passe

    -

    - Choisissez un mot de passe d'au moins 8 caractères. Vous serez redirigé vers la - connexion une fois enregistré. -

    -
    - - - -

    - - Retour à la connexion - -

    -
    -
    - ); -} diff --git a/src/app/mot-de-passe-oublie/_components/ResetRequestForm.tsx b/src/app/mot-de-passe-oublie/_components/ResetRequestForm.tsx deleted file mode 100644 index 563622a..0000000 --- a/src/app/mot-de-passe-oublie/_components/ResetRequestForm.tsx +++ /dev/null @@ -1,52 +0,0 @@ -"use client"; - -import { useState, useTransition } from "react"; - -export function ResetRequestForm() { - const [pending, startTransition] = useTransition(); - const [done, setDone] = useState(false); - - function onSubmit(fd: FormData) { - const email = (fd.get("email") as string | null)?.trim() ?? ""; - if (!email) return; - startTransition(async () => { - await fetch("/api/password/reset-request", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email }), - }).catch(() => {}); - setDone(true); - }); - } - - if (done) { - return ( -
    - Si un compte existe pour cet email, vous recevrez un lien dans quelques instants. Pensez à - vérifier vos spams. -
    - ); - } - - return ( -
    -
    - - -
    -
    - ); -} diff --git a/src/app/mot-de-passe-oublie/page.tsx b/src/app/mot-de-passe-oublie/page.tsx deleted file mode 100644 index 1ac4a60..0000000 --- a/src/app/mot-de-passe-oublie/page.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { redirect } from "next/navigation"; -import Link from "next/link"; - -import { auth } from "@/auth"; -import { ResetRequestForm } from "./_components/ResetRequestForm"; - -export const dynamic = "force-dynamic"; - -export default async function ForgotPasswordPage() { - const session = await auth(); - if (session?.user?.id) redirect("/"); - - return ( -
    -
    -
    -

    Mot de passe oublié

    -

    - Saisissez votre email. Si un compte existe, vous recevrez un lien valable 1 heure pour - choisir un nouveau mot de passe. -

    -
    - - - -

    - - Retour à la connexion - -

    -
    -
    - ); -} diff --git a/src/app/page.tsx b/src/app/page.tsx index ad5f2bd..5d0099b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,9 +1,63 @@ -import { redirect } from "next/navigation"; +import Link from "next/link"; +import { IfPluginEnabled } from "@/components/IfPluginEnabled"; +import { HeroSection } from "@/components/landing/HeroSection"; +import { ExperiencesSection } from "@/components/landing/ExperiencesSection"; +import { HowItWorksSection } from "@/components/landing/HowItWorksSection"; +import { CESection } from "@/components/landing/CESection"; +import { TestimonialsSection } from "@/components/landing/TestimonialsSection"; +import { LandingFooter } from "@/components/landing/Footer"; /** - * Home redirige vers le mode immersif « Au fil de l'eau » par défaut. - * L'ancien hero/landing reste accessible via /accueil. + * Page d'accueil — la majorité du contenu est conditionnée par les plugins : + * - `landing-hero` → hero plein écran + * - `landing-sections` → 2 expériences + comment ça marche + CE + témoignages + footer riche + * + * Si aucun de ces plugins n'est activé, on retombe sur la home historique + * minimaliste (fallback). Activable depuis /admin/plugins. */ export default function Home() { - redirect("/decouvrir"); + return ( + <> + +
    +

    + Karbé — carbets fluviaux de Guyane +

    +

    + La marketplace pour louer des carbets le long des fleuves de Guyane. +

    +
    + + Découvrir les carbets + + + Espace hôte + +
    +
    +
+ } + > + + + + + + + + + + + + ); } diff --git a/src/app/reservations/[id]/page.tsx b/src/app/reservations/[id]/page.tsx deleted file mode 100644 index 857bf1d..0000000 --- a/src/app/reservations/[id]/page.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { notFound, redirect } from "next/navigation"; -import Link from "next/link"; - -import { auth } from "@/auth"; -import { UserRole } from "@/generated/prisma/enums"; -import { prisma } from "@/lib/prisma"; - -export const dynamic = "force-dynamic"; - -type PageProps = { params: Promise<{ id: string }> }; - -const STATUS_LABEL: Record = { - PENDING: "En attente de confirmation", - CONFIRMED: "Confirmée", - CANCELLED: "Annulée", - COMPLETED: "Terminée", -}; - -const PAYMENT_LABEL: Record = { - PENDING: "Paiement en attente", - AUTHORIZED: "Paiement autorisé", - SUCCEEDED: "Paiement reçu", - FAILED: "Paiement échoué", - REFUNDED: "Remboursé", -}; - -export default async function ReservationPage({ params }: PageProps) { - const { id } = await params; - const session = await auth(); - if (!session?.user?.id) redirect(`/connexion?next=/reservations/${id}`); - - const booking = await prisma.booking.findUnique({ - where: { id }, - include: { - carbet: { select: { title: true, slug: true, river: true } }, - tenant: { select: { id: true, email: true } }, - }, - }); - if (!booking) notFound(); - - const isOwner = booking.tenant.id === session.user.id; - const isAdmin = session.user.role === UserRole.ADMIN; - if (!isOwner && !isAdmin) notFound(); - - const dateFmt = new Intl.DateTimeFormat("fr-FR", { day: "2-digit", month: "long", year: "numeric" }); - const nights = Math.max(1, Math.round((booking.endDate.getTime() - booking.startDate.getTime()) / 86400000)); - - return ( -
-

Demande de réservation envoyée

-

- Votre demande pour {booking.carbet.title} a bien été enregistrée. Vous recevrez - un email dès que l'hôte ou l'équipe Karbé l'aura confirmée. -

- -
-
- Référence - {booking.id} -
-
-
-
Carbet
- - {booking.carbet.title} - -
{booking.carbet.river}
-
-
-
Voyageurs
-
- {booking.guestCount} personne{booking.guestCount > 1 ? "s" : ""} -
-
-
-
Arrivée
-
{dateFmt.format(booking.startDate)}
-
-
-
Départ
-
{dateFmt.format(booking.endDate)}
-
-
-
Total ({nights} nuit{nights > 1 ? "s" : ""})
-
- {Number(booking.amount).toFixed(2)} {booking.currency} -
-
-
-
- - {STATUS_LABEL[booking.status] ?? booking.status} - - - {PAYMENT_LABEL[booking.paymentStatus] ?? booking.paymentStatus} - -
-
- -
- - ← Retour au carbet - - - Accueil - -
-
- ); -} diff --git a/src/components/MediaUploader.tsx b/src/components/MediaUploader.tsx deleted file mode 100644 index 815d991..0000000 --- a/src/components/MediaUploader.tsx +++ /dev/null @@ -1,380 +0,0 @@ -"use client"; - -import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"; -import { - DndContext, - PointerSensor, - TouchSensor, - KeyboardSensor, - closestCenter, - useSensor, - useSensors, - type DragEndEvent, -} from "@dnd-kit/core"; -import { - SortableContext, - arrayMove, - rectSortingStrategy, - useSortable, - sortableKeyboardCoordinates, -} from "@dnd-kit/sortable"; -import { CSS } from "@dnd-kit/utilities"; - -export type MediaItem = { - id: string; - type: "PHOTO" | "VIDEO"; - s3Url: string; - s3Key: string; - sortOrder: number; -}; - -type Props = { - carbetId: string; - initialMedia: MediaItem[]; -}; - -type UploadEntry = { - tempId: string; - name: string; - sizeBytes: number; - mime: string; - progress: number; - error?: string; - done: boolean; -}; - -const MAX_PARALLEL = 3; - -export function MediaUploader({ carbetId, initialMedia }: Props) { - const [items, setItems] = useState( - [...initialMedia].sort((a, b) => a.sortOrder - b.sortOrder), - ); - const [uploads, setUploads] = useState([]); - const [dragging, setDragging] = useState(false); - const inputId = useId(); - const fileInput = useRef(null); - const queueRef = useRef([]); - const activeRef = useRef(0); - - const sensors = useSensors( - useSensor(PointerSensor, { activationConstraint: { distance: 6 } }), - useSensor(TouchSensor, { activationConstraint: { delay: 150, tolerance: 6 } }), - useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }), - ); - - const allIds = useMemo(() => items.map((i) => i.id), [items]); - - const reorderOnServer = useCallback( - async (orderedIds: string[]) => { - await fetch("/api/media/reorder", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ carbetId, orderedIds }), - }).catch(() => {}); - }, - [carbetId], - ); - - function onDragEnd(e: DragEndEvent) { - const { active, over } = e; - if (!over || active.id === over.id) return; - setItems((prev) => { - const oldIdx = prev.findIndex((p) => p.id === active.id); - const newIdx = prev.findIndex((p) => p.id === over.id); - if (oldIdx < 0 || newIdx < 0) return prev; - const next = arrayMove(prev, oldIdx, newIdx); - reorderOnServer(next.map((m) => m.id)); - return next; - }); - } - - const setCover = useCallback( - (id: string) => { - setItems((prev) => { - const idx = prev.findIndex((p) => p.id === id); - if (idx <= 0) return prev; - const next = arrayMove(prev, idx, 0); - reorderOnServer(next.map((m) => m.id)); - return next; - }); - }, - [reorderOnServer], - ); - - const removeItem = useCallback(async (id: string) => { - if (!confirm("Supprimer ce média ?")) return; - const res = await fetch(`/api/media/${id}`, { method: "DELETE" }); - if (res.ok) setItems((prev) => prev.filter((p) => p.id !== id)); - }, []); - - const processFile = useCallback(async function processFile(file: File): Promise { - const tempId = crypto.randomUUID(); - setUploads((u) => [ - ...u, - { tempId, name: file.name, sizeBytes: file.size, mime: file.type, progress: 0, done: false }, - ]); - try { - const presignRes = await fetch("/api/uploads/presign", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ carbetId, mime: file.type, sizeBytes: file.size }), - }); - const presign = await presignRes.json(); - if (!presignRes.ok) throw new Error(presign?.error || "presign refusé"); - - await new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - xhr.upload.addEventListener("progress", (ev) => { - if (!ev.lengthComputable) return; - const pct = Math.round((ev.loaded / ev.total) * 100); - setUploads((u) => u.map((x) => (x.tempId === tempId ? { ...x, progress: pct } : x))); - }); - xhr.addEventListener("load", () => - xhr.status >= 200 && xhr.status < 300 ? resolve() : reject(new Error(`HTTP ${xhr.status}`)), - ); - xhr.addEventListener("error", () => reject(new Error("Réseau coupé"))); - xhr.open("PUT", presign.uploadUrl); - xhr.setRequestHeader("Content-Type", file.type); - xhr.send(file); - }); - - const finalizeRes = await fetch("/api/uploads/finalize", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - carbetId, - s3Key: presign.s3Key, - s3Url: presign.publicUrl, - mime: file.type, - }), - }); - const finalize = await finalizeRes.json(); - if (!finalizeRes.ok) throw new Error(finalize?.error || "finalize refusé"); - setItems((prev) => [...prev, finalize.media]); - setUploads((u) => u.map((x) => (x.tempId === tempId ? { ...x, progress: 100, done: true } : x))); - // Cleanup après 2s - setTimeout(() => { - setUploads((u) => u.filter((x) => x.tempId !== tempId)); - }, 2000); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - setUploads((u) => u.map((x) => (x.tempId === tempId ? { ...x, error: msg } : x))); - } - }, [carbetId]); - - const popQueueRef = useRef<() => void>(() => {}); - const popQueue = useCallback(() => { - while (activeRef.current < MAX_PARALLEL && queueRef.current.length > 0) { - const file = queueRef.current.shift()!; - activeRef.current++; - processFile(file).finally(() => { - activeRef.current--; - popQueueRef.current(); - }); - } - }, [processFile]); - useEffect(() => { - popQueueRef.current = popQueue; - }, [popQueue]); - - function addFiles(files: FileList | File[]) { - const arr = Array.from(files); - queueRef.current.push(...arr); - popQueue(); - } - - function onChange(e: React.ChangeEvent) { - if (e.target.files) addFiles(e.target.files); - if (fileInput.current) fileInput.current.value = ""; - } - - function onDrop(e: React.DragEvent) { - e.preventDefault(); - setDragging(false); - if (e.dataTransfer.files) addFiles(e.dataTransfer.files); - } - - // Permet le coller depuis presse-papier - useEffect(() => { - function onPaste(e: ClipboardEvent) { - if (!e.clipboardData?.files?.length) return; - addFiles(e.clipboardData.files); - } - window.addEventListener("paste", onPaste); - return () => window.removeEventListener("paste", onPaste); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( -
-
{ - e.preventDefault(); - setDragging(true); - }} - onDragLeave={() => setDragging(false)} - onDrop={onDrop} - className={ - "rounded-lg border-2 border-dashed p-4 text-center transition " + - (dragging - ? "border-emerald-500 bg-emerald-50" - : "border-zinc-300 bg-zinc-50 hover:border-zinc-400") - } - > - - -
- - {uploads.length > 0 ? ( -
    - {uploads.map((u) => ( -
  • -
    - {u.name} - - {u.error - ? "❌" - : u.done - ? "✓" - : `${Math.round(u.sizeBytes / 1000)} ko · ${u.progress}%`} - -
    -
    -
    -
    - {u.error ?
    {u.error}
    : null} -
  • - ))} -
- ) : null} - - {items.length > 0 ? ( - - -
- {items.map((item, idx) => ( - setCover(item.id)} - onDelete={() => removeItem(item.id)} - /> - ))} -
-
-
- ) : ( -

- Pas encore de média. Ajoutez votre premier ci-dessus. -

- )} - -

- Glissez-déposez pour réordonner · Étoile = cover (image principale sur le catalogue) -

-
- ); -} - -function SortableTile({ - item, - isCover, - onSetCover, - onDelete, -}: { - item: MediaItem; - isCover: boolean; - onSetCover: () => void; - onDelete: () => void; -}) { - const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ - id: item.id, - }); - const style = { - transform: CSS.Transform.toString(transform), - transition, - opacity: isDragging ? 0.5 : 1, - }; - return ( -
-
- {item.type === "VIDEO" ? ( -
- {isCover ? ( - - Cover - - ) : null} - - {item.type} - -
- {!isCover ? ( - - ) : null} - -
-
- ); -} diff --git a/src/components/OperationalBadges.tsx b/src/components/OperationalBadges.tsx deleted file mode 100644 index e2f3ea0..0000000 --- a/src/components/OperationalBadges.tsx +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Badges opérationnels Karbé : 4 critères dealbreakers affichés en compact - * sur les cards catalog + en gros sur la fiche carbet. - * - * - Route (NONE / DRY_SEASON_ONLY / ALL_YEAR) - * - Capacité (X voyageurs max) - * - Électricité (NONE / SOLAR / GENERATOR_READY / EDF) - * - GSM (au carbet OUI / à X km / zone blanche) - */ - -import { Electricity, RoadAccess } from "@/generated/prisma/enums"; - -type Props = { - roadAccess: RoadAccess | null; - capacity: number; - electricity: Electricity | null; - gsmAtCarbet: boolean; - gsmExitDistanceKm: number | null; - /** "compact" pour les cards, "full" pour la fiche détail. */ - variant?: "compact" | "full"; -}; - -type Badge = { - emoji: string; - label: string; - tone: "good" | "neutral" | "warn"; -}; - -function roadBadge(r: RoadAccess | null): Badge { - if (r === RoadAccess.ALL_YEAR) return { emoji: "🛣️", label: "Route toute saison", tone: "good" }; - if (r === RoadAccess.DRY_SEASON_ONLY) return { emoji: "🛣️", label: "Route saison sèche", tone: "warn" }; - if (r === RoadAccess.NONE) return { emoji: "🛶", label: "Pirogue uniquement", tone: "neutral" }; - return { emoji: "🛣️", label: "Accès non précisé", tone: "neutral" }; -} - -function capacityBadge(c: number): Badge { - return { emoji: "👥", label: `${c} voyageur${c > 1 ? "s" : ""}`, tone: "neutral" }; -} - -function electricityBadge(e: Electricity | null): Badge { - if (e === Electricity.EDF) return { emoji: "⚡", label: "EDF / raccordé", tone: "good" }; - if (e === Electricity.GENERATOR_READY) return { emoji: "🔌", label: "Préinstall groupe", tone: "good" }; - if (e === Electricity.SOLAR) return { emoji: "☀️", label: "Solaire", tone: "neutral" }; - if (e === Electricity.NONE) return { emoji: "🕯️", label: "Aucune électricité", tone: "warn" }; - return { emoji: "⚡", label: "Électricité non précisée", tone: "neutral" }; -} - -function gsmBadge(atCarbet: boolean, exitKm: number | null): Badge { - if (atCarbet) return { emoji: "📶", label: "Réseau au carbet", tone: "good" }; - if (exitKm !== null) { - const tone: Badge["tone"] = exitKm <= 1 ? "neutral" : "warn"; - return { emoji: "📵", label: `Réseau à ${exitKm.toFixed(exitKm < 1 ? 1 : 0)} km`, tone }; - } - return { emoji: "📵", label: "Zone blanche", tone: "warn" }; -} - -const TONE_CLASSES_COMPACT: Record = { - good: "bg-emerald-50 text-emerald-800 ring-emerald-200", - neutral: "bg-zinc-100 text-zinc-700 ring-zinc-200", - warn: "bg-amber-50 text-amber-800 ring-amber-200", -}; - -const TONE_CLASSES_FULL: Record = { - good: "bg-emerald-50 text-emerald-900 ring-emerald-300 border-emerald-200", - neutral: "bg-white text-zinc-900 ring-zinc-300 border-zinc-200", - warn: "bg-amber-50 text-amber-900 ring-amber-300 border-amber-200", -}; - -export function OperationalBadges({ - roadAccess, - capacity, - electricity, - gsmAtCarbet, - gsmExitDistanceKm, - variant = "compact", -}: Props) { - const badges: Badge[] = [ - roadBadge(roadAccess), - capacityBadge(capacity), - electricityBadge(electricity), - gsmBadge(gsmAtCarbet, gsmExitDistanceKm), - ]; - - if (variant === "compact") { - return ( -
    - {badges.map((b, i) => ( -
  • - {b.emoji} - {b.label} -
  • - ))} -
- ); - } - - // full : grille 2×2 pour la fiche - return ( -
    - {badges.map((b, i) => ( -
  • - {b.emoji} - {b.label} -
  • - ))} -
- ); -} diff --git a/src/components/ResponsiveImage.tsx b/src/components/ResponsiveImage.tsx deleted file mode 100644 index 61bfaa6..0000000 --- a/src/components/ResponsiveImage.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/** - * avec srcset/sizes pré-rempli sur les variantes Karbé. - * Drop-in remplacement pour les balises `` côté front. - */ - -import { buildSrcSet } from "@/lib/image-variants"; - -type Props = { - src: string; - alt: string; - /** Indication CSS pour le browser. Ex: "(min-width: 768px) 800px, 100vw" */ - sizes?: string; - className?: string; - loading?: "lazy" | "eager"; - fetchPriority?: "high" | "low" | "auto"; - width?: number; - height?: number; - decoding?: "async" | "sync" | "auto"; - draggable?: boolean; - style?: React.CSSProperties; - onClick?: () => void; -}; - -export function ResponsiveImage({ - src, - alt, - sizes = "(min-width: 768px) 800px, 100vw", - className, - loading = "lazy", - fetchPriority = "auto", - width, - height, - decoding = "async", - draggable, - style, - onClick, -}: Props) { - return ( - // eslint-disable-next-line @next/next/no-img-element - {alt} - ); -} diff --git a/src/components/SignOutButton.tsx b/src/components/SignOutButton.tsx deleted file mode 100644 index 9837157..0000000 --- a/src/components/SignOutButton.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { signOut } from "@/auth"; - -export function SignOutButton() { - return ( -
{ - "use server"; - await signOut({ redirectTo: "/" }); - }} - > - -
- ); -} diff --git a/src/components/SiteHeader.tsx b/src/components/SiteHeader.tsx deleted file mode 100644 index 08ffe77..0000000 --- a/src/components/SiteHeader.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Header global affiché sur toutes les pages PUBLIQUES (hors /admin qui a son - * propre shell). Charge la session côté serveur pour adapter les liens. - */ - -import Link from "next/link"; - -import { auth } from "@/auth"; -import { UserRole } from "@/generated/prisma/enums"; - -import { SignOutButton } from "./SignOutButton"; - -export async function SiteHeader() { - const session = await auth(); - const u = session?.user; - const isAdmin = u?.role === UserRole.ADMIN; - const isOwner = u?.role === UserRole.OWNER || isAdmin; - - return ( -
-
- - - K - - Karbé - - - - -
- {u ? ( - <> - - Favoris - - - Mes réservations - - - Mon compte - - {isOwner ? ( - - Espace hôte - - ) : null} - {isAdmin ? ( - - Admin - - ) : null} - - {u.name || u.email} - - - - ) : ( - <> - - Connexion - - - Créer un compte - - - )} -
-
-
- ); -} diff --git a/src/components/SiteHeaderGuard.tsx b/src/components/SiteHeaderGuard.tsx deleted file mode 100644 index b1530d1..0000000 --- a/src/components/SiteHeaderGuard.tsx +++ /dev/null @@ -1,28 +0,0 @@ -/** - * N'affiche le SiteHeader QUE sur les pages publiques. - * Sur /admin, le shell admin a déjà sa propre TopBar + Sidebar. - * Sur /connexion et /inscription, on garde la page nue. - */ - -import { headers } from "next/headers"; - -import { SiteHeader } from "./SiteHeader"; - -export async function SiteHeaderGuard() { - const h = await headers(); - // Next.js 16 expose le pathname via le header x-pathname si on l'a posé, - // sinon on retombe sur next-url ou referer. On utilise une heuristique simple : - // pathname depuis x-invoke-path (Next internal) ou x-next-url-path-prefix. - const pathname = - h.get("x-pathname") ?? - h.get("x-invoke-path") ?? - h.get("next-url") ?? - ""; - - if (pathname.startsWith("/admin")) return null; - if (pathname === "/connexion" || pathname === "/inscription") return null; - // Mode immersif : on cache le header pour donner 100dvh aux Reels - if (pathname === "/decouvrir" || pathname.startsWith("/decouvrir/")) return null; - - return ; -} diff --git a/src/components/admin/CommandPalette.tsx b/src/components/admin/CommandPalette.tsx index 2d395da..16f82dd 100644 --- a/src/components/admin/CommandPalette.tsx +++ b/src/components/admin/CommandPalette.tsx @@ -50,15 +50,12 @@ export function CommandPalette() { }, []); useEffect(() => { - if (!open) return; - // Différé via microtask pour éviter le warning "Calling setState synchronously - // within an effect can trigger cascading renders" (react-hooks/purity). - queueMicrotask(() => { + if (open) { setQuery(""); setHits([]); setSelected(0); setTimeout(() => inputRef.current?.focus(), 50); - }); + } }, [open]); const runSearch = useCallback(async (q: string) => { diff --git a/src/components/admin/TopBar.tsx b/src/components/admin/TopBar.tsx index 339ef48..e06f7c0 100644 --- a/src/components/admin/TopBar.tsx +++ b/src/components/admin/TopBar.tsx @@ -1,22 +1,12 @@ "use client"; -import { useSyncExternalStore } from "react"; - -function subscribe() { - // navigator.userAgent ne change pas durant la session, pas d'abonnement réel. - return () => {}; -} - -function getSnapshot(): boolean { - return navigator.userAgent.includes("Mac"); -} - -function getServerSnapshot(): boolean { - return false; -} +import { useEffect, useState } from "react"; export function TopBar({ userEmail }: { userEmail: string }) { - const isMac = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); + const [isMac, setIsMac] = useState(false); + useEffect(() => { + setIsMac(navigator.userAgent.includes("Mac")); + }, []); return (
diff --git a/src/lib/admin/carbets.ts b/src/lib/admin/carbets.ts index bf773e2..b6e1a92 100644 --- a/src/lib/admin/carbets.ts +++ b/src/lib/admin/carbets.ts @@ -13,7 +13,6 @@ export type AdminCarbetListItem = { title: string; river: string; capacity: number; - nightlyPrice: string; status: CarbetStatus; accessType: AccessType; ownerName: string; @@ -53,7 +52,6 @@ export async function listCarbetsAdmin(filters: AdminCarbetFilters = {}): Promis title: true, river: true, capacity: true, - nightlyPrice: true, status: true, accessType: true, updatedAt: true, @@ -68,7 +66,6 @@ export async function listCarbetsAdmin(filters: AdminCarbetFilters = {}): Promis title: r.title, river: r.river, capacity: r.capacity, - nightlyPrice: r.nightlyPrice.toString(), status: r.status, accessType: r.accessType, ownerName: `${r.owner.firstName} ${r.owner.lastName}`.trim() || r.owner.email, diff --git a/src/lib/carbet-public.ts b/src/lib/carbet-public.ts index c09b2fb..82ed693 100644 --- a/src/lib/carbet-public.ts +++ b/src/lib/carbet-public.ts @@ -27,11 +27,6 @@ export type PublicCarbetDetail = { accessType: AccessType; roadAccessNote: string | null; capacity: number; - nightlyPrice: string; - roadAccess: import("@/generated/prisma/enums").RoadAccess | null; - electricity: import("@/generated/prisma/enums").Electricity | null; - gsmAtCarbet: boolean; - gsmExitDistanceKm: number | null; minStayNights: number | null; maxStayNights: number | null; minCapacity: number | null; @@ -65,11 +60,6 @@ export const getPublicCarbet = cache( accessType: true, roadAccessNote: true, capacity: true, - nightlyPrice: true, - roadAccess: true, - electricity: true, - gsmAtCarbet: true, - gsmExitDistanceKm: true, minStayNights: true, maxStayNights: true, minCapacity: true, @@ -120,11 +110,6 @@ export const getPublicCarbet = cache( accessType: carbet.accessType, roadAccessNote: carbet.roadAccessNote, capacity: carbet.capacity, - nightlyPrice: carbet.nightlyPrice.toString(), - roadAccess: carbet.roadAccess, - electricity: carbet.electricity, - gsmAtCarbet: carbet.gsmAtCarbet, - gsmExitDistanceKm: carbet.gsmExitDistanceKm !== null ? Number(carbet.gsmExitDistanceKm) : null, minStayNights: carbet.minStayNights, maxStayNights: carbet.maxStayNights, minCapacity: carbet.minCapacity, diff --git a/src/lib/carbet-search.ts b/src/lib/carbet-search.ts index cd53126..0f25da3 100644 --- a/src/lib/carbet-search.ts +++ b/src/lib/carbet-search.ts @@ -5,8 +5,6 @@ import { AvailabilityBlockReason, AvailabilityScope, CarbetStatus, - Electricity, - RoadAccess, } from "@/generated/prisma/enums"; import { getCarbetReviewStatsMany } from "@/lib/reviews-server"; @@ -15,16 +13,9 @@ export type CarbetSearchFilters = { startDate?: Date; endDate?: Date; capacity?: number; - capacityMax?: number; + // Filtre plugin access-type : si "river-only" exclu, on garde uniquement + // ROAD_AND_RIVER. Si "all" ou non spécifié, tout passe. accessibility?: "road-only" | "all"; - priceMax?: number; - amenities?: string[]; - /** Niveaux d'accès route acceptés (multi). */ - roadAccess?: RoadAccess[]; - /** Niveaux d'électricité acceptés (multi). */ - electricity?: Electricity[]; - /** Distance max en km pour atteindre le réseau GSM. 0 = exige le réseau au carbet. */ - gsmMaxKm?: number; }; export type RawSearchParams = { @@ -78,63 +69,6 @@ export function parseSearchFilters( filters.accessibility = accessibility; } - const capacityMaxRaw = pickString(searchParams.capacityMax); - if (capacityMaxRaw) { - const cmax = Number(capacityMaxRaw); - if (Number.isInteger(cmax) && cmax > 0 && cmax <= 100) filters.capacityMax = cmax; - } - - const roadRaw = searchParams.roadAccess; - if (roadRaw) { - const arr = Array.isArray(roadRaw) ? roadRaw : [roadRaw]; - const keys = arr - .flatMap((s) => s.split(",")) - .map((s) => s.trim()) - .filter((s): s is RoadAccess => - s === RoadAccess.NONE || s === RoadAccess.DRY_SEASON_ONLY || s === RoadAccess.ALL_YEAR, - ); - if (keys.length > 0) filters.roadAccess = Array.from(new Set(keys)); - } - - const elecRaw = searchParams.electricity; - if (elecRaw) { - const arr = Array.isArray(elecRaw) ? elecRaw : [elecRaw]; - const keys = arr - .flatMap((s) => s.split(",")) - .map((s) => s.trim()) - .filter((s): s is Electricity => - s === Electricity.NONE || - s === Electricity.SOLAR || - s === Electricity.GENERATOR_READY || - s === Electricity.EDF, - ); - if (keys.length > 0) filters.electricity = Array.from(new Set(keys)); - } - - const gsmMaxRaw = pickString(searchParams.gsmMaxKm); - if (gsmMaxRaw) { - const km = Number(gsmMaxRaw); - if (Number.isFinite(km) && km >= 0 && km <= 50) filters.gsmMaxKm = km; - } - - const priceMaxRaw = pickString(searchParams.priceMax); - if (priceMaxRaw) { - const priceMax = Number(priceMaxRaw); - if (Number.isFinite(priceMax) && priceMax > 0 && priceMax <= 10000) { - filters.priceMax = priceMax; - } - } - - const amenitiesRaw = searchParams.amenities; - if (amenitiesRaw) { - const arr = Array.isArray(amenitiesRaw) ? amenitiesRaw : [amenitiesRaw]; - const keys = arr - .flatMap((s) => s.split(",")) - .map((s) => s.trim()) - .filter((s) => /^[a-z0-9-]{1,40}$/.test(s)); - if (keys.length > 0) filters.amenities = keys.slice(0, 10); - } - return filters; } @@ -156,13 +90,6 @@ export type CarbetSearchResult = { mediaCount: number; reviewCount: number; averageRating: number | null; - nightlyPrice: string; - latitude: number; - longitude: number; - roadAccess: RoadAccess | null; - electricity: Electricity | null; - gsmAtCarbet: boolean; - gsmExitDistanceKm: number | null; }; // Build the Prisma where-clause for a public carbet search. A carbet is only @@ -177,46 +104,14 @@ function buildWhere(filters: CarbetSearchFilters): Prisma.CarbetWhereInput { where.river = { contains: filters.river, mode: "insensitive" }; } - if (filters.capacity || filters.capacityMax) { - where.capacity = {}; - if (filters.capacity) where.capacity.gte = filters.capacity; - if (filters.capacityMax) where.capacity.lte = filters.capacityMax; - } - - if (filters.roadAccess && filters.roadAccess.length > 0) { - where.roadAccess = { in: filters.roadAccess }; - } - - if (filters.electricity && filters.electricity.length > 0) { - where.electricity = { in: filters.electricity }; - } - - if (filters.gsmMaxKm !== undefined) { - if (filters.gsmMaxKm === 0) { - where.gsmAtCarbet = true; - } else { - where.OR = [ - ...(where.OR ?? []), - { gsmAtCarbet: true }, - { gsmExitDistanceKm: { lte: filters.gsmMaxKm } }, - ]; - } + if (filters.capacity) { + where.capacity = { gte: filters.capacity }; } if (filters.accessibility === "road-only") { where.accessType = AccessType.ROAD_AND_RIVER; } - if (filters.priceMax !== undefined) { - where.nightlyPrice = { lte: filters.priceMax }; - } - - if (filters.amenities && filters.amenities.length > 0) { - where.AND = filters.amenities.map((key) => ({ - amenities: { some: { amenity: { key } } }, - })); - } - if (filters.startDate && filters.endDate) { where.availabilities = { some: { @@ -254,13 +149,6 @@ export async function searchCarbets( maxStayNights: true, minCapacity: true, description: true, - roadAccess: true, - electricity: true, - gsmAtCarbet: true, - gsmExitDistanceKm: true, - nightlyPrice: true, - latitude: true, - longitude: true, media: { orderBy: { sortOrder: "asc" }, take: 1, @@ -295,13 +183,6 @@ export async function searchCarbets( mediaCount: carbet._count.media, reviewCount: stats.count, averageRating: stats.averageRating, - nightlyPrice: carbet.nightlyPrice.toString(), - latitude: Number(carbet.latitude), - longitude: Number(carbet.longitude), - roadAccess: carbet.roadAccess, - electricity: carbet.electricity, - gsmAtCarbet: carbet.gsmAtCarbet, - gsmExitDistanceKm: carbet.gsmExitDistanceKm !== null ? Number(carbet.gsmExitDistanceKm) : null, }; }); } diff --git a/src/lib/email.ts b/src/lib/email.ts deleted file mode 100644 index 4652796..0000000 --- a/src/lib/email.ts +++ /dev/null @@ -1,224 +0,0 @@ -/** - * Service email — Resend si `RESEND_API_KEY` est configuré, sinon log console. - * - * Le code consommateur ne doit jamais bloquer ni jeter d'erreur sur un échec - * d'envoi (best-effort, le booking est l'action principale). - */ - -import "server-only"; - -let resendClient: import("resend").Resend | null | undefined; - -async function getResend(): Promise { - if (resendClient !== undefined) return resendClient; - const key = process.env.RESEND_API_KEY?.trim(); - if (!key) { - resendClient = null; - return null; - } - try { - const { Resend } = await import("resend"); - resendClient = new Resend(key); - return resendClient; - } catch (e) { - console.error("[email] resend init failed:", e instanceof Error ? e.message : e); - resendClient = null; - return null; - } -} - -export type EmailOpts = { - to: string | string[]; - subject: string; - html: string; - text?: string; - replyTo?: string; -}; - -const DEFAULT_FROM = process.env.RESEND_FROM ?? "Karbé "; - -export async function sendEmail(opts: EmailOpts): Promise<{ ok: boolean; id?: string; reason?: string }> { - const client = await getResend(); - if (!client) { - console.log( - "[email] dry-run (no RESEND_API_KEY):", - JSON.stringify({ to: opts.to, subject: opts.subject }), - ); - return { ok: true, reason: "dry-run" }; - } - try { - const { data, error } = await client.emails.send({ - from: DEFAULT_FROM, - to: Array.isArray(opts.to) ? opts.to : [opts.to], - subject: opts.subject, - html: opts.html, - text: opts.text, - replyTo: opts.replyTo, - }); - if (error) { - console.error("[email] resend error:", error); - return { ok: false, reason: error.message }; - } - return { ok: true, id: data?.id }; - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - console.error("[email] send failed:", msg); - return { ok: false, reason: msg }; - } -} - -// ---------- Templates ---------- - -const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://karbe.cosmolan.fr"; - -const baseStyle = ` - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - color: #18181b; - max-width: 580px; - margin: 0 auto; - padding: 24px; - line-height: 1.5; -`; - -function wrap(title: string, content: string): string { - return ` -
-

${title}

- ${content} -
-

- Karbé · ${SITE_URL}
- Cet email a été envoyé suite à une action sur votre compte. Si ce n'est pas vous, ignorez-le. -

-
- `; -} - -export async function sendSignupWelcome(to: string, firstName: string): Promise { - await sendEmail({ - to, - subject: "Bienvenue sur Karbé", - html: wrap( - `Bienvenue ${firstName} !`, - `

Votre compte Karbé est créé. Vous pouvez désormais réserver un séjour ou, si vous êtes hôte, publier votre carbet.

-

Découvrir les carbets

`, - ), - text: `Bienvenue ${firstName} ! Votre compte Karbé est créé. ${SITE_URL}/carbets`, - }); -} - -export async function sendBookingRequestToTenant( - to: string, - firstName: string, - bookingId: string, - carbetTitle: string, - startDate: Date, - endDate: Date, - amount: string, - currency: string, -): Promise { - const fmt = (d: Date) => - new Intl.DateTimeFormat("fr-FR", { day: "2-digit", month: "long", year: "numeric" }).format(d); - await sendEmail({ - to, - subject: `Demande de réservation enregistrée — ${carbetTitle}`, - html: wrap( - "Demande de réservation envoyée", - `

Bonjour ${firstName},

-

Votre demande de réservation pour ${carbetTitle} a bien été enregistrée :

-
    -
  • Arrivée : ${fmt(startDate)}
  • -
  • Départ : ${fmt(endDate)}
  • -
  • Montant : ${Number(amount).toFixed(2)} ${currency}
  • -
-

Vous recevrez un nouvel email dès que l'hôte ou l'équipe Karbé confirmera votre séjour.

-

Voir ma réservation

`, - ), - }); -} - -export async function sendBookingRequestToOwner( - to: string, - ownerFirstName: string, - bookingId: string, - carbetTitle: string, - tenantName: string, - startDate: Date, - endDate: Date, -): Promise { - const fmt = (d: Date) => - new Intl.DateTimeFormat("fr-FR", { day: "2-digit", month: "long", year: "numeric" }).format(d); - await sendEmail({ - to, - subject: `Nouvelle demande de réservation — ${carbetTitle}`, - html: wrap( - "Nouvelle demande à confirmer", - `

Bonjour ${ownerFirstName},

-

${tenantName} souhaite réserver ${carbetTitle} :

-
    -
  • Du ${fmt(startDate)} au ${fmt(endDate)}
  • -
-

Connectez-vous à votre espace hôte pour confirmer ou refuser.

-

Mon espace hôte

`, - ), - }); -} - -export async function sendBookingConfirmed( - to: string, - firstName: string, - bookingId: string, - carbetTitle: string, - startDate: Date, - endDate: Date, -): Promise { - const fmt = (d: Date) => - new Intl.DateTimeFormat("fr-FR", { day: "2-digit", month: "long", year: "numeric" }).format(d); - await sendEmail({ - to, - subject: `Réservation confirmée — ${carbetTitle}`, - html: wrap( - "Votre séjour est confirmé", - `

Bonjour ${firstName},

-

Votre réservation pour ${carbetTitle} du ${fmt(startDate)} au ${fmt(endDate)} est confirmée.

-

Voir ma réservation

`, - ), - }); -} - -export async function sendPasswordReset( - to: string, - resetUrl: string, -): Promise { - await sendEmail({ - to, - subject: "Réinitialisation de votre mot de passe Karbé", - html: wrap( - "Réinitialiser votre mot de passe", - `

Vous avez demandé à réinitialiser votre mot de passe Karbé. Cliquez sur le lien ci-dessous pour choisir un nouveau mot de passe (valable 1 heure) :

-

Réinitialiser mon mot de passe

-

Si vous n'avez pas fait cette demande, ignorez simplement cet email — votre mot de passe ne change pas.

`, - ), - text: `Réinitialiser votre mot de passe Karbé : ${resetUrl} (valable 1h).`, - }); -} - -export async function sendBookingRefunded( - to: string, - firstName: string, - bookingId: string, - carbetTitle: string, - amount: string, - currency: string, -): Promise { - await sendEmail({ - to, - subject: `Remboursement traité — ${carbetTitle}`, - html: wrap( - "Remboursement en cours", - `

Bonjour ${firstName},

-

Votre réservation pour ${carbetTitle} a été annulée et le remboursement de ${Number(amount).toFixed(2)} ${currency} est en cours de traitement par Stripe (3 à 5 jours ouvrés).

-

Détails de la réservation

`, - ), - }); -} diff --git a/src/lib/host-dashboard.ts b/src/lib/host-dashboard.ts deleted file mode 100644 index 586ff65..0000000 --- a/src/lib/host-dashboard.ts +++ /dev/null @@ -1,203 +0,0 @@ -import "server-only"; - -import { BookingStatus, PaymentStatus, UserRole } from "@/generated/prisma/enums"; -import { prisma } from "@/lib/prisma"; - -export type HostKpis = { - revenueTotal: string; - revenue30d: string; - revenue365d: string; - bookingsPending: number; - bookingsConfirmedUpcoming: number; - bookingsTotal: number; - carbetsCount: number; - carbetsPublished: number; - occupancyRate30d: number; // 0..1 - nextArrival: { id: string; carbetTitle: string; startDate: Date; tenantName: string } | null; -}; - -type Scope = { ownerId: string; isAdmin: boolean }; - -function scopeWhere(scope: Scope) { - return scope.isAdmin ? {} : { carbet: { ownerId: scope.ownerId } }; -} - -function carbetWhere(scope: Scope) { - return scope.isAdmin ? {} : { ownerId: scope.ownerId }; -} - -export async function getHostKpis(scope: Scope): Promise { - const now = new Date(); - const last30 = new Date(now.getTime() - 30 * 86_400_000); - const last365 = new Date(now.getTime() - 365 * 86_400_000); - - const [revAll, rev30, rev365, pending, upcomingConfirmed, total, carbetsTotal, carbetsPub, nextArrival] = - await Promise.all([ - prisma.booking.aggregate({ - where: { - ...scopeWhere(scope), - status: { in: [BookingStatus.CONFIRMED, BookingStatus.COMPLETED] }, - paymentStatus: { in: [PaymentStatus.SUCCEEDED, PaymentStatus.AUTHORIZED] }, - }, - _sum: { amount: true }, - }), - prisma.booking.aggregate({ - where: { - ...scopeWhere(scope), - status: { in: [BookingStatus.CONFIRMED, BookingStatus.COMPLETED] }, - paymentStatus: { in: [PaymentStatus.SUCCEEDED, PaymentStatus.AUTHORIZED] }, - createdAt: { gte: last30 }, - }, - _sum: { amount: true }, - }), - prisma.booking.aggregate({ - where: { - ...scopeWhere(scope), - status: { in: [BookingStatus.CONFIRMED, BookingStatus.COMPLETED] }, - paymentStatus: { in: [PaymentStatus.SUCCEEDED, PaymentStatus.AUTHORIZED] }, - createdAt: { gte: last365 }, - }, - _sum: { amount: true }, - }), - prisma.booking.count({ - where: { ...scopeWhere(scope), status: BookingStatus.PENDING }, - }), - prisma.booking.count({ - where: { - ...scopeWhere(scope), - status: BookingStatus.CONFIRMED, - startDate: { gte: now }, - }, - }), - prisma.booking.count({ where: scopeWhere(scope) }), - prisma.carbet.count({ where: carbetWhere(scope) }), - prisma.carbet.count({ where: { ...carbetWhere(scope), status: "PUBLISHED" } }), - prisma.booking.findFirst({ - where: { - ...scopeWhere(scope), - status: BookingStatus.CONFIRMED, - startDate: { gte: now }, - }, - orderBy: { startDate: "asc" }, - select: { - id: true, - startDate: true, - carbet: { select: { title: true } }, - tenant: { select: { firstName: true, lastName: true } }, - }, - }), - ]); - - // Taux d'occupation 30j : nuits réservées / (carbets publiés × 30) - const occupiedNights = await prisma.booking.findMany({ - where: { - ...scopeWhere(scope), - status: { in: [BookingStatus.CONFIRMED, BookingStatus.COMPLETED] }, - startDate: { lt: now }, - endDate: { gte: last30 }, - }, - select: { startDate: true, endDate: true }, - }); - let totalNightsOccupied = 0; - for (const b of occupiedNights) { - const s = Math.max(b.startDate.getTime(), last30.getTime()); - const e = Math.min(b.endDate.getTime(), now.getTime()); - if (e > s) totalNightsOccupied += Math.floor((e - s) / 86_400_000); - } - const denom = Math.max(1, carbetsPub * 30); - const occupancyRate30d = Math.min(1, totalNightsOccupied / denom); - - return { - revenueTotal: (revAll._sum.amount ?? 0).toString(), - revenue30d: (rev30._sum.amount ?? 0).toString(), - revenue365d: (rev365._sum.amount ?? 0).toString(), - bookingsPending: pending, - bookingsConfirmedUpcoming: upcomingConfirmed, - bookingsTotal: total, - carbetsCount: carbetsTotal, - carbetsPublished: carbetsPub, - occupancyRate30d, - nextArrival: nextArrival - ? { - id: nextArrival.id, - carbetTitle: nextArrival.carbet.title, - startDate: nextArrival.startDate, - tenantName: `${nextArrival.tenant.firstName} ${nextArrival.tenant.lastName}`.trim(), - } - : null, - }; -} - -export type HostRecentBooking = { - id: string; - carbetId: string; - carbetTitle: string; - carbetSlug: string; - tenantName: string; - startDate: Date; - endDate: Date; - guestCount: number; - status: BookingStatus; - paymentStatus: PaymentStatus; - amount: string; - currency: string; -}; - -export async function listHostRecentBookings( - scope: Scope, - limit = 10, -): Promise { - const rows = await prisma.booking.findMany({ - where: scopeWhere(scope), - orderBy: [{ status: "asc" }, { createdAt: "desc" }], - take: limit, - select: { - id: true, - startDate: true, - endDate: true, - guestCount: true, - status: true, - paymentStatus: true, - amount: true, - currency: true, - carbet: { select: { id: true, title: true, slug: true } }, - tenant: { select: { firstName: true, lastName: true } }, - }, - }); - return rows.map((r) => ({ - id: r.id, - carbetId: r.carbet.id, - carbetTitle: r.carbet.title, - carbetSlug: r.carbet.slug, - tenantName: `${r.tenant.firstName} ${r.tenant.lastName}`.trim(), - startDate: r.startDate, - endDate: r.endDate, - guestCount: r.guestCount, - status: r.status, - paymentStatus: r.paymentStatus, - amount: r.amount.toString(), - currency: r.currency, - })); -} - -export async function listHostCarbets(scope: Scope) { - const rows = await prisma.carbet.findMany({ - where: carbetWhere(scope), - orderBy: [{ updatedAt: "desc" }], - select: { - id: true, - slug: true, - title: true, - status: true, - nightlyPrice: true, - capacity: true, - river: true, - _count: { select: { bookings: true, reviews: true, media: true } }, - }, - }); - return rows.map((r) => ({ ...r, nightlyPrice: r.nightlyPrice.toString() })); -} - -export function isScopeAdmin(role: UserRole | string | undefined): boolean { - return role === UserRole.ADMIN; -} diff --git a/src/lib/image-variants.ts b/src/lib/image-variants.ts deleted file mode 100644 index 5d0e22a..0000000 --- a/src/lib/image-variants.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Variantes responsive : génération + URL helpers. - * - * Convention de nommage : .jpg -> -320.jpg, -800.jpg, -1600.jpg. - * Le format est forcé à JPEG pour les variantes (compression efficace, - * supporté partout). L'original reste tel quel (PNG/WebP/AVIF préservés). - * - * Helper côté client : variantUrl(originalUrl, width) → URL de la variante. - * Le browser fait le fallback automatiquement via srcset si la variante 404. - */ - -export const VARIANT_WIDTHS = [320, 800, 1600] as const; -export type VariantWidth = (typeof VARIANT_WIDTHS)[number]; - -/** Calcule l'URL d'une variante depuis l'URL originale. */ -export function variantUrl(originalUrl: string, width: VariantWidth): string { - const lastDot = originalUrl.lastIndexOf("."); - if (lastDot === -1) return originalUrl; - const base = originalUrl.slice(0, lastDot); - return `${base}-${width}.jpg`; -} - -/** Calcule la s3Key d'une variante depuis la s3Key originale. */ -export function variantS3Key(originalKey: string, width: VariantWidth): string { - const lastDot = originalKey.lastIndexOf("."); - if (lastDot === -1) return originalKey; - const base = originalKey.slice(0, lastDot); - return `${base}-${width}.jpg`; -} - -/** - * srcSet attribut pour un ``. Le browser pick la meilleure variante - * selon viewport+DPR. Si une variante 404, srcset fallback en cascade ; - * on ajoute toujours l'original comme dernière entrée pour garantir - * qu'au moins UNE source fonctionne. - */ -export function buildSrcSet(originalUrl: string): string { - return VARIANT_WIDTHS.map((w) => `${variantUrl(originalUrl, w)} ${w}w`) - .concat([`${originalUrl} 2000w`]) - .join(", "); -} diff --git a/src/lib/password-reset.ts b/src/lib/password-reset.ts deleted file mode 100644 index 31959da..0000000 --- a/src/lib/password-reset.ts +++ /dev/null @@ -1,51 +0,0 @@ -import "server-only"; - -import crypto from "node:crypto"; - -import { prisma } from "@/lib/prisma"; -import { hashPassword } from "@/lib/password"; - -const TOKEN_TTL_MS = 60 * 60 * 1000; // 1 hour - -function hashToken(token: string): string { - return crypto.createHash("sha256").update(token).digest("hex"); -} - -/** Crée un token (renvoie la version *plain* à mettre dans l'URL email). */ -export async function createPasswordResetToken(userId: string): Promise { - const token = crypto.randomBytes(32).toString("base64url"); - const tokenHash = hashToken(token); - const expiresAt = new Date(Date.now() + TOKEN_TTL_MS); - await prisma.passwordResetToken.create({ - data: { tokenHash, userId, expiresAt }, - }); - return token; -} - -/** Vérifie un token plain → renvoie userId si valide & non expiré. */ -export async function consumePasswordResetToken( - plainToken: string, - newPassword: string, -): Promise<{ ok: true; userId: string } | { ok: false; reason: string }> { - const tokenHash = hashToken(plainToken); - const row = await prisma.passwordResetToken.findUnique({ where: { tokenHash } }); - if (!row) return { ok: false, reason: "Lien invalide." }; - if (row.expiresAt < new Date()) { - await prisma.passwordResetToken.delete({ where: { tokenHash } }).catch(() => {}); - return { ok: false, reason: "Lien expiré." }; - } - const passwordHash = await hashPassword(newPassword); - await prisma.$transaction([ - prisma.user.update({ where: { id: row.userId }, data: { passwordHash } }), - prisma.passwordResetToken.deleteMany({ where: { userId: row.userId } }), - ]); - return { ok: true, userId: row.userId }; -} - -/** Cleanup utility — peut être lancé par un cron. */ -export async function purgeExpiredResetTokens(): Promise { - const result = await prisma.passwordResetToken.deleteMany({ - where: { expiresAt: { lt: new Date() } }, - }); - return result.count; -} diff --git a/src/lib/rate-limit.ts b/src/lib/rate-limit.ts deleted file mode 100644 index 41c27f1..0000000 --- a/src/lib/rate-limit.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Token-bucket en mémoire — best-effort par instance. - * - * Pour un déploiement multi-instance, swap pour un store partagé (Redis). - * Ici on tourne en mono-instance Next derrière nginx-proxy-manager, donc - * une Map locale suffit. - * - * Usage : - * const r = await rateLimit({ key: ip + ":signup", windowMs: 60_000, limit: 5 }); - * if (!r.ok) return tooManyRequests(r.retryAfter); - */ - -type Bucket = { - count: number; - resetAt: number; -}; - -const buckets = new Map(); - -const SWEEP_INTERVAL_MS = 60_000; -let lastSweep = 0; - -function sweep(now: number) { - if (now - lastSweep < SWEEP_INTERVAL_MS) return; - lastSweep = now; - for (const [k, b] of buckets) { - if (b.resetAt <= now) buckets.delete(k); - } -} - -export type RateLimitOpts = { - key: string; - /** Fenêtre glissante en ms. */ - windowMs: number; - /** Nombre max d'appels par fenêtre. */ - limit: number; -}; - -export type RateLimitResult = { - ok: boolean; - remaining: number; - retryAfter: number; // secondes -}; - -export function rateLimit(opts: RateLimitOpts): RateLimitResult { - const now = Date.now(); - sweep(now); - - const b = buckets.get(opts.key); - if (!b || b.resetAt <= now) { - buckets.set(opts.key, { count: 1, resetAt: now + opts.windowMs }); - return { ok: true, remaining: opts.limit - 1, retryAfter: 0 }; - } - - if (b.count >= opts.limit) { - return { - ok: false, - remaining: 0, - retryAfter: Math.max(1, Math.ceil((b.resetAt - now) / 1000)), - }; - } - - b.count++; - return { ok: true, remaining: opts.limit - b.count, retryAfter: 0 }; -} - -/** Extract a client IP from a request, fallback to a safe default. */ -export function getClientIp(req: Request): string { - // nginx-proxy-manager pose x-forwarded-for, x-real-ip - const xff = req.headers.get("x-forwarded-for"); - if (xff) return xff.split(",")[0].trim(); - const xri = req.headers.get("x-real-ip"); - if (xri) return xri.trim(); - return "unknown"; -} - -/** Helper pratique : extract IP + applique le bucket. */ -export function rateLimitRequest( - req: Request, - bucket: string, - windowMs: number, - limit: number, -): RateLimitResult { - const ip = getClientIp(req); - return rateLimit({ key: `${ip}:${bucket}`, windowMs, limit }); -} diff --git a/src/lib/reels.ts b/src/lib/reels.ts deleted file mode 100644 index 6ac0033..0000000 --- a/src/lib/reels.ts +++ /dev/null @@ -1,127 +0,0 @@ -import "server-only"; - -import { CarbetStatus } from "@/generated/prisma/enums"; -import { prisma } from "@/lib/prisma"; - -export type ReelMedia = { - id: string; - type: "PHOTO" | "VIDEO"; - url: string; -}; - -export type ReelCarbet = { - id: string; - slug: string; - title: string; - river: string; - embarkPoint: string; - capacity: number; - nightlyPrice: string; - ownerFirstName: string; - averageRating: number | null; - reviewCount: number; - media: ReelMedia[]; -}; - -export async function listReelCarbets(opts: { take?: number } = {}): Promise { - const take = opts.take ?? 30; - const rows = await prisma.carbet.findMany({ - where: { - status: CarbetStatus.PUBLISHED, - media: { some: {} }, // au moins 1 média - }, - orderBy: [{ lastBookedAt: { sort: "desc", nulls: "last" } }, { updatedAt: "desc" }], - take, - select: { - id: true, - slug: true, - title: true, - river: true, - embarkPoint: true, - capacity: true, - nightlyPrice: true, - owner: { select: { firstName: true } }, - media: { - orderBy: { sortOrder: "asc" }, - select: { id: true, type: true, s3Url: true }, - }, - reviews: { select: { rating: true } }, - }, - }); - - return rows.map((c) => { - const ratings = c.reviews.map((r) => r.rating); - const avg = ratings.length === 0 ? null : ratings.reduce((a, b) => a + b, 0) / ratings.length; - return { - id: c.id, - slug: c.slug, - title: c.title, - river: c.river, - embarkPoint: c.embarkPoint, - capacity: c.capacity, - nightlyPrice: c.nightlyPrice.toString(), - ownerFirstName: c.owner.firstName, - averageRating: avg, - reviewCount: ratings.length, - media: c.media.map((m) => ({ - id: m.id, - type: m.type as "PHOTO" | "VIDEO", - url: m.s3Url, - })), - }; - }); -} - -export async function listFavoriteCarbets(userId: string): Promise { - const favs = await prisma.favorite.findMany({ - where: { userId }, - select: { carbetId: true }, - orderBy: { createdAt: "desc" }, - }); - if (favs.length === 0) return []; - const ids = favs.map((f) => f.carbetId); - const rows = await prisma.carbet.findMany({ - where: { id: { in: ids }, status: CarbetStatus.PUBLISHED }, - select: { - id: true, - slug: true, - title: true, - river: true, - embarkPoint: true, - capacity: true, - nightlyPrice: true, - owner: { select: { firstName: true } }, - media: { - orderBy: { sortOrder: "asc" }, - select: { id: true, type: true, s3Url: true }, - }, - reviews: { select: { rating: true } }, - }, - }); - // Respecter l'ordre des favoris (le plus récent en premier) - const byId = new Map(rows.map((r) => [r.id, r])); - return ids - .map((id) => byId.get(id)) - .filter((r): r is NonNullable => Boolean(r)) - .map((c) => { - const ratings = c.reviews.map((r) => r.rating); - const avg = ratings.length === 0 ? null : ratings.reduce((a, b) => a + b, 0) / ratings.length; - return { - id: c.id, - slug: c.slug, - title: c.title, - river: c.river, - embarkPoint: c.embarkPoint, - capacity: c.capacity, - nightlyPrice: c.nightlyPrice.toString(), - ownerFirstName: c.owner.firstName, - averageRating: avg, - reviewCount: ratings.length, - media: c.media.map((m) => ({ - id: m.id, - type: m.type as "PHOTO" | "VIDEO", - url: m.s3Url, - })), - }; - }); -} diff --git a/src/lib/scheduled.ts b/src/lib/scheduled.ts deleted file mode 100644 index d3272f8..0000000 --- a/src/lib/scheduled.ts +++ /dev/null @@ -1,111 +0,0 @@ -/** - * Tâches planifiées exécutables via /api/cron/run/[task] avec le secret - * CRON_TOKEN. Idempotents, retournent un compteur d'actions. - */ - -import "server-only"; - -import { BookingStatus, MediaType } from "@/generated/prisma/enums"; -import { prisma } from "@/lib/prisma"; -import { recordAudit } from "@/lib/admin/audit"; -import { purgeExpiredResetTokens } from "@/lib/password-reset"; -import { generateImageVariants } from "@/lib/variants-server"; - -const PENDING_TTL_DAYS = 7; - -/** Annule les bookings PENDING créés il y a plus de N jours. */ -export async function autoCancelStalePending(): Promise<{ cancelled: number }> { - const cutoff = new Date(Date.now() - PENDING_TTL_DAYS * 86_400_000); - const stale = await prisma.booking.findMany({ - where: { status: BookingStatus.PENDING, createdAt: { lt: cutoff } }, - select: { id: true }, - }); - if (stale.length === 0) return { cancelled: 0 }; - await prisma.booking.updateMany({ - where: { id: { in: stale.map((s) => s.id) } }, - data: { status: BookingStatus.CANCELLED }, - }); - await recordAudit({ - scope: "cron", - event: "bookings.auto-cancel-stale", - actorEmail: null, - details: { count: stale.length, cutoff: cutoff.toISOString() }, - }); - return { cancelled: stale.length }; -} - -/** Purge les password reset tokens expirés. */ -export async function purgeResetTokens(): Promise<{ purged: number }> { - const count = await purgeExpiredResetTokens(); - if (count > 0) { - await recordAudit({ - scope: "cron", - event: "password.purge-expired-tokens", - actorEmail: null, - details: { count }, - }); - } - return { purged: count }; -} - -/** Logique simple : retourne juste la liste des bookings dont l'arrivée est dans 3 jours. - * L'envoi email réel est branché plus tard quand RESEND_API_KEY est posée. */ -export async function listUpcomingArrivalsInThreeDays() { - const now = new Date(); - const in3 = new Date(now.getTime() + 3 * 86_400_000); - const in4 = new Date(now.getTime() + 4 * 86_400_000); - return prisma.booking.findMany({ - where: { - status: BookingStatus.CONFIRMED, - startDate: { gte: in3, lt: in4 }, - }, - select: { - id: true, - startDate: true, - tenant: { select: { email: true, firstName: true } }, - carbet: { select: { title: true, slug: true } }, - }, - }); -} - -/** Régénère les variantes responsives pour tous les Media PHOTO en base. */ -export async function regenerateAllVariants(): Promise<{ scanned: number; ok: number; skipped: number; failed: number }> { - const medias = await prisma.media.findMany({ - where: { type: MediaType.PHOTO }, - select: { id: true, s3Key: true }, - }); - let ok = 0; - let skipped = 0; - let failed = 0; - for (const m of medias) { - const ext = m.s3Key.split(".").pop()?.toLowerCase(); - if (!ext || !["jpg", "jpeg", "png", "webp", "avif"].includes(ext)) { - skipped++; - continue; - } - const mime = - ext === "png" ? "image/png" : - ext === "webp" ? "image/webp" : - ext === "avif" ? "image/avif" : - "image/jpeg"; - const result = await generateImageVariants({ originalS3Key: m.s3Key, mime }); - if (result.skipped) skipped++; - else if (result.results.every((r) => r.ok)) ok++; - else failed++; - } - await recordAudit({ - scope: "cron", - event: "variants.regenerate-all", - actorEmail: null, - details: { scanned: medias.length, ok, skipped, failed }, - }); - return { scanned: medias.length, ok, skipped, failed }; -} - -export const SCHEDULED_TASKS = { - "auto-cancel-stale-pending": autoCancelStalePending, - "purge-reset-tokens": purgeResetTokens, - "regenerate-variants": regenerateAllVariants, -} as const; - -export type ScheduledTaskName = keyof typeof SCHEDULED_TASKS; diff --git a/src/lib/search-profiles.ts b/src/lib/search-profiles.ts deleted file mode 100644 index cff37da..0000000 --- a/src/lib/search-profiles.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Profils de séjour prédéfinis — chips au-dessus des facettes. - * Chaque profil pose un set de query params qui pré-cochent les filtres. - */ - -import { Electricity, RoadAccess } from "@/generated/prisma/enums"; - -export type SearchProfile = { - id: string; - emoji: string; - label: string; - description: string; - params: Record; -}; - -export const SEARCH_PROFILES: SearchProfile[] = [ - { - id: "deconnexion", - emoji: "🌿", - label: "Déconnexion totale", - description: "Zone blanche, pas d'électricité, accès pirogue, 2-4 personnes.", - params: { - roadAccess: RoadAccess.NONE, - electricity: `${Electricity.NONE},${Electricity.SOLAR}`, - capacityMax: "4", - }, - }, - { - id: "teletravail", - emoji: "💻", - label: "Télétravail nature", - description: "Route, EDF, 4G au carbet — bureau au bord du fleuve.", - params: { - roadAccess: RoadAccess.ALL_YEAR, - electricity: Electricity.EDF, - gsmMaxKm: "0", - }, - }, - { - id: "famille-weekend", - emoji: "🏝️", - label: "Famille week-end", - description: "Route toute saison, électricité, capacité 4-8.", - params: { - roadAccess: RoadAccess.ALL_YEAR, - electricity: `${Electricity.EDF},${Electricity.GENERATOR_READY}`, - capacity: "4", - capacityMax: "8", - }, - }, - { - id: "astreinte", - emoji: "📞", - label: "Astreinte sereine", - description: "Réseau accessible (au max 1 km), EDF, route saison sèche min.", - params: { - gsmMaxKm: "1", - electricity: `${Electricity.EDF},${Electricity.GENERATOR_READY}`, - roadAccess: `${RoadAccess.DRY_SEASON_ONLY},${RoadAccess.ALL_YEAR}`, - }, - }, - { - id: "aventure", - emoji: "🛶", - label: "Aventure expédition", - description: "Accès pirogue uniquement, petit groupe 2-4.", - params: { - roadAccess: RoadAccess.NONE, - capacityMax: "4", - }, - }, -]; - -export function buildProfileUrl(profileId: string): string { - const profile = SEARCH_PROFILES.find((p) => p.id === profileId); - if (!profile) return "/carbets"; - const search = new URLSearchParams(profile.params); - return `/carbets?${search.toString()}`; -} diff --git a/src/lib/stripe.ts b/src/lib/stripe.ts index e0d1ca0..adda277 100644 --- a/src/lib/stripe.ts +++ b/src/lib/stripe.ts @@ -1,13 +1,5 @@ import Stripe from "stripe"; -/** Détecte si Stripe est utilisable (clé posée + pas un placeholder). */ -export function isStripeConfigured(): boolean { - const key = (process.env.STRIPE_SECRET_KEY ?? "").trim(); - if (!key) return false; - if (key.includes("REPLACE_ME") || key.includes("PLACEHOLDER")) return false; - return key.startsWith("sk_test_") || key.startsWith("sk_live_") || key.startsWith("rk_"); -} - let stripeClient: Stripe | null = null; export function getStripeClient(): Stripe { diff --git a/src/lib/uploads.ts b/src/lib/uploads.ts deleted file mode 100644 index 708504a..0000000 --- a/src/lib/uploads.ts +++ /dev/null @@ -1,104 +0,0 @@ -import "server-only"; - -import crypto from "node:crypto"; -import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; -import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; - -const ENDPOINT = process.env.S3_ENDPOINT ?? ""; -const PUBLIC_BASE = process.env.S3_PUBLIC_URL ?? ""; -const BUCKET = process.env.S3_BUCKET ?? ""; -const REGION = process.env.S3_REGION ?? "us-east-1"; -const ACCESS_KEY = process.env.MINIO_ROOT_USER ?? process.env.S3_ACCESS_KEY ?? ""; -const SECRET_KEY = process.env.MINIO_ROOT_PASSWORD ?? process.env.S3_SECRET_KEY ?? ""; - -const PUBLIC_BASE_EXTERNAL = - process.env.S3_PUBLIC_URL_EXTERNAL ?? PUBLIC_BASE; -const ENDPOINT_EXTERNAL = process.env.S3_ENDPOINT_EXTERNAL ?? ENDPOINT; - -const s3Internal = new S3Client({ - endpoint: ENDPOINT, - region: REGION, - forcePathStyle: (process.env.S3_FORCE_PATH_STYLE ?? "false") === "true", - credentials: { accessKeyId: ACCESS_KEY, secretAccessKey: SECRET_KEY }, -}); - -const s3Presign = new S3Client({ - endpoint: ENDPOINT_EXTERNAL, - region: REGION, - forcePathStyle: (process.env.S3_FORCE_PATH_STYLE ?? "false") === "true", - credentials: { accessKeyId: ACCESS_KEY, secretAccessKey: SECRET_KEY }, -}); - -export type PresignResult = { - s3Key: string; - uploadUrl: string; - publicUrl: string; - expiresIn: number; -}; - -const ALLOWED_PHOTO_MIMES = new Set(["image/jpeg", "image/png", "image/webp", "image/avif"]); -const ALLOWED_VIDEO_MIMES = new Set(["video/mp4", "video/quicktime", "video/webm"]); - -export type UploadKind = "photo" | "video"; - -export function classifyMime(mime: string): UploadKind | null { - if (ALLOWED_PHOTO_MIMES.has(mime)) return "photo"; - if (ALLOWED_VIDEO_MIMES.has(mime)) return "video"; - return null; -} - -const MAX_PHOTO = 10 * 1024 * 1024; -const MAX_VIDEO = 200 * 1024 * 1024; - -export function maxBytesFor(kind: UploadKind): number { - return kind === "photo" ? MAX_PHOTO : MAX_VIDEO; -} - -export function extensionFor(mime: string): string { - switch (mime) { - case "image/jpeg": - return "jpg"; - case "image/png": - return "png"; - case "image/webp": - return "webp"; - case "image/avif": - return "avif"; - case "video/mp4": - return "mp4"; - case "video/quicktime": - return "mov"; - case "video/webm": - return "webm"; - default: - return "bin"; - } -} - -export async function presignCarbetUpload(opts: { - carbetId: string; - mime: string; - sizeBytes: number; -}): Promise { - const kind = classifyMime(opts.mime); - if (!kind) return { error: `Type non supporté : ${opts.mime}` }; - const max = maxBytesFor(kind); - if (opts.sizeBytes > max) { - return { error: `Fichier trop volumineux (${Math.round(opts.sizeBytes / 1_000_000)} Mo, max ${Math.round(max / 1_000_000)} Mo).` }; - } - const id = crypto.randomBytes(12).toString("hex"); - const ext = extensionFor(opts.mime); - const s3Key = `carbets/${opts.carbetId}/${Date.now()}-${id}.${ext}`; - - const cmd = new PutObjectCommand({ - Bucket: BUCKET, - Key: s3Key, - ContentType: opts.mime, - }); - const uploadUrl = await getSignedUrl(s3Presign, cmd, { expiresIn: 600 }); - const publicUrl = `${PUBLIC_BASE_EXTERNAL.replace(/\/$/, "")}/${s3Key}`; - return { s3Key, uploadUrl, publicUrl, expiresIn: 600 }; -} - -export { s3Internal }; -export { BUCKET as UPLOAD_BUCKET }; diff --git a/src/lib/variants-server.ts b/src/lib/variants-server.ts deleted file mode 100644 index 06f3177..0000000 --- a/src/lib/variants-server.ts +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Génération de variantes responsive côté serveur (Node). - * - * - Télécharge l'original depuis MinIO via l'endpoint interne. - * - sharp → 3 variantes (320 / 800 / 1600 px de large max, JPEG quality 80). - * - Upload chaque variante avec naming convention -.jpg. - * - Skippe vidéos (sharp ne les traite pas). - * - * Best-effort : si une variante échoue, on log et on continue. L'original - * fonctionne toujours côté front grâce au srcset fallback. - */ - -import "server-only"; - -import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3"; -import type { Readable } from "node:stream"; - -import { VARIANT_WIDTHS, variantS3Key, type VariantWidth } from "./image-variants"; - -const ENDPOINT = process.env.S3_ENDPOINT ?? ""; -const BUCKET = process.env.S3_BUCKET ?? ""; -const REGION = process.env.S3_REGION ?? "us-east-1"; -const ACCESS_KEY = process.env.MINIO_ROOT_USER ?? process.env.S3_ACCESS_KEY ?? ""; -const SECRET_KEY = process.env.MINIO_ROOT_PASSWORD ?? process.env.S3_SECRET_KEY ?? ""; - -const s3 = new S3Client({ - endpoint: ENDPOINT, - region: REGION, - forcePathStyle: (process.env.S3_FORCE_PATH_STYLE ?? "false") === "true", - credentials: { accessKeyId: ACCESS_KEY, secretAccessKey: SECRET_KEY }, -}); - -async function streamToBuffer(stream: Readable | ReadableStream): Promise { - if ("getReader" in stream) { - const reader = stream.getReader(); - const chunks: Uint8Array[] = []; - while (true) { - const { value, done } = await reader.read(); - if (done) break; - if (value) chunks.push(value); - } - return Buffer.concat(chunks); - } - const chunks: Buffer[] = []; - for await (const c of stream as Readable) chunks.push(c as Buffer); - return Buffer.concat(chunks); -} - -export type VariantResult = { - width: VariantWidth; - s3Key: string; - ok: boolean; - reason?: string; -}; - -/** - * Génère les 3 variantes responsives pour une image originale. - * Skip silencieusement si mime === video/*. - */ -export async function generateImageVariants(opts: { - originalS3Key: string; - mime: string; -}): Promise<{ skipped: boolean; results: VariantResult[] }> { - if (opts.mime.startsWith("video/")) { - return { skipped: true, results: [] }; - } - - let sharp: (input: Buffer) => import("sharp").Sharp; - try { - const mod = await import("sharp"); - sharp = (mod as unknown as { default: (input: Buffer) => import("sharp").Sharp }).default; - } catch { - return { skipped: true, results: [] }; - } - - // 1. Download original - let originalBuffer: Buffer; - try { - const get = await s3.send(new GetObjectCommand({ Bucket: BUCKET, Key: opts.originalS3Key })); - if (!get.Body) throw new Error("Empty body"); - originalBuffer = await streamToBuffer(get.Body as Readable); - } catch (e) { - return { - skipped: false, - results: VARIANT_WIDTHS.map((w) => ({ - width: w, - s3Key: variantS3Key(opts.originalS3Key, w), - ok: false, - reason: e instanceof Error ? e.message : "download failed", - })), - }; - } - - // 2. Variantes en parallèle - const results = await Promise.all( - VARIANT_WIDTHS.map(async (w): Promise => { - const targetKey = variantS3Key(opts.originalS3Key, w); - try { - const buf = await sharp(originalBuffer) - .rotate() // respecte l'EXIF orientation - .resize({ width: w, withoutEnlargement: true }) - .jpeg({ quality: 80, progressive: true, mozjpeg: true }) - .toBuffer(); - await s3.send( - new PutObjectCommand({ - Bucket: BUCKET, - Key: targetKey, - Body: buf, - ContentType: "image/jpeg", - CacheControl: "public, max-age=31536000, immutable", - }), - ); - return { width: w, s3Key: targetKey, ok: true }; - } catch (e) { - return { - width: w, - s3Key: targetKey, - ok: false, - reason: e instanceof Error ? e.message : "resize/upload failed", - }; - } - }), - ); - - return { skipped: false, results }; -} diff --git a/src/middleware.ts b/src/middleware.ts deleted file mode 100644 index db39867..0000000 --- a/src/middleware.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Middleware Karbé. - * - * Pose `x-pathname` sur tous les requests pour que les server components puissent - * lire le path courant via `headers()` (utile pour SiteHeaderGuard qui décide - * de rendre ou non le header global selon /admin vs reste). - */ - -import { NextResponse } from "next/server"; -import type { NextRequest } from "next/server"; - -export function middleware(request: NextRequest) { - const response = NextResponse.next(); - response.headers.set("x-pathname", request.nextUrl.pathname); - return response; -} - -export const config = { - // Exclut les assets statiques + API auth (qu'on ne veut pas modifier). - matcher: [ - "/((?!_next/static|_next/image|favicon.ico|api/auth|api/health|api/metrics).*)", - ], -}; diff --git a/tests/lib/booking.test.ts b/tests/lib/booking.test.ts deleted file mode 100644 index b25c14d..0000000 --- a/tests/lib/booking.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { describe, it, expect } from "vitest"; - -import { - DAY_MS, - enumerateUtcDays, - hasOverlap, - isPublicAllowedByDefaultPolicy, - isWeekendUtcDay, - normalizeUtcDayStart, - parseIsoDate, -} from "@/lib/booking"; - -describe("parseIsoDate", () => { - it("accepts ISO YYYY-MM-DD", () => { - const d = parseIsoDate("2026-06-15"); - expect(d).toBeInstanceOf(Date); - expect(d?.toISOString().startsWith("2026-06-15")).toBe(true); - }); - - it("returns null for garbage input", () => { - expect(parseIsoDate("not a date")).toBeNull(); - expect(parseIsoDate(null)).toBeNull(); - expect(parseIsoDate(undefined)).toBeNull(); - expect(parseIsoDate(123)).toBeNull(); - }); -}); - -describe("normalizeUtcDayStart", () => { - it("zeroes out time components", () => { - const d = new Date("2026-06-15T17:30:45.123Z"); - const n = normalizeUtcDayStart(d); - expect(n.getUTCHours()).toBe(0); - expect(n.getUTCMinutes()).toBe(0); - expect(n.getUTCSeconds()).toBe(0); - expect(n.getUTCMilliseconds()).toBe(0); - expect(n.toISOString().slice(0, 10)).toBe("2026-06-15"); - }); -}); - -describe("hasOverlap", () => { - const d = (iso: string) => new Date(iso); - - it("detects partial overlap", () => { - expect( - hasOverlap(d("2026-06-10"), d("2026-06-15"), d("2026-06-13"), d("2026-06-20")), - ).toBe(true); - }); - - it("returns false for adjacent intervals (touching)", () => { - expect( - hasOverlap(d("2026-06-10"), d("2026-06-15"), d("2026-06-15"), d("2026-06-20")), - ).toBe(false); - }); - - it("returns false for fully separate", () => { - expect( - hasOverlap(d("2026-06-01"), d("2026-06-05"), d("2026-06-10"), d("2026-06-15")), - ).toBe(false); - }); - - it("returns true when one contains the other", () => { - expect( - hasOverlap(d("2026-06-01"), d("2026-06-30"), d("2026-06-10"), d("2026-06-15")), - ).toBe(true); - }); -}); - -describe("enumerateUtcDays", () => { - it("enumerates each day between start and end (exclusive)", () => { - const days = enumerateUtcDays(new Date("2026-06-10"), new Date("2026-06-13")); - expect(days.length).toBe(3); - expect(days[0].toISOString().slice(0, 10)).toBe("2026-06-10"); - expect(days[2].toISOString().slice(0, 10)).toBe("2026-06-12"); - }); - - it("returns empty when start === end", () => { - const days = enumerateUtcDays(new Date("2026-06-10"), new Date("2026-06-10")); - expect(days).toEqual([]); - }); -}); - -describe("isWeekendUtcDay", () => { - it("flags Saturday (2026-06-13)", () => { - expect(isWeekendUtcDay(new Date("2026-06-13"))).toBe(true); - }); - it("flags Sunday (2026-06-14)", () => { - expect(isWeekendUtcDay(new Date("2026-06-14"))).toBe(true); - }); - it("rejects Monday (2026-06-15)", () => { - expect(isWeekendUtcDay(new Date("2026-06-15"))).toBe(false); - }); -}); - -describe("isPublicAllowedByDefaultPolicy", () => { - it("blocks weekends by default (CE-priority policy)", () => { - expect(isPublicAllowedByDefaultPolicy(new Date("2026-06-13"))).toBe(false); - }); - it("allows weekdays", () => { - expect(isPublicAllowedByDefaultPolicy(new Date("2026-06-15"))).toBe(true); - }); -}); - -describe("DAY_MS constant", () => { - it("equals 86_400_000", () => { - expect(DAY_MS).toBe(86_400_000); - }); -}); diff --git a/tests/lib/email.test.ts b/tests/lib/email.test.ts deleted file mode 100644 index f8a965d..0000000 --- a/tests/lib/email.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from "vitest"; - -// Mock server-only avant l'import du module sous test (sinon ça jette). -vi.mock("server-only", () => ({})); - -describe("sendEmail (dry-run sans RESEND_API_KEY)", () => { - beforeEach(() => { - delete process.env.RESEND_API_KEY; - vi.resetModules(); - }); - - it("renvoie ok=true + reason=dry-run quand pas de clé", async () => { - const { sendEmail } = await import("@/lib/email"); - const res = await sendEmail({ - to: "test@example.com", - subject: "hello", - html: "

world

", - }); - expect(res.ok).toBe(true); - expect(res.reason).toBe("dry-run"); - }); - - it("n'écrit pas d'erreur quand pas de clé", async () => { - const errSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - const { sendEmail } = await import("@/lib/email"); - await sendEmail({ to: "x@y.z", subject: "s", html: "h" }); - expect(errSpy).not.toHaveBeenCalled(); - errSpy.mockRestore(); - }); -}); diff --git a/tests/lib/image-variants.test.ts b/tests/lib/image-variants.test.ts deleted file mode 100644 index 18fec19..0000000 --- a/tests/lib/image-variants.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { describe, it, expect } from "vitest"; - -import { VARIANT_WIDTHS, buildSrcSet, variantS3Key, variantUrl } from "@/lib/image-variants"; - -describe("VARIANT_WIDTHS", () => { - it("contient 320, 800, 1600", () => { - expect(VARIANT_WIDTHS).toEqual([320, 800, 1600]); - }); -}); - -describe("variantUrl", () => { - it("transforme .jpg en -320.jpg", () => { - expect(variantUrl("https://x/y/abc.jpg", 320)).toBe("https://x/y/abc-320.jpg"); - }); - it("force JPEG sortie même pour PNG/WebP en input", () => { - expect(variantUrl("https://x/y/abc.png", 800)).toBe("https://x/y/abc-800.jpg"); - expect(variantUrl("https://x/y/abc.webp", 1600)).toBe("https://x/y/abc-1600.jpg"); - }); - it("renvoie l'original si pas d'extension", () => { - expect(variantUrl("https://x/y/abc", 320)).toBe("https://x/y/abc"); - }); -}); - -describe("variantS3Key", () => { - it("transforme correctement la s3Key", () => { - expect(variantS3Key("carbets/foo/123-abc.jpg", 800)).toBe("carbets/foo/123-abc-800.jpg"); - }); -}); - -describe("buildSrcSet", () => { - it("contient les 3 variantes + fallback original", () => { - const set = buildSrcSet("https://x/abc.jpg"); - expect(set).toContain("abc-320.jpg 320w"); - expect(set).toContain("abc-800.jpg 800w"); - expect(set).toContain("abc-1600.jpg 1600w"); - expect(set).toContain("abc.jpg 2000w"); - }); -}); diff --git a/tests/lib/password.test.ts b/tests/lib/password.test.ts deleted file mode 100644 index 3232701..0000000 --- a/tests/lib/password.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { describe, it, expect } from "vitest"; - -import { hashPassword, verifyPassword } from "@/lib/password"; - -describe("password hashing", () => { - it("round-trips a correct password", async () => { - const plain = "correct horse battery staple"; - const hash = await hashPassword(plain); - expect(hash).not.toEqual(plain); - expect(hash.startsWith("$2")).toBe(true); - expect(await verifyPassword(plain, hash)).toBe(true); - }); - - it("rejects incorrect password", async () => { - const hash = await hashPassword("rightpass123"); - expect(await verifyPassword("wrongpass", hash)).toBe(false); - }); - - it("produces different hashes for the same plaintext (salted)", async () => { - const plain = "samepw"; - const a = await hashPassword(plain); - const b = await hashPassword(plain); - expect(a).not.toEqual(b); - expect(await verifyPassword(plain, a)).toBe(true); - expect(await verifyPassword(plain, b)).toBe(true); - }); -}); diff --git a/tests/lib/rate-limit.test.ts b/tests/lib/rate-limit.test.ts deleted file mode 100644 index 4b97e3e..0000000 --- a/tests/lib/rate-limit.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, it, expect } from "vitest"; - -import { rateLimit } from "@/lib/rate-limit"; - -describe("rateLimit", () => { - it("allows up to limit calls in window", () => { - const key = "test:" + Math.random(); - for (let i = 0; i < 5; i++) { - const r = rateLimit({ key, windowMs: 60_000, limit: 5 }); - expect(r.ok).toBe(true); - } - }); - - it("blocks the (limit+1)th call with retryAfter > 0", () => { - const key = "test:" + Math.random(); - for (let i = 0; i < 3; i++) { - rateLimit({ key, windowMs: 60_000, limit: 3 }); - } - const r = rateLimit({ key, windowMs: 60_000, limit: 3 }); - expect(r.ok).toBe(false); - expect(r.retryAfter).toBeGreaterThan(0); - expect(r.remaining).toBe(0); - }); - - it("isolates different keys", () => { - const k1 = "test:" + Math.random(); - const k2 = "test:" + Math.random(); - for (let i = 0; i < 5; i++) { - rateLimit({ key: k1, windowMs: 60_000, limit: 5 }); - } - const r = rateLimit({ key: k2, windowMs: 60_000, limit: 5 }); - expect(r.ok).toBe(true); - }); - - it("resets after window expires", async () => { - const key = "test:" + Math.random(); - rateLimit({ key, windowMs: 10, limit: 1 }); - const blocked = rateLimit({ key, windowMs: 10, limit: 1 }); - expect(blocked.ok).toBe(false); - await new Promise((r) => setTimeout(r, 15)); - const after = rateLimit({ key, windowMs: 10, limit: 1 }); - expect(after.ok).toBe(true); - }); -}); diff --git a/tests/lib/reviews.test.ts b/tests/lib/reviews.test.ts deleted file mode 100644 index d136b8e..0000000 --- a/tests/lib/reviews.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { describe, it, expect } from "vitest"; - -import { - REVIEW_COMMENT_MAX, - REVIEW_HOST_RESPONSE_MAX, - REVIEW_RATING_MAX, - REVIEW_RATING_MIN, - formatAverageRating, - isValidRating, -} from "@/lib/reviews"; - -describe("rating constants", () => { - it("min=1 max=5", () => { - expect(REVIEW_RATING_MIN).toBe(1); - expect(REVIEW_RATING_MAX).toBe(5); - }); - it("comment + host response caps are sensible", () => { - expect(REVIEW_COMMENT_MAX).toBeGreaterThan(0); - expect(REVIEW_HOST_RESPONSE_MAX).toBeGreaterThan(0); - }); -}); - -describe("isValidRating", () => { - it("accepts integers 1-5", () => { - for (let i = REVIEW_RATING_MIN; i <= REVIEW_RATING_MAX; i++) { - expect(isValidRating(i)).toBe(true); - } - }); - it("rejects out-of-range", () => { - expect(isValidRating(0)).toBe(false); - expect(isValidRating(6)).toBe(false); - expect(isValidRating(-1)).toBe(false); - }); - it("rejects non-integers and non-numbers", () => { - expect(isValidRating(3.5)).toBe(false); - expect(isValidRating("3")).toBe(false); - expect(isValidRating(null)).toBe(false); - expect(isValidRating(undefined)).toBe(false); - }); -}); - -describe("formatAverageRating", () => { - it("returns dash for null", () => { - expect(formatAverageRating(null)).toMatch(/—|-|n\/a/i); - }); - it("formats a number with one decimal", () => { - const s = formatAverageRating(4.567); - expect(s).toMatch(/4[.,]/); - }); -}); diff --git a/vitest.config.ts b/vitest.config.ts deleted file mode 100644 index 997df99..0000000 --- a/vitest.config.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { defineConfig } from "vitest/config"; -import path from "node:path"; - -export default defineConfig({ - test: { - environment: "node", - include: ["tests/**/*.test.ts"], - exclude: ["node_modules", ".next", "dist"], - coverage: { - provider: "v8", - reporter: ["text", "json-summary"], - include: ["src/lib/**/*.ts"], - exclude: ["src/lib/**/*.d.ts", "src/lib/admin/**", "src/lib/plugins/**"], - }, - }, - resolve: { - alias: { - "@": path.resolve(__dirname, "./src"), - }, - }, -});