commit f3ab7d08164e9be505ff1b6f32def9caa153416e Author: Maaxxs <61059039+MaaxxsDev@users.noreply.github.com> Date: Sun Jul 5 13:37:06 2026 +0200 Initial commit: HifiPlanet Car-Hifi Shop (Frontend + Backend) React/Vite Frontend (hifi-src) + PHP/MariaDB Backend (hifi/api) fuer den Car-Hifi Umbau-Shop HifiPlanet in Amorbach, inkl. Admin-Panel, 2FA, Wartungsmodus, Cookie-Consent und rechtlichen Pflichtseiten. Co-Authored-By: Claude Sonnet 5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..67a03a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Dieses Repo lebt im XAMPP htdocs-Ordner zusammen mit anderen, projektfremden +# Verzeichnissen (Standard-XAMPP-Dateien, andere lokale Seiten). Nur die beiden +# HifiPlanet-Ordner (hifi/ und hifi-src/) sollen versioniert werden. +/* +!/hifi/ +!/hifi-src/ +!/.gitignore + +# Von Composer/NPM regenerierbare Abhängigkeiten +/hifi/api/vendor/ +/hifi-src/node_modules/ + +# Vite-Build-Output (wird per "npm run build" in hifi-src erzeugt und landet laut +# vite.config.js direkt in hifi/). Da emptyOutDir:false ist, sammeln sich hier über +# viele Builds hinweg tausende alte, umbenannte Chunk-Dateien an (u.a. durch das +# Code-Splitting der ~1600 Lucide-Icons im IconPicker) - das gehört nicht ins Repo. +/hifi/assets/ +/hifi/index.html +/hifi/favicon.png +/hifi/robots.txt + +# Umgebungsspezifische DB-Zugangsdaten (wird vom Admin-Panel automatisch neu geschrieben, +# unterscheidet sich zwischen Dev-/Live-Umgebung und gehört daher nicht ins Repo) +/hifi/api/config/db.php + +# Claude-Code-Tooling (Skills/lokale Einstellungen) - gehört nicht zum Website-Projekt +/hifi/.claude/ +/hifi/.agents/ +/hifi/skills-lock.json +/hifi-src/.claude/ +/hifi-src/.agents/ +/hifi-src/skills-lock.json + +# Von XAMPP/Editoren erzeugte Laufzeit-/Cache-Dateien +*.log +Thumbs.db +.DS_Store diff --git a/hifi-src/index.html b/hifi-src/index.html new file mode 100644 index 0000000..58697b1 --- /dev/null +++ b/hifi-src/index.html @@ -0,0 +1,46 @@ + + + + + + + HifiPlanet | Car-Hifi Umbauten nach Maß + + + + + + + + + + + + + +
+ + + diff --git a/hifi-src/package-lock.json b/hifi-src/package-lock.json new file mode 100644 index 0000000..02260f4 --- /dev/null +++ b/hifi-src/package-lock.json @@ -0,0 +1,3035 @@ +{ + "name": "hifi-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hifi-frontend", + "version": "1.0.0", + "dependencies": { + "framer-motion": "^12.42.2", + "lucide-react": "^1.23.0", + "qrcode": "^1.5.4", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.26.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.47", + "tailwindcss": "^3.4.13", + "vite": "^5.4.6" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz", + "integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.5.2", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.2.tgz", + "integrity": "sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.4", + "caniuse-lite": "^1.0.30001799", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.41", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.41.tgz", + "integrity": "sha512-WwS7MHhqGHHlaVsqRZnhvCEMS0owDX+SxRlve7JkuH7My1Ara3ZriTmCQupPfYjxMZ8I/tgxtJYr2t7taHaH4A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001800", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001800.tgz", + "integrity": "sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.387", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.387.tgz", + "integrity": "sha512-TaxwufTFDufvPEoXdhwVrA3UdFWBeWGkYoJ1K8ldF1xe6gKfth6iRNS5lTQ5JPNOHdGQm8PT1QYKUqFLCiUefQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "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/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "12.42.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.2.tgz", + "integrity": "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.42.2", + "motion-utils": "^12.39.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "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", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "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", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.23.0.tgz", + "integrity": "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/motion-dom": { + "version": "12.42.2", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.42.2.tgz", + "integrity": "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.39.0" + } + }, + "node_modules/motion-utils": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", + "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz", + "integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz", + "integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.3", + "react-router": "6.30.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/hifi-src/package.json b/hifi-src/package.json new file mode 100644 index 0000000..b1d5a0e --- /dev/null +++ b/hifi-src/package.json @@ -0,0 +1,26 @@ +{ + "name": "hifi-frontend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "framer-motion": "^12.42.2", + "lucide-react": "^1.23.0", + "qrcode": "^1.5.4", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.26.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.47", + "tailwindcss": "^3.4.13", + "vite": "^5.4.6" + } +} diff --git a/hifi-src/postcss.config.js b/hifi-src/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/hifi-src/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/hifi-src/public/favicon.png b/hifi-src/public/favicon.png new file mode 100644 index 0000000..622fdb9 Binary files /dev/null and b/hifi-src/public/favicon.png differ diff --git a/hifi-src/public/robots.txt b/hifi-src/public/robots.txt new file mode 100644 index 0000000..d4acecb --- /dev/null +++ b/hifi-src/public/robots.txt @@ -0,0 +1,6 @@ +User-agent: * +Allow: / +Disallow: /admin +Disallow: /api + +Sitemap: https://hifi-planet.de/sitemap.xml diff --git a/hifi-src/src/App.jsx b/hifi-src/src/App.jsx new file mode 100644 index 0000000..8a168b7 --- /dev/null +++ b/hifi-src/src/App.jsx @@ -0,0 +1,83 @@ +import { Routes, Route, Navigate } from 'react-router-dom'; +import ScrollToTop from './components/ScrollToTop.jsx'; +import PublicLayout from './components/PublicLayout.jsx'; +import AdminLayout from './components/AdminLayout.jsx'; +import ProtectedRoute from './components/ProtectedRoute.jsx'; +import RequirePermission from './components/RequirePermission.jsx'; + +import Home from './pages/public/Home.jsx'; +import VehicleSelect from './pages/public/VehicleSelect.jsx'; +import BrandPage from './pages/public/BrandPage.jsx'; +import ModelPage from './pages/public/ModelPage.jsx'; +import Leistungen from './pages/public/Leistungen.jsx'; +import ContactPage from './pages/public/ContactPage.jsx'; +import Impressum from './pages/public/Impressum.jsx'; +import Datenschutz from './pages/public/Datenschutz.jsx'; +import AGB from './pages/public/AGB.jsx'; + +import Login from './pages/admin/Login.jsx'; +import Dashboard from './pages/admin/Dashboard.jsx'; +import Brands from './pages/admin/Brands.jsx'; +import Models from './pages/admin/Models.jsx'; +import Packages from './pages/admin/Packages.jsx'; +import Services from './pages/admin/Services.jsx'; +import PackageProducts from './pages/admin/PackageProducts.jsx'; +import PackageUpgrades from './pages/admin/PackageUpgrades.jsx'; +import ContactRequests from './pages/admin/ContactRequests.jsx'; +import AccountSettings from './pages/admin/AccountSettings.jsx'; +import AdminUsers from './pages/admin/AdminUsers.jsx'; +import PermissionGroups from './pages/admin/PermissionGroups.jsx'; +import SettingsLayout from './pages/admin/settings/SettingsLayout.jsx'; +import DatabaseSettings from './pages/admin/settings/DatabaseSettings.jsx'; +import ExportImportSettings from './pages/admin/settings/ExportImportSettings.jsx'; +import MaintenanceSettings from './pages/admin/settings/MaintenanceSettings.jsx'; +import ResetSettings from './pages/admin/settings/ResetSettings.jsx'; + +export default function App() { + return ( + <> + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + } /> + + }> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } + > + } /> + } /> + } /> + } /> + } /> + + + + + + ); +} diff --git a/hifi-src/src/api/client.js b/hifi-src/src/api/client.js new file mode 100644 index 0000000..3f68ac4 --- /dev/null +++ b/hifi-src/src/api/client.js @@ -0,0 +1,26 @@ +const API_BASE = '/hifi/api'; + +async function request(path, options = {}) { + const res = await fetch(`${API_BASE}${path}`, { + credentials: 'include', + headers: options.body instanceof FormData ? {} : { 'Content-Type': 'application/json' }, + ...options, + }); + + const isJson = res.headers.get('content-type')?.includes('application/json'); + const data = isJson ? await res.json() : null; + + if (!res.ok) { + throw new Error(data?.error || `Fehler ${res.status}`); + } + + return data; +} + +export const api = { + get: (path) => request(path), + post: (path, body) => request(path, { method: 'POST', body: body instanceof FormData ? body : JSON.stringify(body) }), + put: (path, body) => request(path, { method: 'PUT', body: JSON.stringify(body) }), + patch: (path, body) => request(path, { method: 'PATCH', body: JSON.stringify(body) }), + delete: (path) => request(path, { method: 'DELETE' }), +}; diff --git a/hifi-src/src/assets/leistungen/3d-druck.jpg b/hifi-src/src/assets/leistungen/3d-druck.jpg new file mode 100644 index 0000000..c3b5e46 Binary files /dev/null and b/hifi-src/src/assets/leistungen/3d-druck.jpg differ diff --git a/hifi-src/src/assets/leistungen/alarmanlagen.jpg b/hifi-src/src/assets/leistungen/alarmanlagen.jpg new file mode 100644 index 0000000..1ef0c56 Binary files /dev/null and b/hifi-src/src/assets/leistungen/alarmanlagen.jpg differ diff --git a/hifi-src/src/assets/leistungen/car-hifi.jpg b/hifi-src/src/assets/leistungen/car-hifi.jpg new file mode 100644 index 0000000..0ebcb39 Binary files /dev/null and b/hifi-src/src/assets/leistungen/car-hifi.jpg differ diff --git a/hifi-src/src/assets/leistungen/cnc-laser.jpg b/hifi-src/src/assets/leistungen/cnc-laser.jpg new file mode 100644 index 0000000..1929371 Binary files /dev/null and b/hifi-src/src/assets/leistungen/cnc-laser.jpg differ diff --git a/hifi-src/src/assets/leistungen/cnc-zerspanen.jpg b/hifi-src/src/assets/leistungen/cnc-zerspanen.jpg new file mode 100644 index 0000000..9131087 Binary files /dev/null and b/hifi-src/src/assets/leistungen/cnc-zerspanen.jpg differ diff --git a/hifi-src/src/assets/leistungen/dashcam.jpg b/hifi-src/src/assets/leistungen/dashcam.jpg new file mode 100644 index 0000000..72a4051 Binary files /dev/null and b/hifi-src/src/assets/leistungen/dashcam.jpg differ diff --git a/hifi-src/src/assets/leistungen/hero-highlight.jpg b/hifi-src/src/assets/leistungen/hero-highlight.jpg new file mode 100644 index 0000000..b2ec61d Binary files /dev/null and b/hifi-src/src/assets/leistungen/hero-highlight.jpg differ diff --git a/hifi-src/src/assets/leistungen/oldtimer.jpg b/hifi-src/src/assets/leistungen/oldtimer.jpg new file mode 100644 index 0000000..59cd649 Binary files /dev/null and b/hifi-src/src/assets/leistungen/oldtimer.jpg differ diff --git a/hifi-src/src/assets/leistungen/wohnmobil.jpg b/hifi-src/src/assets/leistungen/wohnmobil.jpg new file mode 100644 index 0000000..dde654a Binary files /dev/null and b/hifi-src/src/assets/leistungen/wohnmobil.jpg differ diff --git a/hifi-src/src/assets/logo.png b/hifi-src/src/assets/logo.png new file mode 100644 index 0000000..6205145 Binary files /dev/null and b/hifi-src/src/assets/logo.png differ diff --git a/hifi-src/src/assets/photos/gallery-amp-purple.jpg b/hifi-src/src/assets/photos/gallery-amp-purple.jpg new file mode 100644 index 0000000..7bcc301 Binary files /dev/null and b/hifi-src/src/assets/photos/gallery-amp-purple.jpg differ diff --git a/hifi-src/src/assets/photos/gallery-dash.jpg b/hifi-src/src/assets/photos/gallery-dash.jpg new file mode 100644 index 0000000..186db96 Binary files /dev/null and b/hifi-src/src/assets/photos/gallery-dash.jpg differ diff --git a/hifi-src/src/assets/photos/gallery-dash2.jpg b/hifi-src/src/assets/photos/gallery-dash2.jpg new file mode 100644 index 0000000..477ed50 Binary files /dev/null and b/hifi-src/src/assets/photos/gallery-dash2.jpg differ diff --git a/hifi-src/src/assets/photos/gallery-rear.jpg b/hifi-src/src/assets/photos/gallery-rear.jpg new file mode 100644 index 0000000..9c5a2f5 Binary files /dev/null and b/hifi-src/src/assets/photos/gallery-rear.jpg differ diff --git a/hifi-src/src/assets/photos/gallery-speaker.jpg b/hifi-src/src/assets/photos/gallery-speaker.jpg new file mode 100644 index 0000000..13af3ea Binary files /dev/null and b/hifi-src/src/assets/photos/gallery-speaker.jpg differ diff --git a/hifi-src/src/assets/photos/gallery-subs.jpg b/hifi-src/src/assets/photos/gallery-subs.jpg new file mode 100644 index 0000000..7d61c79 Binary files /dev/null and b/hifi-src/src/assets/photos/gallery-subs.jpg differ diff --git a/hifi-src/src/assets/photos/hero-trunk.jpg b/hifi-src/src/assets/photos/hero-trunk.jpg new file mode 100644 index 0000000..bccabc2 Binary files /dev/null and b/hifi-src/src/assets/photos/hero-trunk.jpg differ diff --git a/hifi-src/src/components/Accordion.jsx b/hifi-src/src/components/Accordion.jsx new file mode 100644 index 0000000..a9d7227 --- /dev/null +++ b/hifi-src/src/components/Accordion.jsx @@ -0,0 +1,50 @@ +import { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; + +export default function Accordion({ items }) { + const [openIndex, setOpenIndex] = useState(null); + + return ( +
+ {items.map((item, i) => { + const isOpen = openIndex === i; + return ( +
+ + + {isOpen && ( + +

{item.answer}

+
+ )} +
+
+ ); + })} +
+ ); +} diff --git a/hifi-src/src/components/AdminLayout.jsx b/hifi-src/src/components/AdminLayout.jsx new file mode 100644 index 0000000..e56573b --- /dev/null +++ b/hifi-src/src/components/AdminLayout.jsx @@ -0,0 +1,171 @@ +import { useEffect, useState } from 'react'; +import { NavLink, Outlet, useNavigate } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext.jsx'; +import { api } from '../api/client.js'; +import ThemeToggle from './ThemeToggle.jsx'; +import logo from '../assets/logo.png'; + +const ICONS = { + home: 'M3 11.5 12 4l9 7.5M5 10v9a1 1 0 0 0 1 1h4v-6h4v6h4a1 1 0 0 0 1-1v-9', + tag: 'M4 4h7l9 9-7 7-9-9V4Zm3.5 3.5h.01', + car: 'M3 13.5 4.5 9A2 2 0 0 1 6.4 7.5h11.2A2 2 0 0 1 19.5 9L21 13.5M3 13.5v4a1 1 0 0 0 1 1h1a1 1 0 0 0 1-1v-1h12v1a1 1 0 0 0 1 1h1a1 1 0 0 0 1-1v-4M3 13.5h18M6.5 16.5h.01M17.5 16.5h.01', + box: 'M12 3 4 7.5v9L12 21l8-4.5v-9L12 3Zm0 9 8-4.5M12 12v9M12 12 4 7.5', + mail: 'M4 6h16a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1Zm0 0 8 7 8-7', + users: 'M16 7a4 4 0 1 1-8 0 4 4 0 0 1 8 0Zm-4 6c-4 0-7 2-7 4.5V19h14v-1.5C19 15 16 13 12 13Z', + shield: 'M12 3 4 6v6c0 4.5 3.4 7.7 8 9 4.6-1.3 8-4.5 8-9V6l-8-3Zm-1.5 9.5 4-4 1 1-5 5-2.5-2.5 1-1 1.5 1.5Z', + lock: 'M6 10V8a6 6 0 1 1 12 0v2m-13 0h14a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1Zm7 5v2', + briefcase: 'M4 7h16a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1Zm4 0V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M3 12h18', + sliders: 'M4 6h16M4 6a2 2 0 1 0 4 0 2 2 0 0 0-4 0Zm16 6H4m10 0a2 2 0 1 0 4 0 2 2 0 0 0-4 0ZM4 18h16m-12 0a2 2 0 1 0 4 0 2 2 0 0 0-4 0Z', +}; + +const Icon = ({ path }) => ( + + + +); + +export default function AdminLayout() { + const { logout, user, hasPermission } = useAuth(); + const navigate = useNavigate(); + const [mobileOpen, setMobileOpen] = useState(false); + const [newContactCount, setNewContactCount] = useState(0); + + useEffect(() => { + if (!hasPermission('contact.manage')) return; + api.get('/contact').then((rows) => setNewContactCount(rows.filter((r) => r.status === 'new').length)).catch(() => {}); + }, []); + + const navGroups = [ + { + title: null, + links: [ + { to: '/admin', label: 'Dashboard', icon: 'home', end: true }, + { to: '/admin/account', label: 'Mein Konto', icon: 'lock' }, + ], + }, + { + title: 'Fahrzeug-Katalog', + links: [ + { to: '/admin/brands', label: 'Marken', icon: 'tag', permission: 'brands.manage' }, + { to: '/admin/models', label: 'Modelle', icon: 'car', permission: 'models.manage' }, + { to: '/admin/packages', label: 'Pakete', icon: 'box', permission: 'packages.manage' }, + ], + }, + { + title: 'Website', + links: [ + { to: '/admin/services', label: 'Leistungen', icon: 'briefcase', permission: 'services.manage' }, + ], + }, + { + title: 'Kunden', + links: [ + { to: '/admin/contact-requests', label: 'Kontaktanfragen', icon: 'mail', permission: 'contact.manage', badge: newContactCount }, + ], + }, + { + title: 'Verwaltung', + links: [ + { to: '/admin/users', label: 'Benutzer', icon: 'users', permission: 'users.manage' }, + { to: '/admin/permission-groups', label: 'Berechtigungsgruppen', icon: 'shield', permission: 'permission_groups.manage' }, + { to: '/admin/settings', label: 'Einstellungen', icon: 'sliders', permission: 'settings.manage' }, + ], + }, + ] + .map((group) => ({ ...group, links: group.links.filter((l) => !l.permission || hasPermission(l.permission)) })) + .filter((group) => group.links.length > 0); + + const handleLogout = async () => { + await logout(); + navigate('/admin/login'); + }; + + const linkClass = ({ isActive }) => + `flex items-center justify-between gap-2 rounded-md px-3 py-2 text-sm font-medium transition ${ + isActive + ? 'bg-brand-500 text-white' + : 'text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800' + }`; + + const NavLinks = ({ onNavigate }) => ( + + ); + + return ( +
+ + +
+
+
+
+ + + HifiPlanet + +
+

