From 2ed7a48d365a94725c645ecd4e9467f0baddd635 Mon Sep 17 00:00:00 2001 From: Bernard Ngandu <31113941+bernard-ng@users.noreply.github.com> Date: Fri, 10 Oct 2025 16:13:48 +0200 Subject: [PATCH] feat: add distance value object and ci workflows --- .github/workflows/backend-quality.yml | 65 + .github/workflows/backend-tests.yml | 40 + .github/workflows/frontend-quality.yml | 81 + client/.prettierrc | 11 + client/eslint.config.js | 111 +- client/package-lock.json | 2606 +++++++++++++++++ client/package.json | 9 + client/postcss.config.js | 2 +- client/src/App.tsx | 568 ++-- client/src/components/layout/AppHeader.tsx | 83 +- .../src/components/layout/LanguageToggle.tsx | 23 +- client/src/components/layout/ThemeToggle.tsx | 16 +- client/src/components/map/MapViewport.tsx | 40 +- .../src/components/panels/ActivityPanel.tsx | 40 +- .../components/panels/HotspotStatsPanel.tsx | 46 +- .../src/components/panels/OverviewPanel.tsx | 64 +- client/src/components/ui/alert-dialog.tsx | 77 +- client/src/components/ui/badge.tsx | 43 +- client/src/components/ui/button.tsx | 53 +- client/src/components/ui/card.tsx | 70 +- client/src/components/ui/scroll-area.tsx | 33 +- client/src/components/ui/separator.tsx | 20 +- client/src/components/ui/sheet.tsx | 116 +- client/src/components/ui/tabs.tsx | 36 +- client/src/components/ui/toast.tsx | 89 +- client/src/components/ui/toaster.tsx | 19 +- client/src/components/ui/use-toast.ts | 130 +- client/src/hooks/useHotspotFeed.ts | 305 +- client/src/hooks/useLeafletHeatmap.ts | 282 +- client/src/hooks/useTheme.ts | 44 +- client/src/hooks/useUserLocation.ts | 104 +- client/src/index.css | 23 +- client/src/lib/i18n.ts | 22 +- client/src/lib/utils.ts | 80 +- client/src/main.tsx | 21 +- client/src/store/useAppStore.ts | 28 +- client/src/types/api.ts | 40 +- client/src/types/leaflet-heat.d.ts | 25 +- client/tailwind.config.js | 65 +- client/tsconfig.json | 5 +- client/vite.config.ts | 12 +- .../src/Service/PointProximityValidator.php | 16 +- server/src/ValueObject/Distance.php | 38 + .../tests/Unit/ValueObject/DistanceTest.php | 32 + 44 files changed, 4263 insertions(+), 1370 deletions(-) create mode 100644 .github/workflows/backend-quality.yml create mode 100644 .github/workflows/backend-tests.yml create mode 100644 .github/workflows/frontend-quality.yml create mode 100644 client/.prettierrc create mode 100644 server/src/ValueObject/Distance.php create mode 100644 server/tests/Unit/ValueObject/DistanceTest.php diff --git a/.github/workflows/backend-quality.yml b/.github/workflows/backend-quality.yml new file mode 100644 index 0000000..5a95228 --- /dev/null +++ b/.github/workflows/backend-quality.yml @@ -0,0 +1,65 @@ +name: Backend Quality + +on: + push: + paths: + - "server/**" + - ".github/workflows/backend-quality.yml" + pull_request: + paths: + - "server/**" + - ".github/workflows/backend-quality.yml" + +jobs: + static-analysis: + name: PHPStan + runs-on: ubuntu-latest + defaults: + run: + working-directory: server + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.2" + coverage: none + tools: composer + extensions: mbstring + ini-values: memory_limit=-1 + cache: composer + + - name: Install dependencies + run: composer install --no-interaction --prefer-dist + + - name: Run PHPStan + run: vendor/bin/phpstan analyse --memory-limit=1G + + coding-standards: + name: ECS + runs-on: ubuntu-latest + needs: static-analysis + defaults: + run: + working-directory: server + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.2" + coverage: none + tools: composer + extensions: mbstring + ini-values: memory_limit=-1 + cache: composer + + - name: Install dependencies + run: composer install --no-interaction --prefer-dist + + - name: Check coding standards + run: vendor/bin/ecs check diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml new file mode 100644 index 0000000..0d0f197 --- /dev/null +++ b/.github/workflows/backend-tests.yml @@ -0,0 +1,40 @@ +name: Backend Tests + +on: + push: + paths: + - "server/**" + - ".github/workflows/backend-tests.yml" + pull_request: + paths: + - "server/**" + - ".github/workflows/backend-tests.yml" + +jobs: + phpunit: + name: PHPUnit + runs-on: ubuntu-latest + + defaults: + run: + working-directory: server + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.2" + coverage: none + tools: composer + extensions: mbstring + ini-values: memory_limit=-1 + cache: composer + + - name: Install dependencies + run: composer install --no-interaction --prefer-dist + + - name: Run PHPUnit + run: vendor/bin/phpunit diff --git a/.github/workflows/frontend-quality.yml b/.github/workflows/frontend-quality.yml new file mode 100644 index 0000000..ab902f7 --- /dev/null +++ b/.github/workflows/frontend-quality.yml @@ -0,0 +1,81 @@ +name: Frontend Quality + +on: + push: + paths: + - "client/**" + - ".github/workflows/frontend-quality.yml" + pull_request: + paths: + - "client/**" + - ".github/workflows/frontend-quality.yml" + +jobs: + eslint: + name: ESLint + runs-on: ubuntu-latest + defaults: + run: + working-directory: client + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: client/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Run ESLint + run: npm run lint + + prettier: + name: Prettier + runs-on: ubuntu-latest + defaults: + run: + working-directory: client + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: client/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Check formatting + run: npx prettier --check . + + typecheck: + name: Type Check + runs-on: ubuntu-latest + defaults: + run: + working-directory: client + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: client/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Run TypeScript type check + run: npm run typecheck diff --git a/client/.prettierrc b/client/.prettierrc new file mode 100644 index 0000000..96eff78 --- /dev/null +++ b/client/.prettierrc @@ -0,0 +1,11 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": false, + "tabWidth": 2, + "useTabs": false, + "printWidth": 120, + "bracketSpacing": true, + "arrowParens": "avoid", + "endOfLine": "lf" +} diff --git a/client/eslint.config.js b/client/eslint.config.js index b19330b..64697e5 100644 --- a/client/eslint.config.js +++ b/client/eslint.config.js @@ -1,23 +1,100 @@ -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' +import { defineConfig } from "eslint/config"; +import tsParser from "@typescript-eslint/parser"; +import tsPlugin from "@typescript-eslint/eslint-plugin"; +import reactPlugin from "eslint-plugin-react"; +import reactHooksPlugin from "eslint-plugin-react-hooks"; +import importPlugin from "eslint-plugin-import"; +import prettierPlugin from "eslint-plugin-prettier"; +import unusedImportsPlugin from "eslint-plugin-unused-imports"; export default defineConfig([ - globalIgnores(['dist']), { - files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - tseslint.configs.recommended, - reactHooks.configs['recommended-latest'], - reactRefresh.configs.vite, - ], + files: ["src/**/*.{ts,tsx}"], languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, + parser: tsParser, + parserOptions: { + project: "./tsconfig.app.json", + sourceType: "module", + ecmaFeatures: { + jsx: true, + }, + }, + }, + settings: { + react: { + version: "detect", + }, + "import/resolver": { + typescript: { + alwaysTryTypes: true, + project: ["./tsconfig.json", "./tsconfig.app.json"], + }, + node: { + extensions: [".js", ".jsx", ".ts", ".tsx"], + }, + }, + }, + plugins: { + "@typescript-eslint": tsPlugin, + react: reactPlugin, + "react-hooks": reactHooksPlugin, + import: importPlugin, + prettier: prettierPlugin, + "unused-imports": unusedImportsPlugin, + }, + rules: { + "react/react-in-jsx-scope": "off", + "react/prop-types": "off", + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + vars: "all", + varsIgnorePattern: "^_", + args: "after-used", + argsIgnorePattern: "^_", + }, + ], + "no-unused-vars": "off", + "unused-imports/no-unused-imports": "error", + "import/default": "off", + "import/named": "off", + "import/namespace": "error", + "import/export": "error", + "import/order": [ + "error", + { + groups: ["builtin", "external", "internal"], + pathGroups: [ + { + pattern: "react", + group: "external", + position: "before", + }, + ], + pathGroupsExcludedImportTypes: ["react"], + "newlines-between": "always", + alphabetize: { + order: "asc", + caseInsensitive: true, + }, + }, + ], + "import/extensions": [ + "error", + "ignorePackages", + { + ts: "never", + tsx: "never", + js: "never", + jsx: "never", + }, + ], + "prettier/prettier": "error", }, }, -]) + { + ignores: ["dist/*"], + }, +]); diff --git a/client/package-lock.json b/client/package-lock.json index b67d1af..875e722 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -33,14 +33,22 @@ "@types/node": "^24.6.0", "@types/react": "^19.1.16", "@types/react-dom": "^19.1.9", + "@typescript-eslint/eslint-plugin": "^8.24.0", + "@typescript-eslint/parser": "^8.24.0", "@vitejs/plugin-react": "^5.0.4", "autoprefixer": "^10.4.21", "babel-plugin-react-compiler": "^19.1.0-rc.3", "eslint": "^9.36.0", + "eslint-import-resolver-typescript": "^3.7.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-react": "^7.37.2", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.22", + "eslint-plugin-unused-imports": "^4.1.4", "globals": "^16.4.0", "postcss": "^8.5.6", + "prettier": "^3.4.2", "tailwindcss": "^3.4.17", "tailwindcss-animate": "^1.0.7", "typescript": "~5.9.3", @@ -714,6 +722,16 @@ "node": ">= 8" } }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, "node_modules/@oxc-project/runtime": { "version": "0.92.0", "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.92.0.tgz", @@ -745,6 +763,19 @@ "node": ">=14" } }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -1545,6 +1576,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -1622,6 +1660,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/leaflet": { "version": "1.9.20", "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.20.tgz", @@ -1933,6 +1978,288 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@vitejs/plugin-react": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.4.tgz", @@ -2080,6 +2407,176 @@ "node": ">=10" } }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/autoprefixer": { "version": "10.4.21", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", @@ -2118,6 +2615,22 @@ "postcss": "^8.1.0" } }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/babel-plugin-react-compiler": { "version": "19.1.0-rc.3", "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-19.1.0-rc.3.tgz", @@ -2216,6 +2729,56 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2412,6 +2975,60 @@ "devOptional": true, "license": "MIT" }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2437,6 +3054,42 @@ "dev": true, "license": "MIT" }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2467,6 +3120,34 @@ "dev": true, "license": "MIT" }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2488,6 +3169,183 @@ "dev": true, "license": "MIT" }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2572,6 +3430,199 @@ } } }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", + "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, "node_modules/eslint-plugin-react-hooks": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", @@ -2595,6 +3646,40 @@ "eslint": ">=8.40" } }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-unused-imports": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.2.0.tgz", + "integrity": "sha512-hLbJ2/wnjKq4kGA9AUaExVFIbNzyxYdVo49QZmKCnhk5pc9wcYRbfgLHvWJ8tnsdcseGhoUAddm9gn/lt+d74w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", + "eslint": "^9.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, "node_modules/eslint-scope": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", @@ -2696,6 +3781,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -2814,6 +3906,22 @@ "dev": true, "license": "ISC" }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -2870,6 +3978,47 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2880,6 +4029,31 @@ "node": ">=6.9.0" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -2889,6 +4063,51 @@ "node": ">=6" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.12.0.tgz", + "integrity": "sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -2962,6 +4181,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -2969,6 +4218,19 @@ "dev": true, "license": "MIT" }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2979,6 +4241,64 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -3069,6 +4389,75 @@ "node": ">=0.8.19" } }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -3082,6 +4471,59 @@ "node": ">=8" } }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-bun-module/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -3098,6 +4540,41 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3108,6 +4585,22 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -3118,6 +4611,26 @@ "node": ">=8" } }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -3131,6 +4644,32 @@ "node": ">=0.10.0" } }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -3141,6 +4680,175 @@ "node": ">=0.12.0" } }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3148,6 +4856,24 @@ "dev": true, "license": "ISC" }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -3241,6 +4967,22 @@ "node": ">=6" } }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3580,6 +5322,19 @@ "dev": true, "license": "MIT" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3599,6 +5354,16 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3636,6 +5401,16 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -3684,6 +5459,22 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -3738,6 +5529,119 @@ "node": ">= 6" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3756,6 +5660,24 @@ "node": ">= 0.8.0" } }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -3899,6 +5821,16 @@ "node": ">= 6" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -4065,6 +5997,47 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4143,6 +6116,13 @@ } } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -4245,6 +6225,50 @@ "node": ">=8.10.0" } }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -4276,6 +6300,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -4352,6 +6386,61 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -4368,6 +6457,55 @@ "semver": "bin/semver.js" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4391,6 +6529,82 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -4414,6 +6628,27 @@ "node": ">=0.10.0" } }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -4478,6 +6713,104 @@ "node": ">=8" } }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/strip-ansi": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", @@ -4518,6 +6851,16 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -4580,6 +6923,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, "node_modules/tailwind-merge": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", @@ -4742,6 +7101,32 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -4761,6 +7146,84 @@ "node": ">= 0.8.0" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -4799,6 +7262,25 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/undici-types": { "version": "7.14.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", @@ -4806,6 +7288,41 @@ "dev": true, "license": "MIT" }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -5039,6 +7556,95 @@ "node": ">= 8" } }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/client/package.json b/client/package.json index 3dc4d59..77c1e24 100644 --- a/client/package.json +++ b/client/package.json @@ -7,6 +7,7 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", + "typecheck": "tsc --noEmit", "preview": "vite preview" }, "dependencies": { @@ -31,6 +32,8 @@ }, "devDependencies": { "@eslint/js": "^9.36.0", + "@typescript-eslint/eslint-plugin": "^8.24.0", + "@typescript-eslint/parser": "^8.24.0", "@types/leaflet": "^1.9.12", "@types/node": "^24.6.0", "@types/react": "^19.1.16", @@ -39,10 +42,16 @@ "autoprefixer": "^10.4.21", "babel-plugin-react-compiler": "^19.1.0-rc.3", "eslint": "^9.36.0", + "eslint-import-resolver-typescript": "^3.7.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-react": "^7.37.2", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.22", + "eslint-plugin-unused-imports": "^4.1.4", "globals": "^16.4.0", "postcss": "^8.5.6", + "prettier": "^3.4.2", "tailwindcss": "^3.4.17", "tailwindcss-animate": "^1.0.7", "typescript": "~5.9.3", diff --git a/client/postcss.config.js b/client/postcss.config.js index 2e7af2b..2aa7205 100644 --- a/client/postcss.config.js +++ b/client/postcss.config.js @@ -3,4 +3,4 @@ export default { tailwindcss: {}, autoprefixer: {}, }, -} +}; diff --git a/client/src/App.tsx b/client/src/App.tsx index 3c50cec..f0dd43c 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,12 +1,13 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' -import { Layers, Menu, PanelRightClose, PanelRightOpen } from 'lucide-react' -import { useTranslation } from 'react-i18next' +import { useCallback, useEffect, useMemo, useState } from "react"; -import { AppHeader } from '@/components/layout/AppHeader' -import { ActivityPanel } from '@/components/panels/ActivityPanel' -import { HotspotStatsPanel } from '@/components/panels/HotspotStatsPanel' -import { OverviewPanel } from '@/components/panels/OverviewPanel' -import { MapViewport } from '@/components/map/MapViewport' +import { Layers, Menu, PanelRightClose, PanelRightOpen } from "lucide-react"; +import { useTranslation } from "react-i18next"; + +import { AppHeader } from "@/components/layout/AppHeader"; +import { MapViewport } from "@/components/map/MapViewport"; +import { ActivityPanel } from "@/components/panels/ActivityPanel"; +import { HotspotStatsPanel } from "@/components/panels/HotspotStatsPanel"; +import { OverviewPanel } from "@/components/panels/OverviewPanel"; import { AlertDialog, AlertDialogAction, @@ -16,61 +17,61 @@ import { AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, -} from '@/components/ui/alert-dialog' -import { Button } from '@/components/ui/button' -import { ScrollArea } from '@/components/ui/scroll-area' -import { useHotspotFeed } from '@/hooks/useHotspotFeed' -import { useLeafletHeatmap, type TileProvider } from '@/hooks/useLeafletHeatmap' -import { useUserLocation } from '@/hooks/useUserLocation' -import { cn, distanceInKm, formatCoordinate, formatRelativeTime, formatTimestamp } from '@/lib/utils' -import type { Point } from '@/types/api' -import { Toaster } from '@/components/ui/toaster' -import { useToast } from '@/components/ui/use-toast' -import { useAppStore } from '@/store/useAppStore' +} from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Toaster } from "@/components/ui/toaster"; +import { useToast } from "@/components/ui/use-toast"; +import { useHotspotFeed } from "@/hooks/useHotspotFeed"; +import { useLeafletHeatmap, type TileProvider } from "@/hooks/useLeafletHeatmap"; +import { useUserLocation } from "@/hooks/useUserLocation"; +import { cn, distanceInKm, formatCoordinate, formatRelativeTime, formatTimestamp } from "@/lib/utils"; +import { useAppStore } from "@/store/useAppStore"; +import type { Point } from "@/types/api"; -const RADIUS_KM = 1 +const RADIUS_KM = 1; export default function App() { - const [pendingSpot, setPendingSpot] = useState(null) - const [isConfirmOpen, setIsConfirmOpen] = useState(false) - const [isConfirming, setIsConfirming] = useState(false) - const [tileProvider, setTileProvider] = useState('openstreetmap') + const [pendingSpot, setPendingSpot] = useState(null); + const [isConfirmOpen, setIsConfirmOpen] = useState(false); + const [isConfirming, setIsConfirming] = useState(false); + const [tileProvider, setTileProvider] = useState("openstreetmap"); const [isDetailsOpen, setIsDetailsOpen] = useState(() => { - if (typeof window === 'undefined') { - return false + if (typeof window === "undefined") { + return false; } - return window.innerWidth >= 1024 - }) + return window.innerWidth >= 1024; + }); const [isHeaderCollapsed, setIsHeaderCollapsed] = useState(() => { - if (typeof window === 'undefined') { - return false + if (typeof window === "undefined") { + return false; } - return window.innerWidth < 768 - }) + return window.innerWidth < 768; + }); - const { t, i18n } = useTranslation() - const locale = i18n.language === 'fr' ? 'fr-FR' : 'en-US' + const { t, i18n } = useTranslation(); + const locale = i18n.language === "fr" ? "fr-FR" : "en-US"; const distanceFormatter = useMemo( () => new Intl.NumberFormat(locale, { minimumFractionDigits: 2, maximumFractionDigits: 2, }), - [locale], - ) + [locale] + ); const tileOptions = useMemo( () => [ - { value: 'openstreetmap' as TileProvider, label: t('map.tiles.openstreetmap') }, - { value: 'mapbox' as TileProvider, label: t('map.tiles.mapbox') }, + { value: "openstreetmap" as TileProvider, label: t("map.tiles.openstreetmap") }, + { value: "mapbox" as TileProvider, label: t("map.tiles.mapbox") }, ], - [t], - ) + [t] + ); - const userLocation = useAppStore((state) => state.userLocation) - const locationError = useAppStore((state) => state.locationError) - const isRequestingLocation = useAppStore((state) => state.isRequestingLocation) - const { refresh: refreshLocation } = useUserLocation() + const userLocation = useAppStore(state => state.userLocation); + const locationError = useAppStore(state => state.locationError); + const isRequestingLocation = useAppStore(state => state.isRequestingLocation); + const { refresh: refreshLocation } = useUserLocation(); const { status, @@ -82,123 +83,120 @@ export default function App() { selectVisibleLatestByUser, myLatestPoint, lastUpdated, - } = useHotspotFeed({ userLocation: userLocation ?? null }) + } = useHotspotFeed({ userLocation: userLocation ?? null }); const visibleDensity = useMemo( () => selectVisibleDensity(userLocation ?? null), - [selectVisibleDensity, userLocation], - ) + [selectVisibleDensity, userLocation] + ); - const visiblePoints = useMemo( - () => selectVisiblePoints(userLocation ?? null), - [selectVisiblePoints, userLocation], - ) + const visiblePoints = useMemo(() => selectVisiblePoints(userLocation ?? null), [selectVisiblePoints, userLocation]); const visibleLatestByUser = useMemo( () => selectVisibleLatestByUser(userLocation ?? null), - [selectVisibleLatestByUser, userLocation], - ) + [selectVisibleLatestByUser, userLocation] + ); const localTotals = useMemo(() => { - const uniqueUsers = new Set() - visibleLatestByUser.forEach((point) => uniqueUsers.add(point.userKey)) - return { points: visiblePoints.length, contributors: uniqueUsers.size } - }, [visibleLatestByUser, visiblePoints]) + const uniqueUsers = new Set(); + visibleLatestByUser.forEach(point => uniqueUsers.add(point.userKey)); + return { points: visiblePoints.length, contributors: uniqueUsers.size }; + }, [visibleLatestByUser, visiblePoints]); const myVisibleSignal = useMemo(() => { if (!myLatestPoint) { - return null + return null; } if (userLocation && distanceInKm(userLocation, myLatestPoint.signalLocation) > RADIUS_KM) { - return null + return null; } - return myLatestPoint - }, [myLatestPoint, userLocation]) + return myLatestPoint; + }, [myLatestPoint, userLocation]); - const statusLabel = t(`status.${status}`) - const lastUpdatedLabel = lastUpdated ? formatRelativeTime(lastUpdated, locale) : t('common.never') - const isLoading = status === 'loading' - const isPosting = status === 'posting' - const isRefreshing = status === 'refreshing' - const hasLocation = Boolean(userLocation) - const showLocationCta = !hasLocation || Boolean(locationError) + const statusLabel = t(`status.${status}`); + const lastUpdatedLabel = lastUpdated ? formatRelativeTime(lastUpdated, locale) : t("common.never"); + const isLoading = status === "loading"; + const isPosting = status === "posting"; + const isRefreshing = status === "refreshing"; + const hasLocation = Boolean(userLocation); + const showLocationCta = !hasLocation || Boolean(locationError); - const translatedLocationError = locationError ? t(locationError) : null + const translatedLocationError = locationError ? t(locationError) : null; const locationHint = translatedLocationError ? translatedLocationError : hasLocation - ? t('location.hint.showing', { radius: RADIUS_KM }) + ? t("location.hint.showing", { radius: RADIUS_KM }) : isRequestingLocation - ? t('location.hint.requesting') - : t('location.hint.allow') + ? t("location.hint.requesting") + : t("location.hint.allow"); const { mapContainerRef, focusOn, fitToHeat } = useLeafletHeatmap({ heatCells: visibleDensity, userLocation: userLocation ?? null, - onRequestSpot: (position) => { - setPendingSpot(position) - setIsConfirmOpen(true) + onRequestSpot: position => { + setPendingSpot(position); + setIsConfirmOpen(true); }, tileProvider, - }) + }); - const { toast } = useToast() + const { toast } = useToast(); useEffect(() => { if (!error) { - return + return; } - const description = error.detail ?? t(error.key, error.values ?? {}) + const description = error.detail ?? t(error.key, error.values ?? {}); toast({ - variant: 'destructive', - title: t('errors.title'), + variant: "destructive", + title: t("errors.title"), description, duration: 6000, - }) - }, [error, t, toast]) + }); + }, [error, t, toast]); const handleConfirmSignal = useCallback(async () => { if (!pendingSpot) { - return + return; } - setIsConfirming(true) - const result = await submitPoint(pendingSpot) - setIsConfirming(false) + setIsConfirming(true); + const result = await submitPoint(pendingSpot); + setIsConfirming(false); if (result.success) { - setIsConfirmOpen(false) - setPendingSpot(null) + setIsConfirmOpen(false); + setPendingSpot(null); } - }, [pendingSpot, submitPoint]) + }, [pendingSpot, submitPoint]); const handleRefresh = useCallback(() => { - fetchSnapshot().catch(() => undefined) - }, [fetchSnapshot]) + fetchSnapshot().catch(() => undefined); + }, [fetchSnapshot]); const handleFocusHeat = useCallback(() => { - fitToHeat() - }, [fitToHeat]) + fitToHeat(); + }, [fitToHeat]); const handleLocateUser = useCallback(() => { if (userLocation) { - focusOn(userLocation, 14) + focusOn(userLocation, 14); } else { - refreshLocation() + refreshLocation(); } - }, [focusOn, refreshLocation, userLocation]) + }, [focusOn, refreshLocation, userLocation]); const handleFocusMySignal = useCallback(() => { if (myVisibleSignal) { - focusOn({ lat: myVisibleSignal.signalLocation.lat, lng: myVisibleSignal.signalLocation.lng }, 15) + focusOn({ lat: myVisibleSignal.signalLocation.lat, lng: myVisibleSignal.signalLocation.lng }, 15); } - }, [focusOn, myVisibleSignal]) + }, [focusOn, myVisibleSignal]); const handleManualReport = useCallback(() => { if (!userLocation) { - return + return; } - setPendingSpot(userLocation) - setIsConfirmOpen(true) - }, [userLocation]) + setPendingSpot(userLocation); + setIsConfirmOpen(true); + }, [userLocation]); const dangerCells = useMemo( () => @@ -206,239 +204,239 @@ export default function App() { .sort((a, b) => b.intensity - a.intensity) .slice(0, 5) .map((cell, index) => { - const coordinates = t('common.coordinates', { + const coordinates = t("common.coordinates", { lat: formatCoordinate(cell.lat, locale), lng: formatCoordinate(cell.lng, locale), - }) + }); const distanceLabel = userLocation ? `${distanceFormatter.format(distanceInKm(userLocation, cell))} km` - : null + : null; return { id: `${cell.lat}-${cell.lng}-${index}`, - title: t('hotspots.itemTitle', { index: index + 1 }), + title: t("hotspots.itemTitle", { index: index + 1 }), subtitle: distanceLabel !== null - ? t('hotspots.itemSubtitleWithDistance', { distance: distanceLabel, coordinates }) - : t('hotspots.itemSubtitle', { coordinates }), + ? t("hotspots.itemSubtitleWithDistance", { distance: distanceLabel, coordinates }) + : t("hotspots.itemSubtitle", { coordinates }), intensity: cell.intensity, onFocus: () => focusOn({ lat: cell.lat, lng: cell.lng }, 15), - } + }; }), - [visibleDensity, userLocation, focusOn, distanceFormatter, t, locale], - ) + [visibleDensity, userLocation, focusOn, distanceFormatter, t, locale] + ); const recentActivity = useMemo( () => [...visiblePoints] .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) .slice(0, 8) - .map((point) => { - const coordinates = t('common.coordinates', { + .map(point => { + const coordinates = t("common.coordinates", { lat: formatCoordinate(point.signalLocation.lat, locale), lng: formatCoordinate(point.signalLocation.lng, locale), - }) + }); const distanceLabel = userLocation - ? t('activityItem.distance', { + ? t("activityItem.distance", { distance: `${distanceFormatter.format(distanceInKm(userLocation, point.signalLocation))} km`, }) - : formatTimestamp(point.createdAt, locale) + : formatTimestamp(point.createdAt, locale); return { id: point.id, title: coordinates, - subtitle: t('activityItem.user', { id: point.userKey.slice(0, 4).toUpperCase() }), + subtitle: t("activityItem.user", { id: point.userKey.slice(0, 4).toUpperCase() }), timestampLabel: formatRelativeTime(point.createdAt, locale), distanceLabel, onFocus: () => focusOn({ lat: point.signalLocation.lat, lng: point.signalLocation.lng }, 15), - } + }; }), - [visiblePoints, userLocation, focusOn, distanceFormatter, t, locale], - ) + [visiblePoints, userLocation, focusOn, distanceFormatter, t, locale] + ); - const confirmationLat = pendingSpot ? formatCoordinate(pendingSpot.lat, locale) : '--' - const confirmationLng = pendingSpot ? formatCoordinate(pendingSpot.lng, locale) : '--' - const isDialogDisabled = !pendingSpot || isConfirming - const detailsToggleLabel = isDetailsOpen ? t('details.close') : t('details.open') + const confirmationLat = pendingSpot ? formatCoordinate(pendingSpot.lat, locale) : "--"; + const confirmationLng = pendingSpot ? formatCoordinate(pendingSpot.lng, locale) : "--"; + const isDialogDisabled = !pendingSpot || isConfirming; + const detailsToggleLabel = isDetailsOpen ? t("details.close") : t("details.open"); const detailsPanelClassName = cn( - 'pointer-events-auto fixed inset-x-0 bottom-0 z-40 flex max-h-[85vh] w-full flex-col overflow-hidden rounded-t-3xl border border-border/70 bg-background/95 shadow-2xl backdrop-blur transition-transform duration-300 sm:left-auto sm:right-6 sm:top-24 sm:bottom-6 sm:max-h-[calc(100vh-8rem)] sm:w-[min(380px,calc(100vw-4rem))] sm:rounded-3xl', - isDetailsOpen - ? 'translate-y-0 sm:translate-x-0' - : 'translate-y-[calc(100%+1rem)] sm:translate-x-[calc(100%+2rem)]', - ) + "pointer-events-auto fixed inset-x-0 bottom-0 z-40 flex max-h-[85vh] w-full flex-col overflow-hidden rounded-t-3xl border border-border/70 bg-background/95 shadow-2xl backdrop-blur transition-transform duration-300 sm:left-auto sm:right-6 sm:top-24 sm:bottom-6 sm:max-h-[calc(100vh-8rem)] sm:w-[min(380px,calc(100vw-4rem))] sm:rounded-3xl", + isDetailsOpen ? "translate-y-0 sm:translate-x-0" : "translate-y-[calc(100%+1rem)] sm:translate-x-[calc(100%+2rem)]" + ); return ( <>
+ isPosting={isPosting || isConfirming} + isLoading={isLoading} + confirmationHint={isConfirmOpen ? t("map.confirmationHint") : null} + className="min-h-screen" + /> -
-
-
- setIsHeaderCollapsed((prev) => !prev)} - /> +
+
+
+ setIsHeaderCollapsed(prev => !prev)} + /> +
+
+ +
-
+
+ + {!isDetailsOpen && ( +
-
-
+ )} - {!isDetailsOpen && ( -
- -
- )} - - - - { - setIsConfirmOpen(nextOpen) - if (!nextOpen) { - setPendingSpot(null) - setIsConfirming(false) - } - }} - > - - - {t('dialog.confirmSignal.title')} - {t('dialog.confirmSignal.description')} - -
-
- {t('dialog.confirmSignal.latitude')} - {confirmationLat}° -
-
- {t('dialog.confirmSignal.longitude')} - {confirmationLng}° -
-

{t('dialog.confirmSignal.reach', { radius: RADIUS_KM })}

-
- - {t('dialog.confirmSignal.cancel')} - - {isConfirming ? t('dialog.confirmSignal.sending') : t('dialog.confirmSignal.confirm')} - - -
-
+ + {t("dialog.confirmSignal.cancel")} + + {isConfirming ? t("dialog.confirmSignal.sending") : t("dialog.confirmSignal.confirm")} + + + +
- ) + ); } diff --git a/client/src/components/layout/AppHeader.tsx b/client/src/components/layout/AppHeader.tsx index f62f1be..3f9630a 100644 --- a/client/src/components/layout/AppHeader.tsx +++ b/client/src/components/layout/AppHeader.tsx @@ -1,28 +1,28 @@ -import { ChevronDown, ChevronUp, Flame, Focus, LocateFixed, MapPin, RefreshCw } from 'lucide-react' -import { useTranslation } from 'react-i18next' +import { ChevronDown, ChevronUp, Flame, Focus, LocateFixed, MapPin, RefreshCw } from "lucide-react"; +import { useTranslation } from "react-i18next"; -import { Badge } from '@/components/ui/badge' -import { Button } from '@/components/ui/button' -import { LanguageToggle } from '@/components/layout/LanguageToggle' -import { ThemeToggle } from '@/components/layout/ThemeToggle' -import { cn } from '@/lib/utils' -import type { FeedStatus } from '@/types/api' +import { LanguageToggle } from "@/components/layout/LanguageToggle"; +import { ThemeToggle } from "@/components/layout/ThemeToggle"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import type { FeedStatus } from "@/types/api"; interface AppHeaderProps { - status: FeedStatus - statusLabel: string - lastUpdatedLabel: string - onRefresh: () => void - onFocusHeat: () => void - onLocateUser: () => void - onFocusMySignal: () => void - disableRefresh: boolean - disableHeat: boolean - disableLocate: boolean - disableMySignal: boolean - collapsed: boolean - onToggleCollapse: () => void - className?: string + status: FeedStatus; + statusLabel: string; + lastUpdatedLabel: string; + onRefresh: () => void; + onFocusHeat: () => void; + onLocateUser: () => void; + onFocusMySignal: () => void; + disableRefresh: boolean; + disableHeat: boolean; + disableLocate: boolean; + disableMySignal: boolean; + collapsed: boolean; + onToggleCollapse: () => void; + className?: string; } export function AppHeader({ @@ -41,21 +41,18 @@ export function AppHeader({ onToggleCollapse, className, }: AppHeaderProps) { - const { t } = useTranslation() - const isError = status === 'error' + const { t } = useTranslation(); + const isError = status === "error"; const statusBadge = ( - + @@ -63,18 +60,18 @@ export function AppHeader({ {!collapsed && ( - {t('header.badge.updated', { time: lastUpdatedLabel })} + {t("header.badge.updated", { time: lastUpdatedLabel })} )} - ) + ); return (
@@ -83,15 +80,15 @@ export function AppHeader({
- {t('app.name')} - {!collapsed && {t('app.tagline')}} + {t("app.name")} + {!collapsed && {t("app.tagline")}}
@@ -114,7 +111,7 @@ export function AppHeader({ size="icon" onClick={onFocusHeat} disabled={disableHeat} - aria-label={t('header.actions.focusHeat')} + aria-label={t("header.actions.focusHeat")} > @@ -123,7 +120,7 @@ export function AppHeader({ size="icon" onClick={onLocateUser} disabled={disableLocate} - aria-label={t('header.actions.locate')} + aria-label={t("header.actions.locate")} > @@ -132,7 +129,7 @@ export function AppHeader({ size="icon" onClick={onFocusMySignal} disabled={disableMySignal} - aria-label={t('header.actions.mySignal')} + aria-label={t("header.actions.mySignal")} > @@ -141,5 +138,5 @@ export function AppHeader({
)} - ) + ); } diff --git a/client/src/components/layout/LanguageToggle.tsx b/client/src/components/layout/LanguageToggle.tsx index 63480c4..4a4b94d 100644 --- a/client/src/components/layout/LanguageToggle.tsx +++ b/client/src/components/layout/LanguageToggle.tsx @@ -1,28 +1,27 @@ -import { Globe } from 'lucide-react' -import { useTranslation } from 'react-i18next' +import { Globe } from "lucide-react"; +import { useTranslation } from "react-i18next"; -import { Button } from '@/components/ui/button' +import { Button } from "@/components/ui/button"; export function LanguageToggle() { - const { i18n, t } = useTranslation() - const current = i18n.language === 'fr' ? 'fr' : 'en' - const next = current === 'en' ? 'fr' : 'en' - const nextLabel = t(next === 'en' ? 'language.english' : 'language.french') + const { i18n, t } = useTranslation(); + const current = i18n.language === "fr" ? "fr" : "en"; + const next = current === "en" ? "fr" : "en"; + const nextLabel = t(next === "en" ? "language.english" : "language.french"); return ( - ) + ); } - diff --git a/client/src/components/layout/ThemeToggle.tsx b/client/src/components/layout/ThemeToggle.tsx index 373ae3e..c23f8c9 100644 --- a/client/src/components/layout/ThemeToggle.tsx +++ b/client/src/components/layout/ThemeToggle.tsx @@ -1,22 +1,22 @@ -import { Moon, Sun } from 'lucide-react' -import { useTranslation } from 'react-i18next' +import { Moon, Sun } from "lucide-react"; +import { useTranslation } from "react-i18next"; -import { Button } from '@/components/ui/button' -import { useTheme } from '@/hooks/useTheme' +import { Button } from "@/components/ui/button"; +import { useTheme } from "@/hooks/useTheme"; export function ThemeToggle() { - const { toggleTheme, isDark } = useTheme() - const { t } = useTranslation() + const { toggleTheme, isDark } = useTheme(); + const { t } = useTranslation(); return ( - ) + ); } diff --git a/client/src/components/map/MapViewport.tsx b/client/src/components/map/MapViewport.tsx index 83b1e79..5dca1de 100644 --- a/client/src/components/map/MapViewport.tsx +++ b/client/src/components/map/MapViewport.tsx @@ -1,39 +1,29 @@ -import type { MutableRefObject } from 'react' -import { useTranslation } from 'react-i18next' +import type { MutableRefObject } from "react"; -import { cn } from '@/lib/utils' +import { useTranslation } from "react-i18next"; + +import { cn } from "@/lib/utils"; interface MapViewportProps { - containerRef: MutableRefObject - isPosting: boolean - isLoading: boolean - confirmationHint?: string | null - className?: string + containerRef: MutableRefObject; + isPosting: boolean; + isLoading: boolean; + confirmationHint?: string | null; + className?: string; } -export function MapViewport({ - containerRef, - isPosting, - isLoading, - confirmationHint, - className, -}: MapViewportProps) { - const { t } = useTranslation() +export function MapViewport({ containerRef, isPosting, isLoading, confirmationHint, className }: MapViewportProps) { + const { t } = useTranslation(); return ( -
-
+
+
{(isPosting || isLoading) && (
- {isPosting ? t('map.posting') : t('map.loading')} + {isPosting ? t("map.posting") : t("map.loading")}
)} @@ -43,5 +33,5 @@ export function MapViewport({
)}
- ) + ); } diff --git a/client/src/components/panels/ActivityPanel.tsx b/client/src/components/panels/ActivityPanel.tsx index 09e3461..021b805 100644 --- a/client/src/components/panels/ActivityPanel.tsx +++ b/client/src/components/panels/ActivityPanel.tsx @@ -1,43 +1,43 @@ -import { Activity, ArrowRight } from 'lucide-react' -import { useTranslation } from 'react-i18next' +import { Activity, ArrowRight } from "lucide-react"; +import { useTranslation } from "react-i18next"; -import { Badge } from '@/components/ui/badge' -import { Button } from '@/components/ui/button' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import { ScrollArea } from '@/components/ui/scroll-area' +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { ScrollArea } from "@/components/ui/scroll-area"; interface ActivityItem { - id: string | number - title: string - subtitle: string - timestampLabel: string - distanceLabel: string - onFocus: () => void + id: string | number; + title: string; + subtitle: string; + timestampLabel: string; + distanceLabel: string; + onFocus: () => void; } interface ActivityPanelProps { - items: ActivityItem[] - emptyMessage: string + items: ActivityItem[]; + emptyMessage: string; } export function ActivityPanel({ items, emptyMessage }: ActivityPanelProps) { - const { t } = useTranslation() + const { t } = useTranslation(); return ( - {t('activity.title')} + {t("activity.title")} - {t('activity.description')} + {t("activity.description")} {items.length === 0 &&

{emptyMessage}

} {items.length > 0 && (
    - {items.map((item) => ( + {items.map(item => (
  • @@ -51,7 +51,7 @@ export function ActivityPanel({ items, emptyMessage }: ActivityPanelProps) {
    {item.distanceLabel}
    @@ -62,5 +62,5 @@ export function ActivityPanel({ items, emptyMessage }: ActivityPanelProps) { )} - ) + ); } diff --git a/client/src/components/panels/HotspotStatsPanel.tsx b/client/src/components/panels/HotspotStatsPanel.tsx index e3ff0d9..b4f17a7 100644 --- a/client/src/components/panels/HotspotStatsPanel.tsx +++ b/client/src/components/panels/HotspotStatsPanel.tsx @@ -1,47 +1,45 @@ -import { Flame, MapPin } from 'lucide-react' -import { useTranslation } from 'react-i18next' +import { Flame, MapPin } from "lucide-react"; +import { useTranslation } from "react-i18next"; -import { Badge } from '@/components/ui/badge' -import { Button } from '@/components/ui/button' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import { ScrollArea } from '@/components/ui/scroll-area' +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { ScrollArea } from "@/components/ui/scroll-area"; interface HotspotCellInfo { - id: string - title: string - subtitle: string - intensity: number - onFocus: () => void + id: string; + title: string; + subtitle: string; + intensity: number; + onFocus: () => void; } interface HotspotStatsPanelProps { - hasLocation: boolean - radiusKm: number - locationHint: string - cells: HotspotCellInfo[] + hasLocation: boolean; + radiusKm: number; + locationHint: string; + cells: HotspotCellInfo[]; } export function HotspotStatsPanel({ hasLocation, radiusKm, locationHint, cells }: HotspotStatsPanelProps) { - const { t } = useTranslation() + const { t } = useTranslation(); return ( - {t('hotspots.title')} + {t("hotspots.title")} - {t('hotspots.description', { radius: radiusKm })} + {t("hotspots.description", { radius: radiusKm })} {!hasLocation &&

    {locationHint}

    } - {hasLocation && cells.length === 0 && ( -

    {t('hotspots.empty')}

    - )} + {hasLocation && cells.length === 0 &&

    {t("hotspots.empty")}

    } {cells.length > 0 && (
      - {cells.map((cell) => ( + {cells.map(cell => (
    • @@ -58,7 +56,7 @@ export function HotspotStatsPanel({ hasLocation, radiusKm, locationHint, cells } className="mt-2 w-full justify-center gap-2 text-xs" onClick={cell.onFocus} > - {t('hotspots.focus')} + {t("hotspots.focus")}
    • ))} @@ -67,5 +65,5 @@ export function HotspotStatsPanel({ hasLocation, radiusKm, locationHint, cells } )} - ) + ); } diff --git a/client/src/components/panels/OverviewPanel.tsx b/client/src/components/panels/OverviewPanel.tsx index c343198..2c18a05 100644 --- a/client/src/components/panels/OverviewPanel.tsx +++ b/client/src/components/panels/OverviewPanel.tsx @@ -1,23 +1,23 @@ -import { AlertCircle, Radio } from 'lucide-react' -import { useTranslation } from 'react-i18next' +import { AlertCircle, Radio } from "lucide-react"; +import { useTranslation } from "react-i18next"; -import { Badge } from '@/components/ui/badge' -import { Button } from '@/components/ui/button' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import type { FeedError } from '@/hooks/useHotspotFeed' +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import type { FeedError } from "@/hooks/useHotspotFeed"; interface OverviewPanelProps { - nearbySignals: number - uniqueContributors: number - lastUpdatedLabel: string - mySignalLabel: string | null - error: FeedError | null - onReport: () => void - onRetry: () => void - isPosting: boolean - locationHint: string - showLocationCta: boolean - disableReport: boolean + nearbySignals: number; + uniqueContributors: number; + lastUpdatedLabel: string; + mySignalLabel: string | null; + error: FeedError | null; + onReport: () => void; + onRetry: () => void; + isPosting: boolean; + locationHint: string; + showLocationCta: boolean; + disableReport: boolean; } export function OverviewPanel({ @@ -33,32 +33,32 @@ export function OverviewPanel({ showLocationCta, disableReport, }: OverviewPanelProps) { - const { t } = useTranslation() - const errorMessage = error ? t(error.key, error.values) : null + const { t } = useTranslation(); + const errorMessage = error ? t(error.key, error.values) : null; return ( - {t('overview.title')} + {t("overview.title")} - {t('overview.description')} + {t("overview.description")}
      - {t('overview.stats.signals')} + {t("overview.stats.signals")}

      {nearbySignals}

      - {t('overview.stats.contributors')} + {t("overview.stats.contributors")}

      {uniqueContributors}

      {mySignalLabel && ( - {t('overview.badge', { time: mySignalLabel })} + {t("overview.badge", { time: mySignalLabel })} )} {errorMessage ? ( @@ -67,7 +67,7 @@ export function OverviewPanel({

      {errorMessage}

    @@ -75,17 +75,15 @@ export function OverviewPanel({

    {locationHint}

    )} {showLocationCta && !errorMessage && ( -

    {t('overview.locationPermission')}

    +

    {t("overview.locationPermission")}

    )} -

    {t('overview.lastSynced', { time: lastUpdatedLabel })}

    +

    + {t("overview.lastSynced", { time: lastUpdatedLabel })} +

    - ) + ); } diff --git a/client/src/components/ui/alert-dialog.tsx b/client/src/components/ui/alert-dialog.tsx index 26a38ba..fccd9a0 100644 --- a/client/src/components/ui/alert-dialog.tsx +++ b/client/src/components/ui/alert-dialog.tsx @@ -1,13 +1,14 @@ -import * as React from 'react' -import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' +import * as React from "react"; -import { cn } from '@/lib/utils' +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; -const AlertDialog = AlertDialogPrimitive.Root +import { cn } from "@/lib/utils"; -const AlertDialogTrigger = AlertDialogPrimitive.Trigger +const AlertDialog = AlertDialogPrimitive.Root; -const AlertDialogPortal = AlertDialogPrimitive.Portal +const AlertDialogTrigger = AlertDialogPrimitive.Trigger; + +const AlertDialogPortal = AlertDialogPrimitive.Portal; const AlertDialogOverlay = React.forwardRef< React.ElementRef, @@ -16,13 +17,13 @@ const AlertDialogOverlay = React.forwardRef< -)) -AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName +)); +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; const AlertDialogContent = React.forwardRef< React.ElementRef, @@ -33,44 +34,40 @@ const AlertDialogContent = React.forwardRef< -)) -AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName +)); +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( -
    -) -AlertDialogHeader.displayName = 'AlertDialogHeader' +
    +); +AlertDialogHeader.displayName = "AlertDialogHeader"; const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( -
    -) -AlertDialogFooter.displayName = 'AlertDialogFooter' +
    +); +AlertDialogFooter.displayName = "AlertDialogFooter"; const AlertDialogTitle = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +)); +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; const AlertDialogDescription = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName + +)); +AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName; const AlertDialogAction = React.forwardRef< React.ElementRef, @@ -78,11 +75,14 @@ const AlertDialogAction = React.forwardRef< >(({ className, ...props }, ref) => ( -)) -AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName +)); +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; const AlertDialogCancel = React.forwardRef< React.ElementRef, @@ -90,11 +90,14 @@ const AlertDialogCancel = React.forwardRef< >(({ className, ...props }, ref) => ( -)) -AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName +)); +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; export { AlertDialog, @@ -106,4 +109,4 @@ export { AlertDialogDescription, AlertDialogAction, AlertDialogCancel, -} +}; diff --git a/client/src/components/ui/badge.tsx b/client/src/components/ui/badge.tsx index cb4e14a..7bc17d4 100644 --- a/client/src/components/ui/badge.tsx +++ b/client/src/components/ui/badge.tsx @@ -1,36 +1,33 @@ -import * as React from 'react' -import { cva, type VariantProps } from 'class-variance-authority' +import * as React from "react"; -import { cn } from '@/lib/utils' +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; const badgeVariants = cva( - 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", { variants: { variant: { - default: 'border-transparent bg-primary/15 text-primary', - secondary: 'border-transparent bg-secondary text-secondary-foreground', - destructive: 'border-transparent bg-destructive/15 text-destructive', - outline: 'text-foreground', - success: 'border-transparent bg-emerald-500/15 text-emerald-300', - muted: 'border-transparent bg-muted/60 text-muted-foreground', + default: "border-transparent bg-primary/15 text-primary", + secondary: "border-transparent bg-secondary text-secondary-foreground", + destructive: "border-transparent bg-destructive/15 text-destructive", + outline: "text-foreground", + success: "border-transparent bg-emerald-500/15 text-emerald-300", + muted: "border-transparent bg-muted/60 text-muted-foreground", }, }, defaultVariants: { - variant: 'default', + variant: "default", }, - }, -) + } +); -export interface BadgeProps - extends React.HTMLAttributes, - VariantProps {} +export interface BadgeProps extends React.HTMLAttributes, VariantProps {} -const Badge = React.forwardRef( - ({ className, variant, ...props }, ref) => ( -
    - ), -) -Badge.displayName = 'Badge' +const Badge = React.forwardRef(({ className, variant, ...props }, ref) => ( +
    +)); +Badge.displayName = "Badge"; -export { Badge } +export { Badge }; diff --git a/client/src/components/ui/button.tsx b/client/src/components/ui/button.tsx index a071b92..c9259ea 100644 --- a/client/src/components/ui/button.tsx +++ b/client/src/components/ui/button.tsx @@ -1,48 +1,45 @@ -import * as React from 'react' -import { Slot } from '@radix-ui/react-slot' -import { cva, type VariantProps } from 'class-variance-authority' +import * as React from "react"; -import { cn } from '@/lib/utils' +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; const buttonVariants = cva( - 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 focus-visible:ring-offset-background', + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 focus-visible:ring-offset-background", { variants: { variant: { - default: 'bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm', - secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', - ghost: 'hover:bg-accent hover:text-accent-foreground', - outline: - 'border border-input bg-transparent hover:bg-accent hover:text-accent-foreground shadow-sm', - destructive: - 'bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm', + default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + outline: "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground shadow-sm", + destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm", }, size: { - default: 'h-10 px-4 py-2', - sm: 'h-9 rounded-md px-3', - lg: 'h-11 rounded-md px-5 text-base', - icon: 'h-10 w-10', + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-5 text-base", + icon: "h-10 w-10", }, }, defaultVariants: { - variant: 'default', - size: 'default', + variant: "default", + size: "default", }, - }, -) + } +); export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { - asChild?: boolean + asChild?: boolean; } export const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : 'button' - return ( - - ) - }, -) -Button.displayName = 'Button' + const Comp = asChild ? Slot : "button"; + return ; + } +); +Button.displayName = "Button"; diff --git a/client/src/components/ui/card.tsx b/client/src/components/ui/card.tsx index a2dc460..6721835 100644 --- a/client/src/components/ui/card.tsx +++ b/client/src/components/ui/card.tsx @@ -1,54 +1,50 @@ -import * as React from 'react' +import * as React from "react"; -import { cn } from '@/lib/utils' +import { cn } from "@/lib/utils"; -const Card = React.forwardRef>( - ({ className, ...props }, ref) => ( -
    - ), -) -Card.displayName = 'Card' +const Card = React.forwardRef>(({ className, ...props }, ref) => ( +
    +)); +Card.displayName = "Card"; const CardHeader = React.forwardRef>( ({ className, ...props }, ref) => ( -
    - ), -) -CardHeader.displayName = 'CardHeader' +
    + ) +); +CardHeader.displayName = "CardHeader"; const CardTitle = React.forwardRef>( ({ className, ...props }, ref) => ( -

    - ), -) -CardTitle.displayName = 'CardTitle' +

    + ) +); +CardTitle.displayName = "CardTitle"; const CardDescription = React.forwardRef>( ({ className, ...props }, ref) => ( -

    - ), -) -CardDescription.displayName = 'CardDescription' +

    + ) +); +CardDescription.displayName = "CardDescription"; const CardContent = React.forwardRef>( - ({ className, ...props }, ref) => ( -

    - ), -) -CardContent.displayName = 'CardContent' + ({ className, ...props }, ref) =>
    +); +CardContent.displayName = "CardContent"; const CardFooter = React.forwardRef>( ({ className, ...props }, ref) => ( -
    - ), -) -CardFooter.displayName = 'CardFooter' +
    + ) +); +CardFooter.displayName = "CardFooter"; -export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } +export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }; diff --git a/client/src/components/ui/scroll-area.tsx b/client/src/components/ui/scroll-area.tsx index cc1174e..1198835 100644 --- a/client/src/components/ui/scroll-area.tsx +++ b/client/src/components/ui/scroll-area.tsx @@ -1,40 +1,39 @@ -import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area' -import * as React from 'react' +import * as React from "react"; -import { cn } from '@/lib/utils' +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; + +import { cn } from "@/lib/utils"; const ScrollArea = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( - - - {children} - + + {children} -)) -ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName +)); +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; const ScrollBar = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ className, orientation = 'vertical', ...props }, ref) => ( +>(({ className, orientation = "vertical", ...props }, ref) => ( -)) -ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName +)); +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; -export { ScrollArea, ScrollBar } +export { ScrollArea, ScrollBar }; diff --git a/client/src/components/ui/separator.tsx b/client/src/components/ui/separator.tsx index fbd4354..7625a29 100644 --- a/client/src/components/ui/separator.tsx +++ b/client/src/components/ui/separator.tsx @@ -1,22 +1,18 @@ -import * as React from 'react' +import * as React from "react"; -import { cn } from '@/lib/utils' +import { cn } from "@/lib/utils"; const Separator = React.forwardRef< HTMLDivElement, - React.HTMLAttributes & { orientation?: 'horizontal' | 'vertical' } ->(({ className, orientation = 'horizontal', ...props }, ref) => ( + React.HTMLAttributes & { orientation?: "horizontal" | "vertical" } +>(({ className, orientation = "horizontal", ...props }, ref) => (
    -)) -Separator.displayName = 'Separator' +)); +Separator.displayName = "Separator"; -export { Separator } +export { Separator }; diff --git a/client/src/components/ui/sheet.tsx b/client/src/components/ui/sheet.tsx index d94e86f..82ed994 100644 --- a/client/src/components/ui/sheet.tsx +++ b/client/src/components/ui/sheet.tsx @@ -1,16 +1,17 @@ -import * as React from 'react' -import * as SheetPrimitive from '@radix-ui/react-dialog' -import { useTranslation } from 'react-i18next' +import * as React from "react"; -import { cn } from '@/lib/utils' +import * as SheetPrimitive from "@radix-ui/react-dialog"; +import { useTranslation } from "react-i18next"; -const Sheet = SheetPrimitive.Root +import { cn } from "@/lib/utils"; -const SheetTrigger = SheetPrimitive.Trigger +const Sheet = SheetPrimitive.Root; -const SheetClose = SheetPrimitive.Close +const SheetTrigger = SheetPrimitive.Trigger; -const SheetPortal = SheetPrimitive.Portal +const SheetClose = SheetPrimitive.Close; + +const SheetPortal = SheetPrimitive.Portal; const SheetOverlay = React.forwardRef< React.ElementRef, @@ -18,79 +19,78 @@ const SheetOverlay = React.forwardRef< >(({ className, ...props }, ref) => ( -)) -SheetOverlay.displayName = SheetPrimitive.Overlay.displayName +)); +SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; -type SheetSide = 'top' | 'bottom' | 'left' | 'right' +type SheetSide = "top" | "bottom" | "left" | "right"; interface SheetContentProps extends React.ComponentPropsWithoutRef { - side?: SheetSide + side?: SheetSide; } -const SheetContent = React.forwardRef< - React.ElementRef, - SheetContentProps ->(({ side = 'bottom', className, children, ...props }, ref) => { - const { t } = useTranslation() +const SheetContent = React.forwardRef, SheetContentProps>( + ({ side = "bottom", className, children, ...props }, ref) => { + const { t } = useTranslation(); - return ( - - - - {children} - - {t('common.actions.close')} - {t('common.aria.sheet.close')} - - - - ) -}) -SheetContent.displayName = SheetPrimitive.Content.displayName + return ( + + + + {children} + + {t("common.actions.close")} + {t("common.aria.sheet.close")} + + + + ); + } +); +SheetContent.displayName = SheetPrimitive.Content.displayName; const SheetHeader = ({ className, ...props }: React.HTMLAttributes) => ( -
    -) -SheetHeader.displayName = 'SheetHeader' +
    +); +SheetHeader.displayName = "SheetHeader"; const SheetFooter = ({ className, ...props }: React.HTMLAttributes) => ( -
    -) -SheetFooter.displayName = 'SheetFooter' +
    +); +SheetFooter.displayName = "SheetFooter"; const SheetTitle = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -SheetTitle.displayName = SheetPrimitive.Title.displayName + +)); +SheetTitle.displayName = SheetPrimitive.Title.displayName; const SheetDescription = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -SheetDescription.displayName = SheetPrimitive.Description.displayName + +)); +SheetDescription.displayName = SheetPrimitive.Description.displayName; export { Sheet, @@ -103,4 +103,4 @@ export { SheetFooter, SheetTitle, SheetDescription, -} +}; diff --git a/client/src/components/ui/tabs.tsx b/client/src/components/ui/tabs.tsx index 566dec5..856bab2 100644 --- a/client/src/components/ui/tabs.tsx +++ b/client/src/components/ui/tabs.tsx @@ -1,9 +1,10 @@ -import * as TabsPrimitive from '@radix-ui/react-tabs' -import * as React from 'react' +import * as React from "react"; -import { cn } from '@/lib/utils' +import * as TabsPrimitive from "@radix-ui/react-tabs"; -const Tabs = TabsPrimitive.Root +import { cn } from "@/lib/utils"; + +const Tabs = TabsPrimitive.Root; const TabsList = React.forwardRef< React.ElementRef, @@ -11,11 +12,14 @@ const TabsList = React.forwardRef< >(({ className, ...props }, ref) => ( -)) -TabsList.displayName = TabsPrimitive.List.displayName +)); +TabsList.displayName = TabsPrimitive.List.displayName; const TabsTrigger = React.forwardRef< React.ElementRef, @@ -24,13 +28,13 @@ const TabsTrigger = React.forwardRef< -)) -TabsTrigger.displayName = TabsPrimitive.Trigger.displayName +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; const TabsContent = React.forwardRef< React.ElementRef, @@ -39,12 +43,12 @@ const TabsContent = React.forwardRef< -)) -TabsContent.displayName = TabsPrimitive.Content.displayName +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; -export { Tabs, TabsList, TabsTrigger, TabsContent } +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/client/src/components/ui/toast.tsx b/client/src/components/ui/toast.tsx index 432ca88..035e6f6 100644 --- a/client/src/components/ui/toast.tsx +++ b/client/src/components/ui/toast.tsx @@ -1,11 +1,12 @@ -import * as React from 'react' -import * as ToastPrimitives from '@radix-ui/react-toast' -import { cva, type VariantProps } from 'class-variance-authority' -import { X } from 'lucide-react' +import * as React from "react"; -import { cn } from '@/lib/utils' +import * as ToastPrimitives from "@radix-ui/react-toast"; +import { cva, type VariantProps } from "class-variance-authority"; +import { X } from "lucide-react"; -const ToastProvider = ToastPrimitives.Provider +import { cn } from "@/lib/utils"; + +const ToastProvider = ToastPrimitives.Provider; const ToastViewport = React.forwardRef< React.ElementRef, @@ -14,28 +15,28 @@ const ToastViewport = React.forwardRef< -)) -ToastViewport.displayName = ToastPrimitives.Viewport.displayName +)); +ToastViewport.displayName = ToastPrimitives.Viewport.displayName; const toastVariants = cva( - 'group pointer-events-auto relative flex w-full items-start gap-3 overflow-hidden rounded-2xl border border-border bg-background p-4 shadow-lg transition-all', + "group pointer-events-auto relative flex w-full items-start gap-3 overflow-hidden rounded-2xl border border-border bg-background p-4 shadow-lg transition-all", { variants: { variant: { - default: 'bg-background text-foreground', - destructive: 'border-destructive/60 bg-destructive text-destructive-foreground', + default: "bg-background text-foreground", + destructive: "border-destructive/60 bg-destructive text-destructive-foreground", }, }, defaultVariants: { - variant: 'default', + variant: "default", }, - }, -) + } +); const Toast = React.forwardRef< React.ElementRef, @@ -45,9 +46,9 @@ const Toast = React.forwardRef< {props.children} - ) -}) -Toast.displayName = ToastPrimitives.Root.displayName + ); +}); +Toast.displayName = ToastPrimitives.Root.displayName; const ToastAction = React.forwardRef< React.ElementRef, @@ -56,13 +57,13 @@ const ToastAction = React.forwardRef< -)) -ToastAction.displayName = ToastPrimitives.Action.displayName +)); +ToastAction.displayName = ToastPrimitives.Action.displayName; const ToastClose = React.forwardRef< React.ElementRef, @@ -71,46 +72,38 @@ const ToastClose = React.forwardRef< -)) -ToastClose.displayName = ToastPrimitives.Close.displayName +)); +ToastClose.displayName = ToastPrimitives.Close.displayName; const ToastTitle = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -ToastTitle.displayName = ToastPrimitives.Title.displayName + +)); +ToastTitle.displayName = ToastPrimitives.Title.displayName; const ToastDescription = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -ToastDescription.displayName = ToastPrimitives.Description.displayName + +)); +ToastDescription.displayName = ToastPrimitives.Description.displayName; -export type ToastProps = React.ComponentPropsWithoutRef -export type ToastActionElement = React.ReactElement +export type ToastProps = React.ComponentPropsWithoutRef; +export type ToastActionElement = React.ReactElement; -export { - ToastProvider, - ToastViewport, - Toast, - ToastTitle, - ToastDescription, - ToastClose, - ToastAction, -} +export { ToastProvider, ToastViewport, Toast, ToastTitle, ToastDescription, ToastClose, ToastAction }; diff --git a/client/src/components/ui/toaster.tsx b/client/src/components/ui/toaster.tsx index 8ec63b7..348b4ba 100644 --- a/client/src/components/ui/toaster.tsx +++ b/client/src/components/ui/toaster.tsx @@ -1,15 +1,8 @@ -import { - Toast, - ToastClose, - ToastDescription, - ToastProvider, - ToastTitle, - ToastViewport, -} from '@/components/ui/toast' -import { useToast } from '@/components/ui/use-toast' +import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from "@/components/ui/toast"; +import { useToast } from "@/components/ui/use-toast"; export function Toaster() { - const { toasts, dismiss } = useToast() + const { toasts, dismiss } = useToast(); return ( @@ -17,9 +10,9 @@ export function Toaster() { { + onOpenChange={open => { if (!open) { - dismiss(id) + dismiss(id); } }} > @@ -33,5 +26,5 @@ export function Toaster() { ))} - ) + ); } diff --git a/client/src/components/ui/use-toast.ts b/client/src/components/ui/use-toast.ts index b6d02ae..4b2daf8 100644 --- a/client/src/components/ui/use-toast.ts +++ b/client/src/components/ui/use-toast.ts @@ -1,129 +1,125 @@ -import * as React from 'react' +import * as React from "react"; -import type { ToastActionElement, ToastProps } from '@/components/ui/toast' +import type { ToastActionElement, ToastProps } from "@/components/ui/toast"; type ToasterToast = ToastProps & { - id: string - title?: React.ReactNode - description?: React.ReactNode - action?: ToastActionElement -} + id: string; + title?: React.ReactNode; + description?: React.ReactNode; + action?: ToastActionElement; +}; type ToastState = { - toasts: ToasterToast[] -} + toasts: ToasterToast[]; +}; type ToastAction = - | { type: 'ADD_TOAST'; toast: ToasterToast } - | { type: 'UPDATE_TOAST'; toast: Partial & { id: string } } - | { type: 'DISMISS_TOAST'; toastId?: string } - | { type: 'REMOVE_TOAST'; toastId?: string } + | { type: "ADD_TOAST"; toast: ToasterToast } + | { type: "UPDATE_TOAST"; toast: Partial & { id: string } } + | { type: "DISMISS_TOAST"; toastId?: string } + | { type: "REMOVE_TOAST"; toastId?: string }; -const TOAST_LIMIT = 5 -const TOAST_REMOVE_DELAY = 1000 +const TOAST_LIMIT = 5; +const TOAST_REMOVE_DELAY = 1000; -const toastTimeouts = new Map>() +const toastTimeouts = new Map>(); -const listeners = new Set<(state: ToastState) => void>() +const listeners = new Set<(state: ToastState) => void>(); -let memoryState: ToastState = { toasts: [] } +let memoryState: ToastState = { toasts: [] }; function dispatch(action: ToastAction) { - memoryState = reducer(memoryState, action) - listeners.forEach((listener) => { - listener(memoryState) - }) + memoryState = reducer(memoryState, action); + listeners.forEach(listener => { + listener(memoryState); + }); } function reducer(state: ToastState, action: ToastAction): ToastState { switch (action.type) { - case 'ADD_TOAST': { + case "ADD_TOAST": { return { ...state, toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), - } + }; } - case 'UPDATE_TOAST': { + case "UPDATE_TOAST": { return { ...state, - toasts: state.toasts.map((toast) => - toast.id === action.toast.id ? { ...toast, ...action.toast } : toast, - ), - } + toasts: state.toasts.map(toast => (toast.id === action.toast.id ? { ...toast, ...action.toast } : toast)), + }; } - case 'DISMISS_TOAST': { - const { toastId } = action + case "DISMISS_TOAST": { + const { toastId } = action; if (toastId) { - addToRemoveQueue(toastId) + addToRemoveQueue(toastId); } else { - state.toasts.forEach((toast) => { - addToRemoveQueue(toast.id) - }) + state.toasts.forEach(toast => { + addToRemoveQueue(toast.id); + }); } return { ...state, - toasts: state.toasts.map((toast) => - toast.id === toastId || toastId === undefined - ? { ...toast, open: false } - : toast, + toasts: state.toasts.map(toast => + toast.id === toastId || toastId === undefined ? { ...toast, open: false } : toast ), - } + }; } - case 'REMOVE_TOAST': { + case "REMOVE_TOAST": { if (action.toastId === undefined) { - return { ...state, toasts: [] } + return { ...state, toasts: [] }; } return { ...state, - toasts: state.toasts.filter((toast) => toast.id !== action.toastId), - } + toasts: state.toasts.filter(toast => toast.id !== action.toastId), + }; } default: - return state + return state; } } function addToRemoveQueue(toastId: string) { if (toastTimeouts.has(toastId)) { - return + return; } const timeout = setTimeout(() => { - toastTimeouts.delete(toastId) - dispatch({ type: 'REMOVE_TOAST', toastId }) - }, TOAST_REMOVE_DELAY) + toastTimeouts.delete(toastId); + dispatch({ type: "REMOVE_TOAST", toastId }); + }, TOAST_REMOVE_DELAY); - toastTimeouts.set(toastId, timeout) + toastTimeouts.set(toastId, timeout); } function genId() { - return Math.random().toString(36).slice(2, 10) + return Math.random().toString(36).slice(2, 10); } export function useToast() { - const [state, setState] = React.useState(memoryState) + const [state, setState] = React.useState(memoryState); React.useEffect(() => { - listeners.add(setState) + listeners.add(setState); return () => { - listeners.delete(setState) - } - }, []) + listeners.delete(setState); + }; + }, []); return { ...state, - toast: (props: Omit) => { - const id = genId() - dispatch({ type: 'ADD_TOAST', toast: { ...props, id, open: true } }) - return id + toast: (props: Omit) => { + const id = genId(); + dispatch({ type: "ADD_TOAST", toast: { ...props, id, open: true } }); + return id; }, - dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }), - } + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), + }; } -export const toast = (props: Omit) => { - const id = genId() - dispatch({ type: 'ADD_TOAST', toast: { ...props, id, open: true } }) - return id -} +export const toast = (props: Omit) => { + const id = genId(); + dispatch({ type: "ADD_TOAST", toast: { ...props, id, open: true } }); + return id; +}; diff --git a/client/src/hooks/useHotspotFeed.ts b/client/src/hooks/useHotspotFeed.ts index 0e7273d..350e1a1 100644 --- a/client/src/hooks/useHotspotFeed.ts +++ b/client/src/hooks/useHotspotFeed.ts @@ -1,50 +1,43 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { distanceInKm } from '@/lib/utils' -import type { - ApiDensityCell, - ApiPoint, - ApiSnapshot, - FeedStatus, - Point, - SnapshotEventPayload, -} from '@/types/api' +import { distanceInKm } from "@/lib/utils"; +import type { ApiDensityCell, ApiPoint, ApiSnapshot, FeedStatus, Point, SnapshotEventPayload } from "@/types/api"; -const API_BASE = import.meta.env.VITE_API_BASE ?? 'http://localhost:8000/api/signals' -const SNAPSHOT_LIMIT = 750 -const MERCURE_HUB = import.meta.env.VITE_MERCURE_HUB ?? 'http://localhost:3000/.well-known/mercure' -const MERCURE_TOPIC = import.meta.env.VITE_MERCURE_TOPIC ?? 'https://points-of-interest.local/signals' -const VISIBLE_RADIUS_KM = 1 +const API_BASE = import.meta.env.VITE_API_BASE ?? "http://localhost:8000/api/signals"; +const SNAPSHOT_LIMIT = 750; +const MERCURE_HUB = import.meta.env.VITE_MERCURE_HUB ?? "http://localhost:3000/.well-known/mercure"; +const MERCURE_TOPIC = import.meta.env.VITE_MERCURE_TOPIC ?? "https://points-of-interest.local/signals"; +const VISIBLE_RADIUS_KM = 1; type ProblemDetails = { - detail?: unknown -} + detail?: unknown; +}; function extractProblemDetail(payload: unknown): string | null { - if (payload && typeof payload === 'object') { - const { detail } = payload as ProblemDetails - if (typeof detail === 'string' && detail.trim().length > 0) { - return detail + if (payload && typeof payload === "object") { + const { detail } = payload as ProblemDetails; + if (typeof detail === "string" && detail.trim().length > 0) { + return detail; } } - return null + return null; } interface UseHotspotFeedOptions { - userLocation: Point | null - snapshotLimit?: number - mercureHub?: string - mercureTopic?: string + userLocation: Point | null; + snapshotLimit?: number; + mercureHub?: string; + mercureTopic?: string; } interface SubmitResult { - success: boolean + success: boolean; } export interface FeedError { - key: string - values?: Record - detail?: string + key: string; + values?: Record; + detail?: string; } export function useHotspotFeed({ @@ -53,228 +46,222 @@ export function useHotspotFeed({ mercureHub = MERCURE_HUB, mercureTopic = MERCURE_TOPIC, }: UseHotspotFeedOptions) { - const [status, setStatus] = useState('loading') - const [error, setError] = useState(null) - const [rawPoints, setRawPoints] = useState([]) - const [rawDensity, setRawDensity] = useState([]) - const [rawLatestByUser, setRawLatestByUser] = useState([]) - const [clientKey, setClientKey] = useState(null) - const [lastUpdated, setLastUpdated] = useState(null) + const [status, setStatus] = useState("loading"); + const [error, setError] = useState(null); + const [rawPoints, setRawPoints] = useState([]); + const [rawDensity, setRawDensity] = useState([]); + const [rawLatestByUser, setRawLatestByUser] = useState([]); + const [clientKey, setClientKey] = useState(null); + const [lastUpdated, setLastUpdated] = useState(null); - const statusRef = useRef('loading') - const initialLoadRef = useRef(true) - const eventSourceRef = useRef(null) + const statusRef = useRef("loading"); + const initialLoadRef = useRef(true); + const eventSourceRef = useRef(null); const setStatusSafe = useCallback((next: FeedStatus) => { - statusRef.current = next - setStatus(next) - }, []) + statusRef.current = next; + setStatus(next); + }, []); const applySnapshot = useCallback( (snapshot: ApiSnapshot, options?: { preserveClientKey?: boolean }) => { - setRawPoints(snapshot.points) - setRawDensity(snapshot.density) - setRawLatestByUser(snapshot.latestByUser) + setRawPoints(snapshot.points); + setRawDensity(snapshot.density); + setRawLatestByUser(snapshot.latestByUser); if (!options?.preserveClientKey) { - setClientKey(snapshot.clientKey ?? null) + setClientKey(snapshot.clientKey ?? null); } - setLastUpdated(snapshot.updatedAt ?? new Date().toISOString()) - setError(null) - initialLoadRef.current = false - if (statusRef.current !== 'posting') { - setStatusSafe('idle') + setLastUpdated(snapshot.updatedAt ?? new Date().toISOString()); + setError(null); + initialLoadRef.current = false; + if (statusRef.current !== "posting") { + setStatusSafe("idle"); } }, - [setStatusSafe], - ) + [setStatusSafe] + ); const fetchSnapshot = useCallback( async (options?: { silent?: boolean }) => { - const silent = options?.silent ?? false - const previousStatus = statusRef.current - const isInitial = initialLoadRef.current + const silent = options?.silent ?? false; + const previousStatus = statusRef.current; + const isInitial = initialLoadRef.current; - if (previousStatus !== 'posting') { + if (previousStatus !== "posting") { if (isInitial) { - setStatusSafe('loading') + setStatusSafe("loading"); } else if (!silent) { - setStatusSafe('refreshing') + setStatusSafe("refreshing"); } } try { - const response = await fetch(`${API_BASE}?limit=${snapshotLimit}`, { cache: 'no-store' }) + const response = await fetch(`${API_BASE}?limit=${snapshotLimit}`, { cache: "no-store" }); if (!response.ok) { - const payload = await response.json().catch(() => null) - const detail = extractProblemDetail(payload) - setError({ key: 'errors.feedUnavailable', detail: detail ?? undefined }) + const payload = await response.json().catch(() => null); + const detail = extractProblemDetail(payload); + setError({ key: "errors.feedUnavailable", detail: detail ?? undefined }); if (initialLoadRef.current) { - setStatusSafe('error') - } else if (previousStatus !== 'posting') { - setStatusSafe('idle') + setStatusSafe("error"); + } else if (previousStatus !== "posting") { + setStatusSafe("idle"); } - return + return; } - const data: ApiSnapshot = await response.json() - applySnapshot(data) + const data: ApiSnapshot = await response.json(); + applySnapshot(data); } catch (error) { - const detail = error instanceof Error && error.message ? error.message : null - setError({ key: 'errors.feedUnknown', detail: detail ?? undefined }) + const detail = error instanceof Error && error.message ? error.message : null; + setError({ key: "errors.feedUnknown", detail: detail ?? undefined }); if (initialLoadRef.current) { - setStatusSafe('error') - } else if (previousStatus !== 'posting') { - setStatusSafe('idle') + setStatusSafe("error"); + } else if (previousStatus !== "posting") { + setStatusSafe("idle"); } } }, - [snapshotLimit, setStatusSafe, applySnapshot], - ) + [snapshotLimit, setStatusSafe, applySnapshot] + ); const connectToStream = useCallback(() => { try { - eventSourceRef.current?.close() - eventSourceRef.current = null + eventSourceRef.current?.close(); + eventSourceRef.current = null; - const url = new URL(mercureHub) - url.searchParams.append('topic', mercureTopic) + const url = new URL(mercureHub); + url.searchParams.append("topic", mercureTopic); - const eventSource = new EventSource(url.toString()) - eventSource.onmessage = (event) => { + const eventSource = new EventSource(url.toString()); + eventSource.onmessage = event => { try { - const payload: SnapshotEventPayload = JSON.parse(event.data) - if (payload?.type === 'snapshot' && payload.payload) { - applySnapshot(payload.payload, { preserveClientKey: true }) + const payload: SnapshotEventPayload = JSON.parse(event.data); + if (payload?.type === "snapshot" && payload.payload) { + applySnapshot(payload.payload, { preserveClientKey: true }); } } catch (parseError) { - console.error('Failed to parse stream payload', parseError) + console.error("Failed to parse stream payload", parseError); } - } + }; eventSource.onerror = () => { - if (statusRef.current !== 'loading') { - setError({ key: 'errors.feedUnknown' }) - setStatusSafe('error') + if (statusRef.current !== "loading") { + setError({ key: "errors.feedUnknown" }); + setStatusSafe("error"); } - } + }; - eventSourceRef.current = eventSource + eventSourceRef.current = eventSource; } catch (connectionError) { - console.error('Unable to subscribe to live updates', connectionError) - setError({ key: 'errors.feedUnavailable' }) - setStatusSafe('error') + console.error("Unable to subscribe to live updates", connectionError); + setError({ key: "errors.feedUnavailable" }); + setStatusSafe("error"); } - }, [applySnapshot, mercureHub, mercureTopic, setStatusSafe]) + }, [applySnapshot, mercureHub, mercureTopic, setStatusSafe]); useEffect(() => { - fetchSnapshot().catch(() => undefined) - connectToStream() + fetchSnapshot().catch(() => undefined); + connectToStream(); return () => { - eventSourceRef.current?.close() - eventSourceRef.current = null - } - }, [connectToStream, fetchSnapshot]) + eventSourceRef.current?.close(); + eventSourceRef.current = null; + }; + }, [connectToStream, fetchSnapshot]); const submitPoint = useCallback( async (target: Point): Promise => { if (!userLocation) { setError({ - key: 'errors.submitWithReason', - values: { message: 'Location required before submitting.' }, - detail: 'Location required before submitting.', - }) - setStatusSafe('error') - return { success: false } + key: "errors.submitWithReason", + values: { message: "Location required before submitting." }, + detail: "Location required before submitting.", + }); + setStatusSafe("error"); + return { success: false }; } - setStatusSafe('posting') + setStatusSafe("posting"); try { const response = await fetch(API_BASE, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ signalLocation: target, userLocation, }), - }) + }); if (!response.ok) { - const payload = await response.json().catch(() => null) - const detail = extractProblemDetail(payload) + const payload = await response.json().catch(() => null); + const detail = extractProblemDetail(payload); if (detail) { setError({ - key: 'errors.submitWithReason', + key: "errors.submitWithReason", values: { message: detail }, detail, - }) + }); } else { - setError({ key: 'errors.submitUnavailable' }) + setError({ key: "errors.submitUnavailable" }); } - setStatusSafe('error') - return { success: false } + setStatusSafe("error"); + return { success: false }; } - setError(null) - setStatusSafe('idle') - return { success: true } + setError(null); + setStatusSafe("idle"); + return { success: true }; } catch (error) { - const detail = error instanceof Error && error.message ? error.message : null + const detail = error instanceof Error && error.message ? error.message : null; if (detail) { - setError({ key: 'errors.submitWithReason', values: { message: detail }, detail }) + setError({ key: "errors.submitWithReason", values: { message: detail }, detail }); } else { - setError({ key: 'errors.submitUnknown' }) + setError({ key: "errors.submitUnknown" }); } - setStatusSafe('error') - return { success: false } + setStatusSafe("error"); + return { success: false }; } }, - [setStatusSafe, userLocation], - ) + [setStatusSafe, userLocation] + ); - const hasClientKey = Boolean(clientKey) + const hasClientKey = Boolean(clientKey); - const filterDensityWithinRadius = useCallback( - (collection: ApiDensityCell[], origin: Point | null) => { - if (!origin) { - return collection - } - return collection.filter((item) => distanceInKm(origin, item) <= VISIBLE_RADIUS_KM) - }, - [], - ) + const filterDensityWithinRadius = useCallback((collection: ApiDensityCell[], origin: Point | null) => { + if (!origin) { + return collection; + } + return collection.filter(item => distanceInKm(origin, item) <= VISIBLE_RADIUS_KM); + }, []); - const filterPointsWithinRadius = useCallback( - (collection: ApiPoint[], origin: Point | null) => { - if (!origin) { - return collection - } - return collection.filter((item) => distanceInKm(origin, item.signalLocation) <= VISIBLE_RADIUS_KM) - }, - [], - ) + const filterPointsWithinRadius = useCallback((collection: ApiPoint[], origin: Point | null) => { + if (!origin) { + return collection; + } + return collection.filter(item => distanceInKm(origin, item.signalLocation) <= VISIBLE_RADIUS_KM); + }, []); const selectVisibleDensity = useCallback( (origin: Point | null) => filterDensityWithinRadius(rawDensity, origin), - [filterDensityWithinRadius, rawDensity], - ) + [filterDensityWithinRadius, rawDensity] + ); const selectVisiblePoints = useCallback( (origin: Point | null) => filterPointsWithinRadius(rawPoints, origin), - [filterPointsWithinRadius, rawPoints], - ) + [filterPointsWithinRadius, rawPoints] + ); const selectVisibleLatestByUser = useCallback( (origin: Point | null) => filterPointsWithinRadius(rawLatestByUser, origin), - [filterPointsWithinRadius, rawLatestByUser], - ) + [filterPointsWithinRadius, rawLatestByUser] + ); const myLatestPoint = useMemo(() => { if (!hasClientKey) { - return null + return null; } - return rawLatestByUser.find((point) => point.userKey === clientKey) ?? null - }, [clientKey, hasClientKey, rawLatestByUser]) + return rawLatestByUser.find(point => point.userKey === clientKey) ?? null; + }, [clientKey, hasClientKey, rawLatestByUser]); return { status, @@ -291,5 +278,5 @@ export function useHotspotFeed({ selectVisiblePoints, selectVisibleLatestByUser, myLatestPoint, - } + }; } diff --git a/client/src/hooks/useLeafletHeatmap.ts b/client/src/hooks/useLeafletHeatmap.ts index 2d22075..03fa186 100644 --- a/client/src/hooks/useLeafletHeatmap.ts +++ b/client/src/hooks/useLeafletHeatmap.ts @@ -1,66 +1,67 @@ -import { useCallback, useEffect, useRef } from 'react' -import type { MutableRefObject } from 'react' +import { useCallback, useEffect, useRef } from "react"; +import type { MutableRefObject } from "react"; + import L, { type LeafletMouseEvent, type Map as LeafletMap, type LayerGroup, type TileLayer, type TileLayerOptions, -} from 'leaflet' -import 'leaflet.heat' +} from "leaflet"; +import "leaflet.heat"; -import type { ApiDensityCell, Point } from '@/types/api' +import type { ApiDensityCell, Point } from "@/types/api"; -type HeatPoint = [number, number, number?] +type HeatPoint = [number, number, number?]; type LeafletHeatLayer = L.Layer & { - setLatLngs(points: HeatPoint[]): LeafletHeatLayer - getBounds?: () => L.LatLngBounds -} + setLatLngs(points: HeatPoint[]): LeafletHeatLayer; + getBounds?: () => L.LatLngBounds; +}; type LeafletWithHeat = typeof L & { - heatLayer?: (points: HeatPoint[], options?: Record) => LeafletHeatLayer -} + heatLayer?: (points: HeatPoint[], options?: Record) => LeafletHeatLayer; +}; interface UseLeafletHeatmapParams { - heatCells: ApiDensityCell[] - userLocation: Point | null - onRequestSpot?: (position: Point) => void - tileProvider: TileProvider + heatCells: ApiDensityCell[]; + userLocation: Point | null; + onRequestSpot?: (position: Point) => void; + tileProvider: TileProvider; } interface UseLeafletHeatmapResult { - mapContainerRef: MutableRefObject - focusOn: (position: Point, zoom?: number) => void - fitToHeat: () => void - map: LeafletMap | null + mapContainerRef: MutableRefObject; + focusOn: (position: Point, zoom?: number) => void; + fitToHeat: () => void; + map: LeafletMap | null; } type TileSource = { - readonly url: string - readonly attribution: string - readonly options?: TileLayerOptions -} + readonly url: string; + readonly attribution: string; + readonly options?: TileLayerOptions; +}; const TILE_SOURCES = { openstreetmap: { - url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - attribution: '© OpenStreetMap contributors', + url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", + attribution: "© OpenStreetMap contributors", }, mapbox: { - url: 'https://api.mapbox.com/styles/v1/mapbox/navigation-day-v1/tiles/{z}/{x}/{y}?access_token=pk.eyJ1IjoiMWNhbnNhIiwiYSI6ImNsdzZ5cHp3bTFheWUydHJ6dHA4empteWEifQ.a3bODguIOY5HqhsVIvW48Q', - attribution: '© Mapbox, © OpenStreetMap contributors', + url: "https://api.mapbox.com/styles/v1/mapbox/navigation-day-v1/tiles/{z}/{x}/{y}?access_token=pk.eyJ1IjoiMWNhbnNhIiwiYSI6ImNsdzZ5cHp3bTFheWUydHJ6dHA4empteWEifQ.a3bODguIOY5HqhsVIvW48Q", + attribution: "© Mapbox, © OpenStreetMap contributors", options: { tileSize: 512, zoomOffset: -1, } satisfies TileLayerOptions, }, -} as const satisfies Record +} as const satisfies Record; -export type TileProvider = keyof typeof TILE_SOURCES +export type TileProvider = keyof typeof TILE_SOURCES; -const INITIAL_VIEW: Point = { lat: 20, lng: 0 } -const DEFAULT_ZOOM = 3 +const INITIAL_VIEW: Point = { lat: 20, lng: 0 }; +const DEFAULT_ZOOM = 3; export function useLeafletHeatmap({ heatCells, @@ -68,37 +69,34 @@ export function useLeafletHeatmap({ onRequestSpot, tileProvider, }: UseLeafletHeatmapParams): UseLeafletHeatmapResult { - const mapRef = useRef(null) - const heatLayerRef = useRef(null) - const userLayerRef = useRef(null) - const tileLayerRef = useRef(null) - const hasCenteredOnUserRef = useRef(false) - const mapContainerRef = useRef(null) - const onRequestSpotRef = useRef(onRequestSpot) - const tileProviderRef = useRef(tileProvider) + const mapRef = useRef(null); + const heatLayerRef = useRef(null); + const userLayerRef = useRef(null); + const tileLayerRef = useRef(null); + const hasCenteredOnUserRef = useRef(false); + const mapContainerRef = useRef(null); + const onRequestSpotRef = useRef(onRequestSpot); + const tileProviderRef = useRef(tileProvider); - const createTileLayer = useCallback( - (provider: TileProvider) => { - const leaflet = L as LeafletWithHeat - const source: TileSource = TILE_SOURCES[provider] - const options = source.options ?? {} - return leaflet.tileLayer(source.url, { - attribution: source.attribution, - crossOrigin: true, - maxZoom: 19, - ...options, - }) - }, - [], - ) + const createTileLayer = useCallback((provider: TileProvider) => { + const leaflet = L as LeafletWithHeat; + const source: TileSource = TILE_SOURCES[provider]; + const options = source.options ?? {}; + return leaflet.tileLayer(source.url, { + attribution: source.attribution, + crossOrigin: true, + maxZoom: 19, + ...options, + }); + }, []); const initialiseMap = useCallback(() => { if (mapRef.current || !mapContainerRef.current) { - return + return; } - const leaflet = L as LeafletWithHeat - const container = mapContainerRef.current + const leaflet = L as LeafletWithHeat; + const container = mapContainerRef.current; const map = leaflet .map(container, { @@ -107,179 +105,179 @@ export function useLeafletHeatmap({ zoomControl: false, attributionControl: false, }) - .setView([INITIAL_VIEW.lat, INITIAL_VIEW.lng], DEFAULT_ZOOM) + .setView([INITIAL_VIEW.lat, INITIAL_VIEW.lng], DEFAULT_ZOOM); - const tileLayer = createTileLayer(tileProviderRef.current) - tileLayer.addTo(map) + const tileLayer = createTileLayer(tileProviderRef.current); + tileLayer.addTo(map); const heatLayer = - typeof leaflet.heatLayer === 'function' + typeof leaflet.heatLayer === "function" ? leaflet.heatLayer([], { radius: 32, blur: 24, maxZoom: 12, gradient: { - 0.2: '#38bdf8', - 0.4: '#0ea5e9', - 0.6: '#fbbf24', - 0.8: '#fb923c', - 1: '#ef4444', + 0.2: "#38bdf8", + 0.4: "#0ea5e9", + 0.6: "#fbbf24", + 0.8: "#fb923c", + 1: "#ef4444", }, }) - : null + : null; - const userLayer = leaflet.layerGroup().addTo(map) + const userLayer = leaflet.layerGroup().addTo(map); if (heatLayer) { - heatLayer.addTo(map) + heatLayer.addTo(map); } const handleClick = (event: LeafletMouseEvent) => { - const { lat, lng } = event.latlng - if (typeof lat === 'number' && typeof lng === 'number') { - onRequestSpotRef.current?.({ lat, lng }) + const { lat, lng } = event.latlng; + if (typeof lat === "number" && typeof lng === "number") { + onRequestSpotRef.current?.({ lat, lng }); } - } + }; - map.on('click', handleClick) + map.on("click", handleClick); map.whenReady(() => { requestAnimationFrame(() => { - map.invalidateSize() - }) - }) + map.invalidateSize(); + }); + }); const onResize = () => { requestAnimationFrame(() => { - map.invalidateSize() - }) - } + map.invalidateSize(); + }); + }; - window.addEventListener('resize', onResize) + window.addEventListener("resize", onResize); - mapRef.current = map - heatLayerRef.current = heatLayer - userLayerRef.current = userLayer - tileLayerRef.current = tileLayer + mapRef.current = map; + heatLayerRef.current = heatLayer; + userLayerRef.current = userLayer; + tileLayerRef.current = tileLayer; return () => { - map.off('click', handleClick) - window.removeEventListener('resize', onResize) - map.remove() - mapRef.current = null - heatLayerRef.current = null - userLayerRef.current = null - tileLayerRef.current = null - hasCenteredOnUserRef.current = false - } - }, [createTileLayer]) + map.off("click", handleClick); + window.removeEventListener("resize", onResize); + map.remove(); + mapRef.current = null; + heatLayerRef.current = null; + userLayerRef.current = null; + tileLayerRef.current = null; + hasCenteredOnUserRef.current = false; + }; + }, [createTileLayer]); useEffect(() => { - onRequestSpotRef.current = onRequestSpot - }, [onRequestSpot]) + onRequestSpotRef.current = onRequestSpot; + }, [onRequestSpot]); useEffect(() => { - const cleanup = initialiseMap() + const cleanup = initialiseMap(); return () => { - if (typeof cleanup === 'function') { - cleanup() + if (typeof cleanup === "function") { + cleanup(); } - } - }, [initialiseMap]) + }; + }, [initialiseMap]); useEffect(() => { - tileProviderRef.current = tileProvider - const map = mapRef.current + tileProviderRef.current = tileProvider; + const map = mapRef.current; if (!map) { - return + return; } - const nextLayer = createTileLayer(tileProvider) - const currentLayer = tileLayerRef.current + const nextLayer = createTileLayer(tileProvider); + const currentLayer = tileLayerRef.current; if (currentLayer) { - currentLayer.removeFrom(map) + currentLayer.removeFrom(map); } - nextLayer.addTo(map) - tileLayerRef.current = nextLayer - }, [createTileLayer, tileProvider]) + nextLayer.addTo(map); + tileLayerRef.current = nextLayer; + }, [createTileLayer, tileProvider]); useEffect(() => { - const heatLayer = heatLayerRef.current + const heatLayer = heatLayerRef.current; if (!heatLayer) { - return + return; } if (!heatCells.length) { - heatLayer.setLatLngs([]) - return + heatLayer.setLatLngs([]); + return; } - const maxIntensity = Math.max(...heatCells.map((cell) => cell.intensity)) || 1 - const heatPoints: HeatPoint[] = heatCells.map((cell) => [ + const maxIntensity = Math.max(...heatCells.map(cell => cell.intensity)) || 1; + const heatPoints: HeatPoint[] = heatCells.map(cell => [ cell.lat, cell.lng, Math.max(0.2, cell.intensity / maxIntensity), - ]) + ]); - heatLayer.setLatLngs(heatPoints) - }, [heatCells]) + heatLayer.setLatLngs(heatPoints); + }, [heatCells]); useEffect(() => { - const userLayer = userLayerRef.current - const map = mapRef.current + const userLayer = userLayerRef.current; + const map = mapRef.current; if (!userLayer || !map) { - return + return; } - userLayer.clearLayers() + userLayer.clearLayers(); if (!userLocation) { - hasCenteredOnUserRef.current = false - return + hasCenteredOnUserRef.current = false; + return; } L.circleMarker([userLocation.lat, userLocation.lng], { radius: 7, - color: '#38bdf8', + color: "#38bdf8", weight: 3, opacity: 0.9, - fillColor: '#38bdf8', + fillColor: "#38bdf8", fillOpacity: 0.35, - }).addTo(userLayer) + }).addTo(userLayer); if (!hasCenteredOnUserRef.current) { - map.setView([userLocation.lat, userLocation.lng], 13, { animate: true }) - hasCenteredOnUserRef.current = true + map.setView([userLocation.lat, userLocation.lng], 13, { animate: true }); + hasCenteredOnUserRef.current = true; } - }, [userLocation]) + }, [userLocation]); const focusOn = useCallback((position: Point, zoom = 14) => { - const map = mapRef.current + const map = mapRef.current; if (!map) { - return + return; } - map.setView([position.lat, position.lng], zoom, { animate: true }) - }, []) + map.setView([position.lat, position.lng], zoom, { animate: true }); + }, []); const fitToHeat = useCallback(() => { - const map = mapRef.current - const heatLayer = heatLayerRef.current + const map = mapRef.current; + const heatLayer = heatLayerRef.current; if (!map || !heatLayer) { - return + return; } - const bounds = heatLayer.getBounds?.() + const bounds = heatLayer.getBounds?.(); if (bounds && bounds.isValid()) { - map.fitBounds(bounds.pad(0.25), { animate: true }) + map.fitBounds(bounds.pad(0.25), { animate: true }); } - }, []) + }, []); return { mapContainerRef, focusOn, fitToHeat, map: mapRef.current, - } + }; } diff --git a/client/src/hooks/useTheme.ts b/client/src/hooks/useTheme.ts index 8533712..69c3bad 100644 --- a/client/src/hooks/useTheme.ts +++ b/client/src/hooks/useTheme.ts @@ -1,46 +1,46 @@ -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from "react"; -type Theme = 'light' | 'dark' +type Theme = "light" | "dark"; -const STORAGE_KEY = 'signalmap-theme' +const STORAGE_KEY = "signalmap-theme"; function applyTheme(theme: Theme) { - const root = document.documentElement - if (theme === 'dark') { - root.classList.add('dark') + const root = document.documentElement; + if (theme === "dark") { + root.classList.add("dark"); } else { - root.classList.remove('dark') + root.classList.remove("dark"); } } function getPreferredTheme(): Theme { - if (typeof window === 'undefined') { - return 'dark' + if (typeof window === "undefined") { + return "dark"; } - const stored = window.localStorage.getItem(STORAGE_KEY) - if (stored === 'light' || stored === 'dark') { - return stored + const stored = window.localStorage.getItem(STORAGE_KEY); + if (stored === "light" || stored === "dark") { + return stored; } - const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches - return prefersDark ? 'dark' : 'light' + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + return prefersDark ? "dark" : "light"; } export function useTheme() { - const [theme, setTheme] = useState(() => getPreferredTheme()) + const [theme, setTheme] = useState(() => getPreferredTheme()); useEffect(() => { - applyTheme(theme) - window.localStorage.setItem(STORAGE_KEY, theme) - }, [theme]) + applyTheme(theme); + window.localStorage.setItem(STORAGE_KEY, theme); + }, [theme]); const toggleTheme = useCallback(() => { - setTheme((current) => (current === 'dark' ? 'light' : 'dark')) - }, []) + setTheme(current => (current === "dark" ? "light" : "dark")); + }, []); return { theme, setTheme, toggleTheme, - isDark: theme === 'dark', - } + isDark: theme === "dark", + }; } diff --git a/client/src/hooks/useUserLocation.ts b/client/src/hooks/useUserLocation.ts index f8f4e04..0c7fa0c 100644 --- a/client/src/hooks/useUserLocation.ts +++ b/client/src/hooks/useUserLocation.ts @@ -1,90 +1,90 @@ -import { useCallback, useEffect, useRef } from 'react' +import { useCallback, useEffect, useRef } from "react"; -import type { Point } from '@/types/api' -import { useAppStore } from '@/store/useAppStore' +import { useAppStore } from "@/store/useAppStore"; +import type { Point } from "@/types/api"; function geolocationErrorMessage(error: GeolocationPositionError): string { switch (error.code) { case error.PERMISSION_DENIED: - return 'location.error.permissionDenied' + return "location.error.permissionDenied"; case error.POSITION_UNAVAILABLE: - return 'location.error.unavailable' + return "location.error.unavailable"; case error.TIMEOUT: - return 'location.error.timeout' + return "location.error.timeout"; default: - return 'location.error.generic' + return "location.error.generic"; } } export function useUserLocation() { - const setUserLocation = useAppStore((state) => state.setUserLocation) - const setLocationError = useAppStore((state) => state.setLocationError) - const setIsRequestingLocation = useAppStore((state) => state.setIsRequestingLocation) - const watchIdRef = useRef(null) + const setUserLocation = useAppStore(state => state.setUserLocation); + const setLocationError = useAppStore(state => state.setLocationError); + const setIsRequestingLocation = useAppStore(state => state.setIsRequestingLocation); + const watchIdRef = useRef(null); const clearWatch = useCallback(() => { - if (typeof navigator === 'undefined' || !navigator.geolocation) { - return + if (typeof navigator === "undefined" || !navigator.geolocation) { + return; } if (watchIdRef.current !== null) { - navigator.geolocation.clearWatch(watchIdRef.current) - watchIdRef.current = null + navigator.geolocation.clearWatch(watchIdRef.current); + watchIdRef.current = null; } - }, []) + }, []); const start = useCallback(() => { - if (typeof navigator === 'undefined' || !navigator.geolocation) { - setLocationError('location.error.unsupported') - setIsRequestingLocation(false) - return + if (typeof navigator === "undefined" || !navigator.geolocation) { + setLocationError("location.error.unsupported"); + setIsRequestingLocation(false); + return; } - clearWatch() - setIsRequestingLocation(true) - setLocationError(null) + clearWatch(); + setIsRequestingLocation(true); + setLocationError(null); navigator.geolocation.getCurrentPosition( - (position) => { - const coords: Point = { lat: position.coords.latitude, lng: position.coords.longitude } - setUserLocation(coords) - setIsRequestingLocation(false) - setLocationError(null) + position => { + const coords: Point = { lat: position.coords.latitude, lng: position.coords.longitude }; + setUserLocation(coords); + setIsRequestingLocation(false); + setLocationError(null); }, - (geoError) => { - setUserLocation(null) - setLocationError(geolocationErrorMessage(geoError)) - setIsRequestingLocation(false) + geoError => { + setUserLocation(null); + setLocationError(geolocationErrorMessage(geoError)); + setIsRequestingLocation(false); }, - { enableHighAccuracy: true, timeout: 10000 }, - ) + { enableHighAccuracy: true, timeout: 10000 } + ); const watchId = navigator.geolocation.watchPosition( - (position) => { - const coords: Point = { lat: position.coords.latitude, lng: position.coords.longitude } - setUserLocation(coords) - setLocationError(null) - setIsRequestingLocation(false) + position => { + const coords: Point = { lat: position.coords.latitude, lng: position.coords.longitude }; + setUserLocation(coords); + setLocationError(null); + setIsRequestingLocation(false); }, - (geoError) => { - setUserLocation(null) - setLocationError(geolocationErrorMessage(geoError)) - setIsRequestingLocation(false) + geoError => { + setUserLocation(null); + setLocationError(geolocationErrorMessage(geoError)); + setIsRequestingLocation(false); }, - { enableHighAccuracy: true, maximumAge: 15000, timeout: 10000 }, - ) + { enableHighAccuracy: true, maximumAge: 15000, timeout: 10000 } + ); - watchIdRef.current = watchId - }, [clearWatch, setIsRequestingLocation, setLocationError, setUserLocation]) + watchIdRef.current = watchId; + }, [clearWatch, setIsRequestingLocation, setLocationError, setUserLocation]); useEffect(() => { - start() + start(); return () => { - clearWatch() - } - }, [clearWatch, start]) + clearWatch(); + }; + }, [clearWatch, start]); return { refresh: start, - } + }; } diff --git a/client/src/index.css b/client/src/index.css index 16deda9..e1f5cdd 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1,4 +1,4 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"); @tailwind base; @tailwind components; @@ -58,9 +58,16 @@ } body { - font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + font-family: + "Inter", + system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + sans-serif; @apply min-h-screen bg-background text-foreground antialiased; - background-image: radial-gradient(circle at top, hsla(var(--primary) / 0.1), transparent 45%), + background-image: + radial-gradient(circle at top, hsla(var(--primary) / 0.1), transparent 45%), radial-gradient(circle at bottom, hsla(var(--destructive) / 0.08), transparent 55%); } @@ -77,7 +84,13 @@ } .leaflet-container { - font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + font-family: + "Inter", + system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + sans-serif; z-index: 0; } @@ -91,7 +104,7 @@ @layer utilities { .status-dot::after { - content: ''; + content: ""; position: absolute; inset: -0.35rem; border-radius: 9999px; diff --git a/client/src/lib/i18n.ts b/client/src/lib/i18n.ts index d5b6ce3..0a9db75 100644 --- a/client/src/lib/i18n.ts +++ b/client/src/lib/i18n.ts @@ -1,11 +1,10 @@ -import i18n from 'i18next' -import { initReactI18next } from 'react-i18next' +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; -import enCommon from '@/locales/en/common.json' -import frCommon from '@/locales/fr/common.json' +import enCommon from "@/locales/en/common.json"; +import frCommon from "@/locales/fr/common.json"; -const browserLanguage = - typeof window !== 'undefined' ? window.navigator.language.split('-')[0]?.toLowerCase() : 'en' +const browserLanguage = typeof window !== "undefined" ? window.navigator.language.split("-")[0]?.toLowerCase() : "en"; i18n .use(initReactI18next) @@ -14,14 +13,13 @@ i18n en: { common: enCommon }, fr: { common: frCommon }, }, - lng: browserLanguage === 'fr' ? 'fr' : 'en', - fallbackLng: 'en', - defaultNS: 'common', + lng: browserLanguage === "fr" ? "fr" : "en", + fallbackLng: "en", + defaultNS: "common", interpolation: { escapeValue: false, }, }) - .catch(() => undefined) - -export { i18n } + .catch(() => undefined); +export { i18n }; diff --git a/client/src/lib/utils.ts b/client/src/lib/utils.ts index d0703cb..2c6f9b9 100644 --- a/client/src/lib/utils.ts +++ b/client/src/lib/utils.ts @@ -1,80 +1,80 @@ -import { type ClassValue, clsx } from 'clsx' -import { twMerge } from 'tailwind-merge' +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; export type Coordinates = { - lat: number - lng: number -} + lat: number; + lng: number; +}; export function cn(...inputs: ClassValue[]): string { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); } -export function formatCoordinate(value: number, locale = 'en-US'): string { +export function formatCoordinate(value: number, locale = "en-US"): string { const formatter = new Intl.NumberFormat(locale, { minimumFractionDigits: 3, maximumFractionDigits: 3, - }) - return formatter.format(value) + }); + return formatter.format(value); } -export function formatRelativeTime(dateIso: string, locale = 'en-US'): string { - const date = new Date(dateIso) - const now = new Date() - const diff = Math.max(0, now.getTime() - date.getTime()) +export function formatRelativeTime(dateIso: string, locale = "en-US"): string { + const date = new Date(dateIso); + const now = new Date(); + const diff = Math.max(0, now.getTime() - date.getTime()); - const seconds = Math.floor(diff / 1000) - const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }) + const seconds = Math.floor(diff / 1000); + const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" }); if (seconds < 60) { - return rtf.format(-seconds, 'second') + return rtf.format(-seconds, "second"); } - const minutes = Math.floor(seconds / 60) + const minutes = Math.floor(seconds / 60); if (minutes < 60) { - return rtf.format(-minutes, 'minute') + return rtf.format(-minutes, "minute"); } - const hours = Math.floor(minutes / 60) + const hours = Math.floor(minutes / 60); if (hours < 24) { - return rtf.format(-hours, 'hour') + return rtf.format(-hours, "hour"); } - const days = Math.floor(hours / 24) + const days = Math.floor(hours / 24); if (days < 7) { - return rtf.format(-days, 'day') + return rtf.format(-days, "day"); } - const weeks = Math.floor(days / 7) - return rtf.format(-weeks, 'week') + const weeks = Math.floor(days / 7); + return rtf.format(-weeks, "week"); } -export function formatTimestamp(dateIso: string, locale = 'en-US'): string { - const date = new Date(dateIso) +export function formatTimestamp(dateIso: string, locale = "en-US"): string { + const date = new Date(dateIso); return new Intl.DateTimeFormat(locale, { - month: 'short', - day: 'numeric', - hour: 'numeric', - minute: '2-digit', - }).format(date) + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + }).format(date); } -const EARTH_RADIUS_KM = 6371 +const EARTH_RADIUS_KM = 6371; export function distanceInKm(a: Coordinates, b: Coordinates): number { - const lat1 = toRadians(a.lat) - const lat2 = toRadians(b.lat) - const deltaLat = toRadians(b.lat - a.lat) - const deltaLng = toRadians(b.lng - a.lng) + const lat1 = toRadians(a.lat); + const lat2 = toRadians(b.lat); + const deltaLat = toRadians(b.lat - a.lat); + const deltaLng = toRadians(b.lng - a.lng); const haversine = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) + - Math.cos(lat1) * Math.cos(lat2) * Math.sin(deltaLng / 2) * Math.sin(deltaLng / 2) + Math.cos(lat1) * Math.cos(lat2) * Math.sin(deltaLng / 2) * Math.sin(deltaLng / 2); - const c = 2 * Math.atan2(Math.sqrt(haversine), Math.sqrt(1 - haversine)) - return EARTH_RADIUS_KM * c + const c = 2 * Math.atan2(Math.sqrt(haversine), Math.sqrt(1 - haversine)); + return EARTH_RADIUS_KM * c; } function toRadians(value: number): number { - return (value * Math.PI) / 180 + return (value * Math.PI) / 180; } diff --git a/client/src/main.tsx b/client/src/main.tsx index 48a499b..cacef1a 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -1,16 +1,17 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import { I18nextProvider } from 'react-i18next' -import 'leaflet/dist/leaflet.css' +import { StrictMode } from "react"; -import '@/index.css' -import App from '@/App.tsx' -import { i18n } from '@/lib/i18n' +import { createRoot } from "react-dom/client"; +import { I18nextProvider } from "react-i18next"; +import "leaflet/dist/leaflet.css"; -createRoot(document.getElementById('root')!).render( +import "@/index.css"; +import App from "@/App"; +import { i18n } from "@/lib/i18n"; + +createRoot(document.getElementById("root")!).render( - , -) + +); diff --git a/client/src/store/useAppStore.ts b/client/src/store/useAppStore.ts index ecd0add..ac61071 100644 --- a/client/src/store/useAppStore.ts +++ b/client/src/store/useAppStore.ts @@ -1,23 +1,23 @@ -import { create } from 'zustand' +import { create } from "zustand"; -import type { Point } from '@/types/api' +import type { Point } from "@/types/api"; -type Nullable = T | null +type Nullable = T | null; interface AppState { - userLocation: Nullable - locationError: Nullable - isRequestingLocation: boolean - setUserLocation: (location: Nullable) => void - setLocationError: (error: Nullable) => void - setIsRequestingLocation: (isRequesting: boolean) => void + userLocation: Nullable; + locationError: Nullable; + isRequestingLocation: boolean; + setUserLocation: (location: Nullable) => void; + setLocationError: (error: Nullable) => void; + setIsRequestingLocation: (isRequesting: boolean) => void; } -export const useAppStore = create((set) => ({ +export const useAppStore = create(set => ({ userLocation: null, locationError: null, isRequestingLocation: false, - setUserLocation: (userLocation) => set({ userLocation }), - setLocationError: (locationError) => set({ locationError }), - setIsRequestingLocation: (isRequestingLocation) => set({ isRequestingLocation }), -})) + setUserLocation: userLocation => set({ userLocation }), + setLocationError: locationError => set({ locationError }), + setIsRequestingLocation: isRequestingLocation => set({ isRequestingLocation }), +})); diff --git a/client/src/types/api.ts b/client/src/types/api.ts index 29f6906..dcca6d5 100644 --- a/client/src/types/api.ts +++ b/client/src/types/api.ts @@ -1,36 +1,36 @@ -export type FeedStatus = 'loading' | 'idle' | 'error' | 'posting' | 'refreshing' +export type FeedStatus = "loading" | "idle" | "error" | "posting" | "refreshing"; export interface Point { - lat: number - lng: number + lat: number; + lng: number; } export interface ApiPoint { - id: number - signalLocation: Point - createdAt: string - userKey: string + id: number; + signalLocation: Point; + createdAt: string; + userKey: string; } export interface ApiDensityCell { - lat: number - lng: number - intensity: number + lat: number; + lng: number; + intensity: number; } export interface ApiSnapshot { - clientKey?: string - points: ApiPoint[] - density: ApiDensityCell[] - latestByUser: ApiPoint[] + clientKey?: string; + points: ApiPoint[]; + density: ApiDensityCell[]; + latestByUser: ApiPoint[]; totals: { - points: number - contributors: number - } - updatedAt: string + points: number; + contributors: number; + }; + updatedAt: string; } export interface SnapshotEventPayload { - type: 'snapshot' - payload: ApiSnapshot + type: "snapshot"; + payload: ApiSnapshot; } diff --git a/client/src/types/leaflet-heat.d.ts b/client/src/types/leaflet-heat.d.ts index 66f2ae4..792212b 100644 --- a/client/src/types/leaflet-heat.d.ts +++ b/client/src/types/leaflet-heat.d.ts @@ -1,22 +1,19 @@ -declare module 'leaflet.heat' { - import type { Layer } from 'leaflet' +declare module "leaflet.heat" { + import type { Layer } from "leaflet"; export interface HeatLayer extends Layer { - setLatLngs(latlngs: Array<[number, number, number?]>): HeatLayer - addLatLng(latlng: [number, number, number?]): HeatLayer + setLatLngs(latlngs: Array<[number, number, number?]>): HeatLayer; + addLatLng(latlng: [number, number, number?]): HeatLayer; } export interface HeatLayerOptions { - radius?: number - blur?: number - maxZoom?: number - max?: number - minOpacity?: number - gradient?: Record + radius?: number; + blur?: number; + maxZoom?: number; + max?: number; + minOpacity?: number; + gradient?: Record; } - export default function heatLayer( - latlngs: Array<[number, number, number?]>, - options?: HeatLayerOptions, - ): HeatLayer + export default function heatLayer(latlngs: Array<[number, number, number?]>, options?: HeatLayerOptions): HeatLayer; } diff --git a/client/tailwind.config.js b/client/tailwind.config.js index de0d74d..a80fb37 100644 --- a/client/tailwind.config.js +++ b/client/tailwind.config.js @@ -1,63 +1,64 @@ /** @type {import('tailwindcss').Config} */ export default { - darkMode: ['class'], - content: ['./index.html', './src/**/*.{ts,tsx}'], + darkMode: ["class"], + content: ["./index.html", "./src/**/*.{ts,tsx}"], theme: { extend: { backgroundImage: { - 'radial-signal': 'radial-gradient(circle at top, rgba(56,189,248,0.15), transparent 45%), radial-gradient(circle at bottom, rgba(248,113,113,0.12), transparent 55%)', + "radial-signal": + "radial-gradient(circle at top, rgba(56,189,248,0.15), transparent 45%), radial-gradient(circle at bottom, rgba(248,113,113,0.12), transparent 55%)", }, colors: { - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))', + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", }, secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))', + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", }, destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))', + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", }, muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))', + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", }, accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))', + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", }, popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))', + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", }, card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))', + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", }, }, borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)', + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", }, keyframes: { - 'status-pulse': { - '0%': { transform: 'scale(0.7)', opacity: '0.6' }, - '70%': { transform: 'scale(1.4)', opacity: '0' }, - '100%': { transform: 'scale(0.7)', opacity: '0' }, + "status-pulse": { + "0%": { transform: "scale(0.7)", opacity: "0.6" }, + "70%": { transform: "scale(1.4)", opacity: "0" }, + "100%": { transform: "scale(0.7)", opacity: "0" }, }, }, animation: { - 'status-pulse': 'status-pulse 2.4s ease-out infinite', + "status-pulse": "status-pulse 2.4s ease-out infinite", }, }, }, - plugins: [require('tailwindcss-animate')], -} + plugins: [require("tailwindcss-animate")], +}; diff --git a/client/tsconfig.json b/client/tsconfig.json index 1ffef60..d32ff68 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -1,7 +1,4 @@ { "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] + "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] } diff --git a/client/vite.config.ts b/client/vite.config.ts index 603ea9e..98e692c 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -1,19 +1,19 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' -import { fileURLToPath, URL } from 'node:url' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import { fileURLToPath, URL } from "node:url"; // https://vite.dev/config/ export default defineConfig({ plugins: [ react({ babel: { - plugins: [['babel-plugin-react-compiler']], + plugins: [["babel-plugin-react-compiler"]], }, }), ], resolve: { alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)), + "@": fileURLToPath(new URL("./src", import.meta.url)), }, }, -}) +}); diff --git a/server/src/Service/PointProximityValidator.php b/server/src/Service/PointProximityValidator.php index 5389515..1839b0a 100644 --- a/server/src/Service/PointProximityValidator.php +++ b/server/src/Service/PointProximityValidator.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Service; +use App\ValueObject\Distance; use App\ValueObject\Point; use App\Exception\PointTooFarException; use Psr\Log\LoggerInterface; @@ -21,7 +22,7 @@ final class PointProximityValidator public function assertWithinRange(Point $userLocation, Point $signalLocation): void { - $distance = $this->distanceInKm($userLocation, $signalLocation); + $distance = Distance::betweenPoints($userLocation, $signalLocation)->inKilometers(); $this->logger->debug('Calculated proximity between user and signal.', [ 'distance_km' => $distance, @@ -36,17 +37,4 @@ final class PointProximityValidator throw new PointTooFarException($this->maximumDistanceKm); } } - - private function distanceInKm(Point $a, Point $b): float - { - $lat1 = deg2rad($a->getLat()); - $lat2 = deg2rad($b->getLat()); - $deltaLat = deg2rad($b->getLat() - $a->getLat()); - $deltaLng = deg2rad($b->getLng() - $a->getLng()); - - $haversine = sin($deltaLat / 2) ** 2 + cos($lat1) * cos($lat2) * sin($deltaLng / 2) ** 2; - $c = 2 * atan2(sqrt($haversine), sqrt(1 - $haversine)); - - return 6371 * $c; - } } diff --git a/server/src/ValueObject/Distance.php b/server/src/ValueObject/Distance.php new file mode 100644 index 0000000..060df69 --- /dev/null +++ b/server/src/ValueObject/Distance.php @@ -0,0 +1,38 @@ +getLat()); + $lat2 = deg2rad($to->getLat()); + $deltaLat = deg2rad($to->getLat() - $from->getLat()); + $deltaLng = deg2rad($to->getLng() - $from->getLng()); + + $haversine = sin($deltaLat / 2) ** 2 + cos($lat1) * cos($lat2) * sin($deltaLng / 2) ** 2; + $centralAngle = 2 * atan2(sqrt($haversine), sqrt(1 - $haversine)); + + return new self(self::EARTH_RADIUS_KM * $centralAngle); + } + + public function inKilometers(): float + { + return $this->kilometers; + } + + public function inMeters(): float + { + return $this->kilometers * 1000; + } +} diff --git a/server/tests/Unit/ValueObject/DistanceTest.php b/server/tests/Unit/ValueObject/DistanceTest.php new file mode 100644 index 0000000..be047fe --- /dev/null +++ b/server/tests/Unit/ValueObject/DistanceTest.php @@ -0,0 +1,32 @@ +inKilometers(), 0.5); + } + + public function testCanConvertDistanceToMeters(): void + { + $origin = Point::fromLatLng(0.0, 0.0); + $destination = Point::fromLatLng(0.0, 0.009); // ~1km east on equator + + $distance = Distance::betweenPoints($origin, $destination); + + self::assertEqualsWithDelta(1000, $distance->inMeters(), 10); + } +}