commit 2307d9a1349a43e1a5900139559f0774c1c3712a Author: neckfire Date: Fri Jul 24 18:43:29 2026 +0200 RegWatch frontend : React/Vite -> nginx + Docker/CI homelab - URL d'API externalisée (VITE_API_URL bakée au build), plus de localhost en dur - Dockerfile multi-stage pnpm build -> nginx (fallback SPA) - docker-compose (Watchtower) + .gitea/workflows/build.yml Co-Authored-By: Claude Opus 4.8 (1M context) diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7ba3810 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git +.env +node_modules +dist +.gitea/ +docker-compose.yml +.DS_Store diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..36e1c3b --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,32 @@ +name: Build & Deploy + +on: + push: + branches: [main] + workflow_dispatch: {} + +env: + IMAGE: git.nfteam.ovh/mouigni/regwatch-frontend + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Build & push image + run: | + set -e + echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login git.nfteam.ovh -u "${{ secrets.REGISTRY_USER }}" --password-stdin + SHA="${GITHUB_SHA::12}" + docker build --build-arg VITE_API_URL=https://regwatch-api.nfteam.ovh \ + -t "${IMAGE}:latest" -t "${IMAGE}:${SHA}" -f Dockerfile . + docker push --all-tags "${IMAGE}" + - name: Notify ntfy + if: always() + run: | + if [ "${{ job.status }}" = "success" ]; then EMOJI="white_check_mark"; PRIO="default"; else EMOJI="rotating_light"; PRIO="high"; fi + curl -s -H "Authorization: Bearer ${{ secrets.NTFY_TOKEN }}" -H "Title: ${GITHUB_REPOSITORY} — ${{ job.status }}" \ + -H "Priority: ${PRIO}" -H "Tags: ${EMOJI}" -H "Click: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions" \ + -d "${GITHUB_WORKFLOW} (${GITHUB_REF_NAME} #${GITHUB_RUN_NUMBER}) : ${{ job.status }}" \ + "${{ secrets.NTFY_URL }}/${{ secrets.NTFY_TOPIC }}" || true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..89b1bfb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# --- build (Vite bake VITE_API_URL au build) --- +FROM node:24-alpine AS build +WORKDIR /app +RUN npm install -g pnpm@latest +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --no-frozen-lockfile +COPY . . +# URL publique de l'API, injectée dans le bundle au build (Vite n'a pas d'env au runtime). +ARG VITE_API_URL=https://regwatch-api.nfteam.ovh +ENV VITE_API_URL=$VITE_API_URL +RUN pnpm build + +# --- serve (nginx statique, fallback SPA) --- +FROM nginx:alpine +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 diff --git a/README.md b/README.md new file mode 100644 index 0000000..c3c21da --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is enabled on this template. See [this documentation](https://react.dev/learn/react-compiler) for more information. + +Note: This will impact Vite dev & build performances. + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3fe66d8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +services: + regwatch-frontend: + image: git.nfteam.ovh/mouigni/regwatch-frontend:latest + container_name: regwatch-frontend + restart: unless-stopped + pull_policy: always + ports: + - "8115:80" + labels: + - "com.centurylinklabs.watchtower.enable=true" + - "homepage.group=RegWatch" + - "homepage.name=RegWatch" + - "homepage.icon=mdi-radar" + - "homepage.href=https://regwatch.nfteam.ovh" + - "homepage.description=Veille réglementaire (front React/Vite)" diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/index.html b/index.html new file mode 100644 index 0000000..bedd93b --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + RegWatch + + +
+ + + diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..ceaab6e --- /dev/null +++ b/nginx.conf @@ -0,0 +1,17 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # SPA : toute route inconnue retombe sur index.html (react-router). + location / { + try_files $uri $uri/ /index.html; + } + + # Cache long pour les assets fingerprintés par Vite. + location /assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c199e1b --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "lucide-react": "^1.14.0", + "react": "^19.2.5", + "react-dom": "^19.2.5", + "react-router-dom": "^7.14.2" + }, + "devDependencies": { + "@babel/core": "^7.29.0", + "@eslint/js": "^9.39.4", + "@rolldown/plugin-babel": "^0.2.3", + "@types/babel__core": "^7.20.5", + "@types/node": "^24.12.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "babel-plugin-react-compiler": "^1.0.0", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.5.0", + "typescript": "~6.0.2", + "typescript-eslint": "^8.58.2", + "vite": "^8.0.9" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..d45277f --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2001 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + lucide-react: + specifier: ^1.14.0 + version: 1.14.0(react@19.2.5) + react: + specifier: ^19.2.5 + version: 19.2.5 + react-dom: + specifier: ^19.2.5 + version: 19.2.5(react@19.2.5) + react-router-dom: + specifier: ^7.14.2 + version: 7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + devDependencies: + '@babel/core': + specifier: ^7.29.0 + version: 7.29.0 + '@eslint/js': + specifier: ^9.39.4 + version: 9.39.4 + '@rolldown/plugin-babel': + specifier: ^0.2.3 + version: 0.2.3(@babel/core@7.29.0)(rolldown@1.0.0-rc.16)(vite@8.0.9(@types/node@24.12.2)) + '@types/babel__core': + specifier: ^7.20.5 + version: 7.20.5 + '@types/node': + specifier: ^24.12.2 + version: 24.12.2 + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.0)(rolldown@1.0.0-rc.16)(vite@8.0.9(@types/node@24.12.2)))(babel-plugin-react-compiler@1.0.0)(vite@8.0.9(@types/node@24.12.2)) + babel-plugin-react-compiler: + specifier: ^1.0.0 + version: 1.0.0 + eslint: + specifier: ^9.39.4 + version: 9.39.4 + eslint-plugin-react-hooks: + specifier: ^7.1.1 + version: 7.1.1(eslint@9.39.4) + eslint-plugin-react-refresh: + specifier: ^0.5.2 + version: 0.5.2(eslint@9.39.4) + globals: + specifier: ^17.5.0 + version: 17.5.0 + typescript: + specifier: ~6.0.2 + version: 6.0.3 + typescript-eslint: + specifier: ^8.58.2 + version: 8.59.0(eslint@9.39.4)(typescript@6.0.3) + vite: + specifier: ^8.0.9 + version: 8.0.9(@types/node@24.12.2) + +packages: + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.126.0': + resolution: {integrity: sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==} + + '@rolldown/binding-android-arm64@1.0.0-rc.16': + resolution: {integrity: sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.16': + resolution: {integrity: sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.16': + resolution: {integrity: sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.16': + resolution: {integrity: sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.16': + resolution: {integrity: sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.16': + resolution: {integrity: sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.16': + resolution: {integrity: sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.16': + resolution: {integrity: sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.16': + resolution: {integrity: sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.16': + resolution: {integrity: sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.16': + resolution: {integrity: sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.16': + resolution: {integrity: sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.16': + resolution: {integrity: sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.16': + resolution: {integrity: sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.16': + resolution: {integrity: sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/plugin-babel@0.2.3': + resolution: {integrity: sha512-+zEk16yGlz1F9STiRr6uG9hmIXb6nprjLczV/htGptYuLoCuxb+itZ03RKCEeOhBpDDd1NU7qF6x1VLMUp62bw==} + engines: {node: '>=22.12.0 || ^24.0.0'} + peerDependencies: + '@babel/core': ^7.29.0 || ^8.0.0-rc.1 + '@babel/plugin-transform-runtime': ^7.29.0 || ^8.0.0-rc.1 + '@babel/runtime': ^7.27.0 || ^8.0.0-rc.1 + rolldown: ^1.0.0-rc.5 + vite: ^8.0.0 + peerDependenciesMeta: + '@babel/plugin-transform-runtime': + optional: true + '@babel/runtime': + optional: true + vite: + optional: true + + '@rolldown/pluginutils@1.0.0-rc.16': + resolution: {integrity: sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==} + + '@rolldown/pluginutils@1.0.0-rc.7': + resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@24.12.2': + resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@typescript-eslint/eslint-plugin@8.59.0': + resolution: {integrity: sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.59.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.59.0': + resolution: {integrity: sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.59.0': + resolution: {integrity: sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.59.0': + resolution: {integrity: sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.59.0': + resolution: {integrity: sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.59.0': + resolution: {integrity: sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.59.0': + resolution: {integrity: sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.59.0': + resolution: {integrity: sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.59.0': + resolution: {integrity: sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.59.0': + resolution: {integrity: sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-react@6.0.1': + resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + babel-plugin-react-compiler@1.0.0: + resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.20: + resolution: {integrity: sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + brace-expansion@1.1.14: + resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001790: + resolution: {integrity: sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + electron-to-chromium@1.5.343: + resolution: {integrity: sha512-YHnQ3MXI08icvL9ZKnEBy05F2EQ8ob01UaMOuMbM8l+4UcAq6MPPbBTJBbsBUg3H8JeZNt+O4fjsoWth3p6IFg==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-react-refresh@0.5.2: + resolution: {integrity: sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==} + peerDependencies: + eslint: ^9 || ^10 + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@17.5.0: + resolution: {integrity: sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==} + engines: {node: '>=18'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@1.14.0: + resolution: {integrity: sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-releases@2.0.38: + resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.10: + resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + react-dom@19.2.5: + resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} + peerDependencies: + react: ^19.2.5 + + react-router-dom@7.14.2: + resolution: {integrity: sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.14.2: + resolution: {integrity: sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react@19.2.5: + resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + rolldown@1.0.0-rc.16: + resolution: {integrity: sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.59.0: + resolution: {integrity: sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite@8.0.9: + resolution: {integrity: sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + +snapshots: + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.2': + dependencies: + '@babel/types': 7.29.0 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': + dependencies: + eslint: 9.39.4 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.14.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@oxc-project/types@0.126.0': {} + + '@rolldown/binding-android-arm64@1.0.0-rc.16': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.16': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.16': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.16': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.16': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.16': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.16': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.16': + optional: true + + '@rolldown/plugin-babel@0.2.3(@babel/core@7.29.0)(rolldown@1.0.0-rc.16)(vite@8.0.9(@types/node@24.12.2))': + dependencies: + '@babel/core': 7.29.0 + picomatch: 4.0.4 + rolldown: 1.0.0-rc.16 + optionalDependencies: + vite: 8.0.9(@types/node@24.12.2) + + '@rolldown/pluginutils@1.0.0-rc.16': {} + + '@rolldown/pluginutils@1.0.0-rc.7': {} + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@24.12.2': + dependencies: + undici-types: 7.16.0 + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@typescript-eslint/eslint-plugin@8.59.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4)(typescript@6.0.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.59.0(eslint@9.39.4)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.59.0 + '@typescript-eslint/type-utils': 8.59.0(eslint@9.39.4)(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.0(eslint@9.39.4)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.0 + eslint: 9.39.4 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.59.0(eslint@9.39.4)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.59.0 + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.0 + debug: 4.4.3 + eslint: 9.39.4 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.59.0(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@6.0.3) + '@typescript-eslint/types': 8.59.0 + debug: 4.4.3 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.59.0': + dependencies: + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/visitor-keys': 8.59.0 + + '@typescript-eslint/tsconfig-utils@8.59.0(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + + '@typescript-eslint/type-utils@8.59.0(eslint@9.39.4)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.0(eslint@9.39.4)(typescript@6.0.3) + debug: 4.4.3 + eslint: 9.39.4 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.59.0': {} + + '@typescript-eslint/typescript-estree@8.59.0(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.59.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@6.0.3) + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/visitor-keys': 8.59.0 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.7.4 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.59.0(eslint@9.39.4)(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@typescript-eslint/scope-manager': 8.59.0 + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@6.0.3) + eslint: 9.39.4 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.59.0': + dependencies: + '@typescript-eslint/types': 8.59.0 + eslint-visitor-keys: 5.0.1 + + '@vitejs/plugin-react@6.0.1(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.0)(rolldown@1.0.0-rc.16)(vite@8.0.9(@types/node@24.12.2)))(babel-plugin-react-compiler@1.0.0)(vite@8.0.9(@types/node@24.12.2))': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.7 + vite: 8.0.9(@types/node@24.12.2) + optionalDependencies: + '@rolldown/plugin-babel': 0.2.3(@babel/core@7.29.0)(rolldown@1.0.0-rc.16)(vite@8.0.9(@types/node@24.12.2)) + babel-plugin-react-compiler: 1.0.0 + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ajv@6.14.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + babel-plugin-react-compiler@1.0.0: + dependencies: + '@babel/types': 7.29.0 + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.10.20: {} + + brace-expansion@1.1.14: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.20 + caniuse-lite: 1.0.30001790 + electron-to-chromium: 1.5.343 + node-releases: 2.0.38 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001790: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + cookie@1.1.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + detect-libc@2.1.2: {} + + electron-to-chromium@1.5.343: {} + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-react-hooks@7.1.1(eslint@9.39.4): + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.2 + eslint: 9.39.4 + hermes-parser: 0.25.1 + zod: 4.3.6 + zod-validation-error: 4.0.2(zod@4.3.6) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-refresh@0.5.2(eslint@9.39.4): + dependencies: + eslint: 9.39.4 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.4: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.14.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@17.5.0: {} + + has-flag@4.0.0: {} + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + isexe@2.0.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@1.14.0(react@19.2.5): + dependencies: + react: 19.2.5 + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.5 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.14 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + node-releases@2.0.38: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + postcss@8.5.10: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + punycode@2.3.1: {} + + react-dom@19.2.5(react@19.2.5): + dependencies: + react: 19.2.5 + scheduler: 0.27.0 + + react-router-dom@7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-router: 7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + + react-router@7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + cookie: 1.1.1 + react: 19.2.5 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.5(react@19.2.5) + + react@19.2.5: {} + + resolve-from@4.0.0: {} + + rolldown@1.0.0-rc.16: + dependencies: + '@oxc-project/types': 0.126.0 + '@rolldown/pluginutils': 1.0.0-rc.16 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.16 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.16 + '@rolldown/binding-darwin-x64': 1.0.0-rc.16 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.16 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.16 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.16 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.16 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.16 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.16 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.16 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.16 + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + semver@7.7.4: {} + + set-cookie-parser@2.7.2: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + source-map-js@1.2.1: {} + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + ts-api-utils@2.5.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + + tslib@2.8.1: + optional: true + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.59.0(eslint@9.39.4)(typescript@6.0.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.59.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4)(typescript@6.0.3) + '@typescript-eslint/parser': 8.59.0(eslint@9.39.4)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.59.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.0(eslint@9.39.4)(typescript@6.0.3) + eslint: 9.39.4 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + typescript@6.0.3: {} + + undici-types@7.16.0: {} + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite@8.0.9(@types/node@24.12.2): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.10 + rolldown: 1.0.0-rc.16 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 24.12.2 + fsevents: 2.3.3 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} + + zod-validation-error@4.0.2(zod@4.3.6): + dependencies: + zod: 4.3.6 + + zod@4.3.6: {} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons.svg b/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App.module.css b/src/App.module.css new file mode 100644 index 0000000..174edb0 --- /dev/null +++ b/src/App.module.css @@ -0,0 +1,26 @@ +.app { + min-height: 100vh; + width: 100%; + background-color: #f8fafc; + display: flex; + flex-direction: column; +} + +.contentWrapper { + display: flex; + flex: 1; + min-height: 100vh; +} + +.main { + flex: 1; + margin-left: 16rem; + padding: 2rem; + min-height: 100vh; +} + +@media (max-width: 768px) { + .main { + margin-left: 0; + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..b4f96d2 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,163 @@ +import { useEffect, useState } from "react"; +import { Routes, Route, Navigate, useLocation, useNavigate } from "react-router-dom"; + +import AuthPage from "./pages/AuthPage"; +import HomePage from "./pages/Home"; +import DocumentsPage from "./pages/Documents"; +import IngredientSearchPage from "./pages/IngredientSearchPage"; +import RecentUpdatesPage from "./pages/RecentUpdatesPage"; +import AboutPage from "./pages/AboutPage"; +import ContactPage from "./pages/ContactPage"; +import CGU from "./pages/CGU"; +import MentionsLegales from "./pages/MentionsLegales"; + +import Sidebar from "./components/Sidebar"; +import Footer from "./components/Footer"; + +import styles from "./App.module.css"; + +import { getDocuments, type ApiDocument } from "./services/theapi"; + +type NavItem = + | "home" + | "ingredient-search" + | "documents" + | "recent-updates" + | "about" + | "contact" + | "cgu" + | "mentions-legales"; + +interface User { + id: number; + email: string; + full_name: string | null; +} + +interface AuthState { + user: User | null; + isAuthenticated: boolean; + loading: boolean; +} + +export default function App() { + const [recentDocuments, setRecentDocuments] = useState([]); + const [authState, setAuthState] = useState(() => { + const savedToken = localStorage.getItem("authToken"); + const savedUser = localStorage.getItem("user"); + + if (savedToken && savedUser) { + const parsedUser = JSON.parse(savedUser); + return { + user: parsedUser, + isAuthenticated: true, + loading: false, + }; + } + + return { + user: null, + isAuthenticated: false, + loading: false, + }; + }); + + const location = useLocation(); + const navigate = useNavigate(); + + const handleAuthSuccess = (newToken: string, newUser: User) => { + localStorage.setItem("authToken", newToken); + localStorage.setItem("user", JSON.stringify(newUser)); + + setAuthState({ + user: newUser, + isAuthenticated: true, + loading: false, + }); + + navigate("/"); + }; + + const handleLogout = () => { + localStorage.removeItem("authToken"); + localStorage.removeItem("user"); + setAuthState({ + user: null, + isAuthenticated: false, + loading: false, + }); + navigate("/"); + }; + + const activeNav: NavItem = + location.pathname === "/ingredient-search" ? "ingredient-search" + : location.pathname === "/documents" ? "documents" + : location.pathname === "/recent-updates" ? "recent-updates" + : location.pathname === "/about" ? "about" + : location.pathname === "/contact" ? "contact" + : location.pathname === "/cgu" ? "cgu" + : location.pathname === "/mentions-legales"? "mentions-legales" + : "home"; + + // LOAD RECENT DOCUMENTS + useEffect(() => { + if (authState.isAuthenticated) { + getDocuments() + .then((data) => { + setRecentDocuments(Array.isArray(data) ? data.slice(0, 4) : []); + }) + .catch((err) => { + console.error("API ERROR:", err); + setRecentDocuments([]); + }); + } + }, [authState.isAuthenticated]); + + const handleNavClick = (item: NavItem) => { + const path = + item === "home" ? "/" + : item === "documents" ? "/documents" + : item === "ingredient-search" ? "/ingredient-search" + : item === "recent-updates" ? "/recent-updates" + : item === "about" ? "/about" + : item === "contact" ? "/contact" + : item === "cgu" ? "/cgu" + : "/mentions-legales"; + navigate(path); + }; + + if (authState.loading) { + return ( +
+