+ Angemeldet als {user?.username}{user?.is_super_admin ? ' · Super-Admin' : ''} +

+
+ + +
+
+ {mobileOpen && ( +
+ setMobileOpen(false)} /> +
+ )} +
+
+ +
+
+
+ ); +} diff --git a/hifi-src/src/components/CookieConsentBanner.jsx b/hifi-src/src/components/CookieConsentBanner.jsx new file mode 100644 index 0000000..050ad23 --- /dev/null +++ b/hifi-src/src/components/CookieConsentBanner.jsx @@ -0,0 +1,120 @@ +import { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { useCookieConsent } from '../context/CookieConsentContext.jsx'; + +function SettingsModal({ onClose }) { + const { consent, saveCustom } = useCookieConsent(); + const [external, setExternal] = useState(consent.external); + + return ( +
+
e.stopPropagation()} + > +

Cookie-Einstellungen

+

+ Lege fest, welche Kategorien du zulassen möchtest. Mehr dazu in unserer{' '} + + Datenschutzerklärung + + . +

+
+
+
+

Notwendig

+

+ Für den Betrieb der Website erforderlich (z. B. Login-Session, Theme- und Cookie-Einstellung). + Kann nicht deaktiviert werden. +

+
+ +
+
+
+

Externe Medien

+

+ Google Maps und YouTube-Videos. Beim Laden werden Daten (u. a. deine IP-Adresse) an Google + übertragen. +

+
+ setExternal(e.target.checked)} + className="mt-1 h-4 w-4 shrink-0" + /> +
+
+
+ + +
+
+
+ ); +} + +export default function CookieConsentBanner() { + const { decided, settingsOpen, acceptAll, rejectNonEssential, openSettings, closeSettings } = useCookieConsent(); + + return ( + <> + {!decided && ( +
+
+

+ Wir verwenden nur technisch notwendige Cookies. Für Google Maps und YouTube-Videos benötigen wir + zusätzlich deine Zustimmung, da dabei Daten an Google übertragen werden. Mehr dazu in unserer{' '} + + Datenschutzerklärung + + . +

+
+ + + +
+
+
+ )} + + {settingsOpen && } + + ); +} diff --git a/hifi-src/src/components/DynamicIcon.jsx b/hifi-src/src/components/DynamicIcon.jsx new file mode 100644 index 0000000..e829ec6 --- /dev/null +++ b/hifi-src/src/components/DynamicIcon.jsx @@ -0,0 +1,8 @@ +import { DynamicIcon as LucideDynamicIcon } from 'lucide-react/dynamic'; + +// Lädt pro Icon-Name nur den einzelnen benötigten Icon-Chunk nach, statt die +// komplette ~2000-Icon-Bibliothek ins Bundle zu ziehen (siehe lucide-react/dynamic). +export default function DynamicIcon({ name, className = 'h-6 w-6', ...props }) { + if (!name) return null; + return ; +} diff --git a/hifi-src/src/components/ExternalEmbed.jsx b/hifi-src/src/components/ExternalEmbed.jsx new file mode 100644 index 0000000..64c7f45 --- /dev/null +++ b/hifi-src/src/components/ExternalEmbed.jsx @@ -0,0 +1,35 @@ +import { useCookieConsent } from '../context/CookieConsentContext.jsx'; + +export default function ExternalEmbed({ name, description, className = '', children }) { + const { consent, acceptAll, openSettings } = useCookieConsent(); + + if (consent.external) { + return children; + } + + return ( +
+