Loading application...

+
+ ); + } + + // Si pas authentifié, afficher la page de login + if (!authState.isAuthenticated) { + return ; + } + + return ( +
+
+ +
+ + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/assets/hero.png b/src/assets/hero.png new file mode 100644 index 0000000..02251f4 Binary files /dev/null and b/src/assets/hero.png differ diff --git a/src/assets/react.svg b/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/vite.svg b/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 0000000..f825ffd --- /dev/null +++ b/src/components/Footer.tsx @@ -0,0 +1,33 @@ +import styles from '../styles/Footer.module.css'; + +interface FooterProps { + onNavClick?: (page: 'about' | 'contact' | 'cgu' | 'mentions-legales') => void; +} + +export default function Footer({ onNavClick }: FooterProps) { + const handleClick = (page: 'about' | 'contact' | 'cgu' | 'mentions-legales') => { + if (onNavClick) { + onNavClick(page); + } + }; + + return ( +
+ © 2026 RegWatch MedLabs. Tous droits réservés. +
+ + + + +
+
+ ); +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx new file mode 100644 index 0000000..a88528e --- /dev/null +++ b/src/components/Sidebar.tsx @@ -0,0 +1,106 @@ +import { Home, Search, FileText, Clock, LogOut } from 'lucide-react'; +import styles from '../styles/Sidebar.module.css'; + +type NavItem = + | 'home' + | 'ingredient-search' + | 'documents' + | 'recent-updates' + | 'about' + | 'contact' + | 'cgu' + | 'mentions-legales'; + +interface User { + id: number; + email: string; + full_name: string | null; +} + +interface SidebarProps { + activeNav: NavItem; + onNavClick: (item: NavItem) => void; + onLogout?: () => void; + user?: User | null; +} + +export default function Sidebar({ + activeNav, + onNavClick, + onLogout, + user, +}: SidebarProps) { + const navItems: { + key: NavItem; + label: string; + icon: React.ReactNode; + }[] = [ + { key: 'home', label: 'Home', icon: }, + { key: 'ingredient-search', label: 'Ingredient Search', icon: }, + { key: 'documents', label: 'Documents', icon: }, + { key: 'recent-updates', label: 'Recent Updates', icon: }, + ]; + + const initials = user + ? (user.full_name || user.email) + .split(/[ .@_-]+/) + .filter(Boolean) + .slice(0, 2) + .map((part) => part[0]) + .join('') + .toUpperCase() + : ''; + + return ( + + ); +} \ No newline at end of file diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..76bdbd3 --- /dev/null +++ b/src/index.css @@ -0,0 +1,120 @@ +:root { + --text: #6b6375; + --text-h: #08060d; + --bg: #fff; + --border: #e5e4e7; + --code-bg: #f4f3ec; + --accent: #aa3bff; + --accent-bg: rgba(170, 59, 255, 0.1); + --accent-border: rgba(170, 59, 255, 0.5); + --social-bg: rgba(244, 243, 236, 0.5); + --shadow: + rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; + + --sans: system-ui, 'Segoe UI', Roboto, sans-serif; + --heading: system-ui, 'Segoe UI', Roboto, sans-serif; + --mono: ui-monospace, Consolas, monospace; + + font: 18px/145% var(--sans); + letter-spacing: 0.18px; + color-scheme: light dark; + color: var(--text); + background: var(--bg); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + @media (max-width: 1024px) { + font-size: 16px; + } +} + +@media (prefers-color-scheme: dark) { + :root { + --text: #9ca3af; + --text-h: #f3f4f6; + --bg: #16171d; + --border: #2e303a; + --code-bg: #1f2028; + --accent: #c084fc; + --accent-bg: rgba(192, 132, 252, 0.15); + --accent-border: rgba(192, 132, 252, 0.5); + --social-bg: rgba(47, 48, 58, 0.5); + --shadow: + rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; + } + + #social .button-icon { + filter: invert(1) brightness(2); + } +} + +html, +body, +#root { + min-height: 100%; +} + +#root { + width: 100%; + max-width: 100%; + margin: 0; + text-align: left; + border-inline: none; + min-height: 100vh; + display: flex; + flex-direction: column; + box-sizing: border-box; + background: var(--bg); +} + +body { + margin: 0; + min-height: 100vh; + background: var(--bg); +} + +h1, +h2 { + font-family: var(--heading); + font-weight: 500; + color: var(--text-h); +} + +h1 { + font-size: 56px; + letter-spacing: -1.68px; + margin: 32px 0; + @media (max-width: 1024px) { + font-size: 36px; + margin: 20px 0; + } +} +h2 { + font-size: 24px; + line-height: 118%; + letter-spacing: -0.24px; + margin: 0 0 8px; + @media (max-width: 1024px) { + font-size: 20px; + } +} +p { + margin: 0; +} + +code, +.counter { + font-family: var(--mono); + display: inline-flex; + border-radius: 4px; + color: var(--text-h); +} + +code { + font-size: 15px; + line-height: 135%; + padding: 4px 8px; + background: var(--code-bg); +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..c5f4e30 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; + +import App from "./App"; + +import "./index.css"; + +ReactDOM.createRoot( + document.getElementById("root")! +).render( + + + + + + + +); \ No newline at end of file diff --git a/src/pages/AboutPage.tsx b/src/pages/AboutPage.tsx new file mode 100644 index 0000000..40a5df3 --- /dev/null +++ b/src/pages/AboutPage.tsx @@ -0,0 +1,141 @@ +import { Target, Users, Shield, BookOpen } from 'lucide-react'; +import styles from '../styles/AboutPage.module.css'; + +export default function AboutPage() { + return ( +
+
+
+

À propos de RegWatch MedLabs

+

Plateforme de veille réglementaire pour les ingrédients cosmétiques

+
+ + {/* Mission Section */} +
+
+
+ +
+

Notre Mission

+
+

+ RegWatch MedLabs est une plateforme spécialisée conçue pour aider les experts scientifiques et réglementaires à accéder rapidement aux documents officiels relatifs aux ingrédients cosmétiques. +

+

+ Notre objectif est de simplifier la veille réglementaire en centralisant les informations provenant des principales autorités sanitaires internationales (SCCS, FDA, EFSA, Commission Européenne, etc.). +

+
+ + {/* Pour Qui Section */} +
+
+
+ +
+

Pour Qui ?

+
+
+
+
+
+ Toxicologues +
+

+ Accès rapide aux évaluations de sécurité et études toxicologiques +

+
+
+
+
+ Consultants Réglementaires +
+

+ Suivi des dernières réglementations et mises à jour normatives +

+
+
+
+
+ Responsables Qualité +
+

+ Vérification de la conformité des ingrédients cosmétiques +

+
+
+
+
+ Chercheurs +
+

+ Accès à la base de données complète pour la recherche scientifique +

+
+
+
+ + {/* Key Features */} +
+
+
+ +
+

Fonctionnalités Clés

+
+
+
+

Recherche par Ingrédient

+

+ Trouvez rapidement tous les documents relatifs à un ingrédient spécifique +

+
+
+

Base de Données Complète

+

+ Accès à l'ensemble des documents collectés par notre système de veille +

+
+
+

Suivi des Mises à Jour

+

+ Notifications des nouveaux documents et révisions réglementaires +

+
+
+

Filtres Avancés

+

+ Filtrez par source, type de document, date de publication +

+
+
+
+ + {/* About Platform */} +
+
+
+ +
+

RegWatch MedLabs

+
+

+ Plateforme développée par des experts en réglementation cosmétique pour répondre aux besoins spécifiques des professionnels du secteur. +

+
+
+
+ Secteur : Cosmétique & Réglementation +
+
+ Spécialité : Veille réglementaire scientifique +
+
+ Public cible : Experts toxicologues et consultants réglementaires +
+
+
+
+
+
+ ); +} diff --git a/src/pages/AuthPage.tsx b/src/pages/AuthPage.tsx new file mode 100644 index 0000000..4983d84 --- /dev/null +++ b/src/pages/AuthPage.tsx @@ -0,0 +1,169 @@ +import { useState } from "react"; +import styles from "../styles/AuthPage.module.css"; + +export default function AuthPage({ onAuthSuccess }: { onAuthSuccess: (token: string, user: { id: number; email: string; full_name: string | null }) => void }) { + const [isLogin, setIsLogin] = useState(true); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [fullName, setFullName] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const API_URL = import.meta.env.VITE_API_URL ?? "http://localhost:8000"; + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + setLoading(true); + + try { + console.log("Tentative de connexion avec:", email); + const response = await fetch(`${API_URL}/login`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email, password }), + }); + + console.log("Status:", response.status); + + const data = await response.json(); + console.log("Response data:", data); + + if (!response.ok) { + throw new Error(data.detail || `Erreur ${response.status}`); + } + + const userResponse = await fetch(`${API_URL}/me?token=${data.access_token}`); + const userData = await userResponse.json(); + + onAuthSuccess(data.access_token, userData); + } catch (err) { + console.error("Error:", err); + setError(err instanceof Error ? err.message : "Erreur lors de la connexion"); + } finally { + setLoading(false); + } + }; + + const handleSignup = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + setLoading(true); + + try { + const response = await fetch(`${API_URL}/signup`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email, + password, + full_name: fullName, + }), + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.detail || "Erreur d'inscription"); + } + + const userData = await response.json(); + + // Automatiquement connecter après l'inscription + const loginResponse = await fetch(`${API_URL}/login`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email, password }), + }); + + const loginData = await loginResponse.json(); + onAuthSuccess(loginData.access_token, userData); + } catch (err) { + setError(err instanceof Error ? err.message : "Erreur lors de l'inscription"); + } finally { + setLoading(false); + } + }; + + const handleSubmit = isLogin ? handleLogin : handleSignup; + + return ( +
+
+