+ {description || `${name} wird erst nach deiner Zustimmung geladen, da dabei Daten an Google übertragen werden.`} +

+
+ + +
+
+ ); +} diff --git a/hifi-src/src/components/Footer.jsx b/hifi-src/src/components/Footer.jsx new file mode 100644 index 0000000..8edfc60 --- /dev/null +++ b/hifi-src/src/components/Footer.jsx @@ -0,0 +1,110 @@ +import { Link } from 'react-router-dom'; +import DynamicIcon from './DynamicIcon.jsx'; +import { useCookieConsent } from '../context/CookieConsentContext.jsx'; + +const SHOP_ADDRESS_ENCODED = encodeURIComponent('Boxbrunner Str. 20a, 63916 Amorbach'); +const DIRECTIONS_URL = `https://www.google.com/maps/dir/?api=1&destination=${SHOP_ADDRESS_ENCODED}`; + +const HOURS = [ + { days: 'Montag – Freitag', from: 9 * 60, to: 18 * 60, label: '9:00–18:00 Uhr' }, + { days: 'Samstag', from: 10 * 60, to: 13 * 60, label: '10:00–13:00 Uhr' }, + { days: 'Sonntag', from: null, to: null, label: 'Geschlossen' }, +]; + +function isOpenNow() { + const now = new Date(); + const minutes = now.getHours() * 60 + now.getMinutes(); + const day = now.getDay(); // 0 = Sonntag, 1-5 = Mo-Fr, 6 = Sa + const todayHours = day === 0 ? HOURS[2] : day === 6 ? HOURS[1] : HOURS[0]; + return todayHours.from !== null && minutes >= todayHours.from && minutes < todayHours.to; +} + +export default function Footer() { + const open = isOpenNow(); + const { openSettings } = useCookieConsent(); + + return ( +
+
+
+
+ +

HifiPlanet

+
+

Boxbrunner Str. 20a

+

63916 Amorbach

+ + + Route planen + +
+ +
+
+ +

Kontakt

+
+

+ 09373 20 62 390 +

+

+ info@hifi-planet-amorbach.de +

+ + Kontaktformular + +
+ +
+
+
+ +

Öffnungszeiten

+
+ + + {open ? 'Geöffnet' : 'Geschlossen'} + +
+
+ {HOURS.map((row) => ( +
+
{row.days}
+
{row.label}
+
+ ))} +
+
+
+
+

+ © {new Date().getFullYear()} HifiPlanet. Alle Angaben ohne Gewähr. + · + Impressum + · + Datenschutz + · + AGB + · + +

+
+
+ ); +} diff --git a/hifi-src/src/components/IconPicker.jsx b/hifi-src/src/components/IconPicker.jsx new file mode 100644 index 0000000..942bd53 --- /dev/null +++ b/hifi-src/src/components/IconPicker.jsx @@ -0,0 +1,63 @@ +import { useMemo, useState } from 'react'; +import { iconNames } from 'lucide-react/dynamic'; +import DynamicIcon from './DynamicIcon.jsx'; + +const ALL_ICON_NAMES = [...iconNames].sort(); + +const DEFAULT_LIMIT = 120; +const SEARCH_LIMIT = 200; + +export default function IconPicker({ value, onChange }) { + const [search, setSearch] = useState(''); + + const results = useMemo(() => { + const q = search.trim().toLowerCase(); + if (!q) return ALL_ICON_NAMES.slice(0, DEFAULT_LIMIT); + return ALL_ICON_NAMES.filter((name) => name.includes(q)).slice(0, SEARCH_LIMIT); + }, [search]); + + return ( +
+
+
+ {value ? : ?} +
+
+

{value || 'Kein Icon gewählt'}

+

{ALL_ICON_NAMES.length} Icons verfügbar

+
+
+ + setSearch(e.target.value)} + placeholder="Icon suchen (z. B. car, shield, zap)…" + className="mb-2 w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-900" + /> + +
+ {results.map((name) => ( + + ))} + {results.length === 0 && ( +

Keine Icons gefunden.

+ )} +
+ {!search && ( +

Tipp: Tippe einen Suchbegriff ein, um alle {ALL_ICON_NAMES.length} Icons zu durchsuchen.

+ )} +
+ ); +} diff --git a/hifi-src/src/components/ImageUploadField.jsx b/hifi-src/src/components/ImageUploadField.jsx new file mode 100644 index 0000000..061d8ba --- /dev/null +++ b/hifi-src/src/components/ImageUploadField.jsx @@ -0,0 +1,37 @@ +import { useState } from 'react'; +import { api } from '../api/client.js'; + +export default function ImageUploadField({ value, onChange, label = 'Bild' }) { + const [uploading, setUploading] = useState(false); + const [error, setError] = useState(''); + + const handleFile = async (e) => { + const file = e.target.files?.[0]; + if (!file) return; + + setUploading(true); + setError(''); + try { + const formData = new FormData(); + formData.append('file', file); + const result = await api.post('/uploads', formData); + onChange(result.path); + } catch (err) { + setError(err.message); + } finally { + setUploading(false); + } + }; + + return ( +
+ +
+ {value && } + + {uploading && Lädt hoch…} +
+ {error &&

{error}

} +
+ ); +} diff --git a/hifi-src/src/components/MaintenanceBypassBanner.jsx b/hifi-src/src/components/MaintenanceBypassBanner.jsx new file mode 100644 index 0000000..e445b3e --- /dev/null +++ b/hifi-src/src/components/MaintenanceBypassBanner.jsx @@ -0,0 +1,22 @@ +const TEXT = ( + <> + Wartungsmodus aktiv – Besucher sehen aktuell eine Wartungsmeldung. Du siehst diese Seite nur, weil + du die Berechtigung "Wartungsmodus umgehen" hast. + +); + +export default function MaintenanceBypassBanner({ inline = false }) { + if (inline) { + return ( +
+ {TEXT} +
+ ); + } + + return ( +
+ {TEXT} +
+ ); +} diff --git a/hifi-src/src/components/MaintenanceNotice.jsx b/hifi-src/src/components/MaintenanceNotice.jsx new file mode 100644 index 0000000..85ec9ff --- /dev/null +++ b/hifi-src/src/components/MaintenanceNotice.jsx @@ -0,0 +1,33 @@ +const WrenchIcon = () => ( + + + +); + +export default function MaintenanceNotice({ message, fullScreen = false }) { + const content = ( +
+
+ +
+

Wartungsarbeiten

+

+ {message || 'Dieser Bereich wird gerade aktualisiert. Bitte schau in Kürze wieder vorbei.'} +

+
+ ); + + if (fullScreen) { + return ( +
+ {content} +
+ ); + } + + return ( +
+ {content} +
+ ); +} diff --git a/hifi-src/src/components/Navbar.jsx b/hifi-src/src/components/Navbar.jsx new file mode 100644 index 0000000..1ac1559 --- /dev/null +++ b/hifi-src/src/components/Navbar.jsx @@ -0,0 +1,46 @@ +import { useState } from 'react'; +import { Link } from 'react-router-dom'; +import ThemeToggle from './ThemeToggle.jsx'; +import logo from '../assets/logo.png'; + +export default function Navbar() { + const [open, setOpen] = useState(false); + + return ( +
+
+ + HifiPlanet + + + + +
+ + +
+
+ + {open && ( + + )} +
+ ); +} diff --git a/hifi-src/src/components/ProtectedRoute.jsx b/hifi-src/src/components/ProtectedRoute.jsx new file mode 100644 index 0000000..7b3be31 --- /dev/null +++ b/hifi-src/src/components/ProtectedRoute.jsx @@ -0,0 +1,16 @@ +import { Navigate, Outlet } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext.jsx'; + +export default function ProtectedRoute() { + const { user, loading } = useAuth(); + + if (loading) { + return
Lädt…
; + } + + if (!user) { + return ; + } + + return ; +} diff --git a/hifi-src/src/components/PublicLayout.jsx b/hifi-src/src/components/PublicLayout.jsx new file mode 100644 index 0000000..6efea9d --- /dev/null +++ b/hifi-src/src/components/PublicLayout.jsx @@ -0,0 +1,59 @@ +import { useEffect, useState } from 'react'; +import { Outlet, useLocation } from 'react-router-dom'; +import Navbar from './Navbar.jsx'; +import Footer from './Footer.jsx'; +import ScrollProgress from './ScrollProgress.jsx'; +import MaintenanceNotice from './MaintenanceNotice.jsx'; +import MaintenanceBypassBanner from './MaintenanceBypassBanner.jsx'; +import CookieConsentBanner from './CookieConsentBanner.jsx'; +import { MaintenanceProvider } from '../context/MaintenanceContext.jsx'; +import { useAuth } from '../context/AuthContext.jsx'; +import { api } from '../api/client.js'; + +const FALLBACK_STATUS = { + global: { enabled: false, message: null }, + services: { enabled: false, message: null }, + vehicles: { enabled: false, message: null }, +}; + +// Das Impressum (und die anderen rechtlichen Pflichtseiten) müssen laut § 5 DDG +// "leicht erkennbar und unmittelbar erreichbar" bleiben – auch während der +// generellen Wartung darf diese Erreichbarkeit nicht durch die Wartungsseite +// verdeckt werden. +const LEGAL_PATHS = ['/impressum', '/datenschutz', '/agb']; + +export default function PublicLayout() { + const [status, setStatus] = useState(null); + const { loading: authLoading, hasPermission } = useAuth(); + const location = useLocation(); + + useEffect(() => { + api.get('/maintenance').then(setStatus).catch(() => setStatus(FALLBACK_STATUS)); + }, []); + + if (status === null || authLoading) { + return
; + } + + const bypass = hasPermission('maintenance.bypass'); + const isLegalPage = LEGAL_PATHS.includes(location.pathname); + + if (status.global.enabled && !bypass && !isLegalPage) { + return ; + } + + return ( + +
+ {bypass && status.global.enabled && } + + +
+ +
+
+ +
+
+ ); +} diff --git a/hifi-src/src/components/RequirePermission.jsx b/hifi-src/src/components/RequirePermission.jsx new file mode 100644 index 0000000..221dedc --- /dev/null +++ b/hifi-src/src/components/RequirePermission.jsx @@ -0,0 +1,22 @@ +import { useAuth } from '../context/AuthContext.jsx'; + +export default function RequirePermission({ permission, children }) { + const { hasPermission } = useAuth(); + + const permissions = Array.isArray(permission) ? permission : [permission]; + const allowed = permissions.some((p) => hasPermission(p)); + + if (!allowed) { + return ( +
+

Keine Berechtigung

+

+ Du hast keinen Zugriff auf diesen Bereich. Bitte wende dich an einen Administrator, falls du hier + Zugriff brauchst. +

+
+ ); + } + + return children; +} diff --git a/hifi-src/src/components/Reveal.jsx b/hifi-src/src/components/Reveal.jsx new file mode 100644 index 0000000..c97c467 --- /dev/null +++ b/hifi-src/src/components/Reveal.jsx @@ -0,0 +1,50 @@ +import { motion } from 'framer-motion'; + +const EASE = [0.16, 1, 0.3, 1]; + +const offsetFor = { + up: { y: 40 }, + left: { x: -56 }, + right: { x: 56 }, +}; + +function buildVariants(direction) { + const offset = offsetFor[direction] || offsetFor.up; + return { + hidden: { opacity: 0, scale: 0.96, filter: 'blur(8px)', ...offset }, + visible: { opacity: 1, scale: 1, x: 0, y: 0, filter: 'blur(0px)' }, + }; +} + +const shineVariants = { + hidden: { x: '-130%' }, + visible: { x: '230%', transition: { duration: 1.1, ease: 'easeInOut', delay: 0.4 } }, +}; + +// Trigger-based reveal: each element fades/slides/un-blurs into place the +// first time it enters the viewport, then stays put. Calm and predictable +// on repeated scrolling in either direction (no re-triggering, no jitter). +export default function Reveal({ children, direction = 'up', className = '', index = 0, shine = false }) { + const delay = Math.min(index * 0.09, 0.45); + + return ( + + {children} + {shine && ( + + )} + + ); +} diff --git a/hifi-src/src/components/ScrollProgress.jsx b/hifi-src/src/components/ScrollProgress.jsx new file mode 100644 index 0000000..9c8e1e4 --- /dev/null +++ b/hifi-src/src/components/ScrollProgress.jsx @@ -0,0 +1,13 @@ +import { motion, useScroll, useSpring } from 'framer-motion'; + +export default function ScrollProgress() { + const { scrollYProgress } = useScroll(); + const scaleX = useSpring(scrollYProgress, { stiffness: 200, damping: 30, restDelta: 0.001 }); + + return ( + + ); +} diff --git a/hifi-src/src/components/ScrollToTop.jsx b/hifi-src/src/components/ScrollToTop.jsx new file mode 100644 index 0000000..ac086f7 --- /dev/null +++ b/hifi-src/src/components/ScrollToTop.jsx @@ -0,0 +1,14 @@ +import { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; + +export default function ScrollToTop() { + const { pathname } = useLocation(); + + useEffect(() => { + // 'instant' statt 'auto', da 'auto' die global gesetzte CSS-Regel scroll-behavior: smooth + // respektiert und dadurch animiert statt sofort zu springen + window.scrollTo({ top: 0, left: 0, behavior: 'instant' }); + }, [pathname]); + + return null; +} diff --git a/hifi-src/src/components/StarRating.jsx b/hifi-src/src/components/StarRating.jsx new file mode 100644 index 0000000..f38e575 --- /dev/null +++ b/hifi-src/src/components/StarRating.jsx @@ -0,0 +1,11 @@ +export default function StarRating({ count = 5, className = 'h-4 w-4' }) { + return ( +
+ {Array.from({ length: count }).map((_, i) => ( + + + + ))} +
+ ); +} diff --git a/hifi-src/src/components/TestimonialSlider.jsx b/hifi-src/src/components/TestimonialSlider.jsx new file mode 100644 index 0000000..f86159a --- /dev/null +++ b/hifi-src/src/components/TestimonialSlider.jsx @@ -0,0 +1,136 @@ +import { useLayoutEffect, useRef } from 'react'; +import StarRating from './StarRating.jsx'; + +const SPEED_PX_PER_SEC = 70; + +// Läuft als Endlos-Marquee (links -> rechts): der Track enthält die Liste doppelt +// hintereinander. Die Position wird per rAF selbst verwaltet (statt CSS-Animation), +// damit sie sich beim Hovern anhalten UND per Drag manuell verschieben lässt. +export default function TestimonialSlider({ testimonials }) { + const viewportRef = useRef(null); + const trackRef = useRef(null); + const state = useRef({ + pos: 0, + half: 0, + hovering: false, + dragging: false, + dragStartX: 0, + dragStartPos: 0, + lastTime: null, + rafId: null, + }).current; + + useLayoutEffect(() => { + const track = trackRef.current; + const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + + const applyPos = () => { + track.style.transform = `translateX(${state.pos}px)`; + }; + + const normalize = (pos) => { + if (state.half <= 0) return pos; + while (pos > 0) pos -= state.half; + while (pos <= -state.half) pos += state.half; + return pos; + }; + + const measure = () => { + state.half = track.scrollWidth / 2; + if (state.pos === 0) { + state.pos = -state.half; + } else { + state.pos = normalize(state.pos); + } + applyPos(); + }; + measure(); + + const resizeObserver = new ResizeObserver(measure); + resizeObserver.observe(track); + + const tick = (time) => { + if (state.lastTime === null) state.lastTime = time; + const dt = (time - state.lastTime) / 1000; + state.lastTime = time; + + if (!state.dragging && !state.hovering && !reduceMotion) { + state.pos = normalize(state.pos + SPEED_PX_PER_SEC * dt); + applyPos(); + } + state.rafId = requestAnimationFrame(tick); + }; + state.rafId = requestAnimationFrame(tick); + + const viewport = viewportRef.current; + + const onPointerEnter = () => { + state.hovering = true; + }; + const onPointerLeave = () => { + state.hovering = false; + }; + const onPointerDown = (e) => { + state.dragging = true; + state.dragStartX = e.clientX; + state.dragStartPos = state.pos; + viewport.setPointerCapture(e.pointerId); + viewport.classList.add('cursor-grabbing'); + viewport.classList.remove('cursor-grab'); + }; + const onPointerMove = (e) => { + if (!state.dragging) return; + state.pos = normalize(state.dragStartPos + (e.clientX - state.dragStartX)); + applyPos(); + }; + const endDrag = (e) => { + if (!state.dragging) return; + state.dragging = false; + if (viewport.hasPointerCapture(e.pointerId)) { + viewport.releasePointerCapture(e.pointerId); + } + viewport.classList.remove('cursor-grabbing'); + viewport.classList.add('cursor-grab'); + }; + + viewport.addEventListener('pointerenter', onPointerEnter); + viewport.addEventListener('pointerleave', onPointerLeave); + viewport.addEventListener('pointerdown', onPointerDown); + viewport.addEventListener('pointermove', onPointerMove); + viewport.addEventListener('pointerup', endDrag); + viewport.addEventListener('pointercancel', endDrag); + + return () => { + cancelAnimationFrame(state.rafId); + resizeObserver.disconnect(); + viewport.removeEventListener('pointerenter', onPointerEnter); + viewport.removeEventListener('pointerleave', onPointerLeave); + viewport.removeEventListener('pointerdown', onPointerDown); + viewport.removeEventListener('pointermove', onPointerMove); + viewport.removeEventListener('pointerup', endDrag); + viewport.removeEventListener('pointercancel', endDrag); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [testimonials.length]); + + const track = [...testimonials, ...testimonials]; + + return ( +
+
+ {track.map((t, i) => ( +
+
+ +

„{t.text}"

+

{t.name}

+
+
+ ))} +
+
+ ); +} diff --git a/hifi-src/src/components/ThemeToggle.jsx b/hifi-src/src/components/ThemeToggle.jsx new file mode 100644 index 0000000..0560d82 --- /dev/null +++ b/hifi-src/src/components/ThemeToggle.jsx @@ -0,0 +1,23 @@ +import { useTheme } from '../context/ThemeContext.jsx'; + +export default function ThemeToggle() { + const { theme, toggleTheme } = useTheme(); + + return ( + + ); +} diff --git a/hifi-src/src/context/AuthContext.jsx b/hifi-src/src/context/AuthContext.jsx new file mode 100644 index 0000000..90de03e --- /dev/null +++ b/hifi-src/src/context/AuthContext.jsx @@ -0,0 +1,54 @@ +import { createContext, useContext, useEffect, useState } from 'react'; +import { api } from '../api/client.js'; + +const AuthContext = createContext(null); + +export function AuthProvider({ children }) { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + api + .get('/auth/me') + .then(setUser) + .catch(() => setUser(null)) + .finally(() => setLoading(false)); + }, []); + + const login = async (username, password) => { + const data = await api.post('/auth/login', { username, password }); + if (data.requires_2fa) { + return { requires2fa: true }; + } + setUser(data); + return { requires2fa: false }; + }; + + const verifyTwoFactor = async (code) => { + const data = await api.post('/auth/verify-2fa', { code }); + setUser(data); + }; + + const logout = async () => { + await api.post('/auth/logout', {}); + setUser(null); + }; + + const refreshUser = async () => { + const data = await api.get('/auth/me'); + setUser(data); + }; + + const hasPermission = (permission) => + !!user && (user.is_super_admin || user.permissions?.includes(permission)); + + return ( + + {children} + + ); +} + +export function useAuth() { + return useContext(AuthContext); +} diff --git a/hifi-src/src/context/CookieConsentContext.jsx b/hifi-src/src/context/CookieConsentContext.jsx new file mode 100644 index 0000000..fced880 --- /dev/null +++ b/hifi-src/src/context/CookieConsentContext.jsx @@ -0,0 +1,60 @@ +import { createContext, useCallback, useContext, useEffect, useState } from 'react'; + +const STORAGE_KEY = 'hifiplanet-cookie-consent'; + +const defaultConsent = { necessary: true, external: false }; + +const CookieConsentContext = createContext({ + consent: defaultConsent, + decided: false, + settingsOpen: false, + acceptAll: () => {}, + rejectNonEssential: () => {}, + saveCustom: () => {}, + openSettings: () => {}, + closeSettings: () => {}, +}); + +export function CookieConsentProvider({ children }) { + const [consent, setConsent] = useState(defaultConsent); + const [decided, setDecided] = useState(false); + const [settingsOpen, setSettingsOpen] = useState(false); + + useEffect(() => { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (raw) { + const parsed = JSON.parse(raw); + setConsent({ necessary: true, external: !!parsed.external }); + setDecided(true); + } + } catch { + // beschädigter Storage-Eintrag -> Banner wird erneut angezeigt + } + }, []); + + const persist = useCallback((next) => { + setConsent(next); + setDecided(true); + setSettingsOpen(false); + localStorage.setItem(STORAGE_KEY, JSON.stringify({ ...next, decidedAt: new Date().toISOString() })); + }, []); + + const acceptAll = useCallback(() => persist({ necessary: true, external: true }), [persist]); + const rejectNonEssential = useCallback(() => persist({ necessary: true, external: false }), [persist]); + const saveCustom = useCallback((partial) => persist({ necessary: true, external: !!partial.external }), [persist]); + const openSettings = useCallback(() => setSettingsOpen(true), []); + const closeSettings = useCallback(() => setSettingsOpen(false), []); + + return ( + + {children} + + ); +} + +export function useCookieConsent() { + return useContext(CookieConsentContext); +} diff --git a/hifi-src/src/context/MaintenanceContext.jsx b/hifi-src/src/context/MaintenanceContext.jsx new file mode 100644 index 0000000..fcc9c8f --- /dev/null +++ b/hifi-src/src/context/MaintenanceContext.jsx @@ -0,0 +1,14 @@ +import { createContext, useContext } from 'react'; + +const MaintenanceContext = createContext({ + global: { enabled: false, message: null }, + services: { enabled: false, message: null }, + vehicles: { enabled: false, message: null }, + bypass: false, +}); + +export const MaintenanceProvider = MaintenanceContext.Provider; + +export function useMaintenance() { + return useContext(MaintenanceContext); +} diff --git a/hifi-src/src/context/ThemeContext.jsx b/hifi-src/src/context/ThemeContext.jsx new file mode 100644 index 0000000..e1c81ca --- /dev/null +++ b/hifi-src/src/context/ThemeContext.jsx @@ -0,0 +1,24 @@ +import { createContext, useContext, useEffect, useState } from 'react'; + +const ThemeContext = createContext(null); + +export function ThemeProvider({ children }) { + const [theme, setTheme] = useState(() => { + const stored = localStorage.getItem('hifi-theme'); + if (stored) return stored; + return 'light'; + }); + + useEffect(() => { + document.documentElement.classList.toggle('dark', theme === 'dark'); + localStorage.setItem('hifi-theme', theme); + }, [theme]); + + const toggleTheme = () => setTheme((t) => (t === 'dark' ? 'light' : 'dark')); + + return {children}; +} + +export function useTheme() { + return useContext(ThemeContext); +} diff --git a/hifi-src/src/hooks/usePageMeta.js b/hifi-src/src/hooks/usePageMeta.js new file mode 100644 index 0000000..bf0f1be --- /dev/null +++ b/hifi-src/src/hooks/usePageMeta.js @@ -0,0 +1,38 @@ +import { useEffect } from 'react'; + +function setMetaTag(name, content, attr = 'name') { + if (!content) return; + let tag = document.head.querySelector(`meta[${attr}="${name}"]`); + if (!tag) { + tag = document.createElement('meta'); + tag.setAttribute(attr, name); + document.head.appendChild(tag); + } + tag.setAttribute('content', content); +} + +function setCanonical(path) { + let link = document.head.querySelector('link[rel="canonical"]'); + if (!link) { + link = document.createElement('link'); + link.setAttribute('rel', 'canonical'); + document.head.appendChild(link); + } + link.setAttribute('href', `https://hifi-planet.de${path}`); +} + +export default function usePageMeta({ title, description, path }) { + useEffect(() => { + if (title) { + document.title = `${title} | HifiPlanet`; + setMetaTag('og:title', `${title} | HifiPlanet`, 'property'); + } + if (description) { + setMetaTag('description', description); + setMetaTag('og:description', description, 'property'); + } + if (path) { + setCanonical(path); + } + }, [title, description, path]); +} diff --git a/hifi-src/src/index.css b/hifi-src/src/index.css new file mode 100644 index 0000000..4c2a901 --- /dev/null +++ b/hifi-src/src/index.css @@ -0,0 +1,12 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html { + scroll-behavior: smooth; +} + +.marquee-viewport { + -webkit-mask-image: linear-gradient(to right, transparent, black 6%, black 94%, transparent); + mask-image: linear-gradient(to right, transparent, black 6%, black 94%, transparent); +} diff --git a/hifi-src/src/main.jsx b/hifi-src/src/main.jsx new file mode 100644 index 0000000..fab029e --- /dev/null +++ b/hifi-src/src/main.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import App from './App.jsx'; +import { ThemeProvider } from './context/ThemeContext.jsx'; +import { AuthProvider } from './context/AuthContext.jsx'; +import { CookieConsentProvider } from './context/CookieConsentContext.jsx'; +import './index.css'; + +ReactDOM.createRoot(document.getElementById('root')).render( + + + + + + + + + + + +); diff --git a/hifi-src/src/pages/admin/AccountSettings.jsx b/hifi-src/src/pages/admin/AccountSettings.jsx new file mode 100644 index 0000000..886e7a9 --- /dev/null +++ b/hifi-src/src/pages/admin/AccountSettings.jsx @@ -0,0 +1,358 @@ +import { useState } from 'react'; +import QRCode from 'qrcode'; +import { api } from '../../api/client.js'; +import { useAuth } from '../../context/AuthContext.jsx'; + +const emptyPasswordForm = { + current_password: '', + new_password: '', + new_password_confirmation: '', + two_factor_code: '', +}; + +export default function AccountSettings() { + const { user, refreshUser } = useAuth(); + const [phase, setPhase] = useState('idle'); // idle | setup | recovery-codes | disable | regenerate + const [qrDataUrl, setQrDataUrl] = useState(''); + const [secret, setSecret] = useState(''); + const [code, setCode] = useState(''); + const [password, setPassword] = useState(''); + const [recoveryCodes, setRecoveryCodes] = useState([]); + const [error, setError] = useState(''); + const [busy, setBusy] = useState(false); + + const [passwordForm, setPasswordForm] = useState(emptyPasswordForm); + const [passwordError, setPasswordError] = useState(''); + const [passwordSuccess, setPasswordSuccess] = useState(''); + const [passwordBusy, setPasswordBusy] = useState(false); + + const handleChangePassword = async (e) => { + e.preventDefault(); + setPasswordError(''); + setPasswordSuccess(''); + + if (passwordForm.new_password !== passwordForm.new_password_confirmation) { + setPasswordError('Die neuen Passwörter stimmen nicht überein'); + return; + } + + setPasswordBusy(true); + try { + await api.post('/auth/change-password', passwordForm); + setPasswordForm(emptyPasswordForm); + setPasswordSuccess('Passwort erfolgreich geändert.'); + } catch (err) { + setPasswordError(err.message); + } finally { + setPasswordBusy(false); + } + }; + + const startSetup = async () => { + setError(''); + setBusy(true); + try { + const data = await api.get('/auth/2fa/setup'); + setSecret(data.secret); + setQrDataUrl(await QRCode.toDataURL(data.otpauth_url)); + setCode(''); + setPhase('setup'); + } catch (err) { + setError(err.message); + } finally { + setBusy(false); + } + }; + + const confirmSetup = async (e) => { + e.preventDefault(); + setError(''); + setBusy(true); + try { + const data = await api.post('/auth/2fa/enable', { code }); + setRecoveryCodes(data.recovery_codes); + setPhase('recovery-codes'); + await refreshUser(); + } catch (err) { + setError(err.message); + } finally { + setBusy(false); + } + }; + + const finishRecoveryCodes = () => { + setPhase('idle'); + setRecoveryCodes([]); + setPassword(''); + }; + + const handleDisable = async (e) => { + e.preventDefault(); + setError(''); + setBusy(true); + try { + await api.post('/auth/2fa/disable', { password }); + setPassword(''); + setPhase('idle'); + await refreshUser(); + } catch (err) { + setError(err.message); + } finally { + setBusy(false); + } + }; + + const handleRegenerate = async (e) => { + e.preventDefault(); + setError(''); + setBusy(true); + try { + const data = await api.post('/auth/2fa/recovery-codes', { password }); + setRecoveryCodes(data.recovery_codes); + setPhase('recovery-codes'); + } catch (err) { + setError(err.message); + } finally { + setBusy(false); + } + }; + + return ( +
+

Mein Konto

+

+ Angemeldet als {user?.username}{user?.is_super_admin ? ' (Super-Admin)' : ''}. +

+ +
+

Passwort ändern

+

+ Vergib ein neues Passwort für dein Konto. + {user?.two_factor_enabled && ' Da 2FA aktiv ist, musst du die Änderung zusätzlich mit deinem Code bestätigen.'} +

+
+
+ + setPasswordForm({ ...passwordForm, current_password: e.target.value })} + required + className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-900" + /> +
+
+ + setPasswordForm({ ...passwordForm, new_password: e.target.value })} + required + minLength={8} + className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-900" + /> +
+
+ + setPasswordForm({ ...passwordForm, new_password_confirmation: e.target.value })} + required + minLength={8} + className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-900" + /> +
+ {user?.two_factor_enabled && ( +
+ + setPasswordForm({ ...passwordForm, two_factor_code: e.target.value })} + required + inputMode="numeric" + placeholder="123456 oder Wiederherstellungscode" + className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-900" + /> +
+ )} + {passwordError &&

{passwordError}

} + {passwordSuccess &&

{passwordSuccess}

} + +
+
+ +
+

Zwei-Faktor-Authentifizierung

+

+ Schützt dein Konto zusätzlich mit einem Code aus der Google Authenticator App. +

+ + {phase === 'idle' && ( + <> +

+ Status:{' '} + {user?.two_factor_enabled ? ( + Aktiv + ) : ( + Nicht aktiv + )} +

+ {user?.two_factor_enabled ? ( +
+ + +
+ ) : ( + + )} + + )} + + {phase === 'setup' && ( +
+
    +
  1. Öffne die Google Authenticator App (oder eine kompatible App).
  2. +
  3. Scanne den folgenden QR-Code:
  4. +
+
+ {qrDataUrl && QR-Code für Zwei-Faktor-Authentifizierung} +
+

+ Geht das Scannen nicht? Gib diesen Code manuell ein:
+ {secret} +

+
+ + setCode(e.target.value)} + required + autoFocus + inputMode="numeric" + placeholder="123456" + className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-center text-lg tracking-widest dark:border-slate-700 dark:bg-slate-900" + /> +
+ {error &&

{error}

} +
+ + +
+
+ )} + + {phase === 'recovery-codes' && ( +
+

+ Wiederherstellungscodes – jetzt sicher speichern! +

+

+ Jeder Code funktioniert einmalig, falls du keinen Zugriff mehr auf die Authenticator-App hast. + Diese Codes werden nur jetzt angezeigt. +

+
+ {recoveryCodes.map((c) => {c})} +
+ +
+ )} + + {phase === 'disable' && ( +
+
+ + setPassword(e.target.value)} + required + autoFocus + className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-900" + /> +
+ {error &&

{error}

} +
+ + +
+
+ )} + + {phase === 'regenerate' && ( +
+

+ Die bisherigen Wiederherstellungscodes werden ungültig. +

+
+ + setPassword(e.target.value)} + required + autoFocus + className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-900" + /> +
+ {error &&

{error}

} +
+ + +
+
+ )} +
+
+ ); +} diff --git a/hifi-src/src/pages/admin/AdminUsers.jsx b/hifi-src/src/pages/admin/AdminUsers.jsx new file mode 100644 index 0000000..4327163 --- /dev/null +++ b/hifi-src/src/pages/admin/AdminUsers.jsx @@ -0,0 +1,235 @@ +import { useEffect, useState } from 'react'; +import { api } from '../../api/client.js'; +import { useAuth } from '../../context/AuthContext.jsx'; + +const emptyForm = { username: '', password: '', is_super_admin: false, permissions: [], group_ids: [] }; + +export default function AdminUsers() { + const { user: currentUser, hasPermission } = useAuth(); + const [users, setUsers] = useState([]); + const [catalog, setCatalog] = useState([]); + const [groups, setGroups] = useState([]); + const [form, setForm] = useState(emptyForm); + const [editingId, setEditingId] = useState(null); + const [error, setError] = useState(''); + + const load = () => { + api.get('/admin-users').then(setUsers); + api.get('/permissions/catalog').then(setCatalog); + if (hasPermission('permission_groups.manage') || hasPermission('users.manage')) { + api.get('/permission-groups').then(setGroups); + } + }; + + useEffect(load, []); + + const startEdit = (u) => { + setEditingId(u.id); + setForm({ + username: u.username, + password: '', + is_super_admin: u.is_super_admin, + permissions: u.permissions, + group_ids: u.groups.map((g) => g.id), + }); + }; + + const resetForm = () => { + setEditingId(null); + setForm(emptyForm); + }; + + const togglePermission = (key) => { + setForm((f) => ({ + ...f, + permissions: f.permissions.includes(key) ? f.permissions.filter((p) => p !== key) : [...f.permissions, key], + })); + }; + + const toggleGroup = (id) => { + setForm((f) => ({ + ...f, + group_ids: f.group_ids.includes(id) ? f.group_ids.filter((g) => g !== id) : [...f.group_ids, id], + })); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setError(''); + try { + if (editingId) { + await api.put(`/admin-users/${editingId}`, form); + } else { + if (form.password.length < 8) { + setError('Passwort muss mindestens 8 Zeichen lang sein'); + return; + } + await api.post('/admin-users', form); + } + resetForm(); + load(); + } catch (err) { + setError(err.message); + } + }; + + const handleDelete = async (id) => { + if (!confirm('Diesen Benutzer wirklich löschen?')) return; + try { + await api.delete(`/admin-users/${id}`); + load(); + } catch (err) { + alert(err.message); + } + }; + + return ( +
+
+

Benutzer

+

+ Jeder Mitarbeiter bekommt ein eigenes Konto mit genau den Rechten, die er braucht. +

+
+ + + + + + + + + + + {users.map((u) => ( + + + + + + + ))} + {users.length === 0 && ( + + )} + +
BenutzernameRolleBerechtigungsgruppenAktionen
+ {u.username} + {u.id === currentUser?.id && (du)} + + {u.is_super_admin ? ( + + Super-Admin + + ) : ( + {u.permissions.length} Einzelrechte + )} + +
+ {u.groups.map((g) => ( + + {g.name} + + ))} + {u.groups.length === 0 && '–'} +
+
+ + {u.id !== currentUser?.id && ( + + )} +
Noch keine Benutzer angelegt.
+
+
+ +
+

{editingId ? 'Benutzer bearbeiten' : 'Neuer Benutzer'}

+
+ + setForm({ ...form, username: e.target.value })} + className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-900" + /> +
+
+ + setForm({ ...form, password: e.target.value })} + className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-900" + /> +
+ + {currentUser?.is_super_admin && ( + + )} + + {!form.is_super_admin && ( + <> +
+

Einzelne Berechtigungen

+
+ {catalog.map((p) => ( + + ))} +
+
+ +
+

Berechtigungsgruppen

+
+ {groups.map((g) => ( + + ))} + {groups.length === 0 &&

Noch keine Berechtigungsgruppen angelegt.

} +
+
+ + )} + + {error &&

{error}

} + +
+ + {editingId && ( + + )} +
+
+
+ ); +} diff --git a/hifi-src/src/pages/admin/Brands.jsx b/hifi-src/src/pages/admin/Brands.jsx new file mode 100644 index 0000000..3362159 --- /dev/null +++ b/hifi-src/src/pages/admin/Brands.jsx @@ -0,0 +1,123 @@ +import { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { api } from '../../api/client.js'; + +const emptyForm = { name: '', sort_order: 0 }; + +export default function Brands() { + const [brands, setBrands] = useState([]); + const [form, setForm] = useState(emptyForm); + const [editingId, setEditingId] = useState(null); + const [error, setError] = useState(''); + + const load = () => api.get('/brands').then(setBrands); + + useEffect(() => { + load(); + }, []); + + const startEdit = (brand) => { + setEditingId(brand.id); + setForm({ name: brand.name, sort_order: brand.sort_order }); + }; + + const resetForm = () => { + setEditingId(null); + setForm(emptyForm); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setError(''); + try { + if (editingId) { + await api.put(`/brands/${editingId}`, form); + } else { + await api.post('/brands', form); + } + resetForm(); + load(); + } catch (err) { + setError(err.message); + } + }; + + const handleDelete = async (id) => { + if (!confirm('Marke inkl. aller Modelle/Pakete/Produkte wirklich löschen?')) return; + await api.delete(`/brands/${id}`); + load(); + }; + + return ( +
+
+

Marken

+
+ + + + + + + + + + {brands.map((brand) => ( + + + + + + ))} + {brands.length === 0 && ( + + )} + +
NameSortierungAktionen
+ + {brand.name} + + {brand.sort_order} + + +
Noch keine Marken angelegt.
+
+
+ +
+

{editingId ? 'Marke bearbeiten' : 'Neue Marke'}

+
+ + setForm({ ...form, name: e.target.value })} + className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-900" + /> +
+
+ + setForm({ ...form, sort_order: Number(e.target.value) })} + className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-900" + /> +
+ + {error &&

{error}

} + +
+ + {editingId && ( + + )} +
+
+
+ ); +} diff --git a/hifi-src/src/pages/admin/ContactRequests.jsx b/hifi-src/src/pages/admin/ContactRequests.jsx new file mode 100644 index 0000000..a1d62f0 --- /dev/null +++ b/hifi-src/src/pages/admin/ContactRequests.jsx @@ -0,0 +1,145 @@ +import { useEffect, useMemo, useState } from 'react'; +import { api } from '../../api/client.js'; +import { useAuth } from '../../context/AuthContext.jsx'; + +const formatPrice = (value) => + new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(value); + +const statusLabel = { new: 'Neu', in_progress: 'In Bearbeitung', done: 'Erledigt' }; +const statusBadge = { + new: 'bg-brand-100 text-brand-700 dark:bg-brand-900/40 dark:text-brand-300', + in_progress: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300', + done: 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300', +}; + +const TrashIcon = () => ( + + + +); + +export default function ContactRequests() { + const { hasPermission } = useAuth(); + const [requests, setRequests] = useState([]); + const [filter, setFilter] = useState('all'); + + const load = () => api.get('/contact').then(setRequests); + + useEffect(() => { + load(); + }, []); + + const handleStatusChange = async (id, status) => { + await api.patch(`/contact/${id}`, { status }); + load(); + }; + + const handleDelete = async (id) => { + if (!confirm('Diese Kontaktanfrage wirklich unwiderruflich löschen?')) return; + await api.delete(`/contact/${id}`); + load(); + }; + + const counts = useMemo( + () => ({ + all: requests.length, + new: requests.filter((r) => r.status === 'new').length, + in_progress: requests.filter((r) => r.status === 'in_progress').length, + done: requests.filter((r) => r.status === 'done').length, + }), + [requests] + ); + + const filtered = filter === 'all' ? requests : requests.filter((r) => r.status === filter); + + const tabs = [ + { value: 'all', label: 'Alle' }, + { value: 'new', label: 'Neu' }, + { value: 'in_progress', label: 'In Bearbeitung' }, + { value: 'done', label: 'Erledigt' }, + ]; + + return ( +
+

Kontaktanfragen

+

+ Alle Anfragen deiner Kunden. Wähle oben einen Reiter, um nur bestimmte Anfragen zu sehen. +

+ +
+ {tabs.map((tab) => ( + + ))} +
+ +
+ {filtered.map((r) => ( +
+
+
+

{r.name} · {r.email}{r.phone ? ` · ${r.phone}` : ''}

+

{new Date(r.created_at.replace(' ', 'T')).toLocaleString('de-DE')}

+
+
+ + {hasPermission('contact.delete') && ( + + )} +
+
+ + {(r.brand_name || r.model_name || r.package_name || r.product_name) && ( +

+ {[r.brand_name, r.model_name, r.package_name, r.product_name].filter(Boolean).join(' · ')} +

+ )} + + {r.selected_upgrades?.length > 0 && ( +
+

Gewünschte Upgrades:

+
    + {r.selected_upgrades.map((u) => ( +
  • {u.name} – {formatPrice(u.price)}
  • + ))} +
+
+ )} + + {r.message &&

{r.message}

} +
+ ))} + + {filtered.length === 0 && ( +

+ {filter === 'all' ? 'Noch keine Kontaktanfragen eingegangen.' : 'Keine Anfragen mit diesem Status.'} +

+ )} +
+
+ ); +} diff --git a/hifi-src/src/pages/admin/Dashboard.jsx b/hifi-src/src/pages/admin/Dashboard.jsx new file mode 100644 index 0000000..d78da2b --- /dev/null +++ b/hifi-src/src/pages/admin/Dashboard.jsx @@ -0,0 +1,75 @@ +import { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +import { api } from '../../api/client.js'; +import { useAuth } from '../../context/AuthContext.jsx'; + +const ICONS = { + tag: 'M4 4h7l9 9-7 7-9-9V4Zm3.5 3.5h.01', + car: 'M3 13.5 4.5 9A2 2 0 0 1 6.4 7.5h11.2A2 2 0 0 1 19.5 9L21 13.5M3 13.5v4a1 1 0 0 0 1 1h1a1 1 0 0 0 1-1v-1h12v1a1 1 0 0 0 1 1h1a1 1 0 0 0 1-1v-4M3 13.5h18M6.5 16.5h.01M17.5 16.5h.01', + box: 'M12 3 4 7.5v9L12 21l8-4.5v-9L12 3Zm0 9 8-4.5M12 12v9M12 12 4 7.5', + mail: 'M4 6h16a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1Zm0 0 8 7 8-7', +}; + +const Icon = ({ path, className = 'h-6 w-6' }) => ( + + + +); + +export default function Dashboard() { + const { user, hasPermission } = useAuth(); + const [stats, setStats] = useState({}); + + useEffect(() => { + if (hasPermission('brands.manage')) api.get('/brands').then((rows) => setStats((s) => ({ ...s, brands: rows.length }))); + if (hasPermission('models.manage') || hasPermission('packages.manage')) { + api.get('/models').then((rows) => setStats((s) => ({ ...s, models: rows.length }))); + } + if (hasPermission('packages.manage')) api.get('/packages').then((rows) => setStats((s) => ({ ...s, packages: rows.length }))); + if (hasPermission('contact.manage')) { + api.get('/contact').then((rows) => setStats((s) => ({ ...s, newContacts: rows.filter((c) => c.status === 'new').length }))); + } + }, []); + + const cards = [ + { key: 'brands', label: 'Marken', icon: 'tag', hint: 'Die Hersteller, z. B. Audi oder BMW.', value: stats.brands, to: '/admin/brands', show: hasPermission('brands.manage') }, + { key: 'models', label: 'Modelle', icon: 'car', hint: 'Die Fahrzeuge einer Marke, z. B. A4.', value: stats.models, to: '/admin/models', show: hasPermission('models.manage') }, + { key: 'packages', label: 'Pakete', icon: 'box', hint: 'Die Sound-Pakete für ein Modell.', value: stats.packages, to: '/admin/packages', show: hasPermission('packages.manage') }, + { key: 'contacts', label: 'Neue Anfragen', icon: 'mail', hint: 'Kunden, die noch auf Antwort warten.', value: stats.newContacts, to: '/admin/contact-requests', show: hasPermission('contact.manage'), highlight: true }, + ].filter((c) => c.show); + + return ( +
+

Willkommen, {user?.username}!

+

+ Hier siehst du auf einen Blick, wie viel in deinem Shop hinterlegt ist. Klicke auf eine Kachel, um dorthin zu gelangen. +

+ {cards.length === 0 ? ( +

+ Für deinen Account sind noch keine Bereiche freigeschaltet. Bitte wende dich an einen Administrator. +

+ ) : ( +
+ {cards.map((card) => ( + 0 + ? 'border-brand-300 bg-brand-50 dark:border-brand-900 dark:bg-brand-900/20' + : 'border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900' + }`} + > +
+ +
+

{card.label}

+

{card.value ?? '…'}

+

{card.hint}

+ + ))} +
+ )} +
+ ); +} diff --git a/hifi-src/src/pages/admin/Login.jsx b/hifi-src/src/pages/admin/Login.jsx new file mode 100644 index 0000000..45c5078 --- /dev/null +++ b/hifi-src/src/pages/admin/Login.jsx @@ -0,0 +1,131 @@ +import { useState } from 'react'; +import { Navigate, useNavigate } from 'react-router-dom'; +import { useAuth } from '../../context/AuthContext.jsx'; +import logo from '../../assets/logo.png'; + +export default function Login() { + const { user, login, verifyTwoFactor } = useAuth(); + const navigate = useNavigate(); + const [step, setStep] = useState('credentials'); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [code, setCode] = useState(''); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + + if (user) { + return ; + } + + const handleCredentials = async (e) => { + e.preventDefault(); + setLoading(true); + setError(''); + try { + const result = await login(username, password); + if (result.requires2fa) { + setStep('code'); + } else { + navigate('/admin'); + } + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + const handleCode = async (e) => { + e.preventDefault(); + setLoading(true); + setError(''); + try { + await verifyTwoFactor(code); + navigate('/admin'); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + return ( +
+ HifiPlanet + {step === 'credentials' ? ( +
+

Admin-Login

+ +
+ + setUsername(e.target.value)} + required + autoFocus + className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm focus:border-brand-500 focus:outline-none dark:border-slate-700 dark:bg-slate-900" + /> +
+
+ + setPassword(e.target.value)} + required + className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm focus:border-brand-500 focus:outline-none dark:border-slate-700 dark:bg-slate-900" + /> +
+ + {error &&

{error}

} + + +
+ ) : ( +
+

Bestätigungscode

+

+ Öffne die Google Authenticator App und gib den 6-stelligen Code ein. Alternativ kannst du auch + einen deiner Wiederherstellungscodes verwenden. +

+ +
+ + setCode(e.target.value)} + required + autoFocus + inputMode="numeric" + placeholder="123456" + className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-center text-lg tracking-widest focus:border-brand-500 focus:outline-none dark:border-slate-700 dark:bg-slate-900" + /> +
+ + {error &&

{error}

} + + + +
+ )} +
+ ); +} diff --git a/hifi-src/src/pages/admin/Models.jsx b/hifi-src/src/pages/admin/Models.jsx new file mode 100644 index 0000000..71a0858 --- /dev/null +++ b/hifi-src/src/pages/admin/Models.jsx @@ -0,0 +1,160 @@ +import { useEffect, useMemo, useState } from 'react'; +import { api } from '../../api/client.js'; + +const emptyForm = { name: '', brand_id: '', sort_order: 0 }; + +export default function Models() { + const [models, setModels] = useState([]); + const [brands, setBrands] = useState([]); + const [form, setForm] = useState(emptyForm); + const [editingId, setEditingId] = useState(null); + const [error, setError] = useState(''); + const [brandFilter, setBrandFilter] = useState('all'); + + const filteredModels = useMemo( + () => (brandFilter === 'all' ? models : models.filter((m) => String(m.brand_id) === brandFilter)), + [models, brandFilter] + ); + + const load = () => { + api.get('/models').then(setModels); + api.get('/brands').then(setBrands); + }; + + useEffect(load, []); + + const startEdit = (model) => { + setEditingId(model.id); + setForm({ name: model.name, brand_id: model.brand_id, sort_order: model.sort_order }); + }; + + const resetForm = () => { + setEditingId(null); + setForm(emptyForm); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setError(''); + const payload = { ...form, brand_id: Number(form.brand_id) }; + try { + if (editingId) { + await api.put(`/models/${editingId}`, payload); + } else { + await api.post('/models', payload); + } + resetForm(); + load(); + } catch (err) { + setError(err.message); + } + }; + + const handleDelete = async (id) => { + if (!confirm('Modell inkl. aller Pakete/Produkte wirklich löschen?')) return; + await api.delete(`/models/${id}`); + load(); + }; + + return ( +
+
+

Modelle

+ +
+ + + {brandFilter !== 'all' && ( + + )} + {filteredModels.length} von {models.length} +
+ +
+ + + + + + + + + + {filteredModels.map((model) => ( + + + + + + ))} + {filteredModels.length === 0 && ( + + )} + +
MarkeModellAktionen
{model.brand_name}{model.name} + + +
+ {models.length === 0 ? 'Noch keine Modelle angelegt.' : 'Keine Modelle für diese Marke.'} +
+
+
+ +
+

{editingId ? 'Modell bearbeiten' : 'Neues Modell'}

+
+ + +
+
+ + setForm({ ...form, name: e.target.value })} + className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-900" + /> +
+
+ + setForm({ ...form, sort_order: Number(e.target.value) })} + className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-900" + /> +
+ + {error &&

{error}

} + +
+ + {editingId && ( + + )} +
+
+
+ ); +} diff --git a/hifi-src/src/pages/admin/PackageProducts.jsx b/hifi-src/src/pages/admin/PackageProducts.jsx new file mode 100644 index 0000000..bf65d64 --- /dev/null +++ b/hifi-src/src/pages/admin/PackageProducts.jsx @@ -0,0 +1,139 @@ +import { useEffect, useState } from 'react'; +import { Link, useParams } from 'react-router-dom'; +import { api } from '../../api/client.js'; + +const formatPrice = (value) => + value == null ? '–' : new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(value); + +const statusBadge = { + ok: 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300', + pending: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300', + error: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300', +}; + +export default function PackageProducts() { + const { packageId } = useParams(); + const [products, setProducts] = useState([]); + const [sourceUrl, setSourceUrl] = useState(''); + const [nameOverride, setNameOverride] = useState(''); + const [error, setError] = useState(''); + const [busy, setBusy] = useState(false); + + const load = () => api.get(`/packages/${packageId}/products`).then(setProducts); + + useEffect(() => { + load(); + }, [packageId]); + + const handleAdd = async (e) => { + e.preventDefault(); + setError(''); + setBusy(true); + try { + await api.post('/products', { package_id: Number(packageId), source_url: sourceUrl, name_override: nameOverride || null }); + setSourceUrl(''); + setNameOverride(''); + load(); + } catch (err) { + setError(err.message); + } finally { + setBusy(false); + } + }; + + const handleRefresh = async (id) => { + setBusy(true); + try { + await api.post(`/products/${id}/refresh-price`, {}); + load(); + } finally { + setBusy(false); + } + }; + + const handleDelete = async (id) => { + if (!confirm('Produkt wirklich entfernen?')) return; + await api.delete(`/products/${id}`); + load(); + }; + + return ( +
+

+ ← Zurück zu den Paketen +

+

Produkte im Paket

+ +
+
+ + setSourceUrl(e.target.value)} + className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-900" + /> +
+
+ + setNameOverride(e.target.value)} + className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-900" + /> +
+ +
+ + {error &&

{error}

} + +
+ + + + + + + + + + + + {products.map((p) => ( + + + + + + + + ))} + {products.length === 0 && ( + + )} + +
ProduktPreisStatusZuletzt aktualisiertAktionen
+ + {p.name_override || p.scraped_name || p.source_url} + + {p.scrape_status === 'error' &&

{p.scrape_error}

} +
{formatPrice(p.scraped_price)} + + {p.scrape_status} + + + {p.price_updated_at ? new Date(p.price_updated_at.replace(' ', 'T')).toLocaleString('de-DE') : '–'} + + + +
Noch keine Produkte im Paket.
+
+
+ ); +} diff --git a/hifi-src/src/pages/admin/PackageUpgrades.jsx b/hifi-src/src/pages/admin/PackageUpgrades.jsx new file mode 100644 index 0000000..b261b1d --- /dev/null +++ b/hifi-src/src/pages/admin/PackageUpgrades.jsx @@ -0,0 +1,154 @@ +import { useEffect, useState } from 'react'; +import { Link, useParams } from 'react-router-dom'; +import { api } from '../../api/client.js'; + +const formatPrice = (value) => + new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(value); + +const emptyForm = { name: '', description: '', price: 0, sort_order: 0 }; + +export default function PackageUpgrades() { + const { packageId } = useParams(); + const [upgrades, setUpgrades] = useState([]); + const [form, setForm] = useState(emptyForm); + const [editingId, setEditingId] = useState(null); + const [error, setError] = useState(''); + + const load = () => api.get(`/packages/${packageId}/upgrades`).then(setUpgrades); + + useEffect(() => { + load(); + }, [packageId]); + + const startEdit = (u) => { + setEditingId(u.id); + setForm({ name: u.name, description: u.description || '', price: u.price, sort_order: u.sort_order }); + }; + + const resetForm = () => { + setEditingId(null); + setForm(emptyForm); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setError(''); + const payload = { ...form, package_id: Number(packageId), price: Number(form.price) }; + try { + if (editingId) { + await api.put(`/package-upgrades/${editingId}`, payload); + } else { + await api.post('/package-upgrades', payload); + } + resetForm(); + load(); + } catch (err) { + setError(err.message); + } + }; + + const handleDelete = async (id) => { + if (!confirm('Upgrade wirklich löschen?')) return; + await api.delete(`/package-upgrades/${id}`); + load(); + }; + + return ( +
+
+

+ ← Zurück zu den Paketen +

+

Optionale Upgrades

+

+ Diese Upgrades erscheinen nicht in der öffentlichen Paketübersicht, sondern erst als Auswahl im Kontaktformular. +

+
+ + + + + + + + + + {upgrades.map((u) => ( + + + + + + ))} + {upgrades.length === 0 && ( + + )} + +
NamePreisAktionen
+

{u.name}

+ {u.description &&

{u.description}

} +
{formatPrice(u.price)} + + +
Noch keine Upgrades hinterlegt.
+
+
+ +
+

{editingId ? 'Upgrade bearbeiten' : 'Neues Upgrade'}

+
+ + setForm({ ...form, name: e.target.value })} + className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm dark:border-slate-700 dark:bg-slate-900" + /> +
+
+ +