RegWatch

+

Gestion des réglementations alimentaires

+ +
+ {error &&
{error}
} + +
+ + setEmail(e.target.value)} + required + placeholder="votre@email.com" + /> +
+ + {!isLogin && ( +
+ + setFullName(e.target.value)} + placeholder="Votre nom complet" + /> +
+ )} + +
+ + setPassword(e.target.value)} + required + placeholder="••••••••" + /> +
+ + +
+ +
+

+ {isLogin ? "Pas encore de compte ?" : "Déjà inscrit ?"} + {" "} + +

+
+
+
+ ); +} diff --git a/src/pages/CGU.tsx b/src/pages/CGU.tsx new file mode 100644 index 0000000..6e5676e --- /dev/null +++ b/src/pages/CGU.tsx @@ -0,0 +1,165 @@ +import { FileText, CheckCircle, XCircle, AlertCircle } from 'lucide-react'; +import styles from '../styles/CGU.module.css'; + +export default function CGU() { + return ( +
+
+
+
+ +

Conditions Générales d'Utilisation (CGU)

+
+

Dernière mise à jour : 15 avril 2026

+
+ +
+
+

Préambule

+
+

+ Les présentes conditions générales d'utilisation (CGU) régissent l'utilisation de la plateforme RegWatch MedLabs. L'accès et + l'utilisation du site impliquent l'acceptation pleine et entière des présentes CGU par l'utilisateur. +

+
+
+ +
+

Article 1 - Objet du Service

+
+

+ RegWatch MedLabs est une plateforme de veille réglementaire destinée aux professionnels du secteur cosmétique. Le service + permet de : +

+
+
+ + Rechercher des documents réglementaires relatifs aux ingrédients cosmétiques +
+
+ + Accéder à une base de données de documents officiels +
+
+ + Suivre les mises à jour réglementaires +
+
+
+
+ +
+

Article 2 - Accès au Service

+
+

+ Le service est accessible gratuitement à tout utilisateur disposant d'un accès à internet. Tous les frais supportés par l'utilisateur + pour accéder au service (accès internet notamment) sont à sa charge. +

+
+
+ +
+

Article 3 - Utilisation du Service

+
+

L'utilisateur s'engage à :

+
+
+ + Utiliser le service de manière loyale et dans le respect des présentes CGU +
+
+ + Ne pas utiliser le service à des fins commerciales sans autorisation +
+
+ + Respecter les droits de propriété intellectuelle +
+
+ +

Il est formellement interdit de :

+
+
+ + Extraire ou télécharger massivement des données (scraping) +
+
+ + Tenter d'accéder aux parties non publiques du service +
+
+ + Perturber ou interrompre le fonctionnement du service +
+
+
+
+ +
+

Article 4 - Responsabilité

+
+ +
+
Avertissement Important
+
+ Les documents et informations fournis sur cette plateforme sont à titre informatif uniquement. RegWatch MedLabs ne + peut garantir l'exactitude, l'exhaustivité ou l'actualité des informations. +
+
+
+
+

+ L'utilisateur est seul responsable de l'utilisation qu'il fait des informations consultées. RegWatch MedLabs ne saurait être tenu + responsable de toute décision prise sur la base des informations disponibles sur la plateforme. +

+

+ RegWatch MedLabs ne garantit pas que le service soit exempt d'erreurs, de virus ou autres composants nuisibles. +

+
+
+ +
+

Article 5 - Propriété Intellectuelle

+
+

+ Tous les éléments du site (structure, textes, logos, images, etc.) sont protégés par le droit d'auteur. Toute reproduction, + représentation, modification, publication, transmission, dénaturtion, totale ou partielle du site ou de son contenu, par quelque + procédé que ce soit, sans autorisation préalable écrite, est interdite. +

+
+
+ +
+

Article 6 - Protection des Données

+
+

+ Les données personnelles collectées sont traitées conformément au RGPD. Pour plus d'informations, veuillez consulter notre + politique de confidentialité. +

+
+
+ +
+

Article 7 - Modification des CGU

+
+

+ RegWatch MedLabs se réserve le droit de modifier les présentes CGU à tout moment. Les nouvelles conditions prendront effet + dès leur publication sur le site. Il est conseillé à l'utilisateur de consulter régulièrement les CGU. +

+
+
+ +
+

Article 8 - Droit Applicable

+
+

+ Les présentes CGU sont régies par le droit français. En cas de litige, et à défaut d'accord amiable, le litige sera porté devant les + tribunaux français compétents. +

+
+
+
+
+
+ ); +} diff --git a/src/pages/ContactPage.tsx b/src/pages/ContactPage.tsx new file mode 100644 index 0000000..c4617b6 --- /dev/null +++ b/src/pages/ContactPage.tsx @@ -0,0 +1,238 @@ +import { useState } from 'react'; +import { Mail, Phone, MapPin, Send } from 'lucide-react'; +import styles from '../styles/ContactPage.module.css'; + +export default function ContactPage() { + const [formData, setFormData] = useState({ + name: '', + email: '', + subject: '', + message: '', + }); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value, + })); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + console.log('Form submitted:', formData); + setFormData({ name: '', email: '', subject: '', message: '' }); + }; + + return ( +
+
+
+
+ +
+

Contact

+

Nous sommes à votre écoute pour toute question ou demande d'information

+
+ + {/* Main Content */} +
+ {/* Contact Info */} +
+ {/* Email */} + + + {/* Phone */} +
+

+ + Téléphone +

+
+
+ +33 (0)1 XX XX XX XX +
+
+ Du lundi au vendredi, 9h-18h +
+
+
+ + {/* Address */} +
+

+ + Adresse +

+
+
+ RegWatch MedLabs +
+
[Adresse complète]
+
[Code postal] [Ville]
+
France
+
+
+ + {/* Opening Hours */} +
+

Horaires d'ouverture

+
+
Lundi - Vendredi : 9h00 - 18h00
+
Samedi - Dimanche : Fermé
+
+
+ + {/* Technical Support */} +
+

Support Technique

+

+ Pour toute assistance technique, merci de préciser : +

+
    +
  • + + Votre navigateur web +
  • +
  • + + Description du problème +
  • +
  • + + Captures d'écran si possible +
  • +
+
+
+ + {/* Form */} +
+

Envoyez-nous un message

+
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ +

+ * Champs obligatoires. Vos données personnelles sont traitées conformément à notre politique de confidentialité et au RGPD. Elles ne seront utilisées que pour répondre à votre demande. +

+ + +
+
+
+ + {/* FAQ Section */} +
+

Questions Fréquentes

+
+
+

Comment rechercher un ingrédient ?

+

+ Utilisez la barre de recherche sur la page d'accueil ou accédez à la page "Recherche d'Ingrédients" via le menu de navigation. +

+
+
+

Les documents sont-ils mis à jour ?

+

+ Oui, notre système de veille collecte régulièrement les nouveaux documents et mises à jour réglementaires. +

+
+
+

Le service est-il gratuit ?

+

+ Contactez-nous pour connaître nos offres et tarifs adaptés à vos besoins. +

+
+
+
+
+
+ ); +} diff --git a/src/pages/Documents.tsx b/src/pages/Documents.tsx new file mode 100644 index 0000000..7e04d98 --- /dev/null +++ b/src/pages/Documents.tsx @@ -0,0 +1,279 @@ +import { useEffect, useMemo, useState } from "react"; +import { + Filter, + ArrowUpDown, + File as FileIcon, +} from "lucide-react"; + +import styles from "../styles/DocumentsPage.module.css"; +import { getDocuments } from "../services/theapi"; + +type ApiDocument = { + title: string; + ingredient: string; + source: string; + type: string; + date?: string | null; + pdf_url: string; + bold?: boolean; +}; + +const ITEMS_PER_PAGE = 8; + +export default function DocumentsPage() { + const [documents, setDocuments] = useState([]); + const [loading, setLoading] = useState(true); + + const [sourceFilter, setSourceFilter] = useState("All Sources"); + const [typeFilter, setTypeFilter] = useState("All Types"); + const [dateFilter, setDateFilter] = useState("All Dates"); + const [sortBy, setSortBy] = useState("Date (Newest)"); + const [currentPage, setCurrentPage] = useState(1); + + useEffect(() => { + getDocuments() + .then((data) => { + setDocuments(Array.isArray(data) ? data : []); + }) + .catch((err) => { + console.error("API Error:", err); + setDocuments([]); + }) + .finally(() => setLoading(false)); + }, []); + + const allSources = useMemo( + () => ["All Sources", ...new Set(documents.map((d) => d.source))], + [documents] + ); + + const allTypes = useMemo( + () => ["All Types", ...new Set(documents.map((d) => d.type))], + [documents] + ); + + const allDates = useMemo( + () => [ + "All Dates", + ...new Set( + documents.map((d) => String(d.date ?? "").slice(0, 4)) + ), + ], + [documents] + ); + + const filtered = useMemo(() => { + return documents + .filter((d) => { + const sourceMatch = + sourceFilter === "All Sources" || d.source === sourceFilter; + + const typeMatch = + typeFilter === "All Types" || d.type === typeFilter; + + const safeDate = String(d.date ?? ""); + const dateMatch = + dateFilter === "All Dates" || + safeDate.startsWith(dateFilter); + + return sourceMatch && typeMatch && dateMatch; + }) + .sort((a, b) => { + const dateA = String(a.date ?? ""); + const dateB = String(b.date ?? ""); + const titleA = String(a.title ?? ""); + const titleB = String(b.title ?? ""); + + if (sortBy === "Date (Newest)") return dateB.localeCompare(dateA); + if (sortBy === "Date (Oldest)") return dateA.localeCompare(dateB); + if (sortBy === "Title (A-Z)") return titleA.localeCompare(titleB); + if (sortBy === "Title (Z-A)") return titleB.localeCompare(titleA); + return 0; + }); + }, [documents, sourceFilter, typeFilter, dateFilter, sortBy]); + + const totalPages = Math.max( + 1, + Math.ceil(filtered.length / ITEMS_PER_PAGE) + ); + + const safePage = Math.min(currentPage, totalPages); + + const paginated = filtered.slice( + (safePage - 1) * ITEMS_PER_PAGE, + safePage * ITEMS_PER_PAGE + ); + + if (loading) { + return ( +
+

Loading documents...

+
+ ); + } + + return ( +
+
+ {/* HEADER */} +
+

Document Database

+

+ Complete collection of regulatory monitoring documents +

+
+ + {/* FILTERS */} +
+
+ + Filters: +
+ + + + + + + +
+ + +
+
+ + {/* STATS */} +
+

+ Total Documents: + + {" "} + {filtered.length} + +

+
+ + {/* TABLE */} +
+
+ Title + Ingredient + Source + Type + Date + PDF +
+ + {paginated.map((doc, index) => ( +
+
+ + {doc.title} +
+ + {doc.ingredient} + {doc.source} + {doc.type} + {doc.date} + + +
+ ))} +
+ + {/* PAGINATION */} + {totalPages > 1 && ( +
+ + + {Array.from({ length: totalPages }, (_, i) => i + 1).map( + (page) => ( + + ) + )} + + +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx new file mode 100644 index 0000000..67c1170 --- /dev/null +++ b/src/pages/Home.tsx @@ -0,0 +1,101 @@ +import { Search, FileText, TrendingUp, Clock } from 'lucide-react'; +import styles from '../styles/HomePage.module.css'; +import { openPdf, type ApiDocument } from '../services/theapi'; + +type NavItem = + | 'home' + | 'ingredient-search' + | 'documents' + | 'recent-updates' + | 'about' + | 'contact' + | 'cgu' + | 'mentions-legales'; + +interface HomePageProps { + handleNavClick: (item: NavItem) => void; + recentDocuments: ApiDocument[]; +} + +export default function HomePage({ handleNavClick, recentDocuments }: HomePageProps) { + return ( +
+
+ + {/* Hero Section */} +
+

RegWatch MedLabs

+

Cosmetic Regulatory Monitoring Platform

+

+ Quickly find official regulatory documents related to cosmetic ingredients +

+
+ + {/* Feature Cards */} +
+ + + + + +
+ + {/* Recent Documents */} +
+
+ +

Recent Regulatory Documents

+
+ +
+ {recentDocuments.map((doc, index) => ( +
+

{doc.title}

+
+ Ingredient: {doc.ingredient} + Source: {doc.source} + Date: {doc.date ?? "—"} + + {doc.pdf_url ? ( + + ) : ( + + )} +
+
+ ))} +
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/src/pages/IngredientSearchPage.tsx b/src/pages/IngredientSearchPage.tsx new file mode 100644 index 0000000..fc47447 --- /dev/null +++ b/src/pages/IngredientSearchPage.tsx @@ -0,0 +1,161 @@ +import { useEffect, useMemo, useState } from "react"; +import { Search, Filter } from "lucide-react"; +import styles from "../styles/IngredientSearchPage.module.css"; +import { getDocuments, openPdf, type ApiDocument } from "../services/theapi"; + +export default function IngredientSearchPage() { + const [documents, setDocuments] = useState([]); + const [loading, setLoading] = useState(true); + + const [searchQuery, setSearchQuery] = useState(""); + const [sourceFilter, setSourceFilter] = useState("All Sources"); + const [typeFilter, setTypeFilter] = useState("All Types"); + const [dateFilter, setDateFilter] = useState("All Dates"); + + // FETCH API + useEffect(() => { + getDocuments() + .then((data) => setDocuments(Array.isArray(data) ? data : [])) + .catch((err) => { + console.error("API ERROR:", err); + setDocuments([]); + }) + .finally(() => setLoading(false)); + }, []); + + // FILTER OPTIONS + const allSources = useMemo(() => [ + "All Sources", + ...new Set(documents.map((d) => d.source)), + ], [documents]); + + const allTypes = useMemo(() => [ + "All Types", + ...new Set(documents.map((d) => d.type)), + ], [documents]); + + const allDates = useMemo(() => [ + "All Dates", + ...new Set(documents.map((d) => String(d.date ?? "").slice(0, 4))).values(), + ], [documents]); + + // FILTERED RESULTS + const filtered = useMemo(() => { + return documents + .filter((d) => { + const queryMatch = + searchQuery === "" || + d.ingredient.toLowerCase().includes(searchQuery.toLowerCase()) || + d.title.toLowerCase().includes(searchQuery.toLowerCase()); + + const sourceMatch = sourceFilter === "All Sources" || d.source === sourceFilter; + const typeMatch = typeFilter === "All Types" || d.type === typeFilter; + const safeDate = String(d.date ?? ""); + const dateMatch = dateFilter === "All Dates" || safeDate.startsWith(dateFilter); + + return queryMatch && sourceMatch && typeMatch && dateMatch; + }) + .sort((a, b) => String(b.date ?? "").localeCompare(String(a.date ?? ""))); + }, [documents, searchQuery, sourceFilter, typeFilter, dateFilter]); + + if (loading) { + return

Loading...

; + } + + return ( +
+
+ + {/* HEADER */} +
+

Ingredient Search

+

Search cosmetic ingredients and regulatory documents

+
+ + {/* SEARCH */} +
+
+ setSearchQuery(e.target.value)} + /> + +
+
+ + {/* FILTERS */} +
+
+ + Filters +
+
+ + + +
+
+ + {/* RESULTS */} +
+ {filtered.length} results found +
+ + {/* DOCUMENTS */} +
+ {filtered.length === 0 ? ( +
+

No documents found.

+
+ ) : ( + filtered.map((doc) => ( +
+
+
+

{doc.title}

+
+
+ Ingredient + {doc.ingredient} +
+
+ Source + {doc.source} +
+
+ Type + {doc.type} +
+
+ Date + {doc.date ?? "—"} +
+
+
+ + {doc.pdf_url && ( + + )} +
+
+ )) + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/pages/MentionsLegales.tsx b/src/pages/MentionsLegales.tsx new file mode 100644 index 0000000..f4691c2 --- /dev/null +++ b/src/pages/MentionsLegales.tsx @@ -0,0 +1,112 @@ +import { Scale, Building2, Globe, Mail } from 'lucide-react'; +import styles from '../styles/MentionsLegales.module.css'; + +export default function MentionsLegales() { + return ( +
+
+
+
+ +

Mentions Légales

+
+

Informations légales et réglementaires

+
+ +
+
+
+ +

Éditeur du Site

+
+
+

Raison sociale : RegWatch MedLabs

+

Forme juridique : [À compléter]

+

Capital social : [À compléter]

+

Siège social : [Adresse complète]

+

N° SIRET : [À compléter]

+

N° TVA intracommunautaire : [À compléter]

+
+
+ +
+

Directeur de la Publication

+
+

Nom : [À compléter]

+

Fonction : [À compléter]

+
+
+ +
+
+ +

Hébergement

+
+
+

Hébergeur : [Nom de l'hébergeur]

+

Adresse : [Adresse complète]

+

Téléphone : [Numéro]

+

Site web : [URL]

+
+
+ +
+
+ +

Contact

+
+
+

Email : contact@regwatch-medlabs.com

+

Téléphone : [À compléter]

+

Adresse postale : [À compléter]

+
+
+ +
+

Propriété Intellectuelle

+
+

+ L'ensemble de ce site relève de la législation française et internationale sur le droit d'auteur et la propriété intellectuelle. Tous les + droits de reproduction sont réservés, y compris pour les documents téléchargeables et les représentations iconographiques et + photographiques. +

+

+ La reproduction de tout ou partie de ce site sur un support électronique quel qu'il soit est formellement interdite sauf + autorisation expresse du directeur de la publication. +

+

+ Les marques de RegWatch MedLabs ainsi que les logos figurant sur le site sont des marques déposées. Toute reproduction totale + ou partielle de ces marques ou de ces logos effectuée à partir des éléments du site sans l'autorisation expresse de RegWatch + MedLabs est donc prohibée. +

+
+
+ +
+

Protection des Données Personnelles

+
+

+ Conformément à la loi « Informatique et Libertés » du 6 janvier 1978 modifiée et au Règlement Général sur la Protection des + Données (RGPD), vous disposez d'un droit d'accès, de rectification, de suppression et d'opposition aux données personnelles + vous concernant. +

+

+ Pour exercer ces droits, vous pouvez contacter : contact@regwatch-medlabs.com +

+
+
+ +
+

Cookies

+
+

+ Ce site peut utiliser des cookies pour améliorer l'expérience utilisateur. Vous pouvez configurer votre navigateur pour refuser les + cookies, mais certaines fonctionnalités du site pourraient ne pas être disponibles. +

+
+
+
+
+
+ ); +} diff --git a/src/pages/RecentUpdatesPage.tsx b/src/pages/RecentUpdatesPage.tsx new file mode 100644 index 0000000..0eea184 --- /dev/null +++ b/src/pages/RecentUpdatesPage.tsx @@ -0,0 +1,233 @@ +import { useEffect, useMemo, useState } from "react"; +import { TrendingUp, Filter, Eye } from "lucide-react"; +import styles from "../styles/RecentUpdatesPage.module.css"; +import { getDocuments, openPdf, type ApiDocument } from "../services/theapi"; + +const ITEMS_PER_PAGE = 8; + +export default function RecentUpdatesPage() { + const [updates, setUpdates] = useState([]); + const [loading, setLoading] = useState(true); + const [sourceFilter, setSourceFilter] = useState("All Sources"); + const [typeFilter, setTypeFilter] = useState("All Types"); + const [currentPage, setCurrentPage] = useState(1); + + useEffect(() => { + getDocuments() + .then((data) => setUpdates(Array.isArray(data) ? data : [])) + .catch((err) => { + console.error("API ERROR:", err); + setUpdates([]); + }) + .finally(() => setLoading(false)); + }, []); + + const allSources = useMemo( + () => ["All Sources", ...new Set(updates.map((u) => u.source))], + [updates] + ); + + const allTypes = useMemo( + () => ["All Types", ...new Set(updates.map((u) => u.type))], + [updates] + ); + + const filtered = useMemo(() => { + return updates + .filter((u) => { + const sourceMatch = + sourceFilter === "All Sources" || u.source === sourceFilter; + + const typeMatch = + typeFilter === "All Types" || u.type === typeFilter; + + return sourceMatch && typeMatch; + }) + .sort((a, b) => + String(b.date ?? "").localeCompare(String(a.date ?? "")) + ); + }, [updates, sourceFilter, typeFilter]); + + const totalPages = Math.max(1, Math.ceil(filtered.length / ITEMS_PER_PAGE)); + const safePage = Math.min(currentPage, totalPages); + + const paginated = filtered.slice( + (safePage - 1) * ITEMS_PER_PAGE, + safePage * ITEMS_PER_PAGE + ); + + if (loading) { + return ( +
+

Loading updates...

+
+ ); + } + + return ( +
+
+ {/* HEADER */} +
+
+ +

Recent Updates

+
+

+ Latest cosmetic regulatory updates and monitoring activity +

+
+ + {/* STATS */} +
+
+

Total Updates

+

{updates.length}

+
+ +
+
+ +
+

Sources

+

{allSources.length - 1}

+
+ +
+
+
+ + {/* FILTERS */} +
+
+ + Filters +
+ + + + +
+ + {/* RESULTS */} +
+ {filtered.length}{" "} + updates found +
+ + {/* TABLE */} +
+
+ Status + Title + Ingredient + Source + Type + Date + View +
+ + {paginated.map((u, index) => ( +
+
+ + New + +
+ +
+ {u.title} +
+ +
{u.ingredient}
+
{u.source}
+
{u.type}
+
{u.date ?? "—"}
+ +
+ {u.pdf_url && ( + + )} +
+
+ ))} +
+ + {/* PAGINATION */} + {totalPages > 1 && ( +
+ + + {Array.from({ length: totalPages }, (_, i) => i + 1).map( + (page) => ( + + ) + )} + + +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/src/services/theapi.ts b/src/services/theapi.ts new file mode 100644 index 0000000..337f78d --- /dev/null +++ b/src/services/theapi.ts @@ -0,0 +1,22 @@ +// Injecté au build par Vite (VITE_API_URL). Repli sur le backend local en dev. +export const API_URL = import.meta.env.VITE_API_URL ?? "http://127.0.0.1:8000"; + +export type ApiDocument = { + id: number; + title: string; + ingredient: string; + source: string; + type: string; + date?: string | null; + pdf_url: string; +}; + +export async function getDocuments(): Promise { + const res = await fetch(`${API_URL}/documents`); + if (!res.ok) throw new Error(`API error: ${res.status}`); + return res.json(); +} + +export function openPdf(pdfUrl: string): void { + window.open(pdfUrl, "_blank", "noopener,noreferrer"); +} \ No newline at end of file diff --git a/src/styles/AboutPage.module.css b/src/styles/AboutPage.module.css new file mode 100644 index 0000000..988d9e5 --- /dev/null +++ b/src/styles/AboutPage.module.css @@ -0,0 +1,173 @@ +.container { + padding: 2rem; +} + +.maxWidth { + max-width: 80rem; +} + +.header { + margin-bottom: 2rem; +} + +.title { + font-size: 2.5rem; + font-weight: 700; + color: rgb(17, 24, 39); + margin-bottom: 0.75rem; + line-height: 1.05; +} + +.subtitle { + font-size: 1.125rem; + color: rgb(75, 85, 99); + line-height: 1.6; + margin: 0; +} + +.section { + background-color: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + padding: 2rem; + margin-bottom: 1.5rem; +} + +.sectionHeader { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1.5rem; +} + +.sectionIcon { + width: 2.5rem; + height: 2.5rem; + background-color: rgb(219, 234, 254); + border-radius: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + color: rgb(37, 99, 235); + flex-shrink: 0; +} + +.sectionTitle { + font-size: 1.5rem; + font-weight: 600; + color: rgb(17, 24, 39); +} + +.sectionText { + color: rgb(75, 85, 99); + line-height: 1.6; + margin-bottom: 1rem; +} + +.sectionText:last-child { + margin-bottom: 0; +} + +.targetList { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.targetItem { + padding-left: 0; +} + +.targetTitle { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 1rem; + font-weight: 600; + color: rgb(17, 24, 39); + margin-bottom: 0.5rem; +} + +.targetDot { + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + background-color: rgb(37, 99, 235); + flex-shrink: 0; +} + +.targetDescription { + color: rgb(107, 114, 128); + font-size: 0.875rem; + margin-left: 1.5rem; +} + +.featuresGrid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1.5rem; +} + +.featureCard { + background-color: rgb(249, 250, 251); + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + padding: 1.5rem; +} + +.featureTitle { + font-size: 1.125rem; + font-weight: 600; + color: rgb(17, 24, 39); + margin-bottom: 0.5rem; +} + +.featureDescription { + color: rgb(107, 114, 128); + font-size: 0.875rem; + line-height: 1.5; +} + +.infoBox { + background-color: rgb(249, 250, 251); + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + padding: 1.5rem; +} + +.infoTitle { + font-size: 1.125rem; + font-weight: 600; + color: rgb(17, 24, 39); + margin-bottom: 1rem; +} + +.infoLines { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.infoLine { + color: rgb(75, 85, 99); + font-size: 0.875rem; +} + +.infoLabel { + font-weight: 600; + color: rgb(17, 24, 39); +} + +@media (max-width: 768px) { + .title { + font-size: 1.875rem; + } + + .featuresGrid { + grid-template-columns: 1fr; + } + + .section { + padding: 1.5rem; + } +} diff --git a/src/styles/AuthPage.module.css b/src/styles/AuthPage.module.css new file mode 100644 index 0000000..c2db038 --- /dev/null +++ b/src/styles/AuthPage.module.css @@ -0,0 +1,145 @@ +.container { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + background-color: #f8fafc; + padding: 20px; +} + +.authCard { + background: white; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + border: 1px solid #e2e8f0; + padding: 40px; + width: 100%; + max-width: 400px; + animation: slideIn 0.3s ease-out; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.title { + font-size: 28px; + font-weight: bold; + margin: 0 0 8px 0; + text-align: center; + color: #1a202c; +} + +.subtitle { + font-size: 14px; + color: #64748b; + text-align: center; + margin: 0 0 30px 0; +} + +.form { + display: flex; + flex-direction: column; + gap: 16px; +} + +.formGroup { + display: flex; + flex-direction: column; + gap: 8px; +} + +.formGroup label { + font-weight: 500; + color: #475569; + font-size: 14px; +} + +.formGroup input { + padding: 10px 12px; + border: 1px solid #cbd5e1; + border-radius: 6px; + font-size: 14px; + font-family: inherit; + transition: border-color 0.2s, box-shadow 0.2s; + background-color: #f8fafc; + color: #000; +} + +.formGroup input:focus { + outline: none; + border-color: #1a202c; + box-shadow: 0 0 0 3px rgba(26, 32, 44, 0.05); + background-color: white; +} + +.formGroup input::placeholder { + color: #94a3b8; +} + +.error { + background-color: #fee2e2; + border: 1px solid #fecaca; + color: #991b1b; + padding: 12px; + border-radius: 6px; + font-size: 14px; + text-align: center; +} + +.submitBtn { + padding: 12px; + background-color: #1a202c; + color: white; + border: none; + border-radius: 6px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; +} + +.submitBtn:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(26, 32, 44, 0.15); +} + +.submitBtn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.toggleAuth { + margin-top: 20px; + text-align: center; +} + +.toggleAuth p { + margin: 0; + font-size: 14px; + color: #64748b; +} + +.toggleBtn { + background: none; + border: none; + color: #1a202c; + font-weight: 600; + cursor: pointer; + text-decoration: underline; + font-size: 14px; + padding: 0; + margin-left: 4px; + transition: color 0.2s; +} + +.toggleBtn:hover { + color: #0f172a; +} diff --git a/src/styles/CGU.module.css b/src/styles/CGU.module.css new file mode 100644 index 0000000..6e0d133 --- /dev/null +++ b/src/styles/CGU.module.css @@ -0,0 +1,180 @@ +.page { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + overflow-y: auto; + background: #f9fafb; +} + +.content { + flex: 1; + padding: 40px 40px 32px; + max-width: 960px; +} + +.header { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 32px; +} + +.titleRow { + display: flex; + align-items: center; + gap: 12px; +} + +.titleIcon { + color: #1d4ed8; + flex-shrink: 0; +} + +.title { + font-size: 2.5rem; + font-weight: 700; + color: #111827; + letter-spacing: -0.02em; + margin: 0; + line-height: 1.05; +} + +.lastUpdate { + font-size: 13px; + color: #6b7280; + margin: 0; + padding-left: 2px; +} + +.sections { + display: flex; + flex-direction: column; + gap: 16px; +} + +.card { + background: #ffffff; + border: 1px solid #e5e7eb; + border-radius: 12px; + padding: 28px 32px; +} + +.cardTitle { + font-size: 17px; + font-weight: 700; + color: #111827; + margin: 0 0 12px 0; +} + +.prose { + display: flex; + flex-direction: column; + gap: 12px; +} + +.prose p { + font-size: 14px; + color: #374151; + line-height: 1.7; + margin: 0; +} + +.prose a { + color: #1d4ed8; + text-decoration: none; +} + +.prose a:hover { + text-decoration: underline; +} + +.checklist { + display: flex; + flex-direction: column; + gap: 10px; + margin: 12px 0; +} + +.checkItem { + display: flex; + align-items: flex-start; + gap: 10px; + font-size: 14px; + color: #374151; + line-height: 1.6; +} + +.checkIcon { + color: #10b981; + flex-shrink: 0; + margin-top: 2px; +} + +.prohibitList { + display: flex; + flex-direction: column; + gap: 10px; + margin: 12px 0; +} + +.prohibitItem { + display: flex; + align-items: flex-start; + gap: 10px; + font-size: 14px; + color: #374151; + line-height: 1.6; +} + +.prohibitIcon { + color: #ef4444; + flex-shrink: 0; + margin-top: 2px; +} + +.warningBox { + background: #fef3c7; + border: 1px solid #fcd34d; + border-radius: 8px; + padding: 16px; + margin: 12px 0; + display: flex; + gap: 12px; +} + +.warningIcon { + color: #b45309; + flex-shrink: 0; + margin-top: 2px; +} + +.warningContent { + display: flex; + flex-direction: column; + gap: 6px; +} + +.warningTitle { + font-weight: 700; + color: #b45309; + font-size: 14px; +} + +.warningText { + font-size: 14px; + color: #78350f; + line-height: 1.6; +} + +.subtitle { + font-size: 1.125rem; + font-weight: 500; + color: #6b7280; + margin: 0.75rem 0 1.5rem 0; + line-height: 1.6; +} + +.highlight { + color: #1d4ed8; +} diff --git a/src/styles/ContactPage.module.css b/src/styles/ContactPage.module.css new file mode 100644 index 0000000..4d5e33c --- /dev/null +++ b/src/styles/ContactPage.module.css @@ -0,0 +1,330 @@ +.container { + padding: 2rem; +} + +.maxWidth { + max-width: 80rem; +} + +.header { + margin-bottom: 2rem; +} + +.headerIcon { + width: 2.5rem; + height: 2.5rem; + display: flex; + align-items: center; + justify-content: center; + color: rgb(37, 99, 235); + margin-bottom: 1rem; +} + +.title { + font-size: 2.5rem; + font-weight: 700; + color: rgb(17, 24, 39); + margin-bottom: 0.75rem; + line-height: 1.05; +} + +.subtitle { + font-size: 1.125rem; + color: rgb(75, 85, 99); + line-height: 1.6; + margin: 0; +} + +.mainContent { + display: grid; + grid-template-columns: 1fr 1.2fr; + gap: 2rem; + margin-bottom: 2rem; +} + +.contactInfo { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.infoSection { + background-color: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + padding: 1.5rem; +} + +.infoSectionTitle { + font-size: 1.125rem; + font-weight: 600; + color: rgb(17, 24, 39); + margin-bottom: 1rem; + display: flex; + align-items: center; + gap: 0.75rem; +} + +.infoIcon { + color: rgb(37, 99, 235); + font-size: 1.25rem; +} + +.infoContent { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.infoItem { + color: rgb(75, 85, 99); + font-size: 0.875rem; + line-height: 1.5; +} + +.infoItemLink { + color: rgb(37, 99, 235); + text-decoration: none; + transition: color 0.2s; +} + +.infoItemLink:hover { + color: rgb(29, 78, 216); +} + +.infoItemBold { + font-weight: 600; + color: rgb(17, 24, 39); +} + +.infoItemSmall { + color: rgb(107, 114, 128); + font-size: 0.8rem; +} + +.openingHoursBox { + background-color: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + padding: 1.5rem; + margin-top: 1rem; +} + +.openingHoursTitle { + font-size: 1rem; + font-weight: 600; + color: rgb(17, 24, 39); + margin-bottom: 0.75rem; +} + +.openingHoursList { + display: flex; + flex-direction: column; + gap: 0.5rem; + font-size: 0.875rem; + color: rgb(75, 85, 99); +} + +.technicalSupport { + background-color: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + padding: 1.5rem; +} + +.technicalSupportTitle { + font-size: 1rem; + font-weight: 600; + color: rgb(17, 24, 39); + margin-bottom: 0.75rem; +} + +.technicalSupportText { + color: rgb(75, 85, 99); + font-size: 0.875rem; + margin-bottom: 1rem; +} + +.technicalSupportList { + display: flex; + flex-direction: column; + gap: 0.5rem; + color: rgb(75, 85, 99); + font-size: 0.875rem; +} + +.technicalSupportItem { + display: flex; + align-items: flex-start; + gap: 0.5rem; +} + +.technicalSupportBullet { + color: rgb(37, 99, 235); + margin-top: 0.25rem; +} + +/* Form Styles */ +.formSection { + background-color: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + padding: 2rem; +} + +.formTitle { + font-size: 1.25rem; + font-weight: 600; + color: rgb(17, 24, 39); + margin-bottom: 1.5rem; +} + +.formGrid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1.5rem; + margin-bottom: 1.5rem; +} + +.formGridFull { + grid-column: 1 / -1; +} + +.formGroup { + display: flex; + flex-direction: column; +} + +.formLabel { + font-size: 0.875rem; + font-weight: 500; + color: rgb(17, 24, 39); + margin-bottom: 0.5rem; +} + +.formLabelRequired::after { + content: ' *'; + color: rgb(220, 38, 38); +} + +.formInput, +.formSelect, +.formTextarea { + border: 1px solid rgb(229, 231, 235); + border-radius: 0.5rem; + padding: 0.75rem; + font-size: 0.875rem; + font-family: inherit; + transition: border-color 0.2s, box-shadow 0.2s; + background-color: white; + color: rgb(17, 24, 39); +} + +.formInput::placeholder, +.formSelect::placeholder, +.formTextarea::placeholder { + color: rgb(156, 163, 175); +} + +.formInput:focus, +.formSelect:focus, +.formTextarea:focus { + outline: none; + border-color: rgb(37, 99, 235); + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); +} + +.formTextarea { + resize: vertical; + min-height: 8rem; +} + +.disclaimerText { + font-size: 0.75rem; + color: rgb(75, 85, 99); + margin-bottom: 1.5rem; + line-height: 1.5; +} + +.submitButton { + display: flex; + align-items: center; + gap: 0.5rem; + background-color: rgb(37, 99, 235); + color: white; + font-weight: 600; + padding: 0.75rem 1.5rem; + border-radius: 0.5rem; + border: none; + cursor: pointer; + transition: background-color 0.2s; + font-size: 1rem; + width: fit-content; +} + +.submitButton:hover { + background-color: rgb(29, 78, 216); +} + +.faqSection { + background-color: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + padding: 2rem; +} + +.faqTitle { + font-size: 1.25rem; + font-weight: 600; + color: rgb(17, 24, 39); + margin-bottom: 1.5rem; +} + +.faqItems { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.faqItem { + border-left: 3px solid rgb(37, 99, 235); + padding-left: 1rem; +} + +.faqQuestion { + font-weight: 600; + color: rgb(17, 24, 39); + margin-bottom: 0.5rem; +} + +.faqAnswer { + color: rgb(75, 85, 99); + font-size: 0.875rem; + line-height: 1.5; +} + +.faqLink { + color: rgb(37, 99, 235); + text-decoration: none; + transition: color 0.2s; +} + +.faqLink:hover { + color: rgb(29, 78, 216); +} + +@media (max-width: 1024px) { + .mainContent { + grid-template-columns: 1fr; + } +} + +@media (max-width: 768px) { + .title { + font-size: 1.875rem; + } + + .formGrid { + grid-template-columns: 1fr; + } +} diff --git a/src/styles/DocumentsPage.module.css b/src/styles/DocumentsPage.module.css new file mode 100644 index 0000000..0210eb3 --- /dev/null +++ b/src/styles/DocumentsPage.module.css @@ -0,0 +1,284 @@ +.container { + padding: 2rem; +} + +.maxWidth { + max-width: 80rem; +} + +.header { + margin-bottom: 1.75rem; +} + +.title { + font-size: 2.25rem; + font-weight: 700; + color: rgb(17, 24, 39); + margin-bottom: 0.5rem; + line-height: 1.1; +} + +.description { + color: rgb(75, 85, 99); + margin-bottom: 1.5rem; + font-size: 1.125rem; + line-height: 1.6; +} + +.filtersBox { + background-color: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + padding: 1.25rem; + margin-bottom: 1.25rem; + display: flex; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; +} + +.filterLabel { + display: flex; + align-items: center; + gap: 0.5rem; + color: rgb(55, 65, 81); + font-weight: 500; + font-size: 0.875rem; +} + +.filterIcon { + font-size: 1rem; +} + +.select { + border: 1px solid rgb(209, 213, 219); + border-radius: 0.5rem; + font-size: 0.875rem; + padding: 0.375rem 0.75rem; + color: rgb(55, 65, 81); + background-color: white; + cursor: pointer; +} + +.select:focus { + outline: none; + box-shadow: 0 0 0 2px rgb(59, 130, 246); +} + +.sortWrapper { + margin-left: auto; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.sortIcon { + color: rgb(107, 114, 128); +} + +.statsRow { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.75rem; +} + +.statsText { + font-size: 0.875rem; + color: rgb(55, 65, 81); +} + +.statsNumber { + font-weight: 600; +} + +.exportButton { + display: flex; + align-items: center; + gap: 0.5rem; + border: 1px solid rgb(209, 213, 219); + border-radius: 0.5rem; + font-size: 0.875rem; + padding: 0.375rem 1rem; + color: rgb(55, 65, 81); + background-color: white; + cursor: pointer; + transition: background-color 0.2s; +} + +.exportButton:hover { + background-color: rgb(249, 250, 251); +} + +.table { + background-color: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + overflow: auto; + -webkit-overflow-scrolling: touch; +} + +.tableHeader { + display: grid; + grid-template-columns: minmax(180px,2fr) 1.2fr 1fr 1.2fr 0.8fr 0.6fr; + background-color: rgb(249, 250, 251); + border-bottom: 1px solid rgb(229, 231, 235); + padding: 0.75rem 1.25rem; + color: rgb(17, 24, 39); +} + +.tableRow { + display: grid; + grid-template-columns: minmax(180px,2fr) 1.2fr 1fr 1.2fr 0.8fr 0.6fr; + padding: 1rem 1.25rem; + align-items: center; + color: rgb(17, 24, 39); +} + +.tableRow span, +.titleCell span, +.titleCell { + color: rgb(17, 24, 39); +} + +.headerCell { + font-size: 0.875rem; + font-weight: 600; + color: rgb(55, 65, 81); +} + +.tableRow { + display: grid; + grid-template-columns: 2fr 1.2fr 1fr 1.2fr 0.8fr 0.6fr; + padding: 1rem 1.25rem; + align-items: center; +} + +.tableRow:hover { + background-color: rgb(249, 250, 251); + transition: background-color 0.2s; +} + +.tableRowBorder { + border-bottom: 1px solid rgb(229, 231, 235); +} + +.titleCell { + display: flex; + align-items: flex-start; + gap: 0.625rem; +} + +.titleIcon { + color: rgb(156, 163, 175); + margin-top: 0.125rem; + flex-shrink: 0; +} + +.titleText { + font-size: 0.875rem; + color: rgb(31, 41, 55); + line-height: 1.5; +} + +.ingredientBadge { + display: inline-block; + font-size: 0.75rem; + padding: 0.625rem; + border-radius: 0.375rem; + border: 1px solid rgb(209, 213, 219); + background-color: rgb(243, 244, 246); +} + +.ingredientBadgeBold { + font-weight: 600; + border-color: rgb(209, 213, 219); + color: rgb(17, 24, 39); +} + +.ingredientBadgeNormal { + border-color: rgb(229, 231, 235); + color: rgb(55, 65, 81); +} + +.cell { + font-size: 0.875rem; + color: rgb(75, 85, 99); +} + +.cellWhitespace { + white-space: nowrap; +} + +.openButton { + background-color: rgb(37, 99, 235); + color: white; + font-size: 0.875rem; + font-weight: 500; + padding: 0.375rem 1rem; + border-radius: 0.5rem; + border: none; + cursor: pointer; + transition: background-color 0.2s; +} + +.openButton:hover { + background-color: rgb(29, 78, 216); +} + +.pagination { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + margin-top: 1.5rem; + flex-wrap: wrap; + overflow-x: auto; + padding-bottom: 0.5rem; +} + +.paginationButton { + padding: 0.45rem 0.85rem; + font-size: 0.85rem; + border: 1px solid rgb(209, 213, 219); + border-radius: 0.5rem; + color: rgb(55, 65, 81); + background-color: white; + cursor: pointer; + transition: all 0.2s; +} + +.paginationButton:hover:not(:disabled) { + background-color: rgb(249, 250, 251); +} + +.paginationButton:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.pageNumber { + min-width: 2rem; + padding: 0.45rem 0.75rem; + height: 2.25rem; + font-size: 0.875rem; + border-radius: 0.5rem; + font-weight: 500; + transition: all 0.2s; +} + +.pageNumberActive { + background-color: rgb(37, 99, 235); + color: white; + border-color: rgb(37, 99, 235); +} + +.pageNumberInactive { + border: 1px solid rgb(209, 213, 219); + color: rgb(55, 65, 81); + background-color: white; +} + +.pageNumberInactive:hover { + background-color: rgb(249, 250, 251); +} diff --git a/src/styles/Footer.module.css b/src/styles/Footer.module.css new file mode 100644 index 0000000..5721c1b --- /dev/null +++ b/src/styles/Footer.module.css @@ -0,0 +1,46 @@ +.footer { + margin-left: 16rem; + border-top: 1px solid rgb(229, 231, 235); + background-color: white; + padding: 1rem 2rem; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 0.875rem; + color: rgb(107, 114, 128); +} + +.copyright { + font-size: 0.875rem; + color: rgb(107, 114, 128); +} + +.links { + display: flex; + gap: 1.5rem; +} + +.link { + color: rgb(107, 114, 128); + text-decoration: none; + transition: color 0.2s; +} + +.link:hover { + color: rgb(55, 65, 81); +} + +@media (max-width: 768px) { + .footer { + margin-left: 0; + flex-direction: column; + gap: 1rem; + text-align: center; + } + + .links { + flex-wrap: wrap; + justify-content: center; + gap: 1rem; + } +} diff --git a/src/styles/HomePage.module.css b/src/styles/HomePage.module.css new file mode 100644 index 0000000..a688880 --- /dev/null +++ b/src/styles/HomePage.module.css @@ -0,0 +1,188 @@ +.container { + padding: 2rem; +} + +.maxWidth { + max-width: 80rem; +} + +.heroSection { + text-align: center; + margin-bottom: 2.5rem; +} + +.title { + font-size: 2.5rem; + font-weight: 700; + color: rgb(17, 24, 39); + margin-bottom: 0.75rem; + line-height: 1.05; +} + +.subtitle { + font-size: 1.125rem; + color: rgb(75, 85, 99); + margin-bottom: 1rem; + line-height: 1.6; +} + +.description { + color: rgb(107, 114, 128); + font-size: 1.125rem; + line-height: 1.7; +} + + +.cardsGrid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1.25rem; + margin-bottom: 2.5rem; +} + +.card { + background-color: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + padding: 1.5rem; + text-align: left; + cursor: pointer; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.card:hover { + border-color: rgb(209, 213, 219); + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); +} + +.cardIconWrapper { + width: 3rem; + height: 3rem; + background-color: rgb(219, 234, 254); + border-radius: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 1rem; +} + +.cardIcon { + font-size: 1.5rem; + color: rgb(37, 99, 235); +} + +.cardTitle { + font-size: 1.125rem; + font-weight: 600; + color: rgb(17, 24, 39); + margin-bottom: 0.5rem; +} + +.cardDescription { + color: rgb(107, 114, 128); + font-size: 0.875rem; +} + +.recentSection { + background-color: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + padding: 1.5rem; +} + +.sectionHeader { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 1.25rem; +} + +.sectionIcon { + color: rgb(17, 24, 39); +} + +.sectionTitle { + font-size: 1.25rem; + font-weight: 600; + color: rgb(17, 24, 39); +} + +.documentsList { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.documentItem { + padding: 1rem; + border-radius: 0.5rem; + transition: background-color 0.2s; +} + +.documentItem:hover { + background-color: rgb(249, 250, 251); +} + +.documentItemNotLast { + border-bottom: 1px solid rgb(229, 231, 235); +} + +.documentTitle { + font-weight: 600; + color: rgb(17, 24, 39); + margin-bottom: 0.5rem; +} + +.documentMeta { + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + gap: 1rem; + row-gap: 0.75rem; + font-size: 0.875rem; + color: rgb(75, 85, 99); +} + +.metaLabel { + color: rgb(17, 24, 39); + font-weight: 500; +} + +.documentButton { + margin-left: 0; + justify-self: end; + background-color: rgb(37, 99, 235); + color: white; + font-size: 0.875rem; + font-weight: 500; + padding: 0.625rem 1rem; + border-radius: 0.5rem; + border: none; + cursor: pointer; + transition: background-color 0.2s; + white-space: nowrap; +} + +.documentButton:hover { + background-color: rgb(29, 78, 216); +} + +@media (max-width: 1024px) { + .cardsGrid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 768px) { + .cardsGrid { + grid-template-columns: 1fr; + } + + .title { + font-size: 2rem; + } + + .subtitle { + font-size: 1rem; + } +} diff --git a/src/styles/IngredientSearchPage.module.css b/src/styles/IngredientSearchPage.module.css new file mode 100644 index 0000000..53688ed --- /dev/null +++ b/src/styles/IngredientSearchPage.module.css @@ -0,0 +1,215 @@ +.container { + padding: 2rem; +} + +.maxWidth { + max-width: 80rem; +} + +.header { + margin-bottom: 1.75rem; +} + +.title { + font-size: 2.25rem; + font-weight: 700; + color: rgb(17, 24, 39); + margin-bottom: 0.75rem; + line-height: 1.1; +} + +.subtitle { + color: rgb(75, 85, 99); + margin-bottom: 1.5rem; + font-size: 1.125rem; + line-height: 1.6; +} + +.searchBarWrapper { + margin-bottom: 1.5rem; +} + +.searchBar { + position: relative; +} + +.searchInput { + width: 100%; + padding: 0.75rem 1.25rem; + color: rgb(55, 65, 81); + background-color: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + font-size: 1rem; +} + +.searchInput::placeholder { + color: rgb(156, 163, 175); +} + +.searchInput:focus { + outline: none; + box-shadow: 0 0 0 2px rgb(59, 130, 246); +} + +.searchIcon { + position: absolute; + right: 1.25rem; + top: 0.875rem; + color: rgb(156, 163, 175); + pointer-events: none; +} + +.filtersBox { + background-color: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + padding: 1.5rem; + margin-bottom: 1.5rem; +} + +.filterHeader { + display: flex; + align-items: center; + gap: 0.5rem; + color: rgb(55, 65, 81); + font-weight: 500; + font-size: 0.875rem; + margin-bottom: 1rem; +} + +.filterIcon { + font-size: 1rem; +} + +.filterControls { + display: flex; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; +} + +.select { + border: 1px solid rgb(209, 213, 219); + border-radius: 0.5rem; + font-size: 0.875rem; + padding: 0.375rem 0.75rem; + color: rgb(55, 65, 81); + background-color: white; + cursor: pointer; +} + +.select:focus { + outline: none; + box-shadow: 0 0 0 2px rgb(59, 130, 246); +} + +.resultsInfo { + color: rgb(75, 85, 99); + margin-bottom: 1.25rem; + font-size: 0.875rem; +} + +.resultsCount { + font-weight: 600; +} + +.resultsQuery { + font-weight: 600; +} + +.documentsList { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.documentCard { + background-color: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + padding: 1.5rem; + transition: box-shadow 0.2s; +} + +.documentCard:hover { + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); +} + +.documentCardFlex { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.documentContent { + flex: 1; +} + +.documentTitle { + font-size: 1.125rem; + font-weight: 600; + color: rgb(17, 24, 39); + margin-bottom: 0.75rem; +} + +.documentMeta { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.75rem; + font-size: 0.875rem; +} + +.metaItem { + display: flex; + flex-direction: column; +} + +.metaLabel { + color: rgb(107, 114, 128); +} + +.metaValue { + font-weight: 600; + color: rgb(17, 24, 39); +} + +.openButton { + background-color: rgb(37, 99, 235); + color: white; + font-weight: 500; + padding: 0.625rem 1.5rem; + border-radius: 0.5rem; + border: none; + cursor: pointer; + transition: background-color 0.2s; + white-space: nowrap; +} + +.openButton:hover { + background-color: rgb(29, 78, 216); +} + +.emptyState { + text-align: center; + padding: 3rem 0; +} + +.emptyMessage { + color: rgb(107, 114, 128); +} + +@media (max-width: 768px) { + .documentCardFlex { + flex-direction: column; + } + + .documentMeta { + grid-template-columns: 1fr; + } + + .openButton { + width: 100%; + } +} diff --git a/src/styles/MentionsLegales.module.css b/src/styles/MentionsLegales.module.css new file mode 100644 index 0000000..e4dd2e2 --- /dev/null +++ b/src/styles/MentionsLegales.module.css @@ -0,0 +1,132 @@ +.page { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + overflow-y: auto; + background: #f9fafb; +} + +.content { + flex: 1; + padding: 40px 40px 32px; + max-width: 960px; +} + +.header { + display: flex; + flex-direction: column; + gap: 6px; + margin-bottom: 32px; +} + +.titleRow { + display: flex; + align-items: center; + gap: 12px; +} + +.titleIcon { + color: #1d4ed8; + flex-shrink: 0; +} + +.title { + font-size: 2.5rem; + font-weight: 700; + color: #111827; + letter-spacing: -0.02em; + margin: 0; + line-height: 1.05; +} + +.subtitle { + font-size: 1.125rem; + color: #6b7280; + margin: 0.75rem 0 1.5rem 0; + line-height: 1.6; + padding-left: 2px; +} +.sections { + display: flex; + flex-direction: column; + gap: 16px; +} + +.card { + background: #ffffff; + border: 1px solid #e5e7eb; + border-radius: 12px; + padding: 28px 32px; +} + +.cardHeader { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 20px; +} + +.cardIcon { + color: #1d4ed8; + flex-shrink: 0; +} + +.cardTitle { + font-size: 17px; + font-weight: 700; + color: #111827; + margin: 0; +} + +.fields { + display: flex; + flex-direction: column; + gap: 10px; +} + +.field { + font-size: 14px; + color: #374151; + line-height: 1.6; +} + +.field strong { + font-weight: 600; + color: #111827; +} + +.field a { + color: #1d4ed8; + text-decoration: none; +} + +.field a:hover { + text-decoration: underline; +} + +.prose { + display: flex; + flex-direction: column; + gap: 12px; +} + +.prose p { + font-size: 14px; + color: #374151; + line-height: 1.7; + margin: 0; +} + +.prose p a, +.prose p span.link { + color: #1d4ed8; +} + +.highlight { + color: #1d4ed8; +} + +.sectionContent { + margin-top: 16px; +} diff --git a/src/styles/RecentUpdatesPage.module.css b/src/styles/RecentUpdatesPage.module.css new file mode 100644 index 0000000..7e1d781 --- /dev/null +++ b/src/styles/RecentUpdatesPage.module.css @@ -0,0 +1,330 @@ +.container { + padding: 2rem; +} + +.maxWidth { + max-width: 96rem; +} + +.header { + margin-bottom: 2rem; +} + +.headerFlex { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.25rem; +} + +.headerIcon { + color: rgb(37, 99, 235); + font-size: 1.75rem; +} + +.title { + font-size: 2.25rem; + font-weight: 700; + color: rgb(17, 24, 39); + line-height: 1.1; +} + +.subtitle { + color: rgb(75, 85, 99); + margin-bottom: 2rem; + font-size: 1.125rem; + line-height: 1.6; +} + +.statsGrid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1rem; + margin-bottom: 2rem; +} + +.statCard { + background-color: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + padding: 1.25rem; +} + +.statLabel { + color: rgb(75, 85, 99); + font-size: 0.875rem; + font-weight: 500; + margin-bottom: 0.5rem; +} + +.statValue { + font-size: 1.875rem; + font-weight: bold; + color: rgb(17, 24, 39); +} + +.statIcon { + margin-top: 0.75rem; + width: 2rem; + height: 2rem; + border-radius: 0.5rem; + display: flex; + align-items: center; + justify-content: center; +} + +.statIconBlue { + background-color: rgb(219, 234, 254); + color: rgb(37, 99, 235); +} + +.statIconGreen { + background-color: rgb(220, 252, 231); + color: rgb(34, 197, 94); +} + +.filtersBox { + background-color: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + padding: 1.5rem; + margin-bottom: 1.5rem; + display: flex; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; +} + +.filterLabel { + display: flex; + align-items: center; + gap: 0.5rem; + color: rgb(55, 65, 81); + font-weight: 500; + font-size: 0.875rem; +} + +.filterIcon { + font-size: 1rem; +} + +.select { + border: 1px solid rgb(209, 213, 219); + border-radius: 0.5rem; + font-size: 0.875rem; + padding: 0.375rem 0.75rem; + color: rgb(55, 65, 81); + background-color: white; + cursor: pointer; +} + +.select:focus { + outline: none; + box-shadow: 0 0 0 2px rgb(59, 130, 246); +} + +.exportWrapper { + margin-left: auto; +} + +.exportButton { + display: flex; + align-items: center; + gap: 0.5rem; + border: 1px solid rgb(209, 213, 219); + border-radius: 0.5rem; + font-size: 0.875rem; + padding: 0.375rem 1rem; + color: rgb(55, 65, 81); + background-color: white; + cursor: pointer; + transition: background-color 0.2s; +} + +.exportButton:hover { + background-color: rgb(249, 250, 251); +} + +.resultsInfo { + color: rgb(75, 85, 99); + margin-bottom: 1rem; + font-size: 0.875rem; +} + +.resultsCount { + font-weight: 600; +} + +.table { + background-color: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + overflow: auto; + -webkit-overflow-scrolling: touch; +} + +.tableHeader { + display: grid; + grid-template-columns: 0.8fr minmax(180px,2fr) 1fr 1.2fr 1fr 0.8fr 0.6fr; + gap: 1rem; + background-color: rgb(249, 250, 251); + border-bottom: 1px solid rgb(229, 231, 235); + padding: 0.75rem 1.5rem; +} + +.headerCell { + font-size: 0.875rem; + font-weight: 600; + color: rgb(55, 65, 81); +} + +.tableRow { + display: grid; + grid-template-columns: 0.8fr minmax(180px,2fr) 1fr 1.2fr 1fr 0.8fr 0.6fr; + gap: 1rem; + padding: 1rem 1.5rem; + align-items: center; +} + +.tableRow:hover { + background-color: rgb(249, 250, 251); + transition: background-color 0.2s; +} + +.tableRowBorder { + border-bottom: 1px solid rgb(229, 231, 235); +} + +.statusBadge { + display: inline-block; + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 600; +} + +.statusBadgeNew { + background-color: rgb(220, 252, 231); + color: rgb(22, 163, 74); +} + +.statusBadgeUpdated { + background-color: rgb(219, 234, 254); + color: rgb(37, 99, 235); +} + +.titleCell { + font-weight: 500; + color: rgb(17, 24, 39); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.cell { + font-size: 0.875rem; + color: rgb(75, 85, 99); +} + +.cellTruncate { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.viewButton { + background-color: rgb(37, 99, 235); + color: white; + font-size: 0.75rem; + font-weight: 500; + padding: 0.375rem 0.75rem; + border-radius: 0.375rem; + border: none; + cursor: pointer; + transition: background-color 0.2s; +} + +.viewButton:hover { + background-color: rgb(29, 78, 216); +} + +.pagination { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + margin-top: 1.5rem; +} + +.paginationButton { + padding: 0.5rem 1rem; + font-size: 0.875rem; + border: 1px solid rgb(209, 213, 219); + border-radius: 0.5rem; + color: rgb(55, 65, 81); + background-color: white; + cursor: pointer; + transition: all 0.2s; +} + +.paginationButton:hover:not(:disabled) { + background-color: rgb(249, 250, 251); +} + +.paginationButton:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.pageNumber { + width: 2.25rem; + height: 2.25rem; + font-size: 0.875rem; + border-radius: 0.5rem; + font-weight: 500; + transition: all 0.2s; +} + +.pageNumberActive { + background-color: rgb(37, 99, 235); + color: white; +} + +.pageNumberInactive { + border: 1px solid rgb(209, 213, 219); + color: rgb(55, 65, 81); + background-color: white; +} + +.pageNumberInactive:hover { + background-color: rgb(249, 250, 251); +} + +@media (max-width: 1024px) { + .statsGrid { + grid-template-columns: repeat(2, 1fr); + } + + .tableHeader, + .tableRow { + grid-template-columns: 0.6fr 1.5fr 0.8fr 1fr 0.8fr 0.6fr 0.5fr; + } +} + +@media (max-width: 768px) { + .statsGrid { + grid-template-columns: 1fr; + } + + .tableHeader, + .tableRow { + grid-template-columns: 0.6fr 1.2fr 0.6fr 0.8fr 0.6fr 0.5fr 0.4fr; + gap: 0.5rem; + padding: 0.75rem 1rem; + } + + .headerCell, + .cell { + font-size: 0.75rem; + } +} diff --git a/src/styles/Sidebar.module.css b/src/styles/Sidebar.module.css new file mode 100644 index 0000000..f6822cf --- /dev/null +++ b/src/styles/Sidebar.module.css @@ -0,0 +1,150 @@ +.sidebar { + width: 16rem; + background-color: white; + border-right: 1px solid rgb(229, 231, 235); + display: flex; + flex-direction: column; + position: fixed; + top: 0; + left: 0; + height: 100%; + z-index: 10; +} + +.header { + padding: 1.5rem 1.25rem; + border-bottom: 1px solid rgb(229, 231, 235); +} + +.title { + font-size: 1.5rem; + font-weight: 700; + color: rgb(17, 24, 39); + line-height: 1.2; + margin-bottom: 0.5rem; +} + +.subtitle { + font-size: 1rem; + color: rgb(75, 85, 99); + margin: 0; + line-height: 1.6; +} + +.nav { + flex: 1; + padding: 1rem 0.75rem; +} + +.navButton { + width: 100%; + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.625rem 0.75rem; + border-radius: 0.5rem; + font-size: 0.875rem; + font-weight: 500; + transition: background-color 0.2s; + margin-bottom: 0.25rem; + border: none; + cursor: pointer; + background: white; +} + +.navButtonActive { + background-color: rgb(243, 244, 246); + color: rgb(17, 24, 39); +} + +.navButtonInactive { + color: rgb(55, 65, 81); +} + +.navButtonInactive:hover { + background-color: rgb(249, 250, 251); +} + +.navIconActive { + color: rgb(75, 85, 99); +} + +.navIconInactive { + color: rgb(156, 163, 175); +} + +/* ===== USER ===== */ + +.userSection { + display: flex; + justify-content: center; + padding: 0.75rem 0; +} + +.userAvatar { + width: 34px; + height: 34px; + + display: flex; + align-items: center; + justify-content: center; + + border-radius: 50%; + + background: rgb(249, 250, 251); + border: 1px solid rgb(229, 231, 235); + + color: rgb(107, 114, 128); + + font-size: 0.75rem; + font-weight: 600; + + cursor: pointer; + + transition: all 0.2s ease; +} + +.userAvatar:hover { + background: rgb(243, 244, 246); + border-color: rgb(209, 213, 219); +} + +/* ===== LOGOUT ===== */ + +.logoutBtn { + display: flex; + align-items: center; + gap: 0.75rem; + + width: calc(100% - 1.5rem); + margin: 0 0.75rem 1rem 0.75rem; + + padding: 0.625rem 0.75rem; + + border-radius: 0.5rem; + border: 1px solid rgb(229, 231, 235); + + background: white; + color: rgb(107, 114, 128); + + font-size: 0.875rem; + font-weight: 500; + + cursor: pointer; + + transition: all 0.2s ease; +} + +.logoutBtn:hover { + background: rgb(249, 250, 251); + border-color: rgb(209, 213, 219); +} + +@media (max-width: 768px) { + .sidebar { + width: 100%; + position: relative; + height: auto; + margin-bottom: 1rem; + } +} \ No newline at end of file diff --git a/src/styles/global.css b/src/styles/global.css new file mode 100644 index 0000000..f7f2bfc --- /dev/null +++ b/src/styles/global.css @@ -0,0 +1,51 @@ +body { + margin: 0; + font-family: "Segoe UI", sans-serif; + background: #f4f6f8; +} + +.layout { + display: flex; +} + +.sidebar { + width: 240px; + height: 100vh; + background: #1f2937; + color: white; + padding: 20px; +} + +.sidebar h2 { + margin-bottom: 30px; +} + +.sidebar a { + display: block; + color: #cbd5e1; + margin-bottom: 15px; + text-decoration: none; +} + +.sidebar a:hover { + color: white; +} + +.content { + flex: 1; + padding: 30px; +} + +.cards { + display: flex; + gap: 20px; + margin-top: 20px; +} + +.card { + background: white; + padding: 20px; + border-radius: 10px; + width: 200px; + box-shadow: 0 2px 5px rgba(0,0,0,0.05); +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..75c0779 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,7 @@ +export interface Document { + id: number + title: string + ingredient: string | null + source: string + pdf: string | null +} diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..7f42e5f --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..d3c52ea --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "module": "esnext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..d536e79 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite' +import react, { reactCompilerPreset } from '@vitejs/plugin-react' +import babel from '@rolldown/plugin-babel' +import path from "path" + +export default defineConfig({ + plugins: [ + react(), + babel({ presets: [reactCompilerPreset()] }) + ], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +}) \ No newline at end of file