commit ae33874ae05005e2fa3e6c9e729de1550d8f85c7 Author: Tom Leonards Date: Thu Mar 26 16:10:45 2026 +0100 initial commit diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml new file mode 100644 index 0000000..27cae75 --- /dev/null +++ b/.github/workflows/deploy-dev.yml @@ -0,0 +1,23 @@ +name: Deploy to Dev Server + +on: + push: + branches: [ main ] + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Deploy via SSH + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.DEV_SERVER_HOST }} + username: ${{ secrets.DEV_SERVER_USER }} + password: ${{ secrets.DEV_SERVER_PASSWORD }} + script: | + cd /home/tom/lub/website + git pull origin main + docker compose down + docker compose up -d --build + docker system prune -f \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8b44059 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# Dependencies +node_modules/ + +# Build outputs +dist/ +tmp/ +out-tsc/ +bazel-out/ + +# Angular +.angular/ +.sass-cache/ + +# Vite +.vite/ +vite/ + +# Environment (aber example behalten!) +.env +.env.development +.env.production +*.local +!.env.example + +# IDE +.idea/ +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# Logs +*.log + +# OS +.DS_Store +Thumbs.db + +# Database +*.sqlite \ No newline at end of file diff --git a/Dockerfile.backend b/Dockerfile.backend new file mode 100644 index 0000000..7941a74 --- /dev/null +++ b/Dockerfile.backend @@ -0,0 +1,8 @@ +FROM node:20-alpine +WORKDIR /app +COPY apps/backend/package*.json ./ +RUN npm install +COPY apps/backend . +RUN npm run build +EXPOSE 3000 +CMD ["node", "dist/main.js"] \ No newline at end of file diff --git a/Dockerfile.frontend b/Dockerfile.frontend new file mode 100644 index 0000000..31eb7bb --- /dev/null +++ b/Dockerfile.frontend @@ -0,0 +1,12 @@ +FROM node:20-alpine AS build +WORKDIR /app +COPY apps/frontend/package*.json ./ +RUN npm install +COPY apps/frontend . +RUN npm run build + +FROM nginx:alpine +COPY --from=build /app/dist/*/browser /usr/share/nginx/html +COPY apps/frontend/nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8ffbe61 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +``` +██╗ ██████╗ ██╗████████╗ +██║ ██╔══██╗ ██║╚══██╔══╝ +██║ ██████╔╝ ██║ ██║ +██║ ██╔══██╗ ██║ ██║ +███████╗██████╔╝ ██║ ██║ +╚══════╝╚═════╝ ╚═╝ ╚═╝ +``` + +
+ +# Leonards & Brandenburger IT + +**Angular • NestJS • PostgreSQL** + +
+ +--- + +## 🛠️ Development + +```bash +npm install +docker compose -f docker-compose.dev.yml up -d +npm run dev +``` + +| Service | URL | +|----------|---------------------------| +| 🎨 Frontend | http://localhost:4200 | +| ⚡ Backend | http://localhost:3000 | + +--- + +## 🚀 Production + +```bash +docker compose up -d --build +``` + +| Service | URL | +|----------|---------------------------| +| 🎨 Frontend | http://localhost | +| ⚡ Backend | http://localhost:3000 | + +--- + +## 🛑 Stop + +```bash +docker compose down +``` + +--- + +
+ +Made with ☕ in Westerwald + +
\ No newline at end of file diff --git a/apps/backend/.env.example b/apps/backend/.env.example new file mode 100644 index 0000000..4fd936e --- /dev/null +++ b/apps/backend/.env.example @@ -0,0 +1,39 @@ +!!IN NEXTCLOUD SIND DIE DATEIEN ZU FINDEN!! +!!IN NEXTCLOUD SIND DIE DATEIEN ZU FINDEN!! +!!IN NEXTCLOUD SIND DIE DATEIEN ZU FINDEN!! +!!IN NEXTCLOUD SIND DIE DATEIEN ZU FINDEN!! +!!IN NEXTCLOUD SIND DIE DATEIEN ZU FINDEN!! +!!IN NEXTCLOUD SIND DIE DATEIEN ZU FINDEN!! + +DB_HOST=localhost +DB_PORT=5432 +DB_USER=app +DB_PASS=secret +DB_NAME=appdb +DB_SSL=false + +#Stays Same on all Clusters +OPENAI_API_KEY=your_openai_api_key_here +JWT_SECRET=your_jwt_secret_here +ADMIN_EMAIL=tom@leonardsmedia.de +EMAILJS_SERVICE_ID=your_emailjs_service_id_here +EMAILJS_TEMPLATE_ID=your_emailjs_template_id_here +EMAILJS_PUBLIC_KEY=your_emailjs_public_key_here +EMAILJS_PRIVATE_KEY=your_emailjs_private_key_here +COMPANY_NAME=LeonardsMedia +COMPANY_EMAIL=tom@leonardsmedia.de +GOOGLE_CLIENT_ID=your_google_client_id_here +GOOGLE_CLIENT_SECRET=your_google_client_secret_here +GOOGLE_REFRESH_TOKEN=your_google_refresh_token_here +COMPANY_WEBSITE=www.leonardsmedia.de + +#ChangeOnCluster +GOOGLE_REDIRECT_URI=http://localhost:3000/auth/google/callback +NODE_ENV=development +PORT=3000 +SEND_MAIL=false +FRONTEND_URL=http://localhost:4200 +DATABASE_URL=postgres://app:secret@localhost:5432/appdb +DB_SSL=falsev + + diff --git a/apps/backend/.eslintrc.js b/apps/backend/.eslintrc.js new file mode 100644 index 0000000..259de13 --- /dev/null +++ b/apps/backend/.eslintrc.js @@ -0,0 +1,25 @@ +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: __dirname, + sourceType: 'module', + }, + plugins: ['@typescript-eslint/eslint-plugin'], + extends: [ + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + root: true, + env: { + node: true, + jest: true, + }, + ignorePatterns: ['.eslintrc.js'], + rules: { + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + }, +}; diff --git a/apps/backend/.prettierrc b/apps/backend/.prettierrc new file mode 100644 index 0000000..dcb7279 --- /dev/null +++ b/apps/backend/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} \ No newline at end of file diff --git a/apps/backend/123 b/apps/backend/123 new file mode 100644 index 0000000..224440b --- /dev/null +++ b/apps/backend/123 @@ -0,0 +1,23 @@ +#Stays Same on all Clusters +OPENAI_API_KEY=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 +JWT_SECRET=420187133769 +ADMIN_EMAIL=tom@leonardsmedia.de +EMAILJS_SERVICE_ID=service_l7xb8om +EMAILJS_TEMPLATE_ID=template_wywhnxf +EMAILJS_PUBLIC_KEY=qtIi7eLKJLQIuT7Ta +EMAILJS_PRIVATE_KEY=cPkM5NiZ2bdeCzqfpOYPw +COMPANY_NAME=LeonardsMedia +COMPANY_EMAIL=tom@leonardsmedia.de +GOOGLE_CLIENT_ID=957243018391-m88a311kc166ealp1qjppp3v3eke900m.apps.googleusercontent.com +GOOGLE_CLIENT_SECRET=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 +GOOGLE_REFRESH_TOKEN=1//036AecH4b3sEeCgYIARAAGAMSNwF-L9IrBAy_t8ARHngnbNdTS9pGFgrkmBIHtd4Vdpnm6Go8SPMPmPf3-djYhBBwulKD7D8jPzM +COMPANY_WEBSITE=www.leonardsmedia.de + +#ChangeOnCluster +GOOGLE_REDIRECT_URI=http://localhost:3000/auth/google/callback +NODE_ENV=production +PORT=3000 +SEND_MAIL=true +FRONTEND_URL=https://leonardsmedia.de +DATABASE_URL=postgres://app:secret@db:5432/appdb +DB_SSL=false \ No newline at end of file diff --git a/apps/backend/README.md b/apps/backend/README.md new file mode 100644 index 0000000..c997880 --- /dev/null +++ b/apps/backend/README.md @@ -0,0 +1,85 @@ +

+ Nest Logo +

+ +[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 +[circleci-url]: https://circleci.com/gh/nestjs/nest + +

A progressive Node.js framework for building efficient and scalable server-side applications.

+

+NPM Version +Package License +NPM Downloads +CircleCI +Coverage +Discord +Backers on Open Collective +Sponsors on Open Collective + Donate us + Support us + Follow us on Twitter +

+ + +## Description + +[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. + +## Project setup + +```bash +$ npm install +``` + +## Compile and run the project + +```bash +# development +$ npm run start + +# watch mode +$ npm run start:dev + +# production mode +$ npm run start:prod +``` + +## Run tests + +```bash +# unit tests +$ npm run test + +# e2e tests +$ npm run test:e2e + +# test coverage +$ npm run test:cov +``` + +## Resources + +Check out a few resources that may come in handy when working with NestJS: + +- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework. +- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy). +- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/). +- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com). +- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com). +- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs). +- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com). + +## Support + +Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). + +## Stay in touch + +- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec) +- Website - [https://nestjs.com](https://nestjs.com/) +- Twitter - [@nestframework](https://twitter.com/nestframework) + +## License + +Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE). diff --git a/apps/backend/initial-data/faq.json b/apps/backend/initial-data/faq.json new file mode 100644 index 0000000..e9a0b27 --- /dev/null +++ b/apps/backend/initial-data/faq.json @@ -0,0 +1,790 @@ +[ + { + "id": "1a2b3c4d-5e6f-7890-abcd-ef1234567890", + "slug": "was-ist-leonards-brandenburger-it", + "question": "Was ist Leonards & Brandenburger IT?", + "answers": [ + "Wir sind ein junges IT-Unternehmen aus dem Westerwald, das sich auf umfassende IT-Dienstleistungen für Privatkunden und kleine Unternehmen spezialisiert hat.", + "Unser Fokus liegt darauf, professionelle IT-Lösungen auch in ländlichen Regionen zugänglich zu machen." + ], + "listItems": [ + "Gegründet von zwei erfahrenen Entwicklern", + "Spezialisiert auf ländliche Regionen", + "Persönlicher Service vor Ort" + ], + "sortOrder": 1, + "isPublished": true, + "category": "Allgemein", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "2b3c4d5e-6f78-90ab-cdef-234567890abc", + "slug": "welche-regionen-bedienen-sie", + "question": "Welche Regionen bedienen Sie?", + "answers": [ + "Wir sind primär im Westerwald und der Region um Altenkirchen tätig.", + "Auf Anfrage kommen wir auch gerne in umliegende Gebiete wie den Kreis Neuwied, Siegen-Wittgenstein oder den Rhein-Sieg-Kreis." + ], + "listItems": [ + "Westerwald", + "Kreis Altenkirchen", + "Umliegende Regionen auf Anfrage" + ], + "sortOrder": 2, + "isPublished": true, + "category": "Allgemein", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "3c4d5e6f-7890-abcd-ef12-34567890abcd", + "slug": "bieten-sie-vor-ort-service", + "question": "Bieten Sie Vor-Ort-Service an?", + "answers": [ + "Ja, wir kommen direkt zu Ihnen nach Hause oder in Ihr Büro.", + "Gerade in ländlichen Regionen wissen wir, wie wichtig persönlicher Service vor Ort ist." + ], + "listItems": [ + "Hausbesuche für Privatkunden", + "Vor-Ort-Support für Unternehmen", + "Flexible Terminvereinbarung" + ], + "sortOrder": 3, + "isPublished": true, + "category": "Service", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "4d5e6f78-90ab-cdef-1234-567890abcdef", + "slug": "was-kostet-eine-beratung", + "question": "Was kostet eine Erstberatung?", + "answers": [ + "Die telefonische Erstberatung ist bei uns kostenlos.", + "So können wir gemeinsam herausfinden, wie wir Ihnen am besten helfen können, bevor Kosten entstehen." + ], + "listItems": [ + "Telefonische Beratung kostenlos", + "Transparente Preisgestaltung", + "Kostenvoranschlag vor Arbeitsbeginn" + ], + "sortOrder": 4, + "isPublished": true, + "category": "Preise", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "5e6f7890-abcd-ef12-3456-7890abcdef12", + "slug": "pc-startet-nicht-mehr", + "question": "Mein PC startet nicht mehr – können Sie helfen?", + "answers": [ + "Ja, Computerreparaturen gehören zu unserem Kerngeschäft.", + "Wir diagnostizieren das Problem und reparieren Ihren PC – oft noch am selben Tag." + ], + "listItems": [ + "Hardware-Diagnose", + "Softwareprobleme beheben", + "Datenrettung wenn möglich" + ], + "sortOrder": 5, + "isPublished": true, + "category": "PC-Reparatur", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "6f7890ab-cdef-1234-5678-90abcdef1234", + "slug": "laptop-ist-langsam", + "question": "Mein Laptop ist sehr langsam geworden – was kann ich tun?", + "answers": [ + "Ein langsamer Laptop kann viele Ursachen haben: veraltete Hardware, zu viele Programme oder sogar Schadsoftware.", + "Wir analysieren Ihr Gerät und optimieren es oder empfehlen sinnvolle Upgrades wie eine SSD." + ], + "listItems": [ + "System-Optimierung", + "SSD-Upgrade", + "RAM-Erweiterung", + "Virenentfernung" + ], + "sortOrder": 6, + "isPublished": true, + "category": "PC-Reparatur", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "7890abcd-ef12-3456-7890-abcdef123456", + "slug": "daten-wiederherstellen", + "question": "Können Sie gelöschte Daten wiederherstellen?", + "answers": [ + "In vielen Fällen ja. Je schneller Sie uns kontaktieren, desto höher die Erfolgschancen.", + "Wir nutzen professionelle Tools zur Datenrettung von Festplatten, SSDs und USB-Sticks." + ], + "listItems": [ + "Datenrettung von Festplatten", + "SSD-Wiederherstellung", + "USB-Stick und SD-Karten" + ], + "sortOrder": 7, + "isPublished": true, + "category": "Datenrettung", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "890abcde-f123-4567-890a-bcdef1234567", + "slug": "neuen-pc-kaufen-beratung", + "question": "Ich möchte einen neuen PC kaufen – beraten Sie mich?", + "answers": [ + "Selbstverständlich! Wir beraten Sie herstellerunabhängig und finden das beste Gerät für Ihre Bedürfnisse und Ihr Budget.", + "Auf Wunsch kümmern wir uns auch um Beschaffung, Einrichtung und Datenübertragung." + ], + "listItems": [ + "Unabhängige Kaufberatung", + "Einrichtung des neuen Geräts", + "Datenübertragung vom Altgerät" + ], + "sortOrder": 8, + "isPublished": true, + "category": "Beratung", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "90abcdef-1234-5678-90ab-cdef12345678", + "slug": "wlan-probleme", + "question": "Mein WLAN funktioniert nicht richtig – können Sie helfen?", + "answers": [ + "Ja, Netzwerkprobleme sind einer unserer häufigsten Einsätze.", + "Wir prüfen Ihre Verbindung, optimieren die Router-Einstellungen und verbessern bei Bedarf die WLAN-Abdeckung." + ], + "listItems": [ + "Router-Konfiguration", + "WLAN-Optimierung", + "Repeater-Installation", + "Mesh-Netzwerke" + ], + "sortOrder": 9, + "isPublished": true, + "category": "Netzwerk", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "0abcdef1-2345-6789-0abc-def123456789", + "slug": "heimnetzwerk-einrichten", + "question": "Können Sie mir ein Heimnetzwerk einrichten?", + "answers": [ + "Ja, wir planen und installieren komplette Heimnetzwerke – von einfachen Lösungen bis hin zu professionellen Setups.", + "Dazu gehören Router-Konfiguration, NAS-Einrichtung, Smart-Home-Integration und mehr." + ], + "listItems": [ + "Router und Access Points", + "NAS-Systeme", + "Smart-Home-Anbindung", + "Netzwerk-Verkabelung" + ], + "sortOrder": 10, + "isPublished": true, + "category": "Netzwerk", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "abcdef12-3456-7890-abcd-ef1234567890", + "slug": "virus-entfernen", + "question": "Ich glaube, mein Computer hat einen Virus – was nun?", + "answers": [ + "Keine Panik! Wir entfernen Viren, Trojaner und andere Schadsoftware zuverlässig von Ihrem System.", + "Anschließend installieren wir einen aktuellen Virenschutz und zeigen Ihnen, wie Sie sich zukünftig schützen." + ], + "listItems": [ + "Virenentfernung", + "Malware-Bereinigung", + "Virenschutz-Installation", + "Sicherheitsberatung" + ], + "sortOrder": 11, + "isPublished": true, + "category": "Sicherheit", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "bcdef123-4567-890a-bcde-f12345678901", + "slug": "webseite-erstellen-lassen", + "question": "Können Sie mir eine Webseite erstellen?", + "answers": [ + "Ja, wir erstellen professionelle Webseiten für Privatpersonen, Vereine und kleine Unternehmen.", + "Von der einfachen Visitenkarten-Seite bis zur komplexen Webanwendung – wir setzen Ihre Wünsche um." + ], + "listItems": [ + "Responsive Design", + "Moderne Technologien", + "Suchmaschinenoptimierung", + "Wartung und Updates" + ], + "sortOrder": 12, + "isPublished": true, + "category": "Webentwicklung", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "cdef1234-5678-90ab-cdef-123456789012", + "slug": "webseite-kosten", + "question": "Was kostet eine Webseite bei Ihnen?", + "answers": [ + "Die Kosten hängen vom Umfang ab. Eine einfache Visitenkarten-Seite gibt es bereits ab wenigen hundert Euro.", + "Wir erstellen Ihnen gerne ein individuelles Angebot nach einem kostenlosen Beratungsgespräch." + ], + "listItems": [ + "Individuelle Angebote", + "Transparente Preisgestaltung", + "Verschiedene Pakete verfügbar" + ], + "sortOrder": 13, + "isPublished": true, + "category": "Webentwicklung", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "def12345-6789-0abc-def1-234567890123", + "slug": "smart-home-einrichten", + "question": "Können Sie mir bei der Smart-Home-Einrichtung helfen?", + "answers": [ + "Ja, wir beraten Sie bei der Auswahl kompatibler Geräte und richten Ihr Smart Home komplett ein.", + "Egal ob Beleuchtung, Heizung, Sicherheit oder Sprachassistenten – wir bringen alles zum Laufen." + ], + "listItems": [ + "Produktberatung", + "Installation und Einrichtung", + "App-Konfiguration", + "Automatisierungen erstellen" + ], + "sortOrder": 14, + "isPublished": true, + "category": "Smart Home", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "ef123456-7890-abcd-ef12-345678901234", + "slug": "drucker-einrichten", + "question": "Mein Drucker will nicht drucken – können Sie helfen?", + "answers": [ + "Druckerprobleme sind oft frustrierend, aber meist lösbar.", + "Wir richten Ihren Drucker ein, beheben Verbindungsprobleme und konfigurieren auch Netzwerkdrucker." + ], + "listItems": [ + "Drucker-Installation", + "Treiber-Updates", + "Netzwerkdrucker einrichten", + "WLAN-Drucker konfigurieren" + ], + "sortOrder": 15, + "isPublished": true, + "category": "Hardware", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "f1234567-890a-bcde-f123-456789012345", + "slug": "windows-neu-installieren", + "question": "Können Sie Windows neu installieren?", + "answers": [ + "Ja, wir installieren Windows komplett neu und richten alle Treiber und wichtigen Programme ein.", + "Auf Wunsch sichern wir vorher Ihre Daten und spielen sie nach der Installation wieder zurück." + ], + "listItems": [ + "Windows-Installation", + "Treiber und Updates", + "Grundprogramme einrichten", + "Datensicherung und -wiederherstellung" + ], + "sortOrder": 16, + "isPublished": true, + "category": "Betriebssysteme", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "12345678-90ab-cdef-1234-567890abcdef", + "slug": "backup-erstellen", + "question": "Wie kann ich meine Daten sichern?", + "answers": [ + "Wir richten für Sie eine automatische Backup-Lösung ein – lokal oder in der Cloud.", + "So sind Ihre wichtigen Daten immer geschützt und im Notfall schnell wiederherstellbar." + ], + "listItems": [ + "Lokale Backups", + "Cloud-Backup", + "Automatische Sicherung", + "NAS als Backup-Ziel" + ], + "sortOrder": 17, + "isPublished": true, + "category": "Datensicherung", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "23456789-0abc-def1-2345-67890abcdef1", + "slug": "smartphone-hilfe", + "question": "Helfen Sie auch bei Smartphone-Problemen?", + "answers": [ + "Ja, wir unterstützen Sie bei der Einrichtung und Problemlösung von Android- und iOS-Geräten.", + "Von der Ersteinrichtung bis zur Datenübertragung auf ein neues Handy – wir helfen gerne." + ], + "listItems": [ + "Smartphone-Einrichtung", + "Datenübertragung", + "App-Installation", + "E-Mail-Konfiguration" + ], + "sortOrder": 18, + "isPublished": true, + "category": "Smartphones", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "34567890-abcd-ef12-3456-7890abcdef12", + "slug": "email-einrichten", + "question": "Können Sie mir bei der E-Mail-Einrichtung helfen?", + "answers": [ + "Selbstverständlich. Wir richten E-Mail-Konten auf allen Geräten ein – PC, Laptop, Tablet und Smartphone.", + "Auch bei Problemen mit bestehenden E-Mail-Konten oder dem Wechsel zu einem neuen Anbieter helfen wir." + ], + "listItems": [ + "E-Mail auf allen Geräten", + "Outlook-Konfiguration", + "Thunderbird-Einrichtung", + "Webmail-Zugang" + ], + "sortOrder": 19, + "isPublished": true, + "category": "E-Mail", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "4567890a-bcde-f123-4567-890abcdef123", + "slug": "unternehmen-it-support", + "question": "Bieten Sie auch IT-Support für Unternehmen?", + "answers": [ + "Ja, wir betreuen kleine und mittelständische Unternehmen mit regelmäßigem IT-Support.", + "Von der Wartung über Problemlösung bis zur Beratung bei IT-Entscheidungen – wir sind Ihr verlässlicher Partner." + ], + "listItems": [ + "Regelmäßige Wartung", + "Schnelle Problemlösung", + "IT-Beratung", + "Flexible Support-Verträge" + ], + "sortOrder": 20, + "isPublished": true, + "category": "Business", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "567890ab-cdef-1234-5678-90abcdef1234", + "slug": "fernwartung", + "question": "Bieten Sie auch Fernwartung an?", + "answers": [ + "Ja, viele Probleme können wir per Fernwartung lösen – schnell und ohne Anfahrt.", + "Sie müssen lediglich unser Fernwartungstool herunterladen, und wir können direkt auf Ihrem Bildschirm helfen." + ], + "listItems": [ + "Schnelle Hilfe ohne Wartezeit", + "Keine Anfahrtskosten", + "Sichere Verbindung", + "Sie behalten die Kontrolle" + ], + "sortOrder": 21, + "isPublished": true, + "category": "Service", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "67890abc-def1-2345-6789-0abcdef12345", + "slug": "wie-erreiche-ich-sie", + "question": "Wie kann ich Sie am besten erreichen?", + "answers": [ + "Sie erreichen uns telefonisch, per E-Mail oder über unser Kontaktformular auf der Webseite.", + "Wir melden uns schnellstmöglich bei Ihnen zurück und vereinbaren einen Termin." + ], + "listItems": [ + "Telefon", + "E-Mail", + "Kontaktformular", + "Social Media" + ], + "sortOrder": 22, + "isPublished": true, + "category": "Kontakt", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "7890abcd-ef12-3456-7890-abcdef123456", + "slug": "wartezeit-termin", + "question": "Wie lange muss ich auf einen Termin warten?", + "answers": [ + "Das hängt von der aktuellen Auslastung ab, aber wir versuchen stets, zeitnah zu helfen.", + "Bei dringenden Problemen bemühen wir uns um einen Termin innerhalb weniger Tage." + ], + "listItems": [ + "Schnelle Terminvergabe", + "Notfälle priorisiert", + "Flexible Zeiten" + ], + "sortOrder": 23, + "isPublished": true, + "category": "Service", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "890abcde-f123-4567-890a-bcdef1234567", + "slug": "zahlung-bar-karte", + "question": "Welche Zahlungsmöglichkeiten gibt es?", + "answers": [ + "Sie können bar, per Überweisung oder auf Rechnung bezahlen.", + "Für Geschäftskunden bieten wir auch monatliche Abrechnungen an." + ], + "listItems": [ + "Barzahlung", + "Überweisung", + "Rechnung", + "Monatliche Abrechnung für Unternehmen" + ], + "sortOrder": 24, + "isPublished": true, + "category": "Preise", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "90abcdef-1234-5678-90ab-cdef12345678", + "slug": "garantie-auf-arbeit", + "question": "Gibt es Garantie auf Ihre Arbeit?", + "answers": [ + "Ja, auf unsere Dienstleistungen geben wir eine Zufriedenheitsgarantie.", + "Sollte ein Problem nach unserer Arbeit erneut auftreten, beheben wir es kostenlos." + ], + "listItems": [ + "Zufriedenheitsgarantie", + "Kostenlose Nachbesserung", + "Transparente Kommunikation" + ], + "sortOrder": 25, + "isPublished": true, + "category": "Service", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "0abcdef1-2345-6789-0abc-def123456789", + "slug": "schulung-angeboten", + "question": "Bieten Sie auch Schulungen an?", + "answers": [ + "Ja, wir zeigen Ihnen gerne, wie Sie Ihren Computer, Smartphone oder andere Geräte besser nutzen können.", + "Unsere Schulungen sind individuell auf Ihre Bedürfnisse zugeschnitten – vom Einsteiger bis zum Fortgeschrittenen." + ], + "listItems": [ + "PC-Grundlagen", + "Smartphone-Schulung", + "Sicherheit im Internet", + "Individuelle Themen" + ], + "sortOrder": 26, + "isPublished": true, + "category": "Schulung", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "abcdef12-3456-7890-abcd-ef1234567891", + "slug": "was-unterscheidet-sie", + "question": "Was unterscheidet Sie von großen IT-Dienstleistern?", + "answers": [ + "Wir setzen auf persönlichen Service und kennen unsere Kunden. Bei uns sind Sie keine Nummer.", + "Als lokales Unternehmen aus der Region verstehen wir die Bedürfnisse vor Ort und sind schnell erreichbar." + ], + "listItems": [ + "Persönlicher Ansprechpartner", + "Kurze Wege", + "Faire Preise", + "Keine Hotline-Warteschleifen" + ], + "sortOrder": 27, + "isPublished": true, + "category": "Allgemein", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "bcdef123-4567-890a-bcde-f12345678902", + "slug": "apps-entwickeln", + "question": "Entwickeln Sie auch Apps?", + "answers": [ + "Ja, wir entwickeln mobile Apps für Android und iOS sowie Web-Applikationen.", + "Von der Idee bis zur fertigen App begleiten wir Sie durch den gesamten Entwicklungsprozess." + ], + "listItems": [ + "Android-Apps", + "iOS-Apps", + "Cross-Platform mit Flutter", + "Web-Applikationen" + ], + "sortOrder": 28, + "isPublished": true, + "category": "Entwicklung", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "cdef1234-5678-90ab-cdef-123456789013", + "slug": "server-einrichten", + "question": "Können Sie mir einen Server einrichten?", + "answers": [ + "Ja, wir richten Server für verschiedene Zwecke ein – vom NAS zu Hause bis zum Firmenserver.", + "Auch Cloud-Lösungen und Hosting-Konfigurationen gehören zu unserem Angebot." + ], + "listItems": [ + "NAS-Einrichtung", + "Windows Server", + "Linux Server", + "Cloud-Hosting" + ], + "sortOrder": 29, + "isPublished": true, + "category": "Server", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "def12345-6789-0abc-def1-234567890124", + "slug": "alte-geraete-entsorgen", + "question": "Können Sie alte Geräte entsorgen?", + "answers": [ + "Wir können Ihnen bei der fachgerechten Entsorgung alter IT-Geräte behilflich sein.", + "Auf Wunsch löschen wir vorher sicher alle Ihre Daten, sodass nichts in falsche Hände gerät." + ], + "listItems": [ + "Sichere Datenlöschung", + "Fachgerechte Entsorgung", + "Recycling-Hinweise" + ], + "sortOrder": 30, + "isPublished": true, + "category": "Service", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "ef123456-7890-abcd-ef12-345678901235", + "slug": "gaming-pc-bauen", + "question": "Bauen Sie auch Gaming-PCs?", + "answers": [ + "Ja, wir stellen individuelle Gaming-PCs nach Ihren Wünschen und Budget zusammen.", + "Von der Komponentenauswahl bis zum fertigen System mit installiertem Betriebssystem." + ], + "listItems": [ + "Individuelle Konfiguration", + "Komponenten-Beratung", + "Zusammenbau und Test", + "Windows-Installation" + ], + "sortOrder": 31, + "isPublished": true, + "category": "Hardware", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "f1234567-890a-bcde-f123-456789012346", + "slug": "kabelmanagement", + "question": "Können Sie mein Kabelchaos ordnen?", + "answers": [ + "Ja, wir bringen Ordnung in Ihr Kabelchaos – zu Hause oder im Büro.", + "Mit durchdachtem Kabelmanagement sieht Ihr Arbeitsplatz nicht nur besser aus, sondern ist auch sicherer." + ], + "listItems": [ + "Kabelkanäle", + "Beschriftung", + "Ordentliche Verlegung", + "Schreibtisch-Organisation" + ], + "sortOrder": 32, + "isPublished": true, + "category": "Service", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "12345678-90ab-cdef-1234-567890abcde0", + "slug": "zoom-teams-einrichten", + "question": "Können Sie mir Zoom oder Teams einrichten?", + "answers": [ + "Ja, wir richten Videokonferenz-Tools wie Zoom, Microsoft Teams oder Google Meet für Sie ein.", + "Dazu gehören Kamera- und Mikrofon-Tests sowie die Einrichtung auf allen Ihren Geräten." + ], + "listItems": [ + "Zoom-Installation", + "Microsoft Teams", + "Google Meet", + "Hardware-Test" + ], + "sortOrder": 33, + "isPublished": true, + "category": "Software", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "23456789-0abc-def1-2345-67890abcde01", + "slug": "homeoffice-einrichten", + "question": "Können Sie mir beim Homeoffice-Setup helfen?", + "answers": [ + "Ja, wir richten Ihr komplettes Homeoffice ein – von der Hardware bis zur Software.", + "VPN-Verbindung zum Arbeitgeber, Drucker, zweiter Monitor – wir kümmern uns um alles." + ], + "listItems": [ + "VPN-Einrichtung", + "Multi-Monitor-Setup", + "Drucker und Scanner", + "Ergonomie-Beratung" + ], + "sortOrder": 34, + "isPublished": true, + "category": "Homeoffice", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "34567890-abcd-ef12-3456-7890abcde012", + "slug": "passwort-vergessen", + "question": "Ich habe mein Windows-Passwort vergessen – können Sie helfen?", + "answers": [ + "In den meisten Fällen ja. Wir können Windows-Passwörter zurücksetzen, ohne dass Daten verloren gehen.", + "Bitte bringen Sie einen Eigentumsnachweis mit, damit wir sicherstellen können, dass es Ihr Gerät ist." + ], + "listItems": [ + "Passwort-Zurücksetzung", + "Daten bleiben erhalten", + "Eigentumsnachweis erforderlich" + ], + "sortOrder": 35, + "isPublished": true, + "category": "Betriebssysteme", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "4567890a-bcde-f123-4567-890abcde0123", + "slug": "domain-hosting", + "question": "Können Sie mir bei Domain und Hosting helfen?", + "answers": [ + "Ja, wir beraten Sie bei der Domain-Auswahl und richten Ihr Hosting-Paket ein.", + "Auf Wunsch übernehmen wir auch die laufende Verwaltung Ihrer Domain und Webseite." + ], + "listItems": [ + "Domain-Registrierung", + "Hosting-Einrichtung", + "E-Mail-Hosting", + "SSL-Zertifikate" + ], + "sortOrder": 36, + "isPublished": true, + "category": "Webentwicklung", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "567890ab-cdef-1234-5678-90abcde01234", + "slug": "senioren-support", + "question": "Bieten Sie speziellen Support für Senioren an?", + "answers": [ + "Ja, wir haben viel Erfahrung mit älteren Kunden und nehmen uns besonders viel Zeit für Erklärungen.", + "Geduld und verständliche Sprache ohne Fachchinesisch sind bei uns selbstverständlich." + ], + "listItems": [ + "Geduldige Erklärungen", + "Keine Fachbegriffe", + "Schritt-für-Schritt-Anleitungen", + "Wiederholungen kein Problem" + ], + "sortOrder": 37, + "isPublished": true, + "category": "Service", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "67890abc-def1-2345-6789-0abcde012345", + "slug": "vpn-einrichten", + "question": "Können Sie mir ein VPN einrichten?", + "answers": [ + "Ja, wir richten VPN-Verbindungen für mehr Sicherheit und Privatsphäre ein.", + "Ob für den Zugriff auf Ihr Heimnetzwerk von unterwegs oder für sicheres Surfen – wir helfen gerne." + ], + "listItems": [ + "VPN für Privatsphäre", + "Zugriff auf Heimnetzwerk", + "Firmen-VPN einrichten", + "VPN auf Router" + ], + "sortOrder": 38, + "isPublished": true, + "category": "Sicherheit", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "7890abcd-ef12-3456-7890-abcde0123456", + "slug": "raspberry-pi-projekte", + "question": "Helfen Sie auch bei Raspberry Pi Projekten?", + "answers": [ + "Ja, wir haben Erfahrung mit Raspberry Pi und unterstützen Sie bei verschiedenen Projekten.", + "Von der Einrichtung eines Mediencenters bis zum Smart-Home-Server – wir setzen Ihre Ideen um." + ], + "listItems": [ + "Raspberry Pi Einrichtung", + "Home-Server", + "Mediacenter", + "Individuelle Projekte" + ], + "sortOrder": 39, + "isPublished": true, + "category": "Projekte", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + }, + { + "id": "890abcde-f123-4567-890a-bcde01234567", + "slug": "office-installation", + "question": "Können Sie Microsoft Office installieren?", + "answers": [ + "Ja, wir installieren und konfigurieren Microsoft Office oder kostenlose Alternativen wie LibreOffice.", + "Auch bei der Einrichtung von Microsoft 365 und der Verknüpfung mit Ihren Geräten helfen wir." + ], + "listItems": [ + "Microsoft Office", + "Microsoft 365", + "LibreOffice", + "E-Mail-Einrichtung in Outlook" + ], + "sortOrder": 40, + "isPublished": true, + "category": "Software", + "createdAt": "2026-01-21T10:00:00.000Z", + "updatedAt": "2026-01-21T10:00:00.000Z" + } +] \ No newline at end of file diff --git a/apps/backend/initial-data/services.json b/apps/backend/initial-data/services.json new file mode 100644 index 0000000..a46a54a --- /dev/null +++ b/apps/backend/initial-data/services.json @@ -0,0 +1,549 @@ +{ + "categories": [ + { + "slug": "hardware", + "name": "Hardware", + "subtitle": "Computer, Laptops & Geräte", + "materialIcon": "memory", + "sortOrder": 0, + "services": [ + { + "slug": "pc-reparatur", + "icon": "🔧", + "title": "PC & Laptop Reparatur", + "description": "Startet nicht, stürzt ab, wird heiß – wir finden das Problem.", + "longDescription": "Dein Computer macht Probleme? Egal ob er nicht mehr startet, ständig abstürzt, überhitzt oder merkwürdige Geräusche macht – wir diagnostizieren das Problem und beheben es. Wir reparieren sowohl Desktop-PCs als auch Laptops aller Marken.", + "tags": ["Fehlerdiagnose", "Komponentenaustausch"], + "keywords": "pc reparatur computer laptop notebook reparieren kaputt defekt", + "sortOrder": 0 + }, + { + "slug": "hardware-upgrade", + "icon": "⚡", + "title": "Hardware-Aufrüstung", + "description": "Mehr RAM, größere SSD, neue Grafikkarte – schneller machen.", + "longDescription": "Dein Rechner ist zu langsam? Oft hilft ein gezieltes Upgrade: Mehr Arbeitsspeicher für flüssiges Multitasking, eine SSD für schnelleres Booten und Laden, oder eine bessere Grafikkarte für Gaming und Videobearbeitung. Wir beraten dich, was sich lohnt.", + "tags": ["RAM", "SSD", "Grafikkarte"], + "keywords": "aufrüstung upgrade ram ssd festplatte arbeitsspeicher schneller", + "sortOrder": 1 + }, + { + "slug": "datenrettung", + "icon": "💾", + "title": "Datenrettung", + "description": "Festplatte kaputt? Daten gelöscht? Wir retten was geht.", + "longDescription": "Wichtige Dateien verschwunden oder Festplatte defekt? Wir versuchen deine Daten zu retten – von HDDs, SSDs, USB-Sticks und SD-Karten. Je früher du dich meldest, desto besser die Chancen. Keine Rettung, keine Kosten.", + "tags": ["HDD", "SSD", "USB-Stick"], + "keywords": "datenrettung daten retten festplatte kaputt backup wiederherstellen recovery", + "sortOrder": 2 + }, + { + "slug": "pc-zusammenbau", + "icon": "🖥️", + "title": "PC-Zusammenbau", + "description": "Wir bauen deinen Wunsch-PC zusammen.", + "longDescription": "Du willst einen maßgeschneiderten PC? Wir stellen die Komponenten zusammen, bauen alles sauber auf, installieren das Betriebssystem und testen alles durch. Ob Gaming-Monster, leise Workstation oder kompakter Office-PC.", + "tags": ["Gaming-PC", "Workstation", "Office"], + "keywords": "zusammenbau pc bauen computer custom gaming workstation zusammenstellen", + "sortOrder": 3 + }, + { + "slug": "kaufberatung", + "icon": "🛒", + "title": "Kaufberatung", + "description": "Welcher PC oder Laptop passt zu dir? Ehrliche Beratung.", + "longDescription": "Überfordert von der Auswahl? Wir beraten dich herstellerunabhängig und ehrlich. Kein Upselling, keine Provision – nur die Empfehlung, die zu deinem Budget und deinen Anforderungen passt.", + "tags": ["Laptop", "PC", "Preis-Leistung"], + "keywords": "kaufberatung hardware beratung welcher pc laptop kaufen", + "sortOrder": 4 + }, + { + "slug": "geraete-einrichtung", + "icon": "📦", + "title": "Geräte-Einrichtung", + "description": "Neues Gerät? Wir richten alles ein.", + "longDescription": "Neuen PC oder Laptop gekauft? Wir kümmern uns um alles: Windows einrichten, Programme installieren, Drucker verbinden, Daten vom alten Gerät übertragen. Du bekommst ein fertiges, einsatzbereites System.", + "tags": ["Windows", "Datenumzug", "Software"], + "keywords": "einrichtung setup neuer pc laptop einrichten installieren konfigurieren", + "sortOrder": 5 + }, + { + "slug": "reinigung-wartung", + "icon": "🧹", + "title": "Reinigung & Wartung", + "description": "Staub raus, neue Wärmeleitpaste – leise und kühl.", + "longDescription": "Nach ein paar Jahren sammelt sich Staub an, die Wärmeleitpaste trocknet aus – der PC wird laut und heiß. Wir reinigen alles gründlich, tragen neue Wärmeleitpaste auf und tauschen bei Bedarf Lüfter aus.", + "tags": ["Entstaubung", "Wärmeleitpaste"], + "keywords": "reinigung pc reinigen staub lüfter wärmeleitpaste thermal wartung", + "sortOrder": 6 + }, + { + "slug": "display-reparatur", + "icon": "🖼️", + "title": "Display-Reparatur", + "description": "Laptop-Display kaputt? Wir tauschen es aus.", + "longDescription": "Display gesprungen, Pixelfehler oder Bildausfall? Wir tauschen Laptop-Displays aus und reparieren auch Scharniere und Displaykabel. Die meisten Reparaturen sind innerhalb weniger Tage erledigt.", + "tags": ["Displaytausch", "Scharnier"], + "keywords": "display bildschirm monitor reparatur kaputt austausch", + "sortOrder": 7 + }, + { + "slug": "drucker-scanner", + "icon": "🖨️", + "title": "Drucker & Scanner", + "description": "Drucker einrichten, WLAN-Druck konfigurieren.", + "longDescription": "Drucker will nicht? Wir richten deinen Drucker ein – lokal oder im Netzwerk, per Kabel oder WLAN. Auch Scanner und Multifunktionsgeräte. Inklusive Treiber-Installation und Testdruck.", + "tags": ["WLAN-Drucker", "Scanner"], + "keywords": "drucker einrichtung scanner multifunktion wlan drucker installieren", + "sortOrder": 8 + }, + { + "slug": "smartphone-tablet", + "icon": "📱", + "title": "Smartphone & Tablet", + "description": "Display-Tausch, Akku-Wechsel, Datenübertragung.", + "longDescription": "Handy-Display kaputt oder Akku schwach? Wir reparieren Smartphones und Tablets. Außerdem helfen wir beim Umzug auf ein neues Gerät – alle Daten, Kontakte und Apps sicher übertragen.", + "tags": ["Display", "Akku", "Daten"], + "keywords": "smartphone handy tablet reparatur display akku tauschen", + "sortOrder": 9 + }, + { + "slug": "nas-speicher", + "icon": "💿", + "title": "NAS & Speicher", + "description": "NAS einrichten, RAID konfigurieren, Backup-Strategien.", + "longDescription": "Eigene Cloud zuhause? Wir richten NAS-Systeme von Synology, QNAP und anderen ein. RAID-Konfiguration, Benutzer anlegen, Backup einrichten, Fernzugriff – alles aus einer Hand.", + "tags": ["Synology", "QNAP", "RAID"], + "keywords": "nas netzwerkspeicher speicher server storage synology qnap", + "sortOrder": 10 + } + ] + }, + { + "slug": "software", + "name": "Software", + "subtitle": "Programme, Apps & Automatisierung", + "materialIcon": "code", + "sortOrder": 1, + "services": [ + { + "slug": "desktop-anwendungen", + "icon": "💻", + "title": "Desktop-Anwendungen", + "description": "Individuelle Windows-Programme nach deinen Wünschen.", + "longDescription": "Du brauchst ein Programm, das genau das macht, was du willst? Wir entwickeln individuelle Desktop-Anwendungen für Windows – von kleinen Tools bis zu komplexen Business-Anwendungen.", + "tags": ["Windows", "Cross-Platform"], + "keywords": "desktop anwendung programm windows software entwickeln programmieren", + "sortOrder": 0 + }, + { + "slug": "mobile-apps", + "icon": "📲", + "title": "Mobile Apps", + "description": "Apps für iOS und Android mit Flutter.", + "longDescription": "Eine App für dein Business? Mit Flutter entwickeln wir Apps, die auf iPhone und Android laufen – mit einer Codebasis. Schneller, günstiger und einfacher zu warten als native Entwicklung.", + "tags": ["Flutter", "iOS", "Android"], + "keywords": "app mobile ios android smartphone tablet flutter", + "sortOrder": 1 + }, + { + "slug": "automatisierungen", + "icon": "🤖", + "title": "Automatisierungen", + "description": "Wiederkehrende Aufgaben automatisch erledigen lassen.", + "longDescription": "Jeden Tag die gleichen Klicks? Wir automatisieren wiederkehrende Aufgaben mit Scripts, Bots und Workflows. Daten übertragen, Reports erstellen, E-Mails versenden – alles auf Autopilot.", + "tags": ["Scripts", "Bots", "Workflows"], + "keywords": "automatisierung script skript automatisch automatisieren", + "sortOrder": 2 + }, + { + "slug": "api-entwicklung", + "icon": "🔌", + "title": "API-Entwicklung", + "description": "Systeme verbinden, Daten austauschen.", + "longDescription": "Deine Systeme sollen miteinander reden? Wir entwickeln REST-APIs und Schnittstellen, die deine Anwendungen verbinden. Sauber dokumentiert und sicher.", + "tags": ["REST-API", "NestJS", "Node.js"], + "keywords": "api schnittstelle integration rest backend", + "sortOrder": 3 + }, + { + "slug": "datenbanken", + "icon": "🗄️", + "title": "Datenbanken", + "description": "Datenbanken designen, optimieren, verwalten.", + "longDescription": "Daten sind das Fundament. Wir designen Datenbankstrukturen, optimieren langsame Queries und migrieren bestehende Datenbanken. PostgreSQL, MySQL, MongoDB – wir kennen sie alle.", + "tags": ["PostgreSQL", "MySQL", "MongoDB"], + "keywords": "datenbank sql database mysql postgresql mongodb", + "sortOrder": 4 + }, + { + "slug": "excel-vba", + "icon": "📊", + "title": "Excel & VBA", + "description": "Makros, VBA-Scripts, komplexe Formeln.", + "longDescription": "Excel kann mehr als du denkst. Wir erstellen Makros und VBA-Scripts, die dir stundenlange Arbeit ersparen. Komplexe Formeln, automatische Reports, Datenvalidierung.", + "tags": ["Makros", "VBA", "Formeln"], + "keywords": "excel makro vba access tabelle automatisieren", + "sortOrder": 5 + }, + { + "slug": "business-software", + "icon": "🏢", + "title": "Business-Software", + "description": "Lager, Kunden, Aufträge – individuelle Lösungen.", + "longDescription": "Standardsoftware passt nicht? Wir entwickeln individuelle Lösungen für Lagerverwaltung, Kundenverwaltung, Auftragsabwicklung – genau auf deine Prozesse zugeschnitten.", + "tags": ["Warenwirtschaft", "CRM"], + "keywords": "erp crm system business software warenwirtschaft", + "sortOrder": 6 + }, + { + "slug": "daten-import-export", + "icon": "🔄", + "title": "Daten-Import & Export", + "description": "Daten zwischen Systemen austauschen und migrieren.", + "longDescription": "Daten müssen von A nach B? Wir schreiben Import/Export-Routinen, konvertieren Formate und migrieren Datenbestände. CSV, XML, JSON, Excel – kein Problem.", + "tags": ["CSV", "XML", "JSON"], + "keywords": "schnittstelle import export datenaustausch csv xml json", + "sortOrder": 7 + } + ] + }, + { + "slug": "web", + "name": "Web", + "subtitle": "Websites & Web-Apps", + "materialIcon": "language", + "sortOrder": 2, + "services": [ + { + "slug": "firmenwebsites", + "icon": "🌐", + "title": "Firmenwebsites", + "description": "Professionelle Websites für Unternehmen.", + "longDescription": "Dein digitales Aushängeschild. Wir bauen moderne, schnelle Websites, die auf allen Geräten gut aussehen. Für Unternehmen, Handwerker, Freiberufler – individuell gestaltet, nicht von der Stange.", + "tags": ["Responsive", "Modern", "Schnell"], + "keywords": "website webseite homepage firmenwebsite internetseite erstellen", + "sortOrder": 0 + }, + { + "slug": "landingpages", + "icon": "🎯", + "title": "Landingpages", + "description": "Conversion-optimierte Seiten für Kampagnen.", + "longDescription": "Eine Seite, ein Ziel. Wir bauen Landingpages, die konvertieren – für Produktlaunches, Kampagnen oder Lead-Generierung. Klare Struktur, überzeugender Text, schnelle Ladezeit.", + "tags": ["Conversion", "Marketing"], + "keywords": "landingpage landing page marketing conversion", + "sortOrder": 1 + }, + { + "slug": "web-applikationen", + "icon": "⚡", + "title": "Web-Applikationen", + "description": "Komplexe Browser-Anwendungen mit Angular.", + "longDescription": "Mehr als eine Website – eine vollwertige Anwendung im Browser. Dashboards, interne Tools, Kundenportale. Mit Angular, TypeScript und modernen Technologien.", + "tags": ["Angular", "TypeScript", "SPA"], + "keywords": "web app webanwendung angular react frontend browser", + "sortOrder": 2 + }, + { + "slug": "online-shops", + "icon": "🛍️", + "title": "Online-Shops", + "description": "E-Commerce Lösungen von einfach bis komplex.", + "longDescription": "Verkaufen im Internet? Wir bauen Online-Shops – von einfachen Shopify-Lösungen bis zu individuellen E-Commerce-Plattformen. Payment, Versand, Bestandsverwaltung inklusive.", + "tags": ["Shopify", "WooCommerce"], + "keywords": "online shop e-commerce webshop shopify woocommerce", + "sortOrder": 3 + }, + { + "slug": "buchungssysteme", + "icon": "📅", + "title": "Buchungssysteme", + "description": "Online-Terminbuchung und Reservierungen.", + "longDescription": "Kunden sollen online buchen können? Wir entwickeln Buchungssysteme für Termine, Kurse, Ressourcen. Mit Kalenderansicht, automatischen Bestätigungen und Erinnerungen.", + "tags": ["Terminbuchung", "Kalender"], + "keywords": "buchungssystem terminbuchung kalender online buchen reservierung", + "sortOrder": 4 + }, + { + "slug": "wordpress-cms", + "icon": "📝", + "title": "WordPress & CMS", + "description": "WordPress-Seiten, Themes, Plugins.", + "longDescription": "WordPress ist der Klassiker – und wir kennen ihn in- und auswendig. Neue Seiten aufsetzen, Themes anpassen, Plugins entwickeln, bestehende Seiten reparieren.", + "tags": ["WordPress", "Themes", "Blog"], + "keywords": "wordpress cms content management blog", + "sortOrder": 5 + }, + { + "slug": "seo-optimierung", + "icon": "📈", + "title": "SEO-Optimierung", + "description": "Google-Rankings verbessern.", + "longDescription": "Gefunden werden bei Google. Wir optimieren deine Website technisch: Ladezeit, Struktur, Meta-Tags, Schema Markup. Damit du bei relevanten Suchanfragen oben stehst.", + "tags": ["On-Page SEO", "Core Web Vitals"], + "keywords": "seo suchmaschine google optimierung ranking", + "sortOrder": 6 + }, + { + "slug": "hosting-domains", + "icon": "☁️", + "title": "Hosting & Domains", + "description": "Domain, Hosting, SSL, DNS – alles einrichten.", + "longDescription": "Das technische Fundament. Wir registrieren Domains, richten Hosting ein, konfigurieren DNS und SSL-Zertifikate. Damit deine Website sicher und erreichbar ist.", + "tags": ["Domain", "SSL", "DNS"], + "keywords": "hosting domain webspace server ssl", + "sortOrder": 7 + }, + { + "slug": "website-wartung", + "icon": "🛠️", + "title": "Website-Wartung", + "description": "Updates, Backups, Security-Patches.", + "longDescription": "Eine Website braucht Pflege. Wir kümmern uns um Updates, Backups, Sicherheits-Patches und kleine Änderungen. Damit du dich um dein Business kümmern kannst.", + "tags": ["Updates", "Backups", "Security"], + "keywords": "website wartung pflege update aktualisierung", + "sortOrder": 8 + } + ] + }, + { + "slug": "netzwerk", + "name": "Netzwerk", + "subtitle": "WLAN, Server & Cloud", + "materialIcon": "lan", + "sortOrder": 3, + "services": [ + { + "slug": "wlan-einrichtung", + "icon": "📶", + "title": "WLAN-Einrichtung", + "description": "WLAN optimieren, Reichweite verbessern, Mesh.", + "longDescription": "WLAN zu langsam oder Funklöcher? Wir analysieren dein Netzwerk und optimieren es. Access Points platzieren, Mesh-Systeme einrichten, Kanäle optimieren.", + "tags": ["Mesh", "Access Points", "5GHz"], + "keywords": "wlan wifi wireless funk netzwerk einrichten langsam reichweite", + "sortOrder": 0 + }, + { + "slug": "netzwerk-verkabelung", + "icon": "🔗", + "title": "Netzwerk-Verkabelung", + "description": "LAN-Verkabelung planen und umsetzen.", + "longDescription": "Kabel ist King. Wir planen und verlegen Netzwerkkabel, richten Switches ein und dokumentieren alles sauber. Cat6, Cat7 – für stabiles, schnelles Internet.", + "tags": ["Cat6/Cat7", "Switches"], + "keywords": "netzwerk lan kabel ethernet switch router verkabelung", + "sortOrder": 1 + }, + { + "slug": "router-firewall", + "icon": "🌐", + "title": "Router & Firewall", + "description": "Router, Firewall-Regeln, Port-Forwarding.", + "longDescription": "Das Tor zum Internet. Wir konfigurieren Router und Firewalls, richten Port-Forwarding ein und sorgen für Sicherheit. FritzBox, pfSense, Ubiquiti – wir kennen sie alle.", + "tags": ["FritzBox", "pfSense", "VPN"], + "keywords": "router firewall fritz fritzbox konfiguration internet", + "sortOrder": 2 + }, + { + "slug": "vpn-loesungen", + "icon": "🔒", + "title": "VPN-Lösungen", + "description": "Sichere Fernzugriffe, Homeoffice-Anbindung.", + "longDescription": "Sicher von überall arbeiten. Wir richten VPN-Verbindungen ein – für Homeoffice, Außendienst oder Standortvernetzung. WireGuard, OpenVPN, IPSec.", + "tags": ["WireGuard", "OpenVPN"], + "keywords": "vpn virtual private network fernzugriff remote tunnel", + "sortOrder": 3 + }, + { + "slug": "server-administration", + "icon": "🖥️", + "title": "Server-Administration", + "description": "Windows Server und Linux einrichten und warten.", + "longDescription": "Server sind unser Ding. Windows Server oder Linux – wir installieren, konfigurieren und warten. Updates, Monitoring, Troubleshooting.", + "tags": ["Windows Server", "Linux", "Debian"], + "keywords": "server windows linux debian ubuntu einrichten administrieren", + "sortOrder": 4 + }, + { + "slug": "active-directory", + "icon": "👥", + "title": "Active Directory", + "description": "Domänen, Benutzer, Gruppen, Richtlinien.", + "longDescription": "Zentrale Benutzerverwaltung für Unternehmen. Wir richten Active Directory ein, verwalten Benutzer und Gruppen, konfigurieren Gruppenrichtlinien.", + "tags": ["AD", "GPO", "Benutzer"], + "keywords": "active directory ad domäne benutzer gruppen rechte", + "sortOrder": 5 + }, + { + "slug": "cloud-loesungen", + "icon": "☁️", + "title": "Cloud-Lösungen", + "description": "Microsoft 365, Azure, AWS einrichten.", + "longDescription": "Ab in die Cloud. Wir richten Microsoft 365 ein, migrieren Daten zu Azure oder AWS und verwalten Cloud-Infrastruktur. Hybrid oder Full Cloud.", + "tags": ["Microsoft 365", "Azure", "AWS"], + "keywords": "cloud microsoft 365 office azure aws google cloud", + "sortOrder": 6 + }, + { + "slug": "backup-strategien", + "icon": "💾", + "title": "Backup-Strategien", + "description": "Backup-Konzepte, automatische Sicherungen.", + "longDescription": "Daten sind Gold wert. Wir entwickeln Backup-Konzepte nach der 3-2-1 Regel, richten automatische Sicherungen ein und testen die Wiederherstellung.", + "tags": ["3-2-1 Regel", "Cloud-Backup"], + "keywords": "backup datensicherung sicherung restore wiederherstellung", + "sortOrder": 7 + }, + { + "slug": "virtualisierung", + "icon": "📦", + "title": "Virtualisierung", + "description": "VMs, Hyper-V, Proxmox, Docker.", + "longDescription": "Mehr aus der Hardware rausholen. Wir richten Virtualisierung ein – Hyper-V, Proxmox, VMware. Oder Container mit Docker für moderne Anwendungen.", + "tags": ["Hyper-V", "Proxmox", "Docker"], + "keywords": "virtualisierung vm vmware hyper-v proxmox docker container", + "sortOrder": 8 + }, + { + "slug": "it-sicherheit", + "icon": "🛡️", + "title": "IT-Sicherheit", + "description": "Firewall, Virenschutz, Security-Audits.", + "longDescription": "Sicherheit ist kein Zustand, sondern ein Prozess. Wir prüfen deine IT auf Schwachstellen, richten Firewalls und Virenschutz ein, implementieren 2FA.", + "tags": ["Firewall", "Antivirus", "2FA"], + "keywords": "sicherheit security firewall antivirus virenschutz malware", + "sortOrder": 9 + }, + { + "slug": "email-systeme", + "icon": "📧", + "title": "E-Mail-Systeme", + "description": "Exchange, IMAP/SMTP einrichten.", + "longDescription": "E-Mail ist Kommunikation Nr. 1. Wir richten E-Mail-Server ein, migrieren Postfächer, konfigurieren Spam-Filter und sorgen für zuverlässige Zustellung.", + "tags": ["Exchange", "IMAP", "Spam-Filter"], + "keywords": "e-mail mail exchange postfach mailserver imap smtp", + "sortOrder": 10 + }, + { + "slug": "monitoring", + "icon": "📊", + "title": "Monitoring", + "description": "Server und Netzwerk überwachen, Alerts.", + "longDescription": "Probleme erkennen, bevor sie eskalieren. Wir richten Monitoring ein – für Server, Netzwerk, Dienste. Mit Dashboards und Alerts bei Problemen.", + "tags": ["Uptime", "Alerts", "Grafana"], + "keywords": "monitoring überwachung netzwerk server nagios zabbix", + "sortOrder": 11 + } + ] + }, + { + "slug": "support", + "name": "Support", + "subtitle": "Hilfe remote oder vor Ort", + "materialIcon": "support_agent", + "sortOrder": 4, + "services": [ + { + "slug": "remote-support", + "icon": "🖱️", + "title": "Remote-Support", + "description": "Schnelle Hilfe per Fernwartung.", + "longDescription": "Problem schildern, wir schalten uns drauf. Per TeamViewer oder AnyDesk helfen wir dir sofort – ohne Anfahrt, ohne Wartezeit. Die schnellste Lösung für die meisten Probleme.", + "tags": ["TeamViewer", "AnyDesk"], + "keywords": "remote support fernwartung fernzugriff teamviewer hilfe", + "sortOrder": 0 + }, + { + "slug": "vor-ort-service", + "icon": "🚗", + "title": "Vor-Ort-Service", + "description": "Wir kommen zu dir – im Westerwald und Umgebung.", + "longDescription": "Manchmal muss man vor Ort sein. Wir kommen zu dir – im Westerwald, Altenkirchen und Umgebung. Für Hardware-Probleme, Netzwerk-Einrichtung oder wenn Remote nicht reicht.", + "tags": ["Westerwald", "Altenkirchen"], + "keywords": "vor ort vor-ort vorort service techniker kommen westerwald", + "sortOrder": 1 + }, + { + "slug": "wartungsvertraege", + "icon": "📋", + "title": "Wartungsverträge", + "description": "Regelmäßige Wartung, bevorzugter Support.", + "longDescription": "Planbare IT-Kosten und bevorzugter Support. Mit einem Wartungsvertrag kümmern wir uns regelmäßig um deine Systeme und du hast einen festen Ansprechpartner.", + "tags": ["Regelmäßig", "Priorität"], + "keywords": "wartung wartungsvertrag regelmäßig service monatlich", + "sortOrder": 2 + }, + { + "slug": "schulungen", + "icon": "🎓", + "title": "Schulungen", + "description": "Einweisungen, IT-Grundlagen, Workshops.", + "longDescription": "Wissen ist Macht. Wir schulen dich und dein Team – in neuer Software, IT-Grundlagen oder speziellen Themen. Einzeln oder als Workshop.", + "tags": ["Einweisung", "Workshop"], + "keywords": "schulung training einweisung lernen erklären workshop", + "sortOrder": 3 + }, + { + "slug": "software-installation", + "icon": "📀", + "title": "Software-Installation", + "description": "Programme installieren und konfigurieren.", + "longDescription": "Neue Software soll aufs System? Wir installieren und konfigurieren Programme, sorgen für die richtigen Einstellungen und weisen dich ein.", + "tags": ["Installation", "Konfiguration"], + "keywords": "installation software installieren programm einrichten", + "sortOrder": 4 + }, + { + "slug": "updates-patches", + "icon": "🔄", + "title": "Updates & Patches", + "description": "Betriebssystem, Treiber, Security-Patches.", + "longDescription": "Aktuell bleiben ist wichtig. Wir bringen deine Systeme auf den neuesten Stand – Windows-Updates, Treiber, Security-Patches. Kontrolliert und ohne böse Überraschungen.", + "tags": ["Windows Update", "Treiber"], + "keywords": "update aktualisierung windows treiber patch", + "sortOrder": 5 + }, + { + "slug": "virenentfernung", + "icon": "🦠", + "title": "Virenentfernung", + "description": "Malware, Trojaner, Adware – sauber machen.", + "longDescription": "System verseucht? Wir entfernen Viren, Trojaner, Adware und andere Schadsoftware. Gründlich und nachhaltig – damit dein System wieder sauber läuft.", + "tags": ["Malware", "Trojaner"], + "keywords": "virus malware trojaner entfernen reinigen säubern infiziert", + "sortOrder": 6 + }, + { + "slug": "performance-optimierung", + "icon": "🚀", + "title": "Performance-Optimierung", + "description": "PC zu langsam? Wir machen ihn wieder flott.", + "longDescription": "Rechner lahm? Wir finden die Ursache: Autostart aufräumen, Bloatware entfernen, Festplatte bereinigen, Dienste optimieren. Danach läuft er wieder.", + "tags": ["Autostart", "Bereinigung"], + "keywords": "performance langsam optimieren schneller tuning beschleunigen", + "sortOrder": 7 + }, + { + "slug": "it-beratung", + "icon": "💡", + "title": "IT-Beratung", + "description": "Welche Lösung passt? Ehrliche Beratung.", + "longDescription": "Nicht sicher, was du brauchst? Wir beraten dich herstellerunabhängig und ehrlich. Keine versteckten Interessen – nur die Lösung, die für dich passt.", + "tags": ["Strategie", "Neutral"], + "keywords": "beratung consulting it-beratung strategie konzept planen", + "sortOrder": 8 + }, + { + "slug": "notfall-support", + "icon": "🚨", + "title": "Notfall-Support", + "description": "Server down? Hilfe auch außerhalb der Geschäftszeiten.", + "longDescription": "IT-Notfall wartet nicht auf Bürozeiten. Bei dringenden Problemen helfen wir auch außerhalb der regulären Zeiten. Server down, Datenverlust, Hackerangriff – wir sind da.", + "tags": ["24/7", "Notfall"], + "keywords": "notfall notdienst dringend schnell sofort hilfe", + "sortOrder": 9 + } + ] + } + ] +} \ No newline at end of file diff --git a/apps/backend/nest-cli.json b/apps/backend/nest-cli.json new file mode 100644 index 0000000..f9aa683 --- /dev/null +++ b/apps/backend/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/apps/backend/package.json b/apps/backend/package.json new file mode 100644 index 0000000..92b056b --- /dev/null +++ b/apps/backend/package.json @@ -0,0 +1,90 @@ +{ + "name": "leonards-media-api", + "version": "0.0.1", + "description": "", + "author": "", + "private": true, + "license": "UNLICENSED", + "scripts": { + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json" + }, + "dependencies": { + "@emailjs/nodejs": "^5.0.2", + "@nestjs-modules/mailer": "^2.0.2", + "@nestjs/common": "^10.0.0", + "@nestjs/config": "^4.0.2", + "@nestjs/core": "^10.0.0", + "@nestjs/jwt": "^11.0.0", + "@nestjs/passport": "^11.0.5", + "@nestjs/platform-express": "^10.4.22", + "@nestjs/throttler": "^6.5.0", + "@nestjs/typeorm": "^11.0.0", + "bcrypt": "^6.0.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.2", + "googleapis": "^163.0.0", + "helmet": "^8.1.0", + "nodemailer": "^7.0.9", + "openai": "^6.2.0", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "pg": "^8.16.3", + "reflect-metadata": "^0.2.0", + "rxjs": "^7.8.1", + "sqlite3": "^5.1.7", + "typeorm": "^0.3.27" + }, + "devDependencies": { + "@nestjs/cli": "^10.0.0", + "@nestjs/schematics": "^10.0.0", + "@nestjs/testing": "^10.0.0", + "@types/bcrypt": "^6.0.0", + "@types/express": "^4.17.17", + "@types/jest": "^29.5.2", + "@types/node": "^20.3.1", + "@types/passport-jwt": "^4.0.1", + "@types/supertest": "^6.0.0", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "eslint": "^8.42.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "jest": "^29.5.0", + "prettier": "^3.0.0", + "source-map-support": "^0.5.21", + "supertest": "^7.0.0", + "ts-jest": "^29.1.0", + "ts-loader": "^9.4.3", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.1.3" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage", + "testEnvironment": "node" + } +} diff --git a/apps/backend/src/analytics/analytics.controller.ts b/apps/backend/src/analytics/analytics.controller.ts new file mode 100644 index 0000000..1bbf876 --- /dev/null +++ b/apps/backend/src/analytics/analytics.controller.ts @@ -0,0 +1,167 @@ +import { Controller, Post, Get, Body, Req, UseGuards, HttpCode, HttpStatus, Delete, Query } from '@nestjs/common'; +import { Request } from 'express'; +import { AnalyticsService } from './analytics.service'; +import { CreateAnalyticsEventDto, AnalyticsDashboardDto } from './analytics.dto'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { AdminGuard } from '../auth/guards/admin.guard'; + +@Controller('analytics') +export class AnalyticsController { + constructor(private readonly analyticsService: AnalyticsService) {} + + /** + * Empfängt Analytics-Events vom Frontend + * Nur mit Consent-Header erlaubt + */ + @Post('event') + @HttpCode(HttpStatus.NO_CONTENT) + async trackEvent( + @Body() dto: CreateAnalyticsEventDto, + @Req() req: Request, + ): Promise { + // Consent-Header prüfen + const consentHeader = req.headers['x-consent-analytics']; + const hasConsent = consentHeader === 'true'; + + console.log('[Analytics] Received event:', dto.type, dto.page); + console.log('[Analytics] Consent header:', consentHeader, '-> hasConsent:', hasConsent); + + // IP aus verschiedenen Headers extrahieren (Proxy-Support) + const ip = this.getClientIP(req); + + await this.analyticsService.trackEvent(dto, ip, hasConsent); + console.log('[Analytics] Event saved successfully'); + } + + /** + * Dashboard-Daten für Admin + */ + @Get('dashboard') + @UseGuards(JwtAuthGuard, AdminGuard) + async getDashboard(): Promise { + return this.analyticsService.getDashboard(); + } + + /** + * Overview-Statistiken + */ + @Get('overview') + @UseGuards(JwtAuthGuard, AdminGuard) + async getOverview() { + return this.analyticsService.getOverview(); + } + + /** + * Zeitreihen-Daten + */ + @Get('timeseries') + @UseGuards(JwtAuthGuard, AdminGuard) + async getTimeSeries(@Query('days') days?: string) { + const numDays = parseInt(days || '30', 10); + return this.analyticsService.getTimeSeries(Math.min(numDays, 90)); + } + + /** + * Top-Seiten + */ + @Get('pages') + @UseGuards(JwtAuthGuard, AdminGuard) + async getTopPages(@Query('days') days?: string) { + const numDays = parseInt(days || '30', 10); + return this.analyticsService.getTopPages(Math.min(numDays, 90)); + } + + /** + * Referrer-Statistiken + */ + @Get('referrers') + @UseGuards(JwtAuthGuard, AdminGuard) + async getReferrers(@Query('days') days?: string) { + const numDays = parseInt(days || '30', 10); + return this.analyticsService.getReferrers(Math.min(numDays, 90)); + } + + /** + * Geräte-Statistiken + */ + @Get('devices') + @UseGuards(JwtAuthGuard, AdminGuard) + async getDevices(@Query('days') days?: string) { + const numDays = parseInt(days || '30', 10); + return this.analyticsService.getDeviceStats(Math.min(numDays, 90)); + } + + /** + * Cleanup alter Events (Admin-Only) + */ + @Delete('cleanup') + @UseGuards(JwtAuthGuard, AdminGuard) + async cleanup(@Query('days') days?: string): Promise<{ deleted: number }> { + const daysToKeep = parseInt(days || '90', 10); + const deleted = await this.analyticsService.cleanupOldEvents(daysToKeep); + return { deleted }; + } + + /** + * DSGVO: Auskunftsrecht - Gibt alle Daten zu einer Session-ID zurück + * Nutzer können ihre Session-ID aus dem SessionStorage auslesen + */ + @Get('my-data') + async getMyData(@Query('sessionId') sessionId: string, @Req() req: Request) { + if (!sessionId || sessionId.length < 10) { + return { error: 'Ungültige Session-ID', data: [] }; + } + + const data = await this.analyticsService.getDataBySessionId(sessionId); + return { + sessionId, + recordCount: data.length, + data: data.map(event => ({ + type: event.type, + page: event.page, + timestamp: event.createdAt, + screenSize: event.screenSize, + userAgent: event.userAgent, + // Keine IP, keine sensiblen Metadaten + })), + info: 'Diese Daten werden nach 90 Tagen automatisch gelöscht.' + }; + } + + /** + * DSGVO: Löschrecht - Löscht alle Daten zu einer Session-ID + */ + @Delete('my-data') + async deleteMyData(@Query('sessionId') sessionId: string) { + if (!sessionId || sessionId.length < 10) { + return { error: 'Ungültige Session-ID', deleted: 0 }; + } + + const deleted = await this.analyticsService.deleteDataBySessionId(sessionId); + return { + sessionId, + deleted, + message: deleted > 0 + ? `${deleted} Analytics-Einträge wurden gelöscht.` + : 'Keine Daten zu dieser Session-ID gefunden.' + }; + } + + /** + * Extrahiert die echte Client-IP (berücksichtigt Proxys) + */ + private getClientIP(req: Request): string { + const forwarded = req.headers['x-forwarded-for']; + if (forwarded) { + const ips = Array.isArray(forwarded) ? forwarded[0] : forwarded; + return ips.split(',')[0].trim(); + } + + const realIp = req.headers['x-real-ip']; + if (realIp) { + return Array.isArray(realIp) ? realIp[0] : realIp; + } + + return req.ip || req.socket?.remoteAddress || ''; + } +} diff --git a/apps/backend/src/analytics/analytics.dto.ts b/apps/backend/src/analytics/analytics.dto.ts new file mode 100644 index 0000000..e099088 --- /dev/null +++ b/apps/backend/src/analytics/analytics.dto.ts @@ -0,0 +1,109 @@ +import { IsString, IsOptional, IsNumber, IsObject, MaxLength, IsIn } from 'class-validator'; + +const EVENT_TYPES = ['pageview', 'click', 'scroll', 'form_submit', 'conversion', 'error', 'custom'] as const; + +export class CreateAnalyticsEventDto { + @IsString() + @IsIn(EVENT_TYPES) + type: typeof EVENT_TYPES[number]; + + @IsString() + @MaxLength(500) + page: string; + + @IsOptional() + @IsString() + @MaxLength(500) + referrer?: string; + + @IsOptional() + @IsString() + @MaxLength(500) + userAgent?: string; + + @IsOptional() + @IsString() + @MaxLength(50) + screenSize?: string; + + @IsNumber() + timestamp: number; + + @IsString() + @MaxLength(50) + sessionId: string; + + @IsOptional() + @IsObject() + metadata?: Record; +} + +// ===== RESPONSE DTOs ===== + +export interface AnalyticsOverviewDto { + today: { + pageviews: number; + uniqueSessions: number; + conversions: number; + }; + yesterday: { + pageviews: number; + uniqueSessions: number; + conversions: number; + }; + last7Days: { + pageviews: number; + uniqueSessions: number; + conversions: number; + }; + last30Days: { + pageviews: number; + uniqueSessions: number; + conversions: number; + }; +} + +export interface PageStatsDto { + page: string; + views: number; + uniqueSessions: number; +} + +export interface TimeSeriesPointDto { + date: string; + pageviews: number; + uniqueSessions: number; + conversions: number; +} + +export interface ReferrerStatsDto { + referrer: string; + count: number; +} + +export interface DeviceStatsDto { + desktop: number; + mobile: number; + tablet: number; +} + +export interface RecentEventDto { + type: string; + page: string; + timestamp: Date; + sessionId: string; + userAgent?: string; + screenSize?: string; + referrer?: string; + metadata?: Record; + deviceType?: 'desktop' | 'mobile' | 'tablet'; +} + +export interface AnalyticsDashboardDto { + overview: AnalyticsOverviewDto; + timeSeries: TimeSeriesPointDto[]; + topPages: PageStatsDto[]; + referrers: ReferrerStatsDto[]; + devices: DeviceStatsDto; + recentEvents: RecentEventDto[]; +} diff --git a/apps/backend/src/analytics/analytics.entity.ts b/apps/backend/src/analytics/analytics.entity.ts new file mode 100644 index 0000000..c235d6d --- /dev/null +++ b/apps/backend/src/analytics/analytics.entity.ts @@ -0,0 +1,85 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, Index } from 'typeorm'; + +export type AnalyticsEventType = + | 'pageview' + | 'click' + | 'scroll' + | 'form_submit' + | 'conversion' + | 'error' + | 'custom'; + +@Entity('analytics_events') +@Index(['createdAt']) +@Index(['sessionId']) +@Index(['type']) +@Index(['page']) +export class AnalyticsEvent { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'varchar', length: 50 }) + type: AnalyticsEventType; + + @Column({ type: 'varchar', length: 500 }) + page: string; + + @Column({ type: 'varchar', length: 500, nullable: true }) + referrer?: string; + + @Column({ type: 'varchar', length: 500, nullable: true }) + userAgent?: string; + + @Column({ type: 'varchar', length: 50, nullable: true }) + screenSize?: string; + + @Column({ type: 'varchar', length: 50 }) + sessionId: string; + + @Column({ type: 'varchar', length: 50, nullable: true }) + ipAnonymized?: string; // Anonymisierte IP (z.B. 192.168.1.0) + + @Column({ type: 'varchar', length: 100, nullable: true }) + country?: string; + + @Column({ type: 'jsonb', nullable: true }) + metadata?: Record; + + @Column({ type: 'bigint' }) + clientTimestamp: number; + + @CreateDateColumn() + createdAt: Date; +} + +// ===== AGGREGATED STATS ENTITY ===== +@Entity('analytics_daily_stats') +@Index(['date'], { unique: true }) +export class AnalyticsDailyStats { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'date', unique: true }) + date: string; + + @Column({ type: 'int', default: 0 }) + pageviews: number; + + @Column({ type: 'int', default: 0 }) + uniqueSessions: number; + + @Column({ type: 'int', default: 0 }) + conversions: number; + + @Column({ type: 'jsonb', nullable: true }) + topPages?: Record; + + @Column({ type: 'jsonb', nullable: true }) + referrers?: Record; + + @Column({ type: 'jsonb', nullable: true }) + devices?: { desktop: number; mobile: number; tablet: number }; + + @CreateDateColumn() + createdAt: Date; +} diff --git a/apps/backend/src/analytics/analytics.module.ts b/apps/backend/src/analytics/analytics.module.ts new file mode 100644 index 0000000..80567ed --- /dev/null +++ b/apps/backend/src/analytics/analytics.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AnalyticsController } from './analytics.controller'; +import { AnalyticsService } from './analytics.service'; +import { AnalyticsEvent, AnalyticsDailyStats } from './analytics.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([AnalyticsEvent, AnalyticsDailyStats]), + ], + controllers: [AnalyticsController], + providers: [AnalyticsService], + exports: [AnalyticsService], +}) +export class AnalyticsModule {} diff --git a/apps/backend/src/analytics/analytics.service.ts b/apps/backend/src/analytics/analytics.service.ts new file mode 100644 index 0000000..b6e212d --- /dev/null +++ b/apps/backend/src/analytics/analytics.service.ts @@ -0,0 +1,424 @@ +import { Injectable, ForbiddenException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, MoreThanOrEqual, Between, Raw } from 'typeorm'; +import { AnalyticsEvent, AnalyticsDailyStats } from './analytics.entity'; +import { + CreateAnalyticsEventDto, + AnalyticsDashboardDto, + AnalyticsOverviewDto, + TimeSeriesPointDto, + PageStatsDto, + ReferrerStatsDto, + DeviceStatsDto, + RecentEventDto +} from './analytics.dto'; + +@Injectable() +export class AnalyticsService { + constructor( + @InjectRepository(AnalyticsEvent) + private eventRepo: Repository, + @InjectRepository(AnalyticsDailyStats) + private dailyStatsRepo: Repository, + ) {} + + /** + * Speichert ein Analytics-Event + */ + async trackEvent(dto: CreateAnalyticsEventDto, ip: string, hasConsent: boolean): Promise { + if (!hasConsent) { + throw new ForbiddenException('Analytics consent required'); + } + + const event = this.eventRepo.create({ + type: dto.type, + page: dto.page, + referrer: this.cleanReferrer(dto.referrer), + userAgent: this.anonymizeUserAgent(dto.userAgent), + screenSize: this.categorizeScreenSize(dto.screenSize), + sessionId: dto.sessionId, + clientTimestamp: dto.timestamp, + ipAnonymized: this.anonymizeIP(ip), + metadata: dto.metadata, + }); + + await this.eventRepo.save(event); + } + + /** + * Anonymisiert IP-Adresse (DSGVO-konform) + */ + private anonymizeIP(ip: string): string { + if (!ip) return ''; + + // IPv4: Letztes Oktett auf 0 setzen + if (ip.includes('.')) { + return ip.replace(/\.\d+$/, '.0'); + } + + // IPv6: Letzte 80 Bits nullen + if (ip.includes(':')) { + const parts = ip.split(':'); + return parts.slice(0, 3).join(':') + '::0'; + } + + return ''; + } + + /** + * Anonymisiert User-Agent (DSGVO-konform) + * Behält nur OS und Browser-Typ, keine Versionen oder Details + */ + private anonymizeUserAgent(userAgent?: string): string { + if (!userAgent) return 'Unknown'; + + // Betriebssystem erkennen + let os = 'Unknown OS'; + if (/Windows/i.test(userAgent)) os = 'Windows'; + else if (/Mac OS X|Macintosh/i.test(userAgent)) os = 'MacOS'; + else if (/iPhone|iPad|iPod/i.test(userAgent)) os = 'iOS'; + else if (/Android/i.test(userAgent)) os = 'Android'; + else if (/Linux/i.test(userAgent)) os = 'Linux'; + else if (/CrOS/i.test(userAgent)) os = 'ChromeOS'; + + // Browser erkennen (Reihenfolge wichtig wegen User-Agent-Strings) + let browser = 'Unknown Browser'; + if (/Edg\//i.test(userAgent)) browser = 'Edge'; + else if (/OPR|Opera/i.test(userAgent)) browser = 'Opera'; + else if (/Firefox/i.test(userAgent)) browser = 'Firefox'; + else if (/Chrome/i.test(userAgent)) browser = 'Chrome'; + else if (/Safari/i.test(userAgent)) browser = 'Safari'; + else if (/MSIE|Trident/i.test(userAgent)) browser = 'IE'; + + return `${os} / ${browser}`; + } + + /** + * Kategorisiert Bildschirmgröße (DSGVO-konform) + * Keine exakten Werte, nur Kategorien + */ + private categorizeScreenSize(screenSize?: string): string { + if (!screenSize) return 'unknown'; + + const match = screenSize.match(/^(\d+)x(\d+)$/); + if (!match) return 'unknown'; + + const width = parseInt(match[1], 10); + + // Kategorisieren statt exakte Werte + if (width <= 480) return 'mobile-small'; + if (width <= 768) return 'mobile'; + if (width <= 1024) return 'tablet'; + if (width <= 1440) return 'desktop'; + return 'desktop-large'; + } + + /** + * Bereinigt Referrer (entfernt Query-Parameter mit sensiblen Daten) + */ + private cleanReferrer(referrer?: string): string | undefined { + if (!referrer) return undefined; + + try { + const url = new URL(referrer); + // Nur Origin + Pathname, keine Query-Parameter + return url.origin + url.pathname; + } catch { + return referrer; + } + } + + /** + * Dashboard-Daten für Admin + */ + async getDashboard(): Promise { + const [overview, timeSeries, topPages, referrers, devices, recentEvents] = await Promise.all([ + this.getOverview(), + this.getTimeSeries(30), + this.getTopPages(30), + this.getReferrers(30), + this.getDeviceStats(30), + this.getRecentEvents(20), + ]); + + return { + overview, + timeSeries, + topPages, + referrers, + devices, + recentEvents, + }; + } + + /** + * Overview-Statistiken + */ + async getOverview(): Promise { + const now = new Date(); + const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + const yesterday = new Date(today); + yesterday.setDate(yesterday.getDate() - 1); + const last7Days = new Date(today); + last7Days.setDate(last7Days.getDate() - 7); + const last30Days = new Date(today); + last30Days.setDate(last30Days.getDate() - 30); + + const [todayStats, yesterdayStats, last7Stats, last30Stats] = await Promise.all([ + this.getStatsForPeriod(today, now), + this.getStatsForPeriod(yesterday, today), + this.getStatsForPeriod(last7Days, now), + this.getStatsForPeriod(last30Days, now), + ]); + + return { + today: todayStats, + yesterday: yesterdayStats, + last7Days: last7Stats, + last30Days: last30Stats, + }; + } + + /** + * Statistiken für einen Zeitraum + */ + private async getStatsForPeriod(from: Date, to: Date): Promise<{ pageviews: number; uniqueSessions: number; conversions: number }> { + const events = await this.eventRepo.find({ + where: { + createdAt: Between(from, to), + }, + select: ['type', 'sessionId'], + }); + + const pageviews = events.filter(e => e.type === 'pageview').length; + const uniqueSessions = new Set(events.map(e => e.sessionId)).size; + const conversions = events.filter(e => e.type === 'conversion').length; + + return { pageviews, uniqueSessions, conversions }; + } + + /** + * Zeitreihen-Daten + */ + async getTimeSeries(days: number): Promise { + const result: TimeSeriesPointDto[] = []; + const now = new Date(); + + for (let i = days - 1; i >= 0; i--) { + const date = new Date(now); + date.setDate(date.getDate() - i); + const dateStr = date.toISOString().split('T')[0]; + + const dayStart = new Date(date.getFullYear(), date.getMonth(), date.getDate()); + const dayEnd = new Date(dayStart); + dayEnd.setDate(dayEnd.getDate() + 1); + + const events = await this.eventRepo.find({ + where: { + createdAt: Between(dayStart, dayEnd), + }, + select: ['type', 'sessionId'], + }); + + result.push({ + date: dateStr, + pageviews: events.filter(e => e.type === 'pageview').length, + uniqueSessions: new Set(events.map(e => e.sessionId)).size, + conversions: events.filter(e => e.type === 'conversion').length, + }); + } + + return result; + } + + /** + * Top-Seiten + */ + async getTopPages(days: number): Promise { + const since = new Date(); + since.setDate(since.getDate() - days); + + const events = await this.eventRepo.find({ + where: { + type: 'pageview', + createdAt: MoreThanOrEqual(since), + }, + select: ['page', 'sessionId'], + }); + + const pageMap = new Map }>(); + + for (const event of events) { + const existing = pageMap.get(event.page) || { views: 0, sessions: new Set() }; + existing.views++; + existing.sessions.add(event.sessionId); + pageMap.set(event.page, existing); + } + + return Array.from(pageMap.entries()) + .map(([page, stats]) => ({ + page, + views: stats.views, + uniqueSessions: stats.sessions.size, + })) + .sort((a, b) => b.views - a.views) + .slice(0, 10); + } + + /** + * Referrer-Statistiken + */ + async getReferrers(days: number): Promise { + const since = new Date(); + since.setDate(since.getDate() - days); + + const events = await this.eventRepo.find({ + where: { + type: 'pageview', + createdAt: MoreThanOrEqual(since), + }, + select: ['referrer'], + }); + + const referrerMap = new Map(); + + for (const event of events) { + const referrer = event.referrer ? this.parseReferrer(event.referrer) : 'Direkt'; + referrerMap.set(referrer, (referrerMap.get(referrer) || 0) + 1); + } + + return Array.from(referrerMap.entries()) + .map(([referrer, count]) => ({ referrer, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 10); + } + + /** + * Extrahiert Domain aus Referrer + */ + private parseReferrer(referrer: string): string { + try { + const url = new URL(referrer); + return url.hostname.replace('www.', ''); + } catch { + return referrer || 'Direkt'; + } + } + + /** + * Geräte-Statistiken (basierend auf User-Agent) + */ + async getDeviceStats(days: number): Promise { + const since = new Date(); + since.setDate(since.getDate() - days); + + const events = await this.eventRepo.find({ + where: { + type: 'pageview', + createdAt: MoreThanOrEqual(since), + }, + select: ['userAgent', 'sessionId'], + }); + + // Unique sessions + const sessionDevices = new Map(); + + for (const event of events) { + if (!sessionDevices.has(event.sessionId)) { + sessionDevices.set(event.sessionId, this.detectDevice(event.userAgent)); + } + } + + const stats: DeviceStatsDto = { desktop: 0, mobile: 0, tablet: 0 }; + + for (const device of sessionDevices.values()) { + if (device === 'mobile') stats.mobile++; + else if (device === 'tablet') stats.tablet++; + else stats.desktop++; + } + + return stats; + } + + /** + * Erkennt Gerätetyp aus User-Agent + */ + private detectDevice(userAgent?: string): 'desktop' | 'mobile' | 'tablet' { + if (!userAgent) return 'desktop'; + + const ua = userAgent.toLowerCase(); + + if (/ipad|tablet|playbook|silk/i.test(ua)) { + return 'tablet'; + } + + if (/mobile|iphone|ipod|android|blackberry|opera mini|iemobile/i.test(ua)) { + return 'mobile'; + } + + return 'desktop'; + } + + /** + * Letzte Events + */ + async getRecentEvents(limit: number): Promise { + const events = await this.eventRepo.find({ + order: { createdAt: 'DESC' }, + take: limit, + select: ['type', 'page', 'createdAt', 'metadata', 'sessionId', 'userAgent', 'screenSize', 'referrer'], + }); + + return events.map(e => ({ + type: e.type, + page: e.page, + timestamp: e.createdAt, + sessionId: e.sessionId, + userAgent: e.userAgent, + screenSize: e.screenSize, + referrer: e.referrer, + metadata: e.metadata, + deviceType: this.detectDevice(e.userAgent), + })); + } + + /** + * Löscht alte Events (Datensparsamkeit) + */ + async cleanupOldEvents(daysToKeep: number = 90): Promise { + const cutoff = new Date(); + cutoff.setDate(cutoff.getDate() - daysToKeep); + + const result = await this.eventRepo + .createQueryBuilder() + .delete() + .where('createdAt < :cutoff', { cutoff }) + .execute(); + + return result.affected || 0; + } + + /** + * DSGVO: Auskunftsrecht - Gibt alle Daten zu einer Session-ID zurück + */ + async getDataBySessionId(sessionId: string): Promise { + return this.eventRepo.find({ + where: { sessionId }, + order: { createdAt: 'DESC' }, + select: ['type', 'page', 'createdAt', 'screenSize', 'userAgent'], + }); + } + + /** + * DSGVO: Löschrecht - Löscht alle Daten zu einer Session-ID + */ + async deleteDataBySessionId(sessionId: string): Promise { + const result = await this.eventRepo + .createQueryBuilder() + .delete() + .where('sessionId = :sessionId', { sessionId }) + .execute(); + + return result.affected || 0; + } +} diff --git a/apps/backend/src/app.controller.ts b/apps/backend/src/app.controller.ts new file mode 100644 index 0000000..2d791f5 --- /dev/null +++ b/apps/backend/src/app.controller.ts @@ -0,0 +1,12 @@ +import { Controller, Get } from '@nestjs/common'; +import { AppService } from './app.service'; + +@Controller() +export class AppController { + constructor(private readonly appService: AppService) { } + + @Get('health') + health() { + return { status: 'ok' }; + } +} diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts new file mode 100644 index 0000000..bbf1d93 --- /dev/null +++ b/apps/backend/src/app.module.ts @@ -0,0 +1,90 @@ +// app.module.ts +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ConfigModule } from '@nestjs/config'; +import { ThrottlerModule } from '@nestjs/throttler'; +import { APP_GUARD } from '@nestjs/core'; +import { ThrottlerBehindProxyGuard } from './guards/throttler-behind-proxy.guard'; +import { UsersModule } from './users/users.module'; +import { ContactRequestsModule } from './contact-requests/contact-requests.module'; +import { ContactRequest } from './contact-requests/contact-requests.entity'; +import { User } from './users/users.entity'; +import { AuthModule } from './auth/auth.module'; +import { EmailModule } from './email/email.module'; +import { BookingModule } from './booking/booking.module'; +import { BookingSlot } from './booking/booking-slots.entity'; +import { Booking } from './booking/bookings.entity'; +import { GoogleCalendarModule } from './booking/google-calendar.module'; +import { NewsletterModule } from './newsletter/newsletter.module'; +import { NewsletterSubscriber } from './newsletter/newsletter.entity'; +import { SettingsModule } from './settings/settings.module'; +import { Settings } from './settings/settings.entity'; +import { FaqModule } from './faq/faq.module'; +import { Faq } from './faq/faq.entity'; +import { ServicesCatalogModule } from './services-catalog/services-catalog.module'; +import { ServiceCategoryEntity, ServiceEntity } from './services-catalog/services-catalog.entity'; +import { InvoicesModule } from './invoices/invoices.module'; +import { Invoice } from './invoices/invoices.entity'; +import { AnalyticsModule } from './analytics/analytics.module'; +import { AnalyticsEvent, AnalyticsDailyStats } from './analytics/analytics.entity'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + envFilePath: process.env.NODE_ENV === 'production' + ? '.env.production' + : '.env.development', + }), + // 🛡️ Global Rate Limiting - DDoS Schutz + ThrottlerModule.forRoot([ + { + name: 'short', + ttl: 1000, // 1 Sekunde + limit: 15, // Max 15 Requests pro Sekunde + }, + { + name: 'medium', + ttl: 10000, // 10 Sekunden + limit: 80, // Max 80 Requests pro 10 Sekunden + }, + { + name: 'long', + ttl: 60000, // 1 Minute + limit: 300, // Max 300 Requests pro Minute + }, + ]), + TypeOrmModule.forRoot({ + type: 'postgres', + host: process.env.DB_HOST ?? 'localhost', + port: parseInt(process.env.DB_PORT ?? '5432', 10), + username: process.env.DB_USER ?? 'app', + password: process.env.DB_PASS ?? 'secret', + database: process.env.DB_NAME ?? 'appdb', + entities: [User, ContactRequest, BookingSlot, Booking, NewsletterSubscriber, Settings, Faq, ServiceCategoryEntity, ServiceEntity, Invoice, AnalyticsEvent, AnalyticsDailyStats], + synchronize: true, + logging: process.env.NODE_ENV === 'development', + ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false, + }), + UsersModule, + AuthModule, + ContactRequestsModule, + EmailModule, + BookingModule, + GoogleCalendarModule, + NewsletterModule, + SettingsModule, + FaqModule, + ServicesCatalogModule, + InvoicesModule, + AnalyticsModule, + ], + providers: [ + // 🛡️ Global Rate Limit Guard mit Proxy-Support und Headers + { + provide: APP_GUARD, + useClass: ThrottlerBehindProxyGuard, + }, + ], +}) +export class AppModule { } diff --git a/apps/backend/src/app.service.ts b/apps/backend/src/app.service.ts new file mode 100644 index 0000000..927d7cc --- /dev/null +++ b/apps/backend/src/app.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AppService { + getHello(): string { + return 'Hello World!'; + } +} diff --git a/apps/backend/src/auth/auth.controller.ts b/apps/backend/src/auth/auth.controller.ts new file mode 100644 index 0000000..cb460a4 --- /dev/null +++ b/apps/backend/src/auth/auth.controller.ts @@ -0,0 +1,52 @@ +import { Controller, Post, Body, Get, UseGuards, Request } from '@nestjs/common'; +import { Throttle, SkipThrottle } from '@nestjs/throttler'; +import { AuthService } from './auth.service'; +import { JwtAuthGuard } from './guards/jwt-auth.guard'; +import { IsEmail, IsString, MinLength } from 'class-validator'; + +export class RegisterDto { + @IsEmail() + email: string; + + @IsString() + @MinLength(2) + name: string; + + @IsString() + @MinLength(8) + password: string; +} + +export class LoginDto { + @IsEmail() + email: string; + + @IsString() + password: string; +} + +@Controller('auth') +@Throttle({ default: { limit: 20, ttl: 60000 } }) // 🛡️ Basis: 20 Requests/Minute +export class AuthController { + constructor(private authService: AuthService) {} + + // 🛡️ STRENG: 3 Versuche pro Minute (Spam-Schutz) + @Throttle({ default: { limit: 3, ttl: 60000 } }) + @Post('register') + async register(@Body() dto: RegisterDto) { + return this.authService.register(dto.email, dto.name, dto.password); + } + + // 🛡️ STRENG: 5 Versuche pro Minute (Brute-Force Schutz) + @Throttle({ default: { limit: 5, ttl: 60000 } }) + @Post('login') + async login(@Body() dto: LoginDto) { + return this.authService.login(dto.email, dto.password); + } + + @Get('me') + @UseGuards(JwtAuthGuard) + async getProfile(@Request() req) { + return req.user; + } +} \ No newline at end of file diff --git a/apps/backend/src/auth/auth.module.ts b/apps/backend/src/auth/auth.module.ts new file mode 100644 index 0000000..9af10d4 --- /dev/null +++ b/apps/backend/src/auth/auth.module.ts @@ -0,0 +1,37 @@ +import { Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { AuthService } from './auth.service'; +import { AuthController } from './auth.controller'; +import { JwtStrategy } from './strategies/jwt.strategy'; +import { User } from 'src/users/users.entity'; +import { GoogleAuthController } from 'src/booking/google-auth.controller'; + + +@Module({ + imports: [ + TypeOrmModule.forFeature([User]), + PassportModule, + // 🛡️ JWT Secret aus Umgebungsvariable laden (KEIN Fallback!) + JwtModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (configService: ConfigService) => { + const secret = configService.get('JWT_SECRET'); + if (!secret) { + throw new Error('❌ JWT_SECRET Umgebungsvariable ist nicht gesetzt! Die Anwendung kann nicht sicher starten.'); + } + return { + secret, + signOptions: { expiresIn: '7d' }, + }; + }, + }), + ], + controllers: [AuthController, GoogleAuthController], + providers: [AuthService, JwtStrategy], + exports: [AuthService], +}) +export class AuthModule { } \ No newline at end of file diff --git a/apps/backend/src/auth/auth.service.ts b/apps/backend/src/auth/auth.service.ts new file mode 100644 index 0000000..ee8e86c --- /dev/null +++ b/apps/backend/src/auth/auth.service.ts @@ -0,0 +1,122 @@ +import { Injectable, UnauthorizedException, ConflictException } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import * as bcrypt from 'bcrypt'; +import { UserRole, User } from 'src/users/users.entity'; + + +export interface JwtPayload { + sub: string; + email: string; + role: UserRole; + createdAt: Date; +} + +export interface LoginResponse { + access_token: string; + user: { + id: string; + email: string; + name: string; + role: UserRole; + createdAt: Date; + }; +} + +@Injectable() +export class AuthService { + constructor( + @InjectRepository(User) + private userRepo: Repository, + private jwtService: JwtService, + ) { } + + async register(email: string, name: string, password: string): Promise { + // 1) Eingaben prüfen + if (!email || !password) { + throw new UnauthorizedException('Email and password are required'); + } + email = email.trim().toLowerCase(); + + // 2) Existenz prüfen (bei CITEXT ist das case-insensitive) + const existing = await this.userRepo.findOne({ where: { email } }); + if (existing) { + throw new ConflictException('Email already exists'); + } + + // 3) Passwort hashen + const hashedPassword = await bcrypt.hash(password, 10); + + // 4) User speichern + const user = this.userRepo.create({ + email, + name, + password: hashedPassword, + role: UserRole.USER, + }); + const savedUser = await this.userRepo.save(user); + + // 5) Token zurückgeben + return this.generateToken(savedUser); + } + + async login(email: string, password: string): Promise { + // 1) Eingaben prüfen + if (!email || !password) { + throw new UnauthorizedException('Invalid credentials'); + } + email = email.trim().toLowerCase(); + + // 2) User inkl. Passwort laden (wichtig!) + // Funktioniert egal ob @Column({ select: false }) gesetzt ist oder nicht + const user = await this.userRepo + .createQueryBuilder('user') + .addSelect('user.password') + .where('LOWER(user.email) = :email', { email }) // falls Spalte kein CITEXT ist + .getOne(); + + if (!user || !user.password) { + throw new UnauthorizedException('Invalid credentials'); + } + + // 3) Passwort prüfen + const ok = await bcrypt.compare(password, user.password); + if (!ok) { + throw new UnauthorizedException('Invalid credentials'); + } + + // 4) Token + return this.generateToken(user); + } + + async validateUser(userId: string): Promise { + return this.userRepo.findOne({ + where: { id: userId }, + select: ['id', 'email', 'name', 'role', 'wantsNewsletter', 'isVerified', 'createdAt'] + }); + } + + // In auth.service.ts - generateToken() + private generateToken(user: User): LoginResponse { + const payload: JwtPayload = { + sub: user.id, + email: user.email, + role: user.role, + createdAt: user.createdAt, + }; + + const token = this.jwtService.sign(payload); + + return { + access_token: token, + user: { + id: user.id, + email: user.email, + name: user.name, + role: user.role, + createdAt: user.createdAt, + }, + }; + } +} \ No newline at end of file diff --git a/apps/backend/src/auth/decorators/current-user.decorator.ts b/apps/backend/src/auth/decorators/current-user.decorator.ts new file mode 100644 index 0000000..342fec0 --- /dev/null +++ b/apps/backend/src/auth/decorators/current-user.decorator.ts @@ -0,0 +1,8 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +export const CurrentUser = createParamDecorator( + (data: unknown, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + return request.user; + }, +); \ No newline at end of file diff --git a/apps/backend/src/auth/guards/admin.guard.ts b/apps/backend/src/auth/guards/admin.guard.ts new file mode 100644 index 0000000..7e96389 --- /dev/null +++ b/apps/backend/src/auth/guards/admin.guard.ts @@ -0,0 +1,20 @@ +import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common'; +import { UserRole } from 'src/users/users.entity'; + +@Injectable() +export class AdminGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest(); + const user = request.user; + + if (!user) { + throw new ForbiddenException('User not authenticated'); + } + + if (user.role !== UserRole.ADMIN) { + throw new ForbiddenException('Admin access required'); + } + + return true; + } +} \ No newline at end of file diff --git a/apps/backend/src/auth/guards/jwt-auth.guard.ts b/apps/backend/src/auth/guards/jwt-auth.guard.ts new file mode 100644 index 0000000..18588a5 --- /dev/null +++ b/apps/backend/src/auth/guards/jwt-auth.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') {} \ No newline at end of file diff --git a/apps/backend/src/auth/strategies/jwt.strategy.ts b/apps/backend/src/auth/strategies/jwt.strategy.ts new file mode 100644 index 0000000..e6cf63b --- /dev/null +++ b/apps/backend/src/auth/strategies/jwt.strategy.ts @@ -0,0 +1,36 @@ +// auth/strategies/jwt.strategy.ts +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { AuthService } from '../auth.service'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor( + private authService: AuthService, + private configService: ConfigService + ) { + // 🛡️ JWT Secret aus Umgebungsvariable - KEIN Fallback! + const secret = configService.get('JWT_SECRET'); + if (!secret) { + throw new Error('❌ JWT_SECRET Umgebungsvariable fehlt!'); + } + + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: secret, + }); + } + + async validate(payload: any) { + const user = await this.authService.validateUser(payload.sub); + + if (!user) { + throw new UnauthorizedException('Benutzer nicht gefunden'); + } + + return user; + } +} \ No newline at end of file diff --git a/apps/backend/src/booking/booking-slots.entity.ts b/apps/backend/src/booking/booking-slots.entity.ts new file mode 100644 index 0000000..170b4de --- /dev/null +++ b/apps/backend/src/booking/booking-slots.entity.ts @@ -0,0 +1,39 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm'; + +@Entity('booking_slots') +export class BookingSlot { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Index() + @Column({ type: 'date' }) + date: string; // Format: YYYY-MM-DD + + @Column({ type: 'time' }) + timeFrom: string; // Format: HH:MM + + @Column({ type: 'time' }) + timeTo: string; // Format: HH:MM + + @Index() + @Column({ default: true }) + isAvailable: boolean; + + @Column({ type: 'int', default: 1 }) + maxBookings: number; // Wie viele Buchungen parallel möglich + + @Column({ type: 'int', default: 0 }) + currentBookings: number; // Aktuelle Anzahl Buchungen + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @Column({ nullable: true }) + googleEventId?: string; + + @Column({ nullable: true }) + meetLink?: string; +} \ No newline at end of file diff --git a/apps/backend/src/booking/booking.controller.ts b/apps/backend/src/booking/booking.controller.ts new file mode 100644 index 0000000..eefbe1f --- /dev/null +++ b/apps/backend/src/booking/booking.controller.ts @@ -0,0 +1,105 @@ +import { Controller, Get, Post, Body, Param, Patch, Delete, UseGuards, Query } from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; +import { BookingService } from './booking.service'; +import { CreateBookingSlotDto, UpdateBookingSlotDto, CreateBookingDto, UpdateBookingDto } from './booking.dto'; +import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'; +import { AdminGuard } from 'src/auth/guards/admin.guard'; + +@Controller('bookings') +@Throttle({ default: { limit: 30, ttl: 60000 } }) // 🛡️ Basis: 30 Requests/Minute +export class BookingController { + constructor(private readonly bookingService: BookingService) {} + + // ==================== SLOTS ==================== + + // PUBLIC: Verfügbare Slots abrufen + @Get('slots/available') + async getAvailableSlots(@Query('fromDate') fromDate?: string) { + return this.bookingService.getAvailableSlots(fromDate); + } + + // PUBLIC: Slots für ein bestimmtes Datum + @Get('slots/date/:date') + async getSlotsByDate(@Param('date') date: string) { + return this.bookingService.getSlotsByDate(date); + } + + // ADMIN: Alle Slots abrufen + @UseGuards(JwtAuthGuard, AdminGuard) + @Get('slots') + async getAllSlots() { + return this.bookingService.getAllSlots(); + } + + // ADMIN: Einzelnen Slot erstellen + @UseGuards(JwtAuthGuard, AdminGuard) + @Post('slots') + async createSlot(@Body() dto: CreateBookingSlotDto) { + return this.bookingService.createSlot(dto); + } + + // ADMIN: Mehrere Slots auf einmal erstellen + @UseGuards(JwtAuthGuard, AdminGuard) + @Post('slots/bulk') + async createMultipleSlots(@Body() dto: { slots: CreateBookingSlotDto[] }) { + return this.bookingService.createMultipleSlots(dto.slots); + } + + // ADMIN: Slot aktualisieren + @UseGuards(JwtAuthGuard, AdminGuard) + @Patch('slots/:id') + async updateSlot(@Param('id') id: string, @Body() dto: UpdateBookingSlotDto) { + return this.bookingService.updateSlot(id, dto); + } + + // ADMIN: Slot löschen + @UseGuards(JwtAuthGuard, AdminGuard) + @Delete('slots/:id') + async deleteSlot(@Param('id') id: string) { + return this.bookingService.deleteSlot(id); + } + + // ==================== BOOKINGS ==================== + + // 🛡️ Öffentlich - STRENG: 5 Buchungen pro Stunde pro IP + @Throttle({ default: { limit: 5, ttl: 3600000 } }) + @Post() + async createBooking(@Body() dto: CreateBookingDto) { + return this.bookingService.createBooking(dto); + } + + // ADMIN: Alle Bookings abrufen + @UseGuards(JwtAuthGuard, AdminGuard) + @Get() + async getAllBookings() { + return this.bookingService.getAllBookings(); + } + + // ADMIN: Einzelne Booking abrufen + @UseGuards(JwtAuthGuard, AdminGuard) + @Get(':id') + async getBookingById(@Param('id') id: string) { + return this.bookingService.getBookingById(id); + } + + // ADMIN: Booking aktualisieren + @UseGuards(JwtAuthGuard, AdminGuard) + @Patch(':id') + async updateBooking(@Param('id') id: string, @Body() dto: UpdateBookingDto) { + return this.bookingService.updateBooking(id, dto); + } + + // ADMIN: Booking stornieren + @UseGuards(JwtAuthGuard, AdminGuard) + @Patch(':id/cancel') + async cancelBooking(@Param('id') id: string) { + return this.bookingService.cancelBooking(id); + } + + // ADMIN: Booking löschen + @UseGuards(JwtAuthGuard, AdminGuard) + @Delete(':id') + async deleteBooking(@Param('id') id: string) { + return this.bookingService.deleteBooking(id); + } +} \ No newline at end of file diff --git a/apps/backend/src/booking/booking.dto.ts b/apps/backend/src/booking/booking.dto.ts new file mode 100644 index 0000000..b425679 --- /dev/null +++ b/apps/backend/src/booking/booking.dto.ts @@ -0,0 +1,80 @@ +import { IsString, IsEmail, IsOptional, IsDateString, IsBoolean, IsInt, Min, Max, Matches, IsEnum, IsUUID } from 'class-validator'; +import { BookingStatus } from './bookings.entity'; + +export class CreateBookingSlotDto { + @IsDateString() + date: string; // YYYY-MM-DD + + @Matches(/^([0-1][0-9]|2[0-3]):[0-5][0-9]$/, { + message: 'timeFrom must be in format HH:MM', + }) + timeFrom: string; + + @Matches(/^([0-1][0-9]|2[0-3]):[0-5][0-9]$/, { + message: 'timeTo must be in format HH:MM', + }) + timeTo: string; + + @IsInt() + @Min(1) + @Max(10) + @IsOptional() + maxBookings?: number; + + @IsBoolean() + @IsOptional() + isAvailable?: boolean; +} + +export class UpdateBookingSlotDto { + @IsDateString() + @IsOptional() + date?: string; + + @Matches(/^([0-1][0-9]|2[0-3]):[0-5][0-9]$/) + @IsOptional() + timeFrom?: string; + + @Matches(/^([0-1][0-9]|2[0-3]):[0-5][0-9]$/) + @IsOptional() + timeTo?: string; + + @IsBoolean() + @IsOptional() + isAvailable?: boolean; + + @IsInt() + @Min(1) + @IsOptional() + maxBookings?: number; +} + +export class CreateBookingDto { + @IsString() + @IsOptional() + name: string; + + @IsEmail() + email: string; + + @IsString() + @IsOptional() + phone?: string; + + @IsString() + @IsOptional() + message?: string; + + @IsUUID() + slotId: string; +} + +export class UpdateBookingDto { + @IsEnum(BookingStatus) + @IsOptional() + status?: BookingStatus; + + @IsString() + @IsOptional() + adminNotes?: string; +} \ No newline at end of file diff --git a/apps/backend/src/booking/booking.module.ts b/apps/backend/src/booking/booking.module.ts new file mode 100644 index 0000000..39b7ba8 --- /dev/null +++ b/apps/backend/src/booking/booking.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { BookingService } from './booking.service'; +import { BookingController } from './booking.controller'; +import { BookingSlot } from './booking-slots.entity'; +import { Booking } from './bookings.entity'; +import { EmailModule } from 'src/email/email.module'; +import { GoogleCalendarModule } from './google-calendar.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([BookingSlot, Booking]), + EmailModule, + GoogleCalendarModule, + ], + providers: [BookingService], + controllers: [BookingController], + exports: [BookingService], +}) +export class BookingModule {} \ No newline at end of file diff --git a/apps/backend/src/booking/booking.service.ts b/apps/backend/src/booking/booking.service.ts new file mode 100644 index 0000000..f120369 --- /dev/null +++ b/apps/backend/src/booking/booking.service.ts @@ -0,0 +1,282 @@ +import { Injectable, NotFoundException, BadRequestException, ConflictException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, MoreThanOrEqual } from 'typeorm'; +import { BookingSlot } from './booking-slots.entity'; +import { Booking, BookingStatus } from './bookings.entity'; +import { CreateBookingSlotDto, UpdateBookingSlotDto, CreateBookingDto, UpdateBookingDto } from './booking.dto'; +import { EmailService } from 'src/email/email.service'; +import { ConfigService } from '@nestjs/config'; +import { GoogleCalendarService } from './google-calendar.service'; + +@Injectable() +export class BookingService { + constructor( + @InjectRepository(BookingSlot) + private readonly slotRepo: Repository, + @InjectRepository(Booking) + private readonly bookingRepo: Repository, + private readonly emailService: EmailService, + private readonly configService: ConfigService, + private readonly googleCalendarService: GoogleCalendarService, + ) { } + + // ==================== SLOTS (ADMIN) ==================== + + async createSlot(dto: CreateBookingSlotDto): Promise { + // Validierung: timeFrom < timeTo + if (dto.timeFrom >= dto.timeTo) { + throw new BadRequestException('timeFrom must be before timeTo'); + } + + const slot = this.slotRepo.create({ + ...dto, + maxBookings: dto.maxBookings || 1, + isAvailable: dto.isAvailable !== false, + }); + + return this.slotRepo.save(slot); + } + + async createMultipleSlots(slots: CreateBookingSlotDto[]): Promise { + const created = slots.map(dto => { + if (dto.timeFrom >= dto.timeTo) { + throw new BadRequestException(`Invalid time range for ${dto.date} ${dto.timeFrom}-${dto.timeTo}`); + } + return this.slotRepo.create({ + ...dto, + maxBookings: dto.maxBookings || 1, + isAvailable: dto.isAvailable !== false, + }); + }); + + return this.slotRepo.save(created); + } + + async getAllSlots(): Promise { + return this.slotRepo.find({ + order: { date: 'ASC', timeFrom: 'ASC' }, + }); + } + + toLocalYMD(date = new Date()): string { + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, '0'); + const d = String(date.getDate()).padStart(2, '0'); + return `${y}-${m}-${d}`; + } + + async getAvailableSlots(fromDate?: string): Promise { + const today = fromDate || this.toLocalYMD(); // statt new Date().toISOString().split('T')[0] + return this.slotRepo.find({ + where: { date: MoreThanOrEqual(today), isAvailable: true }, + order: { date: 'ASC', timeFrom: 'ASC' }, + }); + } + + + async getSlotsByDate(date: string): Promise { + return this.slotRepo.find({ + where: { date }, + order: { timeFrom: 'ASC' }, + }); + } + + async updateSlot(id: string, dto: UpdateBookingSlotDto): Promise { + const slot = await this.slotRepo.findOne({ where: { id } }); + if (!slot) { + throw new NotFoundException(`Slot with ID ${id} not found`); + } + + if (dto.timeFrom && dto.timeTo && dto.timeFrom >= dto.timeTo) { + throw new BadRequestException('timeFrom must be before timeTo'); + } + + Object.assign(slot, dto); + return this.slotRepo.save(slot); + } + + async deleteSlot(id: string): Promise { + const result = await this.slotRepo.delete(id); + if (result.affected === 0) { + throw new NotFoundException(`Slot with ID ${id} not found`); + } + } + + // ==================== BOOKINGS ==================== + + async createBooking(dto: CreateBookingDto): Promise { + const slot = await this.slotRepo.findOne({ where: { id: dto.slotId } }); + + if (!slot) { + throw new NotFoundException('Slot not found'); + } + + if (!slot.isAvailable) { + throw new ConflictException('This slot is not available'); + } + + if (slot.currentBookings >= slot.maxBookings) { + throw new ConflictException('This slot is fully booked'); + } + + // Booking erstellen + const booking = this.bookingRepo.create(dto); + const saved = await this.bookingRepo.save(booking); + + // Google Meet erstellen + try { + // DateTime richtig formatieren + const startDateTime = `${slot.date}T${slot.timeFrom}+02:00`; + const endDateTime = `${slot.date}T${slot.timeTo}+02:00`; + + console.log('🕐 DateTime Debug:'); + console.log(' slot.date:', slot.date); + console.log(' slot.timeFrom:', slot.timeFrom); + console.log(' slot.timeTo:', slot.timeTo); + console.log(' startDateTime:', startDateTime); + console.log(' endDateTime:', endDateTime); + + const meetData = await this.googleCalendarService.createMeeting({ + summary: `Beratungsgespräch LeonardsMedia - ${booking.name}`, + description: booking.message || 'Beratungstermin', + startDateTime, + endDateTime, + attendees: [booking.email], + }); + + // Meet Link im Slot speichern + slot.googleEventId = meetData.eventId; + slot.meetLink = meetData.meetLink; + + console.log('✅ Meet Link gespeichert:', slot.meetLink); + } catch (error) { + // Fehler loggen, aber Buchung nicht abbrechen + console.error('❌ Google Meet konnte nicht erstellt werden:', error); + console.error(' Error Details:', error.message); + // Booking wird trotzdem gespeichert, nur ohne Meet Link + } + + // Slot-Counter erhöhen + slot.currentBookings += 1; + if (slot.currentBookings >= slot.maxBookings) { + slot.isAvailable = false; + } + await this.slotRepo.save(slot); + + // Emails senden (mit Meet Link) + await this.sendBookingEmails(saved, slot); + + return saved; + } + + private async sendBookingEmails(booking: Booking, slot: BookingSlot): Promise { + const adminEmail = this.configService.get('ADMIN_EMAIL'); + + // Email an Customer + await this.emailService.sendBookingConfirmation({ + to: booking.email, + customerName: booking.name, + date: slot.date, + timeFrom: slot.timeFrom, + timeTo: slot.timeTo, + bookingId: booking.id, + meetLink: slot.meetLink, + }); + + // Email an Admin + if (adminEmail) { + await this.emailService.sendBookingNotificationToAdmin({ + to: adminEmail, + customerName: booking.name, + customerEmail: booking.email, + customerPhone: booking.phone, + message: booking.message, + date: slot.date, + timeFrom: slot.timeFrom, + timeTo: slot.timeTo, + bookingId: booking.id, + meetLink: slot.meetLink, // NEU! + }); + } + } + + async getAllBookings(): Promise { + return this.bookingRepo.find({ + relations: ['slot'], + order: { createdAt: 'DESC' }, + }); + } + + async getBookingById(id: string): Promise { + const booking = await this.bookingRepo.findOne({ + where: { id }, + relations: ['slot'], + }); + + if (!booking) { + throw new NotFoundException(`Booking with ID ${id} not found`); + } + + return booking; + } + + async updateBooking(id: string, dto: UpdateBookingDto): Promise { + const booking = await this.getBookingById(id); + Object.assign(booking, dto); + return this.bookingRepo.save(booking); + } + + async cancelBooking(id: string): Promise { + const booking = await this.getBookingById(id); + + if (booking.status === BookingStatus.CANCELLED) { + throw new BadRequestException('Booking is already cancelled'); + } + + // Slot freigeben + const slot = await this.slotRepo.findOne({ where: { id: booking.slotId } }); + if (slot) { + // Google Meet löschen + if (slot.googleEventId) { + try { + await this.googleCalendarService.deleteMeeting(slot.googleEventId); + slot.googleEventId = null; + slot.meetLink = null; + } catch (error) { + console.error('Google Meet konnte nicht gelöscht werden:', error); + } + } + + slot.currentBookings = Math.max(0, slot.currentBookings - 1); + slot.isAvailable = true; + await this.slotRepo.save(slot); + } + + booking.status = BookingStatus.CANCELLED; + return this.bookingRepo.save(booking); + } + + async deleteBooking(id: string): Promise { + const booking = await this.getBookingById(id); + + // Slot freigeben + const slot = await this.slotRepo.findOne({ where: { id: booking.slotId } }); + if (slot) { + // Google Meet löschen + if (slot.googleEventId) { + try { + await this.googleCalendarService.deleteMeeting(slot.googleEventId); + } catch (error) { + console.error('Google Meet konnte nicht gelöscht werden:', error); + } + } + + slot.currentBookings = Math.max(0, slot.currentBookings - 1); + slot.isAvailable = true; + await this.slotRepo.save(slot); + } + + await this.bookingRepo.delete(id); + } + +} \ No newline at end of file diff --git a/apps/backend/src/booking/bookings.entity.ts b/apps/backend/src/booking/bookings.entity.ts new file mode 100644 index 0000000..1be8569 --- /dev/null +++ b/apps/backend/src/booking/bookings.entity.ts @@ -0,0 +1,48 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn, Index } from 'typeorm'; +import { BookingSlot } from './booking-slots.entity'; + +export enum BookingStatus { + PENDING = 'pending', + CONFIRMED = 'confirmed', + CANCELLED = 'cancelled', + COMPLETED = 'completed', +} + +@Entity('bookings') +export class Booking { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + name: string; + + @Column() + email: string; + + @Column({ nullable: true }) + phone: string | null; + + @Column({ type: 'text', nullable: true }) + message: string | null; + + @Column('uuid') + slotId: string; + + @ManyToOne(() => BookingSlot, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'slotId' }) + slot: BookingSlot; + + @Index() + @Column({ + type: 'enum', + enum: BookingStatus, + default: BookingStatus.PENDING, + }) + status: BookingStatus; + + @Column({ type: 'text', nullable: true }) + adminNotes: string | null; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; +} \ No newline at end of file diff --git a/apps/backend/src/booking/google-auth.controller.ts b/apps/backend/src/booking/google-auth.controller.ts new file mode 100644 index 0000000..0db0aaa --- /dev/null +++ b/apps/backend/src/booking/google-auth.controller.ts @@ -0,0 +1,114 @@ +// src/auth/google-auth.controller.ts +import { Controller, Get, Query, Res, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { google } from 'googleapis'; +import { Response } from 'express'; + +@Controller('auth/google') +export class GoogleAuthController { + private readonly logger = new Logger(GoogleAuthController.name); + + constructor(private configService: ConfigService) {} + + @Get('login') + async googleLogin(@Res() res: Response) { + const oauth2Client = new google.auth.OAuth2( + this.configService.get('GOOGLE_CLIENT_ID'), + this.configService.get('GOOGLE_CLIENT_SECRET'), + 'http://localhost:3000/auth/google/callback', + ); + + const url = oauth2Client.generateAuthUrl({ + access_type: 'offline', + scope: ['https://www.googleapis.com/auth/calendar'], + prompt: 'consent', + }); + + this.logger.log('🔗 Redirecting to Google OAuth...'); + res.redirect(url); + } + + @Get('callback') + async googleCallback(@Query('code') code: string, @Res() res: Response) { + if (!code) { + return res.send('❌ Kein Code erhalten. Bitte versuche es erneut.'); + } + + try { + const oauth2Client = new google.auth.OAuth2( + this.configService.get('GOOGLE_CLIENT_ID'), + this.configService.get('GOOGLE_CLIENT_SECRET'), + 'http://localhost:3000/auth/google/callback', + ); + + const { tokens } = await oauth2Client.getToken(code); + + this.logger.log('✅ Tokens erfolgreich erhalten!'); + this.logger.log(`Refresh Token: ${tokens.refresh_token}`); + + return res.send(` + + + Google OAuth Erfolgreich + + + +
+
✅ Erfolgreich authentifiziert!
+

Dein Refresh Token:

+
${tokens.refresh_token}
+ +
+ 📝 Nächste Schritte: +
    +
  1. Kopiere den obigen Refresh Token
  2. +
  3. Füge ihn in deine .env Datei ein:
  4. +
  5. GOOGLE_REFRESH_TOKEN=${tokens.refresh_token}
  6. +
  7. Starte deinen Server neu
  8. +
  9. Du kannst diesen Controller jetzt löschen!
  10. +
+
+
+ + + `); + } catch (error) { + this.logger.error('❌ Fehler beim Token-Austausch:', error); + return res.send(`❌ Fehler: ${error.message}`); + } + } +} \ No newline at end of file diff --git a/apps/backend/src/booking/google-calendar.module.ts b/apps/backend/src/booking/google-calendar.module.ts new file mode 100644 index 0000000..5a885bd --- /dev/null +++ b/apps/backend/src/booking/google-calendar.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { GoogleCalendarService } from './google-calendar.service'; + +@Module({ + providers: [GoogleCalendarService], + exports: [GoogleCalendarService], +}) +export class GoogleCalendarModule {} \ No newline at end of file diff --git a/apps/backend/src/booking/google-calendar.service.ts b/apps/backend/src/booking/google-calendar.service.ts new file mode 100644 index 0000000..d7e8e33 --- /dev/null +++ b/apps/backend/src/booking/google-calendar.service.ts @@ -0,0 +1,170 @@ +// src/google-calendar/google-calendar.service.ts +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { google } from 'googleapis'; +import { OAuth2Client } from 'google-auth-library'; + +@Injectable() +export class GoogleCalendarService { + private readonly logger = new Logger(GoogleCalendarService.name); + private oauth2Client: OAuth2Client; + private calendar; + + constructor(private configService: ConfigService) { + this.oauth2Client = new google.auth.OAuth2( + this.configService.get('GOOGLE_CLIENT_ID'), + this.configService.get('GOOGLE_CLIENT_SECRET'), + this.configService.get('GOOGLE_REDIRECT_URI'), + ); + + this.oauth2Client.setCredentials({ + refresh_token: this.configService.get('GOOGLE_REFRESH_TOKEN'), + }); + + this.calendar = google.calendar({ version: 'v3', auth: this.oauth2Client }); + } + + // google-calendar.service.ts + async createMeeting(data: { + summary: string; + description?: string; + startDateTime: string; + endDateTime: string; + attendees: string[]; + }): Promise<{ + eventId: string; + meetLink: string; + htmlLink: string; + }> { + try { + this.logger.log('🔄 Versuche Google Meet zu erstellen...'); + this.logger.log(` Summary: ${data.summary}`); + this.logger.log(` Start: ${data.startDateTime}`); + this.logger.log(` End: ${data.endDateTime}`); + this.logger.log(` Attendees: ${JSON.stringify(data.attendees)}`); + + const event = { + summary: data.summary, + description: data.description || 'Beratungsgespräch', + start: { + dateTime: data.startDateTime, + timeZone: 'Europe/Berlin', + }, + end: { + dateTime: data.endDateTime, + timeZone: 'Europe/Berlin', + }, + attendees: data.attendees.map(email => ({ email })), + conferenceData: { + createRequest: { + requestId: `meet-${Date.now()}`, + conferenceSolutionKey: { type: 'hangoutsMeet' }, + }, + }, + reminders: { + useDefault: false, + overrides: [ + { method: 'email', minutes: 24 * 60 }, + { method: 'popup', minutes: 30 }, + ], + }, + }; + + this.logger.log('📤 Sende Event an Google Calendar...'); + this.logger.log(` Event: ${JSON.stringify(event, null, 2)}`); + + const response = await this.calendar.events.insert({ + calendarId: 'primary', + requestBody: event, + conferenceDataVersion: 1, + sendUpdates: 'all', + }); + + const meetLink = response.data.conferenceData?.entryPoints?.[0]?.uri || ''; + + this.logger.log(`✅ Google Meet erstellt: ${meetLink}`); + + return { + eventId: response.data.id, + meetLink, + htmlLink: response.data.htmlLink, + }; + } catch (error) { + this.logger.error('❌ Fehler beim Erstellen des Google Meets:'); + this.logger.error(` Message: ${error.message}`); + this.logger.error(` Status: ${error.code}`); + this.logger.error(` Response: ${JSON.stringify(error.response?.data, null, 2)}`); + this.logger.error(` Full Error: ${JSON.stringify(error, null, 2)}`); + throw new Error(`Google Meet konnte nicht erstellt werden: ${error.message}`); + } + } + + async updateMeeting(eventId: string, data: { + summary?: string; + description?: string; + startDateTime?: string; + endDateTime?: string; + attendees?: string[]; + }): Promise { + try { + const event: any = {}; + + if (data.summary) event.summary = data.summary; + if (data.description) event.description = data.description; + if (data.startDateTime) { + event.start = { + dateTime: data.startDateTime, + timeZone: 'Europe/Berlin', + }; + } + if (data.endDateTime) { + event.end = { + dateTime: data.endDateTime, + timeZone: 'Europe/Berlin', + }; + } + if (data.attendees) { + event.attendees = data.attendees.map(email => ({ email })); + } + + await this.calendar.events.patch({ + calendarId: 'primary', + eventId, + requestBody: event, + sendUpdates: 'all', + }); + + this.logger.log(`✅ Google Meet aktualisiert: ${eventId}`); + } catch (error) { + this.logger.error('❌ Fehler beim Aktualisieren des Google Meets:', error); + throw new Error(`Google Meet konnte nicht aktualisiert werden: ${error.message}`); + } + } + + async deleteMeeting(eventId: string): Promise { + try { + await this.calendar.events.delete({ + calendarId: 'primary', + eventId, + sendUpdates: 'all', + }); + this.logger.log(`🗑️ Google Meet gelöscht: ${eventId}`); + } catch (error) { + this.logger.error('❌ Fehler beim Löschen des Google Meets:', error); + // Nicht werfen, da Meeting vielleicht schon gelöscht wurde + } + } + + async getMeeting(eventId: string): Promise { + try { + const response = await this.calendar.events.get({ + calendarId: 'primary', + eventId, + }); + return response.data; + } catch (error) { + this.logger.error('❌ Fehler beim Abrufen des Google Meets:', error); + return null; + } + } +} \ No newline at end of file diff --git a/apps/backend/src/contact-requests/contact-requests.controller.ts b/apps/backend/src/contact-requests/contact-requests.controller.ts new file mode 100644 index 0000000..9e8f0d8 --- /dev/null +++ b/apps/backend/src/contact-requests/contact-requests.controller.ts @@ -0,0 +1,58 @@ +import { Controller, Get, Post, Body, Param, Patch, Delete, UseGuards, HttpCode, HttpStatus } from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; +import { ContactRequestsService } from './contact-requests.service'; +import { CreateContactRequestDto, UpdateContactRequestDto } from './contact-requests.dto'; +import { AdminGuard } from 'src/auth/guards/admin.guard'; +import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'; + +@Controller('contact-requests') +@Throttle({ default: { limit: 30, ttl: 60000 } }) // 🛡️ Basis: 30 Requests/Minute für alle Endpoints +export class ContactRequestsController { + constructor(private readonly contactService: ContactRequestsService) {} + + // 🛡️ Öffentlich - STRENG: 5 Anfragen pro Stunde pro IP (Spam-Schutz) + @Throttle({ default: { limit: 5, ttl: 3600000 } }) + @Post() + @HttpCode(HttpStatus.CREATED) + async create(@Body() dto: CreateContactRequestDto) { + return this.contactService.create(dto); + } + + // NUR ADMIN + @UseGuards(JwtAuthGuard, AdminGuard) + @Get() + async findAll() { + return this.contactService.findAll(); + } + + @UseGuards(JwtAuthGuard, AdminGuard) + @Get('unprocessed') + async findUnprocessed() { + return this.contactService.findUnprocessed(); + } + + @UseGuards(JwtAuthGuard, AdminGuard) + @Get(':id') + async findOne(@Param('id') id: string) { + return this.contactService.findOne(id); + } + + @UseGuards(JwtAuthGuard, AdminGuard) + @Patch(':id') + async update(@Param('id') id: string, @Body() dto: UpdateContactRequestDto) { + return this.contactService.update(id, dto); + } + + @UseGuards(JwtAuthGuard, AdminGuard) + @Patch(':id/process') + async markAsProcessed(@Param('id') id: string) { + return this.contactService.markAsProcessed(id); + } + + @UseGuards(JwtAuthGuard, AdminGuard) + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + async delete(@Param('id') id: string) { + return this.contactService.delete(id); + } +} \ No newline at end of file diff --git a/apps/backend/src/contact-requests/contact-requests.dto.ts b/apps/backend/src/contact-requests/contact-requests.dto.ts new file mode 100644 index 0000000..1ad71b4 --- /dev/null +++ b/apps/backend/src/contact-requests/contact-requests.dto.ts @@ -0,0 +1,42 @@ +import { IsEmail, IsString, IsBoolean, IsOptional, MinLength, MaxLength } from 'class-validator'; + +export class CreateContactRequestDto { + @IsString() + @MinLength(2) + @MaxLength(100) + name: string; + + @IsEmail() + email: string; + + @IsString() + @MinLength(1) + @MaxLength(100) + serviceType: string; // Slug aus dem Services-Katalog + + @IsString() + @MaxLength(2000) + message: string; + + @IsBoolean() + @IsOptional() + prefersCallback?: boolean; + + @IsString() + @IsOptional() + phoneNumber?: string; + + @IsString() + @IsOptional() + userId?: string; // Falls der User eingeloggt ist +} + +export class UpdateContactRequestDto { + @IsBoolean() + @IsOptional() + isProcessed?: boolean; + + @IsString() + @IsOptional() + notes?: string; +} \ No newline at end of file diff --git a/apps/backend/src/contact-requests/contact-requests.entity.ts b/apps/backend/src/contact-requests/contact-requests.entity.ts new file mode 100644 index 0000000..aff14df --- /dev/null +++ b/apps/backend/src/contact-requests/contact-requests.entity.ts @@ -0,0 +1,52 @@ +import { User } from 'src/users/users.entity'; +import { + Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, + JoinColumn, Index +} from 'typeorm'; + +@Entity('contact_requests') +export class ContactRequest { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + name: string; + + // In PG per Migration auf CITEXT umstellen (case-insensitive Unique/Filter möglich) + @Column() + email: string; + + // Service-Slug aus dem Services-Katalog (dynamisch) + @Column({ default: 'allgemeine-anfrage' }) + serviceType: string; + + @Column('text') + message: string; + + @Index() + @Column({ default: false }) + prefersCallback: boolean; + + @Column({ nullable: true }) + phoneNumber: string | null; + + @Index() + @Column({ default: false }) + isProcessed: boolean; + + @Column({ type: 'text', nullable: true }) + notes: string | null; + + @Column('uuid', { nullable: true }) + userId: string | null; + + @ManyToOne(() => User, user => user.contactRequests, { + nullable: true, + onDelete: 'SET NULL', + }) + @JoinColumn({ name: 'userId' }) + user: User | null; + + @CreateDateColumn({ type: 'timestamptz', default: () => 'now()' }) + createdAt: Date; +} diff --git a/apps/backend/src/contact-requests/contact-requests.module.ts b/apps/backend/src/contact-requests/contact-requests.module.ts new file mode 100644 index 0000000..8722af9 --- /dev/null +++ b/apps/backend/src/contact-requests/contact-requests.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { ContactRequestsService } from './contact-requests.service'; +import { ContactRequestsController } from './contact-requests.controller'; +import { ContactRequest } from './contact-requests.entity'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { EmailModule } from 'src/email/email.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ContactRequest]), + EmailModule + ], + providers: [ContactRequestsService], + exports: [ContactRequestsService], + controllers: [ContactRequestsController], +}) +export class ContactRequestsModule {} diff --git a/apps/backend/src/contact-requests/contact-requests.service.ts b/apps/backend/src/contact-requests/contact-requests.service.ts new file mode 100644 index 0000000..3f95158 --- /dev/null +++ b/apps/backend/src/contact-requests/contact-requests.service.ts @@ -0,0 +1,83 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { CreateContactRequestDto, UpdateContactRequestDto } from './contact-requests.dto'; +import { ContactRequest } from './contact-requests.entity'; +import { EmailService } from 'src/email/email.service'; + +@Injectable() +export class ContactRequestsService { + constructor( + @InjectRepository(ContactRequest) + private readonly contactRepo: Repository, + private readonly emailService: EmailService + ) { } + + async create(dto: CreateContactRequestDto): Promise { + const request = this.contactRepo.create(dto); + const saved = await this.contactRepo.save(request); + + // Danke-Email an den User schicken + await this.emailService.sendContactRequestConfirmation({ + userEmail: dto.email, + userName: dto.name, + serviceType: dto.serviceType, + }); + + // Admin-Email an den Admin schicken + await this.emailService.sendContactRequestConfirmationAdmin({ + userEmail: dto.email, + userName: dto.name, + serviceType: dto.serviceType, + message: dto.message, + phoneNumber: dto.phoneNumber, + prefersCallback: dto.prefersCallback, + }); + + return saved; + } + + async findAll(): Promise { + return this.contactRepo.find({ + order: { createdAt: 'DESC' }, + relations: ['user'], + }); + } + + async findUnprocessed(): Promise { + return this.contactRepo.find({ + where: { isProcessed: false }, + order: { createdAt: 'DESC' }, + }); + } + + async findOne(id: string): Promise { + const request = await this.contactRepo.findOne({ + where: { id }, + relations: ['user'], + }); + + if (!request) { + throw new NotFoundException(`Contact request with ID ${id} not found`); + } + + return request; + } + + async update(id: string, dto: UpdateContactRequestDto): Promise { + const request = await this.findOne(id); + Object.assign(request, dto); + return this.contactRepo.save(request); + } + + async markAsProcessed(id: string): Promise { + return this.update(id, { isProcessed: true }); + } + + async delete(id: string): Promise { + const result = await this.contactRepo.delete(id); + if (result.affected === 0) { + throw new NotFoundException(`Contact request with ID ${id} not found`); + } + } +} \ No newline at end of file diff --git a/apps/backend/src/email/email.module.ts b/apps/backend/src/email/email.module.ts new file mode 100644 index 0000000..ddaa8da --- /dev/null +++ b/apps/backend/src/email/email.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { EmailService } from './email.service'; + +@Module({ + providers: [EmailService], + exports: [EmailService], + +}) +export class EmailModule {} \ No newline at end of file diff --git a/apps/backend/src/email/email.service.ts b/apps/backend/src/email/email.service.ts new file mode 100644 index 0000000..148fe9b --- /dev/null +++ b/apps/backend/src/email/email.service.ts @@ -0,0 +1,452 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import emailjs from '@emailjs/nodejs'; + +// Toggle hier: true = wirklich senden, false = nur console.log +const SEND_REAL_EMAILS = true; + +interface EmailParams extends Record { + to_email: string; + subject: string; + company_name: string; + greeting: string; + customer_name: string; + message: string; + highlight_message?: string; + button_url?: string; + button_text?: string; + company_email: string; + company_website: string; + footer_note?: string; +} + +@Injectable() +export class EmailService { + private readonly logger = new Logger(EmailService.name); + private readonly serviceId: string; + private readonly templateId: string; + private readonly publicKey: string; + private readonly privateKey: string; + + constructor(private configService: ConfigService) { + this.serviceId = this.configService.get('EMAILJS_SERVICE_ID'); + this.templateId = this.configService.get('EMAILJS_TEMPLATE_ID'); + this.publicKey = this.configService.get('EMAILJS_PUBLIC_KEY'); + this.privateKey = this.configService.get('EMAILJS_PRIVATE_KEY'); + + if (!SEND_REAL_EMAILS) { + this.logger.warn('⚠️ EMAIL SERVICE IM MOCK MODE - Emails werden nur geloggt!'); + } + } + + formatTimeFromHHMMSStoHHMM(time: string): string { + const [hours, minutes] = time.split(':'); + return `${hours}:${minutes}`; + } + + async sendEmail(params: EmailParams): Promise { + if (!SEND_REAL_EMAILS) { + // Nur Console-Logging + this.logger.log('📧 [MOCK] Email würde gesendet werden:'); + this.logger.log(` An: ${params.to_email}`); + this.logger.log(` Betreff: ${params.subject}`); + this.logger.log(` Kunde: ${params.customer_name}`); + this.logger.log(` Firma: ${params.company_name}`); + this.logger.log(` Nachricht: ${params.message}`); + if (params.highlight_message) { + this.logger.log(` Highlight: ${params.highlight_message}`); + } + if (params.button_url) { + this.logger.log(` Button: ${params.button_text} -> ${params.button_url}`); + } + this.logger.log(` Template: ${this.templateId}`); + return; + } + + // Echtes Senden via EmailJS + try { + const response = await emailjs.send( + this.serviceId, + this.templateId, + params, + { + publicKey: this.publicKey, + privateKey: this.privateKey, + } + ); + + this.logger.log(`✅ Email erfolgreich gesendet: ${response.status} ${response.text}`); + } catch (error) { + this.logger.error('❌ Fehler beim Email-Versand:', error); + throw new Error(`Email konnte nicht gesendet werden: ${error.message}`); + } + } + + async sendContactRequestConfirmation(data: { + userEmail: string; + userName: string; + serviceType: string; + }): Promise { + if (!SEND_REAL_EMAILS) { + this.logger.log('Email would be sent to: ' + JSON.stringify(data)); + return; + } + const emailParams: EmailParams = { + to_email: data.userEmail, + subject: 'Vielen Dank für Ihre Anfrage!', + company_name: this.configService.get('COMPANY_NAME', 'LeonardsMedia'), + greeting: 'Hallo', + customer_name: data.userName, + message: `Vielen Dank für Ihre Anfrage! + + Wir haben Ihre Nachricht erhalten und freuen uns über Ihr Interesse. Unser Team wird Ihre Anfrage prüfen und sich schnellstmöglich bei Ihnen melden. + + In der Zwischenzeit können Sie gerne unsere Website besuchen oder uns direkt kontaktieren, falls Sie weitere Fragen haben.`, + highlight_message: '🎉 Wir melden uns innerhalb von 24 Stunden bei Ihnen!', + button_url: this.configService.get('COMPANY_WEBSITE'), + button_text: 'Zur Website', + company_email: this.configService.get('COMPANY_EMAIL'), + company_website: this.configService.get('COMPANY_WEBSITE'), + footer_note: 'Bei Rückfragen stehen wir Ihnen jederzeit zur Verfügung.', + }; + + await this.sendEmail(emailParams); + } + + async sendContactRequestConfirmationAdmin(data: { + userEmail: string; + userName: string; + serviceType: string; + message: string; + phoneNumber?: string; + prefersCallback: boolean; + }): Promise { + if (!SEND_REAL_EMAILS) { + this.logger.log('📧 [MOCK] Admin Contact Request Email würde gesendet werden:'); + this.logger.log(` Kunde: ${data.userName} (${data.userEmail})`); + this.logger.log(` Service: ${data.serviceType}`); + this.logger.log(` Rückruf: ${data.prefersCallback ? 'Ja' : 'Nein'}`); + return; + } + + const adminEmail = this.configService.get('ADMIN_EMAIL'); + + const emailParams: EmailParams = { + to_email: adminEmail, + subject: '🔔 Neue Kontaktanfrage', + company_name: this.configService.get('COMPANY_NAME', 'LeonardsMedia'), + greeting: 'Hey', + customer_name: 'Admin', + message: data.message, + highlight_message: '📋 Neue Anfrage eingegangen', + button_url: `mailto:${data.userEmail}`, + button_text: '📧 Kunde antworten', + company_email: this.configService.get('COMPANY_EMAIL'), + company_website: this.configService.get('COMPANY_WEBSITE'), + footer_note: data.prefersCallback && data.phoneNumber + ? `⚠️ Kunde bevorzugt Rückruf: ${data.phoneNumber}` + : undefined, + }; + + await this.sendEmail(emailParams); + } + + async sendWebsiteGenerationComplete(data: { + userEmail: string; + userName: string; + projectName: string; + websiteId: string; + typeOfWebsite: string; + }): Promise { + + if (!SEND_REAL_EMAILS) { + this.logger.log('Email would be sent to: ' + JSON.stringify(data)); + return; + } + const previewUrl = `${this.configService.get('FRONTEND_URL')}/preview/${data.websiteId}`; + + + const websiteTypeNames: Record = { + 'praesentation': 'Präsentations-Website', + 'landing': 'Landing Page', + 'event': 'Event-Website' + }; + + const websiteTypeName = websiteTypeNames[data.typeOfWebsite] || 'Website'; + + const emailParams: EmailParams = { + to_email: data.userEmail, + subject: `🎉 Ihre Website "${data.projectName}" ist fertig!`, + company_name: this.configService.get('COMPANY_NAME', 'LeonardsMedia'), + greeting: 'Hallo', + customer_name: data.userName, + message: `Großartige Neuigkeiten! Ihre ${websiteTypeName} "${data.projectName}" wurde erfolgreich generiert und steht nun zur Vorschau bereit. + +Sie können Ihre neue Website jetzt ansehen, testen und bei Bedarf Anpassungen vornehmen. Klicken Sie einfach auf den Button unten, um direkt zur Vorschau zu gelangen. + +Wir hoffen, dass das Ergebnis Ihren Erwartungen entspricht. Bei Fragen oder Änderungswünschen stehen wir Ihnen gerne zur Verfügung.`, + highlight_message: '✨ Ihre Website ist jetzt online und bereit zur Ansicht!', + button_url: previewUrl, + button_text: 'Website-Vorschau öffnen', + company_email: this.configService.get('COMPANY_EMAIL'), + company_website: this.configService.get('COMPANY_WEBSITE'), + footer_note: 'Sie können die Vorschau jederzeit über Ihr Dashboard aufrufen.', + }; + + await this.sendEmail(emailParams); + } + + async sendWebsiteReadyEmail(data: { + to: string; + projectName: string; + pageId: string; + previewUrl: string; + }): Promise { + if (!SEND_REAL_EMAILS) { + this.logger.log('📧 [MOCK] Website Ready Email würde gesendet werden:'); + this.logger.log(` An: ${data.to}`); + this.logger.log(` Projekt: ${data.projectName}`); + this.logger.log(` Page ID: ${data.pageId}`); + this.logger.log(` Preview URL: ${data.previewUrl}`); + return; + } + + const emailParams: EmailParams = { + to_email: data.to, + subject: `🎉 Deine Website "${data.projectName}" ist fertig!`, + company_name: this.configService.get('COMPANY_NAME', 'LeonardsMedia'), + greeting: 'Hey', + customer_name: '', // optional: aus Email extrahieren + message: `Großartige Neuigkeiten! Dein Website-Projekt "${data.projectName}" wurde erfolgreich mit KI generiert und ist jetzt bereit zur Vorschau. + +Die KI hat eine moderne, einzigartige Website basierend auf deinen Vorgaben erstellt. Du kannst sie jetzt ansehen, testen und bei Bedarf weitere Anpassungen vornehmen. + +Klicke einfach auf den Button unten, um direkt zur Vorschau zu gelangen und deine neue Website zu erleben!`, + highlight_message: '✨ Deine Website ist fertig und wartet auf dich!', + button_url: data.previewUrl, + button_text: '🚀 Website jetzt ansehen', + company_email: this.configService.get('COMPANY_EMAIL'), + company_website: this.configService.get('COMPANY_WEBSITE'), + footer_note: 'Du kannst die Vorschau jederzeit über dein Dashboard aufrufen.', + }; + + await this.sendEmail(emailParams); + } + + async sendWebsiteErrorEmail(data: { + to: string; + projectName: string; + error: string; + }): Promise { + if (!SEND_REAL_EMAILS) { + this.logger.log('📧 [MOCK] Website Error Email würde gesendet werden:'); + this.logger.log(` An: ${data.to}`); + this.logger.log(` Projekt: ${data.projectName}`); + this.logger.log(` Fehler: ${data.error}`); + return; + } + + const retryUrl = `${this.configService.get('FRONTEND_URL')}/preview-form`; + + const emailParams: EmailParams = { + to_email: data.to, + subject: `⚠️ Problem bei der Erstellung von "${data.projectName}"`, + company_name: this.configService.get('COMPANY_NAME', 'LeonardsMedia'), + greeting: 'Hallo', + customer_name: '', + message: `Leider ist bei der Generierung deiner Website "${data.projectName}" ein Problem aufgetreten. + +Fehlerdetails: ${data.error} + +Das kann verschiedene Gründe haben: +• Temporäre technische Schwierigkeiten +• Ungewöhnliche Eingabedaten +• Überlastung des KI-Systems + +Wir empfehlen dir: +1. Versuche es in ein paar Minuten erneut +2. Überprüfe deine Eingaben +3. Kontaktiere uns bei wiederholten Problemen + +Wir entschuldigen uns für die Unannehmlichkeiten und helfen dir gerne weiter!`, + highlight_message: '🔧 Keine Sorge – versuch es einfach nochmal oder kontaktiere uns!', + button_url: retryUrl, + button_text: 'Erneut versuchen', + company_email: this.configService.get('COMPANY_EMAIL'), + company_website: this.configService.get('COMPANY_WEBSITE'), + footer_note: 'Bei Fragen stehen wir dir jederzeit zur Verfügung.', + }; + + await this.sendEmail(emailParams); + } + + async sendBookingConfirmation(data: { + to: string; + customerName: string; + date: string; + timeFrom: string; + timeTo: string; + bookingId: string; + meetLink?: string; + }): Promise { + if (!SEND_REAL_EMAILS) { + this.logger.log('📧 [MOCK] Booking Confirmation würde gesendet werden:'); + this.logger.log(` An: ${data.to}`); + this.logger.log(` Meet Link: ${data.meetLink}`); + return; + } + + const formattedDate = new Date(data.date).toLocaleDateString('de-DE', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }); + + const emailParams: EmailParams = { + to_email: data.to, + subject: '✅ Dein Termin wurde gebucht!', + company_name: this.configService.get('COMPANY_NAME', 'LeonardsMedia'), + greeting: 'Hallo', + customer_name: data.customerName, + message: `Perfekt! Dein Termin wurde erfolgreich gebucht. + +Hier sind deine Termin-Details: +📅 ${formattedDate} +🕐 ${this.formatTimeFromHHMMSStoHHMM(data.timeFrom)} - ${this.formatTimeFromHHMMSStoHHMM(data.timeTo)} Uhr +💻 Online per Google Meet`, + highlight_message: '🎉 Wir freuen uns auf das Gespräch mit dir!', + button_url: data.meetLink || `${this.configService.get('FRONTEND_URL')}/booking/${data.bookingId}`, + button_text: data.meetLink ? '🎥 Zum Google Meet' : 'Termin-Details ansehen', + company_email: this.configService.get('COMPANY_EMAIL'), + company_website: this.configService.get('COMPANY_WEBSITE'), + footer_note: 'Falls du den Termin absagen musst, kontaktiere uns bitte rechtzeitig.', + }; + + await this.sendEmail(emailParams); + } + + async sendBookingNotificationToAdmin(data: { + to: string; + customerName: string; + customerEmail: string; + customerPhone: string | null; + message: string | null; + date: string; + timeFrom: string; + timeTo: string; + bookingId: string; + meetLink?: string; + }): Promise { + if (!SEND_REAL_EMAILS) { + this.logger.log('📧 [MOCK] Admin Booking Notification würde gesendet werden:'); + this.logger.log(` An: ${data.to}`); + this.logger.log(` Kunde: ${data.customerName} (${data.customerEmail})`); + this.logger.log(` Termin: ${data.date} ${data.timeFrom}-${data.timeTo}`); + this.logger.log(` Meet Link: ${data.meetLink || 'Nicht vorhanden'}`); + return; + } + + const formattedDate = new Date(data.date).toLocaleDateString('de-DE', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }); + + const emailParams: EmailParams = { + to_email: data.to, + subject: '🔔 Neue Termin-Buchung', + company_name: this.configService.get('COMPANY_NAME', 'LeonardsMedia'), + greeting: 'Hey', + customer_name: 'Admin', + message: `Es gibt eine neue Termin-Buchung! + +Kunden-Details: +👤 Name: ${data.customerName} +📧 Email: ${data.customerEmail} +📞 Telefon: ${data.customerPhone || 'Nicht angegeben'} + +Termin: +📅 ${formattedDate} +🕐 ${this.formatTimeFromHHMMSStoHHMM(data.timeFrom)} - ${this.formatTimeFromHHMMSStoHHMM(data.timeTo)} Uhr +${data.meetLink ? `🔗 Google Meet: ${data.meetLink}` : '⚠️ Kein Meet-Link verfügbar'} + +Nachricht vom Kunden: +${data.message || 'Keine Nachricht hinterlassen'}`, + highlight_message: '📋 Neue Buchung eingegangen', + button_url: data.meetLink || `${this.configService.get('FRONTEND_URL')}/admin/bookings/${data.bookingId}`, + button_text: data.meetLink ? '🎥 Zum Google Meet' : 'Booking verwalten', + company_email: this.configService.get('COMPANY_EMAIL'), + company_website: this.configService.get('COMPANY_WEBSITE'), + footer_note: data.meetLink ? 'Der Meeting-Link wurde auch an den Kunden gesendet.' : undefined, + }; + + await this.sendEmail(emailParams); + } + + // Newsletter abbestellen (PUBLIC) + // DELETE http://localhost:3000/newsletter/unsubscribe?email=user@example.com + async sendNewsletterWelcome(data: { + to: string; + }): Promise { + if (!SEND_REAL_EMAILS) { + this.logger.log('📧 [MOCK] Newsletter Welcome Email würde gesendet werden:'); + this.logger.log(` An: ${data.to}`); + return; + } + + const emailParams: EmailParams = { + to_email: data.to, + subject: '✅ Anmeldung bestätigt', + company_name: this.configService.get('COMPANY_NAME', 'LeonardsMedia'), + greeting: 'Hallo', + customer_name: '', + message: `Danke für deine Anmeldung! + +Du erhältst ab sofort Updates zu neuen Projekten, Features und Angeboten. + +Wir melden uns bald mit spannenden News!`, + highlight_message: '📬 Du bist jetzt dabei!', + button_url: this.configService.get('COMPANY_WEBSITE'), + button_text: 'Zur Website', + company_email: this.configService.get('COMPANY_EMAIL'), + company_website: this.configService.get('COMPANY_WEBSITE'), + footer_note: 'Du kannst dich jederzeit wieder abmelden.', + }; + + await this.sendEmail(emailParams); + } + + async sendNewsletterUnsubscribe(data: { + to: string; + }): Promise { + if (!SEND_REAL_EMAILS) { + this.logger.log('📧 [MOCK] Newsletter Unsubscribe Email würde gesendet werden:'); + this.logger.log(` An: ${data.to}`); + return; + } + + const emailParams: EmailParams = { + to_email: data.to, + subject: '👋 Abmeldung bestätigt', + company_name: this.configService.get('COMPANY_NAME', 'LeonardsMedia'), + greeting: 'Hallo', + customer_name: '', + message: `Du wurdest erfolgreich von unseren Updates abgemeldet. + +Schade, dass du gehst! Falls du deine Meinung änderst, kannst du dich jederzeit wieder anmelden. + +Wir wünschen dir alles Gute!`, + highlight_message: '✓ Abmeldung erfolgreich', + button_url: this.configService.get('COMPANY_WEBSITE'), + button_text: 'Zur Website', + company_email: this.configService.get('COMPANY_EMAIL'), + company_website: this.configService.get('COMPANY_WEBSITE'), + footer_note: 'Du erhältst keine weiteren E-Mails von uns.', + }; + + await this.sendEmail(emailParams); + } + +} \ No newline at end of file diff --git a/apps/backend/src/faq/faq.controller.ts b/apps/backend/src/faq/faq.controller.ts new file mode 100644 index 0000000..58985f4 --- /dev/null +++ b/apps/backend/src/faq/faq.controller.ts @@ -0,0 +1,120 @@ +import { + Controller, Get, Post, Body, Param, Patch, Delete, + UseGuards, HttpCode, HttpStatus +} from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; +import { FaqService } from './faq.service'; +import { CreateFaqDto, UpdateFaqDto, BulkImportFaqDto } from './faq.dto'; +import { AdminGuard } from 'src/auth/guards/admin.guard'; +import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'; + +@Controller('faq') +@Throttle({ default: { limit: 60, ttl: 60000 } }) // 🛡️ Basis: 60 Requests/Minute (Read-heavy) +export class FaqController { + constructor(private readonly faqService: FaqService) { } + + // ===== PUBLIC ENDPOINTS ===== + + /** + * Alle veröffentlichten FAQs abrufen (für öffentliche Seite) + */ + @Get() + async findAllPublished() { + return this.faqService.findAllPublished(); + } + + /** + * Ein FAQ per Slug abrufen (öffentlich) + */ + @Get('slug/:slug') + async findBySlug(@Param('slug') slug: string) { + return this.faqService.findBySlugPublic(slug); + } + + // ===== ADMIN ENDPOINTS ===== + + /** + * Alle FAQs abrufen (inkl. unpublished) - Admin only + */ + @UseGuards(JwtAuthGuard, AdminGuard) + @Get('admin/all') + async findAllAdmin() { + return this.faqService.findAll(); + } + + /** + * Export aller FAQs als JSON - Admin only + */ + @UseGuards(JwtAuthGuard, AdminGuard) + @Get('admin/export') + async exportAll() { + return this.faqService.exportAll(); + } + + /** + * Bulk-Import von FAQs (JSON) - Admin only + */ + @UseGuards(JwtAuthGuard, AdminGuard) + @Post('admin/import') + async bulkImport(@Body() dto: BulkImportFaqDto) { + return this.faqService.bulkImport(dto); + } + + /** + * Sortierung aktualisieren - Admin only + */ + @UseGuards(JwtAuthGuard, AdminGuard) + @Patch('admin/sort-order') + async updateSortOrder(@Body() items: { id: string; sortOrder: number }[]) { + await this.faqService.updateSortOrder(items); + return { success: true }; + } + + /** + * Neues FAQ erstellen - Admin only + */ + @UseGuards(JwtAuthGuard, AdminGuard) + @Post('admin') + @HttpCode(HttpStatus.CREATED) + async create(@Body() dto: CreateFaqDto) { + return this.faqService.create(dto); + } + + /** + * Ein FAQ per ID abrufen - Admin only + * WICHTIG: Diese Route muss NACH spezifischen Routen wie /export, /import, /all kommen! + */ + @UseGuards(JwtAuthGuard, AdminGuard) + @Get('admin/:id') + async findOneAdmin(@Param('id') id: string) { + return this.faqService.findOne(id); + } + + /** + * FAQ aktualisieren - Admin only + */ + @UseGuards(JwtAuthGuard, AdminGuard) + @Patch('admin/:id') + async update(@Param('id') id: string, @Body() dto: UpdateFaqDto) { + return this.faqService.update(id, dto); + } + + /** + * FAQ löschen - Admin only + */ + @UseGuards(JwtAuthGuard, AdminGuard) + @Delete('admin/:id') + @HttpCode(HttpStatus.NO_CONTENT) + async delete(@Param('id') id: string) { + return this.faqService.delete(id); + } + + /** + * Publish-Status togglen - Admin only + */ + @UseGuards(JwtAuthGuard, AdminGuard) + @Patch('admin/:id/toggle-publish') + async togglePublish(@Param('id') id: string) { + return this.faqService.togglePublish(id); + } +} diff --git a/apps/backend/src/faq/faq.dto.ts b/apps/backend/src/faq/faq.dto.ts new file mode 100644 index 0000000..b8f7497 --- /dev/null +++ b/apps/backend/src/faq/faq.dto.ts @@ -0,0 +1,119 @@ +import { IsString, IsArray, IsBoolean, IsOptional, IsNumber, MinLength, ArrayMinSize } from 'class-validator'; + +export class CreateFaqDto { + @IsString() + @MinLength(2) + slug: string; + + @IsString() + @MinLength(5) + question: string; + + @IsArray() + @ArrayMinSize(1) + @IsString({ each: true }) + answers: string[]; + + @IsArray() + @IsOptional() + @IsString({ each: true }) + listItems?: string[]; + + @IsNumber() + @IsOptional() + sortOrder?: number; + + @IsBoolean() + @IsOptional() + isPublished?: boolean; + + @IsString() + @IsOptional() + category?: string; +} + +export class UpdateFaqDto { + @IsString() + @MinLength(2) + @IsOptional() + slug?: string; + + @IsString() + @MinLength(5) + @IsOptional() + question?: string; + + @IsArray() + @ArrayMinSize(1) + @IsString({ each: true }) + @IsOptional() + answers?: string[]; + + @IsArray() + @IsOptional() + @IsString({ each: true }) + listItems?: string[]; + + @IsNumber() + @IsOptional() + sortOrder?: number; + + @IsBoolean() + @IsOptional() + isPublished?: boolean; + + @IsString() + @IsOptional() + category?: string; +} + +// DTO für JSON-Import (Array von FAQs) +export class ImportFaqDto { + @IsString() + @MinLength(2) + slug: string; + + @IsString() + @MinLength(5) + question: string; + + @IsArray() + @ArrayMinSize(1) + @IsString({ each: true }) + answers: string[]; + + @IsArray() + @IsOptional() + @IsString({ each: true }) + listItems?: string[]; + + @IsNumber() + @IsOptional() + sortOrder?: number; + + @IsBoolean() + @IsOptional() + isPublished?: boolean; + + @IsString() + @IsOptional() + category?: string; +} + +export class BulkImportFaqDto { + @IsArray() + @ArrayMinSize(1) + faqs: ImportFaqDto[]; + + @IsBoolean() + @IsOptional() + overwriteExisting?: boolean; +} + +// Response für Import-Ergebnis +export class ImportResultDto { + imported: number; + updated: number; + skipped: number; + errors: string[]; +} diff --git a/apps/backend/src/faq/faq.entity.ts b/apps/backend/src/faq/faq.entity.ts new file mode 100644 index 0000000..b16eed6 --- /dev/null +++ b/apps/backend/src/faq/faq.entity.ts @@ -0,0 +1,39 @@ +import { + Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index +} from 'typeorm'; + +@Entity('faqs') +export class Faq { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Index() + @Column({ unique: true }) + slug: string; + + @Column() + question: string; + + @Column('text', { array: true }) + answers: string[]; + + @Column('text', { array: true, nullable: true }) + listItems: string[] | null; + + @Index() + @Column({ default: 0 }) + sortOrder: number; + + @Index() + @Column({ default: true }) + isPublished: boolean; + + @Column({ nullable: true }) + category: string | null; + + @CreateDateColumn({ type: 'timestamptz', default: () => 'now()' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz', default: () => 'now()' }) + updatedAt: Date; +} diff --git a/apps/backend/src/faq/faq.module.ts b/apps/backend/src/faq/faq.module.ts new file mode 100644 index 0000000..24f667f --- /dev/null +++ b/apps/backend/src/faq/faq.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Faq } from './faq.entity'; +import { FaqService } from './faq.service'; +import { FaqController } from './faq.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([Faq])], + controllers: [FaqController], + providers: [FaqService], + exports: [FaqService], +}) +export class FaqModule { } diff --git a/apps/backend/src/faq/faq.service.ts b/apps/backend/src/faq/faq.service.ts new file mode 100644 index 0000000..181a21e --- /dev/null +++ b/apps/backend/src/faq/faq.service.ts @@ -0,0 +1,192 @@ +import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Faq } from './faq.entity'; +import { CreateFaqDto, UpdateFaqDto, BulkImportFaqDto, ImportResultDto } from './faq.dto'; + +@Injectable() +export class FaqService { + constructor( + @InjectRepository(Faq) + private readonly faqRepo: Repository + ) { } + + // ===== PUBLIC ENDPOINTS ===== + + /** + * Gibt alle veröffentlichten FAQs zurück (für öffentliche Seite) + */ + async findAllPublished(): Promise { + return this.faqRepo.find({ + where: { isPublished: true }, + order: { sortOrder: 'ASC', createdAt: 'ASC' }, + }); + } + + /** + * Gibt ein FAQ per Slug zurück (nur wenn published) + */ + async findBySlugPublic(slug: string): Promise { + const faq = await this.faqRepo.findOne({ + where: { slug, isPublished: true }, + }); + + if (!faq) { + throw new NotFoundException(`FAQ with slug "${slug}" not found`); + } + + return faq; + } + + // ===== ADMIN ENDPOINTS ===== + + /** + * Gibt ALLE FAQs zurück (auch unpublished) - für Admin + */ + async findAll(): Promise { + return this.faqRepo.find({ + order: { sortOrder: 'ASC', createdAt: 'DESC' }, + }); + } + + /** + * Gibt ein FAQ per ID zurück - für Admin + */ + async findOne(id: string): Promise { + const faq = await this.faqRepo.findOne({ where: { id } }); + + if (!faq) { + throw new NotFoundException(`FAQ with ID "${id}" not found`); + } + + return faq; + } + + /** + * Erstellt ein neues FAQ + */ + async create(dto: CreateFaqDto): Promise { + // Check if slug already exists + const existing = await this.faqRepo.findOne({ where: { slug: dto.slug } }); + if (existing) { + throw new ConflictException(`FAQ with slug "${dto.slug}" already exists`); + } + + const faq = this.faqRepo.create({ + ...dto, + sortOrder: dto.sortOrder ?? await this.getNextSortOrder(), + }); + + return this.faqRepo.save(faq); + } + + /** + * Aktualisiert ein FAQ + */ + async update(id: string, dto: UpdateFaqDto): Promise { + const faq = await this.findOne(id); + + // Check if new slug conflicts with existing + if (dto.slug && dto.slug !== faq.slug) { + const existing = await this.faqRepo.findOne({ where: { slug: dto.slug } }); + if (existing) { + throw new ConflictException(`FAQ with slug "${dto.slug}" already exists`); + } + } + + Object.assign(faq, dto); + return this.faqRepo.save(faq); + } + + /** + * Löscht ein FAQ + */ + async delete(id: string): Promise { + const result = await this.faqRepo.delete(id); + if (result.affected === 0) { + throw new NotFoundException(`FAQ with ID "${id}" not found`); + } + } + + /** + * Toggle publish status + */ + async togglePublish(id: string): Promise { + const faq = await this.findOne(id); + faq.isPublished = !faq.isPublished; + return this.faqRepo.save(faq); + } + + /** + * Sortierung aktualisieren (Batch) + */ + async updateSortOrder(items: { id: string; sortOrder: number }[]): Promise { + await Promise.all( + items.map(item => + this.faqRepo.update(item.id, { sortOrder: item.sortOrder }) + ) + ); + } + + /** + * JSON Import - Bulk-Import von FAQs + */ + async bulkImport(dto: BulkImportFaqDto): Promise { + const result: ImportResultDto = { + imported: 0, + updated: 0, + skipped: 0, + errors: [], + }; + + for (const faqData of dto.faqs) { + try { + const existing = await this.faqRepo.findOne({ where: { slug: faqData.slug } }); + + if (existing) { + if (dto.overwriteExisting) { + // Update existing + Object.assign(existing, faqData); + await this.faqRepo.save(existing); + result.updated++; + } else { + result.skipped++; + } + } else { + // Create new + const faq = this.faqRepo.create({ + ...faqData, + sortOrder: faqData.sortOrder ?? await this.getNextSortOrder(), + isPublished: faqData.isPublished ?? true, + }); + await this.faqRepo.save(faq); + result.imported++; + } + } catch (error) { + result.errors.push(`Failed to import FAQ "${faqData.slug}": ${error.message}`); + } + } + + return result; + } + + /** + * Export aller FAQs als JSON + */ + async exportAll(): Promise { + return this.faqRepo.find({ + order: { sortOrder: 'ASC' }, + }); + } + + // ===== HELPER ===== + + private async getNextSortOrder(): Promise { + const result = await this.faqRepo + .createQueryBuilder('faq') + .select('MAX(faq.sortOrder)', 'max') + .getRawOne(); + + return (result?.max ?? 0) + 10; + } +} diff --git a/apps/backend/src/guards/throttler-behind-proxy.guard.ts b/apps/backend/src/guards/throttler-behind-proxy.guard.ts new file mode 100644 index 0000000..1d3753c --- /dev/null +++ b/apps/backend/src/guards/throttler-behind-proxy.guard.ts @@ -0,0 +1,61 @@ +import { Injectable, ExecutionContext, HttpException, HttpStatus } from '@nestjs/common'; +import { ThrottlerGuard, ThrottlerLimitDetail } from '@nestjs/throttler'; +import { Response } from 'express'; + +/** + * Custom ThrottlerGuard der: + * 1. Die echte Client-IP hinter Proxies/Load Balancers erkennt + * 2. Bessere Fehlermeldungen liefert + * 3. Retry-After Header setzt + */ +@Injectable() +export class ThrottlerBehindProxyGuard extends ThrottlerGuard { + /** + * Ermittelt die echte Client-IP (auch hinter Reverse Proxies) + */ + protected async getTracker(req: Record): Promise { + // X-Forwarded-For Header prüfen (von Reverse Proxies wie nginx) + const forwardedFor = req.headers?.['x-forwarded-for']; + if (forwardedFor) { + // Kann mehrere IPs enthalten, die erste ist die echte Client-IP + const ips = forwardedFor.split(',').map((ip: string) => ip.trim()); + return ips[0]; + } + + // X-Real-IP Header (alternative Variante) + const realIp = req.headers?.['x-real-ip']; + if (realIp) { + return realIp; + } + + // Fallback auf direkte IP + return req.ip || req.connection?.remoteAddress || 'unknown'; + } + + /** + * Überschreibt throwThrottlingException um Retry-After Header zu setzen + */ + protected async throwThrottlingException( + context: ExecutionContext, + throttlerLimitDetail: ThrottlerLimitDetail, + ): Promise { + const response = context.switchToHttp().getResponse(); + const retryAfterSeconds = Math.ceil(throttlerLimitDetail.ttl / 1000); + + // Setze Retry-After Header (wichtig für Client!) + response.setHeader('Retry-After', retryAfterSeconds.toString()); + response.setHeader('X-RateLimit-Limit', throttlerLimitDetail.limit.toString()); + response.setHeader('X-RateLimit-Remaining', '0'); + response.setHeader('X-RateLimit-Reset', (Date.now() + throttlerLimitDetail.ttl).toString()); + + throw new HttpException( + { + statusCode: HttpStatus.TOO_MANY_REQUESTS, + message: `Zu viele Anfragen. Bitte warte ${retryAfterSeconds} Sekunden.`, + retryAfter: retryAfterSeconds, + }, + HttpStatus.TOO_MANY_REQUESTS, + ); + } +} + diff --git a/apps/backend/src/invoices/invoices.controller.ts b/apps/backend/src/invoices/invoices.controller.ts new file mode 100644 index 0000000..2ca515f --- /dev/null +++ b/apps/backend/src/invoices/invoices.controller.ts @@ -0,0 +1,58 @@ +import { Controller, Get, Post, Put, Patch, Delete, Body, Param, UseGuards } from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; +import { InvoicesService } from './invoices.service'; +import { CreateInvoiceDto, UpdateInvoiceDto, UpdateStatusDto } from './invoices.dto'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { AdminGuard } from '../auth/guards/admin.guard'; + +@Controller('invoices') +@UseGuards(JwtAuthGuard, AdminGuard) +@Throttle({ default: { limit: 30, ttl: 60000 } }) // 🛡️ Basis: 30 Requests/Minute +export class InvoicesController { + constructor(private readonly invoicesService: InvoicesService) {} + + @Get() + findAll() { + return this.invoicesService.findAll(); + } + + @Get('stats') + getStats() { + return this.invoicesService.getStats(); + } + + @Get('generate-number') + generateNumber() { + return this.invoicesService.generateInvoiceNumber(); + } + + @Get(':id') + findOne(@Param('id') id: string) { + return this.invoicesService.findOne(id); + } + + @Post() + create(@Body() createDto: CreateInvoiceDto) { + return this.invoicesService.create(createDto); + } + + @Put(':id') + update(@Param('id') id: string, @Body() updateDto: UpdateInvoiceDto) { + return this.invoicesService.update(id, updateDto); + } + + @Patch(':id/status') + updateStatus(@Param('id') id: string, @Body() statusDto: UpdateStatusDto) { + return this.invoicesService.updateStatus(id, statusDto.status); + } + + @Post(':id/duplicate') + duplicate(@Param('id') id: string) { + return this.invoicesService.duplicate(id); + } + + @Delete(':id') + remove(@Param('id') id: string) { + return this.invoicesService.remove(id); + } +} diff --git a/apps/backend/src/invoices/invoices.dto.ts b/apps/backend/src/invoices/invoices.dto.ts new file mode 100644 index 0000000..d9ab3a6 --- /dev/null +++ b/apps/backend/src/invoices/invoices.dto.ts @@ -0,0 +1,123 @@ +import { IsString, IsEmail, IsOptional, IsNumber, IsArray, ValidateNested, IsEnum, IsDateString } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class InvoiceItemDto { + @IsString() + id: string; + + @IsString() + description: string; + + @IsNumber() + quantity: number; + + @IsString() + unit: string; + + @IsNumber() + unitPrice: number; +} + +export class CreateInvoiceDto { + @IsString() + invoiceNumber: string; + + @IsDateString() + date: string; + + @IsDateString() + dueDate: string; + + @IsEnum(['draft', 'sent', 'paid', 'overdue']) + @IsOptional() + status?: 'draft' | 'sent' | 'paid' | 'overdue'; + + @IsString() + customerName: string; + + @IsEmail() + @IsOptional() + customerEmail?: string; + + @IsString() + @IsOptional() + customerAddress?: string; + + @IsString() + @IsOptional() + customerCity?: string; + + @IsString() + @IsOptional() + customerZip?: string; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => InvoiceItemDto) + items: InvoiceItemDto[]; + + @IsNumber() + @IsOptional() + taxRate?: number; + + @IsString() + @IsOptional() + notes?: string; +} + +export class UpdateInvoiceDto { + @IsString() + @IsOptional() + invoiceNumber?: string; + + @IsDateString() + @IsOptional() + date?: string; + + @IsDateString() + @IsOptional() + dueDate?: string; + + @IsEnum(['draft', 'sent', 'paid', 'overdue']) + @IsOptional() + status?: 'draft' | 'sent' | 'paid' | 'overdue'; + + @IsString() + @IsOptional() + customerName?: string; + + @IsEmail() + @IsOptional() + customerEmail?: string; + + @IsString() + @IsOptional() + customerAddress?: string; + + @IsString() + @IsOptional() + customerCity?: string; + + @IsString() + @IsOptional() + customerZip?: string; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => InvoiceItemDto) + @IsOptional() + items?: InvoiceItemDto[]; + + @IsNumber() + @IsOptional() + taxRate?: number; + + @IsString() + @IsOptional() + notes?: string; +} + +export class UpdateStatusDto { + @IsEnum(['draft', 'sent', 'paid', 'overdue']) + status: 'draft' | 'sent' | 'paid' | 'overdue'; +} diff --git a/apps/backend/src/invoices/invoices.entity.ts b/apps/backend/src/invoices/invoices.entity.ts new file mode 100644 index 0000000..f4c50dc --- /dev/null +++ b/apps/backend/src/invoices/invoices.entity.ts @@ -0,0 +1,64 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; + +@Entity('invoices') +export class Invoice { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ unique: true }) + invoiceNumber: string; + + @Column({ type: 'date' }) + date: string; + + @Column({ type: 'date' }) + dueDate: string; + + @Column({ default: 'draft' }) + status: 'draft' | 'sent' | 'paid' | 'overdue'; + + // Kunde + @Column() + customerName: string; + + @Column({ nullable: true }) + customerEmail: string; + + @Column({ nullable: true }) + customerAddress: string; + + @Column({ nullable: true }) + customerCity: string; + + @Column({ nullable: true }) + customerZip: string; + + // Positionen als JSON + @Column({ type: 'json' }) + items: { + id: string; + description: string; + quantity: number; + unit: string; + unitPrice: number; + }[]; + + @Column({ type: 'decimal', precision: 5, scale: 2, default: 19 }) + taxRate: number; + + @Column({ type: 'text', nullable: true }) + notes: string; + + // Berechnete Werte für einfache Queries + @Column({ type: 'decimal', precision: 10, scale: 2, default: 0 }) + totalNet: number; + + @Column({ type: 'decimal', precision: 10, scale: 2, default: 0 }) + totalGross: number; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} diff --git a/apps/backend/src/invoices/invoices.module.ts b/apps/backend/src/invoices/invoices.module.ts new file mode 100644 index 0000000..91f2e49 --- /dev/null +++ b/apps/backend/src/invoices/invoices.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Invoice } from './invoices.entity'; +import { InvoicesService } from './invoices.service'; +import { InvoicesController } from './invoices.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([Invoice])], + controllers: [InvoicesController], + providers: [InvoicesService], + exports: [InvoicesService], +}) +export class InvoicesModule {} diff --git a/apps/backend/src/invoices/invoices.service.ts b/apps/backend/src/invoices/invoices.service.ts new file mode 100644 index 0000000..43d7527 --- /dev/null +++ b/apps/backend/src/invoices/invoices.service.ts @@ -0,0 +1,135 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Invoice } from './invoices.entity'; +import { CreateInvoiceDto, UpdateInvoiceDto } from './invoices.dto'; + +@Injectable() +export class InvoicesService { + constructor( + @InjectRepository(Invoice) + private invoicesRepository: Repository, + ) {} + + async findAll(): Promise { + return this.invoicesRepository.find({ + order: { createdAt: 'DESC' }, + }); + } + + async findOne(id: string): Promise { + const invoice = await this.invoicesRepository.findOne({ where: { id } }); + if (!invoice) { + throw new NotFoundException(`Invoice with ID ${id} not found`); + } + return invoice; + } + + async create(createDto: CreateInvoiceDto): Promise { + const { totalNet, totalGross } = this.calculateTotals(createDto.items, createDto.taxRate || 19); + + const invoice = this.invoicesRepository.create({ + ...createDto, + status: createDto.status || 'draft', + taxRate: createDto.taxRate || 19, + totalNet, + totalGross, + }); + + return this.invoicesRepository.save(invoice); + } + + async update(id: string, updateDto: UpdateInvoiceDto): Promise { + const invoice = await this.findOne(id); + + // Recalculate totals if items or taxRate changed + const items = updateDto.items || invoice.items; + const taxRate = updateDto.taxRate !== undefined ? updateDto.taxRate : invoice.taxRate; + const { totalNet, totalGross } = this.calculateTotals(items, taxRate); + + Object.assign(invoice, { + ...updateDto, + totalNet, + totalGross, + }); + + return this.invoicesRepository.save(invoice); + } + + async updateStatus(id: string, status: Invoice['status']): Promise { + const invoice = await this.findOne(id); + invoice.status = status; + return this.invoicesRepository.save(invoice); + } + + async remove(id: string): Promise { + const invoice = await this.findOne(id); + await this.invoicesRepository.remove(invoice); + } + + async duplicate(id: string): Promise { + const original = await this.findOne(id); + + // Generate new invoice number + const newNumber = await this.generateInvoiceNumber(); + + const duplicate = this.invoicesRepository.create({ + ...original, + id: undefined, + invoiceNumber: newNumber, + status: 'draft', + date: new Date().toISOString().split('T')[0], + dueDate: this.getDefaultDueDate(), + createdAt: undefined, + updatedAt: undefined, + }); + + return this.invoicesRepository.save(duplicate); + } + + async generateInvoiceNumber(): Promise { + const year = new Date().getFullYear(); + const prefix = `RE-${year}-`; + + const lastInvoice = await this.invoicesRepository + .createQueryBuilder('invoice') + .where('invoice.invoiceNumber LIKE :prefix', { prefix: `${prefix}%` }) + .orderBy('invoice.invoiceNumber', 'DESC') + .getOne(); + + let nextNumber = 1; + if (lastInvoice) { + const lastNum = parseInt(lastInvoice.invoiceNumber.replace(prefix, ''), 10); + nextNumber = lastNum + 1; + } + + return `${prefix}${nextNumber.toString().padStart(4, '0')}`; + } + + async getStats() { + const invoices = await this.findAll(); + + return { + total: invoices.length, + draft: invoices.filter(i => i.status === 'draft').length, + sent: invoices.filter(i => i.status === 'sent').length, + paid: invoices.filter(i => i.status === 'paid').length, + overdue: invoices.filter(i => i.status === 'overdue').length, + totalRevenue: invoices + .filter(i => i.status === 'paid') + .reduce((sum, i) => sum + Number(i.totalGross), 0), + }; + } + + private calculateTotals(items: any[], taxRate: number) { + const totalNet = items.reduce((sum, item) => sum + (item.quantity * item.unitPrice), 0); + const totalGross = totalNet * (1 + taxRate / 100); + return { totalNet, totalGross }; + } + + private getDefaultDueDate(): string { + const date = new Date(); + date.setDate(date.getDate() + 14); + return date.toISOString().split('T')[0]; + } +} diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts new file mode 100644 index 0000000..8afb59d --- /dev/null +++ b/apps/backend/src/main.ts @@ -0,0 +1,55 @@ +import { NestFactory } from '@nestjs/core'; +import { ValidationPipe } from '@nestjs/common'; +import { AppModule } from './app.module'; +import helmet from 'helmet'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + + // 🛡️ Security Headers mit Helmet + app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'"], + scriptSrc: ["'self'"], + imgSrc: ["'self'", "data:", "https:"], + }, + }, + crossOriginEmbedderPolicy: false, // Für iframes falls benötigt + })); + + const isProduction = process.env.NODE_ENV === 'production'; + + app.enableCors({ + origin: isProduction ? true : (origin, cb) => { + const allowList = [ + 'http://localhost:4200', // Dev lokal + 'http://localhost', // Docker Frontend (Port 80) + 'http://localhost:80', // Docker Frontend explizit + 'http://192.168.178.111:4200', // Dein lokales Netzwerk + 'https://leonardsmedia.de', + 'https://www.leonardsmedia.de', + ]; + // Kein Origin = Postman, curl, server-to-server + if (!origin) return cb(null, true); + return allowList.includes(origin) ? cb(null, true) : cb(new Error('CORS'), false); + }, + credentials: true, + methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS', + allowedHeaders: ['Content-Type', 'Authorization', 'X-Consent-Analytics'], + // 🛡️ Diese Header müssen exposed werden damit das Frontend sie lesen kann + exposedHeaders: ['Retry-After', 'X-RateLimit-Limit', 'X-RateLimit-Remaining', 'X-RateLimit-Reset'], + optionsSuccessStatus: 204, + }); + + app.useGlobalPipes(new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + })); + + await app.listen(process.env.PORT || 3000); + console.log('🚀 Backend läuft auf http://localhost:' + (process.env.PORT || 3000)); +} +bootstrap(); \ No newline at end of file diff --git a/apps/backend/src/newsletter/newsletter.controller.ts b/apps/backend/src/newsletter/newsletter.controller.ts new file mode 100644 index 0000000..3574d7e --- /dev/null +++ b/apps/backend/src/newsletter/newsletter.controller.ts @@ -0,0 +1,92 @@ +// src/newsletter/newsletter.controller.ts +import { Controller, Post, Body, Get, UseGuards, Delete, Query, Patch, Param, NotFoundException } from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; +import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'; +import { AdminGuard } from 'src/auth/guards/admin.guard'; +import { NewsletterService } from './newsletter.service'; +import { SubscribeNewsletterDto } from './newsletter.dto'; + +@Controller('newsletter') +@Throttle({ default: { limit: 30, ttl: 60000 } }) // 🛡️ Basis: 30 Requests/Minute +export class NewsletterController { + constructor(private readonly newsletterService: NewsletterService) { } + + // 🛡️ STRENG: 3 Anmeldungen pro Stunde pro IP + @Throttle({ default: { limit: 3, ttl: 3600000 } }) + @Post('subscribe') + async subscribe(@Body() dto: SubscribeNewsletterDto) { + const subscriber = await this.newsletterService.subscribe(dto); + return { + success: true, + message: 'Erfolgreich für den Newsletter angemeldet! Check deine E-Mails.', + email: subscriber.email, + }; + } + + // PUBLIC: Newsletter abbestellen + @Delete('unsubscribe') + async unsubscribe(@Query('email') email: string) { + if (!email) { + return { + success: false, + message: 'E-Mail-Adresse fehlt', + }; + } + + await this.newsletterService.unsubscribe(email); + return { + success: true, + message: 'Erfolgreich vom Newsletter abgemeldet', + }; + } + + // ADMIN: Alle Subscriber abrufen (inkl. inaktive) + @UseGuards(JwtAuthGuard, AdminGuard) + @Get('subscribers') + async getAllSubscribers() { + const subscribers = await this.newsletterService.getAllSubscribersAdmin(); + const stats = await this.newsletterService.getStats(); + return { + ...stats, + subscribers, + }; + } + + // ADMIN: Statistiken + @UseGuards(JwtAuthGuard, AdminGuard) + @Get('stats') + async getStats() { + return this.newsletterService.getStats(); + } + + // ADMIN: Subscriber Status umschalten + @UseGuards(JwtAuthGuard, AdminGuard) + @Patch('subscribers/:id/toggle') + async toggleStatus(@Param('id') id: string) { + try { + const subscriber = await this.newsletterService.toggleSubscriberStatus(id); + return { + success: true, + message: `Status geändert: ${subscriber.isActive ? 'Aktiv' : 'Inaktiv'}`, + subscriber, + }; + } catch (error) { + throw new NotFoundException('Subscriber nicht gefunden'); + } + } + + // ADMIN: Subscriber löschen + @UseGuards(JwtAuthGuard, AdminGuard) + @Delete('subscribers/:id') + async deleteSubscriber(@Param('id') id: string) { + try { + await this.newsletterService.deleteSubscriber(id); + return { + success: true, + message: 'Subscriber wurde gelöscht', + }; + } catch (error) { + throw new NotFoundException('Subscriber nicht gefunden'); + } + } +} \ No newline at end of file diff --git a/apps/backend/src/newsletter/newsletter.dto.ts b/apps/backend/src/newsletter/newsletter.dto.ts new file mode 100644 index 0000000..89e0e37 --- /dev/null +++ b/apps/backend/src/newsletter/newsletter.dto.ts @@ -0,0 +1,8 @@ +// src/newsletter/newsletter.dto.ts +import { IsEmail, IsNotEmpty } from 'class-validator'; + +export class SubscribeNewsletterDto { + @IsEmail({}, { message: 'Bitte gib eine gültige E-Mail-Adresse ein' }) + @IsNotEmpty({ message: 'E-Mail-Adresse ist erforderlich' }) + email: string; +} \ No newline at end of file diff --git a/apps/backend/src/newsletter/newsletter.entity.ts b/apps/backend/src/newsletter/newsletter.entity.ts new file mode 100644 index 0000000..41a1c78 --- /dev/null +++ b/apps/backend/src/newsletter/newsletter.entity.ts @@ -0,0 +1,17 @@ +// src/newsletter/newsletter.entity.ts +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm'; + +@Entity('newsletter_subscribers') +export class NewsletterSubscriber { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ unique: true }) + email: string; + + @Column({ default: true }) + isActive: boolean; + + @CreateDateColumn() + subscribedAt: Date; +} \ No newline at end of file diff --git a/apps/backend/src/newsletter/newsletter.module.ts b/apps/backend/src/newsletter/newsletter.module.ts new file mode 100644 index 0000000..182f7b5 --- /dev/null +++ b/apps/backend/src/newsletter/newsletter.module.ts @@ -0,0 +1,18 @@ +// src/newsletter/newsletter.module.ts +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { NewsletterController } from './newsletter.controller'; +import { NewsletterService } from './newsletter.service'; +import { NewsletterSubscriber } from './newsletter.entity'; +import { EmailModule } from 'src/email/email.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([NewsletterSubscriber]), + EmailModule, + ], + controllers: [NewsletterController], + providers: [NewsletterService], + exports: [NewsletterService], +}) +export class NewsletterModule {} \ No newline at end of file diff --git a/apps/backend/src/newsletter/newsletter.service.ts b/apps/backend/src/newsletter/newsletter.service.ts new file mode 100644 index 0000000..9a12cfe --- /dev/null +++ b/apps/backend/src/newsletter/newsletter.service.ts @@ -0,0 +1,152 @@ +// src/newsletter/newsletter.service.ts +import { Injectable, ConflictException, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { EmailService } from 'src/email/email.service'; +import { NewsletterSubscriber } from './newsletter.entity'; +import { SubscribeNewsletterDto } from './newsletter.dto'; + +@Injectable() +export class NewsletterService { + private readonly logger = new Logger(NewsletterService.name); + + constructor( + @InjectRepository(NewsletterSubscriber) + private readonly subscriberRepo: Repository, + private readonly emailService: EmailService, + ) { } + + async subscribe(dto: SubscribeNewsletterDto): Promise { + const existing = await this.subscriberRepo.findOne({ + where: { email: dto.email.toLowerCase() }, + }); + + if (existing) { + if (existing.isActive) { + throw new ConflictException('Diese E-Mail-Adresse ist bereits für den Newsletter angemeldet'); + } + existing.isActive = true; + await this.subscriberRepo.save(existing); + await this.sendWelcomeEmail(existing.email); + return existing; + } + + const subscriber = this.subscriberRepo.create({ + email: dto.email.toLowerCase(), + }); + + const saved = await this.subscriberRepo.save(subscriber); + + await this.sendWelcomeEmail(saved.email); + + this.logger.log(`✅ Neue Newsletter-Anmeldung: ${saved.email}`); + + return saved; + } + + private async sendWelcomeEmail(email: string): Promise { + try { + await this.emailService.sendNewsletterWelcome({ + to: email, + }); + } catch (error) { + this.logger.error(`❌ Fehler beim Senden der Willkommens-Email an ${email}:`, error); + } + } + + async getAllSubscribers(): Promise { + return this.subscriberRepo.find({ + where: { isActive: true }, + order: { subscribedAt: 'DESC' }, + }); + } + + async unsubscribe(email: string): Promise { + const subscriber = await this.subscriberRepo.findOne({ + where: { email: email.toLowerCase() }, + }); + + if (!subscriber) { + this.logger.warn(`⚠️ Abmelde-Versuch für nicht existierende Email: ${email}`); + // Nicht werfen, damit Angreifer nicht testen können welche Emails existieren + return; + } + + if (!subscriber.isActive) { + this.logger.warn(`⚠️ Email bereits abgemeldet: ${email}`); + return; + } + + subscriber.isActive = false; + await this.subscriberRepo.save(subscriber); + + this.logger.log(`📭 Newsletter-Abmeldung: ${email}`); + + // Optional: Bestätigungs-Email senden + await this.sendUnsubscribeConfirmation(email); + } + + private async sendUnsubscribeConfirmation(email: string): Promise { + try { + await this.emailService.sendNewsletterUnsubscribe({ + to: email, + }); + } catch (error) { + this.logger.error(`❌ Fehler beim Senden der Abmelde-Bestätigung an ${email}:`, error); + // Fehler nicht werfen, Abmeldung ist trotzdem erfolgt + } + } + + async getSubscriberCount(): Promise { + return this.subscriberRepo.count({ + where: { isActive: true }, + }); + } + + /** + * Alle Subscriber holen (inkl. inaktive) - Admin + */ + async getAllSubscribersAdmin(): Promise { + return this.subscriberRepo.find({ + order: { subscribedAt: 'DESC' }, + }); + } + + /** + * Subscriber Status umschalten - Admin + */ + async toggleSubscriberStatus(id: string): Promise { + const subscriber = await this.subscriberRepo.findOne({ where: { id } }); + if (!subscriber) { + throw new Error('Subscriber nicht gefunden'); + } + subscriber.isActive = !subscriber.isActive; + return this.subscriberRepo.save(subscriber); + } + + /** + * Subscriber löschen (permanent) - Admin + */ + async deleteSubscriber(id: string): Promise { + const subscriber = await this.subscriberRepo.findOne({ where: { id } }); + if (!subscriber) { + throw new Error('Subscriber nicht gefunden'); + } + await this.subscriberRepo.remove(subscriber); + this.logger.log(`🗑️ Subscriber gelöscht: ${subscriber.email}`); + } + + /** + * Statistiken - Admin + */ + async getStats(): Promise<{ total: number; active: number; inactive: number }> { + const total = await this.subscriberRepo.count(); + const active = await this.subscriberRepo.count({ where: { isActive: true } }); + return { + total, + active, + inactive: total - active, + }; + } +} \ No newline at end of file diff --git a/apps/backend/src/services-catalog/services-catalog.controller.ts b/apps/backend/src/services-catalog/services-catalog.controller.ts new file mode 100644 index 0000000..045c5ad --- /dev/null +++ b/apps/backend/src/services-catalog/services-catalog.controller.ts @@ -0,0 +1,201 @@ +import { + Controller, Get, Post, Body, Param, Patch, Delete, + UseGuards, HttpCode, HttpStatus +} from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; +import { ServicesCatalogService } from './services-catalog.service'; +import { + CreateServiceCategoryDto, UpdateServiceCategoryDto, + CreateServiceDto, UpdateServiceDto, + BulkImportServicesCatalogDto +} from './services-catalog.dto'; +import { AdminGuard } from 'src/auth/guards/admin.guard'; +import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'; + +@Controller('services-catalog') +@Throttle({ default: { limit: 60, ttl: 60000 } }) // 🛡️ Basis: 60 Requests/Minute (Read-heavy) +export class ServicesCatalogController { + constructor(private readonly catalogService: ServicesCatalogService) { } + + // ===== PUBLIC ENDPOINTS ===== + + /** + * Alle veröffentlichten Kategorien mit Services (für öffentliche Seite) + */ + @Get() + async findAllPublished() { + return this.catalogService.findAllPublished(); + } + + /** + * Kategorie per Slug abrufen (öffentlich) + */ + @Get('category/:slug') + async findCategoryBySlug(@Param('slug') slug: string) { + return this.catalogService.findCategoryBySlug(slug); + } + + /** + * Service per Slug abrufen (öffentlich) + */ + @Get('service/:slug') + async findServiceBySlug(@Param('slug') slug: string) { + return this.catalogService.findServiceBySlug(slug); + } + + // ===== ADMIN ENDPOINTS - EXPORT/IMPORT (vor :id Routen!) ===== + + /** + * Export aller Kategorien und Services als JSON - Admin only + */ + @UseGuards(JwtAuthGuard, AdminGuard) + @Get('admin/export') + async exportAll() { + return this.catalogService.exportAll(); + } + + /** + * Bulk-Import von Kategorien und Services (JSON) - Admin only + */ + @UseGuards(JwtAuthGuard, AdminGuard) + @Post('admin/import') + async bulkImport(@Body() dto: BulkImportServicesCatalogDto) { + return this.catalogService.bulkImport(dto); + } + + // ===== ADMIN ENDPOINTS - CATEGORIES ===== + + /** + * Alle Kategorien abrufen (inkl. unpublished) - Admin only + */ + @UseGuards(JwtAuthGuard, AdminGuard) + @Get('admin/categories') + async findAllCategoriesAdmin() { + return this.catalogService.findAllCategories(); + } + + /** + * Kategorie per ID abrufen - Admin only + */ + @UseGuards(JwtAuthGuard, AdminGuard) + @Get('admin/categories/:id') + async findCategoryByIdAdmin(@Param('id') id: string) { + return this.catalogService.findCategoryById(id); + } + + /** + * Neue Kategorie erstellen - Admin only + */ + @UseGuards(JwtAuthGuard, AdminGuard) + @Post('admin/categories') + @HttpCode(HttpStatus.CREATED) + async createCategory(@Body() dto: CreateServiceCategoryDto) { + return this.catalogService.createCategory(dto); + } + + /** + * Kategorie aktualisieren - Admin only + */ + @UseGuards(JwtAuthGuard, AdminGuard) + @Patch('admin/categories/:id') + async updateCategory(@Param('id') id: string, @Body() dto: UpdateServiceCategoryDto) { + return this.catalogService.updateCategory(id, dto); + } + + /** + * Kategorie löschen - Admin only + */ + @UseGuards(JwtAuthGuard, AdminGuard) + @Delete('admin/categories/:id') + @HttpCode(HttpStatus.NO_CONTENT) + async deleteCategory(@Param('id') id: string) { + return this.catalogService.deleteCategory(id); + } + + /** + * Kategorie Publish-Status togglen - Admin only + */ + @UseGuards(JwtAuthGuard, AdminGuard) + @Patch('admin/categories/:id/toggle-publish') + async toggleCategoryPublish(@Param('id') id: string) { + return this.catalogService.toggleCategoryPublish(id); + } + + /** + * Kategorien-Sortierung aktualisieren - Admin only + */ + @UseGuards(JwtAuthGuard, AdminGuard) + @Patch('admin/categories/sort-order') + async updateCategorySortOrder(@Body() items: { id: string; sortOrder: number }[]) { + await this.catalogService.updateCategorySortOrder(items); + return { success: true }; + } + + // ===== ADMIN ENDPOINTS - SERVICES ===== + + /** + * Alle Services abrufen - Admin only + */ + @UseGuards(JwtAuthGuard, AdminGuard) + @Get('admin/services') + async findAllServicesAdmin() { + return this.catalogService.findAllServices(); + } + + /** + * Service per ID abrufen - Admin only + */ + @UseGuards(JwtAuthGuard, AdminGuard) + @Get('admin/services/:id') + async findServiceByIdAdmin(@Param('id') id: string) { + return this.catalogService.findServiceById(id); + } + + /** + * Neuen Service erstellen - Admin only + */ + @UseGuards(JwtAuthGuard, AdminGuard) + @Post('admin/services') + @HttpCode(HttpStatus.CREATED) + async createService(@Body() dto: CreateServiceDto) { + return this.catalogService.createService(dto); + } + + /** + * Service aktualisieren - Admin only + */ + @UseGuards(JwtAuthGuard, AdminGuard) + @Patch('admin/services/:id') + async updateService(@Param('id') id: string, @Body() dto: UpdateServiceDto) { + return this.catalogService.updateService(id, dto); + } + + /** + * Service löschen - Admin only + */ + @UseGuards(JwtAuthGuard, AdminGuard) + @Delete('admin/services/:id') + @HttpCode(HttpStatus.NO_CONTENT) + async deleteService(@Param('id') id: string) { + return this.catalogService.deleteService(id); + } + + /** + * Service Publish-Status togglen - Admin only + */ + @UseGuards(JwtAuthGuard, AdminGuard) + @Patch('admin/services/:id/toggle-publish') + async toggleServicePublish(@Param('id') id: string) { + return this.catalogService.toggleServicePublish(id); + } + + /** + * Services-Sortierung aktualisieren - Admin only + */ + @UseGuards(JwtAuthGuard, AdminGuard) + @Patch('admin/services/sort-order') + async updateServiceSortOrder(@Body() items: { id: string; sortOrder: number }[]) { + await this.catalogService.updateServiceSortOrder(items); + return { success: true }; + } +} diff --git a/apps/backend/src/services-catalog/services-catalog.dto.ts b/apps/backend/src/services-catalog/services-catalog.dto.ts new file mode 100644 index 0000000..571e49d --- /dev/null +++ b/apps/backend/src/services-catalog/services-catalog.dto.ts @@ -0,0 +1,224 @@ +import { IsString, IsOptional, IsBoolean, IsArray, IsNumber, IsUUID, ValidateNested, IsNotEmpty } from 'class-validator'; +import { Type } from 'class-transformer'; + +// ===== SERVICE CATEGORY DTOs ===== + +export class CreateServiceCategoryDto { + @IsString() + @IsNotEmpty() + slug: string; + + @IsString() + @IsNotEmpty() + name: string; + + @IsString() + @IsNotEmpty() + subtitle: string; + + @IsString() + @IsNotEmpty() + materialIcon: string; + + @IsNumber() + @IsOptional() + sortOrder?: number; + + @IsBoolean() + @IsOptional() + isPublished?: boolean; +} + +export class UpdateServiceCategoryDto { + @IsString() + @IsOptional() + slug?: string; + + @IsString() + @IsOptional() + name?: string; + + @IsString() + @IsOptional() + subtitle?: string; + + @IsString() + @IsOptional() + materialIcon?: string; + + @IsNumber() + @IsOptional() + sortOrder?: number; + + @IsBoolean() + @IsOptional() + isPublished?: boolean; +} + +// ===== SERVICE DTOs ===== + +export class CreateServiceDto { + @IsString() + @IsNotEmpty() + slug: string; + + @IsString() + @IsNotEmpty() + icon: string; + + @IsString() + @IsNotEmpty() + title: string; + + @IsString() + @IsNotEmpty() + description: string; + + @IsString() + @IsNotEmpty() + longDescription: string; + + @IsArray() + @IsString({ each: true }) + tags: string[]; + + @IsString() + keywords: string; + + @IsUUID() + @IsNotEmpty() + categoryId: string; + + @IsNumber() + @IsOptional() + sortOrder?: number; + + @IsBoolean() + @IsOptional() + isPublished?: boolean; +} + +export class UpdateServiceDto { + @IsString() + @IsOptional() + slug?: string; + + @IsString() + @IsOptional() + icon?: string; + + @IsString() + @IsOptional() + title?: string; + + @IsString() + @IsOptional() + description?: string; + + @IsString() + @IsOptional() + longDescription?: string; + + @IsArray() + @IsString({ each: true }) + @IsOptional() + tags?: string[]; + + @IsString() + @IsOptional() + keywords?: string; + + @IsUUID() + @IsOptional() + categoryId?: string; + + @IsNumber() + @IsOptional() + sortOrder?: number; + + @IsBoolean() + @IsOptional() + isPublished?: boolean; +} + +// ===== BULK IMPORT DTOs ===== + +export class ImportServiceDto { + @IsString() + @IsNotEmpty() + slug: string; + + @IsString() + @IsNotEmpty() + icon: string; + + @IsString() + @IsNotEmpty() + title: string; + + @IsString() + @IsNotEmpty() + description: string; + + @IsString() + @IsNotEmpty() + longDescription: string; + + @IsArray() + @IsString({ each: true }) + tags: string[]; + + @IsString() + keywords: string; + + @IsNumber() + @IsOptional() + sortOrder?: number; +} + +export class ImportCategoryDto { + @IsString() + @IsNotEmpty() + slug: string; + + @IsString() + @IsNotEmpty() + name: string; + + @IsString() + @IsNotEmpty() + subtitle: string; + + @IsString() + @IsNotEmpty() + materialIcon: string; + + @IsNumber() + @IsOptional() + sortOrder?: number; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ImportServiceDto) + services: ImportServiceDto[]; +} + +export class BulkImportServicesCatalogDto { + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ImportCategoryDto) + categories: ImportCategoryDto[]; + + @IsBoolean() + @IsOptional() + overwriteExisting?: boolean; +} + +export class ImportResultDto { + success: boolean; + categoriesCreated: number; + categoriesUpdated: number; + servicesCreated: number; + servicesUpdated: number; + errors: string[]; +} diff --git a/apps/backend/src/services-catalog/services-catalog.entity.ts b/apps/backend/src/services-catalog/services-catalog.entity.ts new file mode 100644 index 0000000..261b388 --- /dev/null +++ b/apps/backend/src/services-catalog/services-catalog.entity.ts @@ -0,0 +1,92 @@ +import { + Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index, + OneToMany, ManyToOne, JoinColumn +} from 'typeorm'; + +// ===== SERVICE CATEGORY ENTITY ===== +@Entity('service_categories') +export class ServiceCategoryEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Index() + @Column({ unique: true }) + slug: string; + + @Column() + name: string; + + @Column() + subtitle: string; + + @Column() + materialIcon: string; + + @Index() + @Column({ default: 0 }) + sortOrder: number; + + @Index() + @Column({ default: true }) + isPublished: boolean; + + @OneToMany(() => ServiceEntity, service => service.category) + services: ServiceEntity[]; + + @CreateDateColumn({ type: 'timestamptz', default: () => 'now()' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz', default: () => 'now()' }) + updatedAt: Date; +} + +// ===== SERVICE ENTITY ===== +@Entity('services') +export class ServiceEntity { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Index() + @Column({ unique: true }) + slug: string; + + @Column() + icon: string; + + @Column() + title: string; + + @Column('text') + description: string; + + @Column('text') + longDescription: string; + + @Column('text', { array: true, default: [] }) + tags: string[]; + + @Column('text') + keywords: string; + + @Index() + @Column({ default: 0 }) + sortOrder: number; + + @Index() + @Column({ default: true }) + isPublished: boolean; + + @Index() + @Column('uuid') + categoryId: string; + + @ManyToOne(() => ServiceCategoryEntity, category => category.services, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'categoryId' }) + category: ServiceCategoryEntity; + + @CreateDateColumn({ type: 'timestamptz', default: () => 'now()' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz', default: () => 'now()' }) + updatedAt: Date; +} diff --git a/apps/backend/src/services-catalog/services-catalog.module.ts b/apps/backend/src/services-catalog/services-catalog.module.ts new file mode 100644 index 0000000..d7f1346 --- /dev/null +++ b/apps/backend/src/services-catalog/services-catalog.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ServicesCatalogController } from './services-catalog.controller'; +import { ServicesCatalogService } from './services-catalog.service'; +import { ServiceCategoryEntity, ServiceEntity } from './services-catalog.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ServiceCategoryEntity, ServiceEntity]) + ], + controllers: [ServicesCatalogController], + providers: [ServicesCatalogService], + exports: [ServicesCatalogService] +}) +export class ServicesCatalogModule { } diff --git a/apps/backend/src/services-catalog/services-catalog.service.ts b/apps/backend/src/services-catalog/services-catalog.service.ts new file mode 100644 index 0000000..853c4e1 --- /dev/null +++ b/apps/backend/src/services-catalog/services-catalog.service.ts @@ -0,0 +1,390 @@ +import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { ServiceCategoryEntity, ServiceEntity } from './services-catalog.entity'; +import { + CreateServiceCategoryDto, UpdateServiceCategoryDto, + CreateServiceDto, UpdateServiceDto, + BulkImportServicesCatalogDto, ImportResultDto +} from './services-catalog.dto'; + +@Injectable() +export class ServicesCatalogService { + constructor( + @InjectRepository(ServiceCategoryEntity) + private readonly categoryRepo: Repository, + @InjectRepository(ServiceEntity) + private readonly serviceRepo: Repository, + ) { } + + // ===== CATEGORIES ===== + + /** + * Alle veröffentlichten Kategorien mit Services (öffentlich) + */ + async findAllPublished(): Promise { + return this.categoryRepo.find({ + where: { isPublished: true }, + relations: ['services'], + order: { sortOrder: 'ASC' } + }).then(categories => { + // Filter nur published services und sortieren + return categories.map(cat => ({ + ...cat, + services: cat.services + .filter(s => s.isPublished) + .sort((a, b) => a.sortOrder - b.sortOrder) + })); + }); + } + + /** + * Alle Kategorien (Admin) + */ + async findAllCategories(): Promise { + return this.categoryRepo.find({ + relations: ['services'], + order: { sortOrder: 'ASC' } + }).then(categories => { + return categories.map(cat => ({ + ...cat, + services: cat.services.sort((a, b) => a.sortOrder - b.sortOrder) + })); + }); + } + + /** + * Kategorie per ID finden + */ + async findCategoryById(id: string): Promise { + const category = await this.categoryRepo.findOne({ + where: { id }, + relations: ['services'] + }); + if (!category) { + throw new NotFoundException(`Kategorie mit ID ${id} nicht gefunden`); + } + return category; + } + + /** + * Kategorie per Slug finden (öffentlich) + */ + async findCategoryBySlug(slug: string): Promise { + const category = await this.categoryRepo.findOne({ + where: { slug, isPublished: true }, + relations: ['services'] + }); + if (!category) { + throw new NotFoundException(`Kategorie "${slug}" nicht gefunden`); + } + category.services = category.services + .filter(s => s.isPublished) + .sort((a, b) => a.sortOrder - b.sortOrder); + return category; + } + + /** + * Kategorie erstellen + */ + async createCategory(dto: CreateServiceCategoryDto): Promise { + const existing = await this.categoryRepo.findOne({ where: { slug: dto.slug } }); + if (existing) { + throw new ConflictException(`Kategorie mit Slug "${dto.slug}" existiert bereits`); + } + + const category = this.categoryRepo.create({ + ...dto, + sortOrder: dto.sortOrder ?? await this.getNextCategorySortOrder() + }); + return this.categoryRepo.save(category); + } + + /** + * Kategorie aktualisieren + */ + async updateCategory(id: string, dto: UpdateServiceCategoryDto): Promise { + const category = await this.findCategoryById(id); + + if (dto.slug && dto.slug !== category.slug) { + const existing = await this.categoryRepo.findOne({ where: { slug: dto.slug } }); + if (existing) { + throw new ConflictException(`Kategorie mit Slug "${dto.slug}" existiert bereits`); + } + } + + Object.assign(category, dto); + return this.categoryRepo.save(category); + } + + /** + * Kategorie löschen (löscht auch alle Services) + */ + async deleteCategory(id: string): Promise { + const result = await this.categoryRepo.delete(id); + if (result.affected === 0) { + throw new NotFoundException(`Kategorie mit ID ${id} nicht gefunden`); + } + } + + /** + * Kategorie Publish-Status togglen + */ + async toggleCategoryPublish(id: string): Promise { + const category = await this.findCategoryById(id); + category.isPublished = !category.isPublished; + return this.categoryRepo.save(category); + } + + // ===== SERVICES ===== + + /** + * Alle Services (Admin) + */ + async findAllServices(): Promise { + return this.serviceRepo.find({ + relations: ['category'], + order: { sortOrder: 'ASC' } + }); + } + + /** + * Service per ID finden + */ + async findServiceById(id: string): Promise { + const service = await this.serviceRepo.findOne({ + where: { id }, + relations: ['category'] + }); + if (!service) { + throw new NotFoundException(`Service mit ID ${id} nicht gefunden`); + } + return service; + } + + /** + * Service per Slug finden (öffentlich) + */ + async findServiceBySlug(slug: string): Promise { + const service = await this.serviceRepo.findOne({ + where: { slug, isPublished: true }, + relations: ['category'] + }); + if (!service) { + throw new NotFoundException(`Service "${slug}" nicht gefunden`); + } + return service; + } + + /** + * Service erstellen + */ + async createService(dto: CreateServiceDto): Promise { + const existing = await this.serviceRepo.findOne({ where: { slug: dto.slug } }); + if (existing) { + throw new ConflictException(`Service mit Slug "${dto.slug}" existiert bereits`); + } + + // Prüfen ob Kategorie existiert + await this.findCategoryById(dto.categoryId); + + const service = this.serviceRepo.create({ + ...dto, + sortOrder: dto.sortOrder ?? await this.getNextServiceSortOrder(dto.categoryId) + }); + return this.serviceRepo.save(service); + } + + /** + * Service aktualisieren + */ + async updateService(id: string, dto: UpdateServiceDto): Promise { + const service = await this.findServiceById(id); + + if (dto.slug && dto.slug !== service.slug) { + const existing = await this.serviceRepo.findOne({ where: { slug: dto.slug } }); + if (existing) { + throw new ConflictException(`Service mit Slug "${dto.slug}" existiert bereits`); + } + } + + if (dto.categoryId) { + await this.findCategoryById(dto.categoryId); + } + + Object.assign(service, dto); + return this.serviceRepo.save(service); + } + + /** + * Service löschen + */ + async deleteService(id: string): Promise { + const result = await this.serviceRepo.delete(id); + if (result.affected === 0) { + throw new NotFoundException(`Service mit ID ${id} nicht gefunden`); + } + } + + /** + * Service Publish-Status togglen + */ + async toggleServicePublish(id: string): Promise { + const service = await this.findServiceById(id); + service.isPublished = !service.isPublished; + return this.serviceRepo.save(service); + } + + // ===== SORT ORDER ===== + + async updateCategorySortOrder(items: { id: string; sortOrder: number }[]): Promise { + for (const item of items) { + await this.categoryRepo.update(item.id, { sortOrder: item.sortOrder }); + } + } + + async updateServiceSortOrder(items: { id: string; sortOrder: number }[]): Promise { + for (const item of items) { + await this.serviceRepo.update(item.id, { sortOrder: item.sortOrder }); + } + } + + // ===== IMPORT / EXPORT ===== + + /** + * Bulk-Import von Kategorien und Services + */ + async bulkImport(dto: BulkImportServicesCatalogDto): Promise { + const result: ImportResultDto = { + success: true, + categoriesCreated: 0, + categoriesUpdated: 0, + servicesCreated: 0, + servicesUpdated: 0, + errors: [] + }; + + for (const catData of dto.categories) { + try { + let category = await this.categoryRepo.findOne({ where: { slug: catData.slug } }); + + if (category && dto.overwriteExisting) { + // Update existing category + category.name = catData.name; + category.subtitle = catData.subtitle; + category.materialIcon = catData.materialIcon; + category.sortOrder = catData.sortOrder ?? category.sortOrder; + category.isPublished = true; + await this.categoryRepo.save(category); + result.categoriesUpdated++; + } else if (!category) { + // Create new category + category = this.categoryRepo.create({ + slug: catData.slug, + name: catData.name, + subtitle: catData.subtitle, + materialIcon: catData.materialIcon, + sortOrder: catData.sortOrder ?? await this.getNextCategorySortOrder(), + isPublished: true + }); + category = await this.categoryRepo.save(category); + result.categoriesCreated++; + } + + // Import services for this category + for (const svcData of catData.services) { + try { + let service = await this.serviceRepo.findOne({ where: { slug: svcData.slug } }); + + if (service && dto.overwriteExisting) { + // Update existing service + service.icon = svcData.icon; + service.title = svcData.title; + service.description = svcData.description; + service.longDescription = svcData.longDescription; + service.tags = svcData.tags; + service.keywords = svcData.keywords; + service.sortOrder = svcData.sortOrder ?? service.sortOrder; + service.categoryId = category.id; + service.isPublished = true; + await this.serviceRepo.save(service); + result.servicesUpdated++; + } else if (!service) { + // Create new service + service = this.serviceRepo.create({ + slug: svcData.slug, + icon: svcData.icon, + title: svcData.title, + description: svcData.description, + longDescription: svcData.longDescription, + tags: svcData.tags, + keywords: svcData.keywords, + sortOrder: svcData.sortOrder ?? await this.getNextServiceSortOrder(category.id), + categoryId: category.id, + isPublished: true + }); + await this.serviceRepo.save(service); + result.servicesCreated++; + } + } catch (err) { + result.errors.push(`Service "${svcData.slug}": ${err.message}`); + } + } + } catch (err) { + result.errors.push(`Kategorie "${catData.slug}": ${err.message}`); + } + } + + result.success = result.errors.length === 0; + return result; + } + + /** + * Export aller Kategorien und Services + */ + async exportAll(): Promise<{ categories: any[] }> { + const categories = await this.categoryRepo.find({ + relations: ['services'], + order: { sortOrder: 'ASC' } + }); + + return { + categories: categories.map(cat => ({ + slug: cat.slug, + name: cat.name, + subtitle: cat.subtitle, + materialIcon: cat.materialIcon, + sortOrder: cat.sortOrder, + services: cat.services + .sort((a, b) => a.sortOrder - b.sortOrder) + .map(svc => ({ + slug: svc.slug, + icon: svc.icon, + title: svc.title, + description: svc.description, + longDescription: svc.longDescription, + tags: svc.tags, + keywords: svc.keywords, + sortOrder: svc.sortOrder + })) + })) + }; + } + + // ===== HELPERS ===== + + private async getNextCategorySortOrder(): Promise { + const max = await this.categoryRepo.createQueryBuilder('cat') + .select('MAX(cat.sortOrder)', 'max') + .getRawOne(); + return (max?.max ?? -1) + 1; + } + + private async getNextServiceSortOrder(categoryId: string): Promise { + const max = await this.serviceRepo.createQueryBuilder('svc') + .where('svc.categoryId = :categoryId', { categoryId }) + .select('MAX(svc.sortOrder)', 'max') + .getRawOne(); + return (max?.max ?? -1) + 1; + } +} diff --git a/apps/backend/src/settings/settings.controller.ts b/apps/backend/src/settings/settings.controller.ts new file mode 100644 index 0000000..95f6eee --- /dev/null +++ b/apps/backend/src/settings/settings.controller.ts @@ -0,0 +1,50 @@ +import { + Controller, + Get, + Patch, + Body, + HttpCode, + HttpStatus, + UseGuards, + Post +} from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; +import { SettingsService } from './settings.service'; +import { UpdateSettingsDto } from './settings.dto'; +import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'; +import { AdminGuard } from 'src/auth/guards/admin.guard'; + +@Controller('settings') +@Throttle({ default: { limit: 30, ttl: 60000 } }) // 🛡️ Basis: 30 Requests/Minute +export class SettingsController { + constructor(private readonly settingsService: SettingsService) {} + + // Öffentliche Settings (für Frontend Check) + @Get('public') + @HttpCode(HttpStatus.OK) + async getPublicSettings() { + return this.settingsService.getPublicSettings(); + } + + // 🛡️ STRENG: 5 Versuche pro Minute (Brute-Force Schutz) + @Throttle({ default: { limit: 5, ttl: 60000 } }) + @Post('check-maintenance-password') + @HttpCode(HttpStatus.OK) + async checkMaintenancePassword(@Body('password') password: string) { + const isValid = await this.settingsService.checkMaintenancePassword(password); + return { valid: isValid }; + } + + // Admin Routen + @UseGuards(JwtAuthGuard, AdminGuard) + @Get() + async getSettings() { + return this.settingsService.getSettings(); + } + + @UseGuards(JwtAuthGuard, AdminGuard) + @Patch() + async updateSettings(@Body() dto: UpdateSettingsDto) { + return this.settingsService.updateSettings(dto); + } +} \ No newline at end of file diff --git a/apps/backend/src/settings/settings.dto.ts b/apps/backend/src/settings/settings.dto.ts new file mode 100644 index 0000000..117d06e --- /dev/null +++ b/apps/backend/src/settings/settings.dto.ts @@ -0,0 +1,52 @@ +import { IsBoolean, IsEmail, IsOptional, IsString, MaxLength } from 'class-validator'; + +export class UpdateSettingsDto { + @IsBoolean() + @IsOptional() + isUnderConstruction?: boolean; + + @IsString() + @MaxLength(255) + @IsOptional() + maintenanceMessage?: string; + + @IsString() + @MaxLength(50) + @IsOptional() + maintenancePassword?: string; + + @IsBoolean() + @IsOptional() + allowRegistration?: boolean; + + @IsBoolean() + @IsOptional() + allowNewsletter?: boolean; + + @IsString() + @MaxLength(255) + @IsOptional() + siteTitle?: string; + + @IsString() + @IsOptional() + siteDescription?: string; + + @IsEmail() + @IsOptional() + contactEmail?: string; + + @IsString() + @MaxLength(50) + @IsOptional() + contactPhone?: string; +} + +export class PublicSettingsDto { + isUnderConstruction: boolean; + maintenanceMessage?: string; + siteTitle?: string; + siteDescription?: string; + allowRegistration?: boolean; + allowNewsletter?: boolean; +} \ No newline at end of file diff --git a/apps/backend/src/settings/settings.entity.ts b/apps/backend/src/settings/settings.entity.ts new file mode 100644 index 0000000..ba3333f --- /dev/null +++ b/apps/backend/src/settings/settings.entity.ts @@ -0,0 +1,40 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; + +@Entity('settings') +export class Settings { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'boolean', default: false }) + isUnderConstruction: boolean; + + @Column({ type: 'varchar', length: 255, nullable: true }) + maintenanceMessage: string; + + @Column({ type: 'varchar', length: 50, nullable: true }) + maintenancePassword: string; + + @Column({ type: 'boolean', default: true }) + allowRegistration: boolean; + + @Column({ type: 'boolean', default: true }) + allowNewsletter: boolean; + + @Column({ type: 'varchar', length: 255, nullable: true }) + siteTitle: string; + + @Column({ type: 'text', nullable: true }) + siteDescription: string; + + @Column({ type: 'varchar', length: 255, nullable: true }) + contactEmail: string; + + @Column({ type: 'varchar', length: 50, nullable: true }) + contactPhone: string; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} \ No newline at end of file diff --git a/apps/backend/src/settings/settings.module.ts b/apps/backend/src/settings/settings.module.ts new file mode 100644 index 0000000..4e8f664 --- /dev/null +++ b/apps/backend/src/settings/settings.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Settings } from './settings.entity'; +import { SettingsService } from './settings.service'; +import { SettingsController } from './settings.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([Settings])], + controllers: [SettingsController], + providers: [SettingsService], + exports: [SettingsService], +}) +export class SettingsModule {} \ No newline at end of file diff --git a/apps/backend/src/settings/settings.service.ts b/apps/backend/src/settings/settings.service.ts new file mode 100644 index 0000000..e02fe57 --- /dev/null +++ b/apps/backend/src/settings/settings.service.ts @@ -0,0 +1,117 @@ +import { Injectable, OnModuleInit } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Settings } from './settings.entity'; +import { UpdateSettingsDto, PublicSettingsDto } from './settings.dto'; + +@Injectable() +export class SettingsService implements OnModuleInit { + private settingsId: string | null = null; + + constructor( + @InjectRepository(Settings) + private readonly settingsRepo: Repository, + ) {} + + async onModuleInit() { + // Initialisiere Settings falls noch nicht vorhanden + await this.initializeSettings(); + } + + private async initializeSettings(): Promise { + // Prüfe ob Settings existieren + const settings = await this.settingsRepo.find({ + take: 1, + order: { createdAt: 'ASC' } + }); + + if (settings.length === 0) { + // Erstelle Default Settings + const newSettings = await this.createDefaultSettings(); + this.settingsId = newSettings.id; + } else { + // Speichere die ID für schnelleren Zugriff + this.settingsId = settings[0].id; + } + } + + private async createDefaultSettings(): Promise { + const settings = this.settingsRepo.create({ + isUnderConstruction: false, + maintenanceMessage: 'Die Seite wird gerade gewartet. Bitte versuchen Sie es später erneut.', + maintenancePassword: 'lm', + allowRegistration: true, + allowNewsletter: true, + siteTitle: 'LeonardsMedia', + siteDescription: 'Webentwicklung und digitale Lösungen', + contactEmail: 'info@leonardsmedia.de', + contactPhone: '+49 123 456789', + }); + + return this.settingsRepo.save(settings); + } + + async getSettings(): Promise { + // Wenn wir die ID haben, nutze sie + if (this.settingsId) { + const settings = await this.settingsRepo.findOne({ + where: { id: this.settingsId } + }); + + if (settings) { + return settings; + } + } + + // Fallback: Finde die ersten Settings + const allSettings = await this.settingsRepo.find({ + take: 1, + order: { createdAt: 'ASC' } + }); + + if (allSettings.length === 0) { + // Keine Settings gefunden, erstelle neue + const newSettings = await this.createDefaultSettings(); + this.settingsId = newSettings.id; + return newSettings; + } + + // Cache die ID für zukünftige Anfragen + this.settingsId = allSettings[0].id; + return allSettings[0]; + } + + async getPublicSettings(): Promise { + const settings = await this.getSettings(); + + return { + isUnderConstruction: settings.isUnderConstruction, + maintenanceMessage: settings.maintenanceMessage, + siteTitle: settings.siteTitle, + siteDescription: settings.siteDescription, + allowRegistration: settings.allowRegistration, + allowNewsletter: settings.allowNewsletter, + }; + } + + async updateSettings(dto: UpdateSettingsDto): Promise { + const settings = await this.getSettings(); + Object.assign(settings, dto); + const updated = await this.settingsRepo.save(settings); + + // Update cached ID + this.settingsId = updated.id; + + return updated; + } + + async checkMaintenancePassword(password: string): Promise { + const settings = await this.getSettings(); + return settings.maintenancePassword === password; + } + + async isUnderConstruction(): Promise { + const settings = await this.getSettings(); + return settings.isUnderConstruction; + } +} \ No newline at end of file diff --git a/apps/backend/src/users/users.controller.ts b/apps/backend/src/users/users.controller.ts new file mode 100644 index 0000000..33a10d4 --- /dev/null +++ b/apps/backend/src/users/users.controller.ts @@ -0,0 +1,116 @@ +import { + Controller, + Get, + Post, + Body, + Param, + Patch, + Delete, + HttpCode, + HttpStatus, + UseGuards + } from '@nestjs/common'; + import { Throttle } from '@nestjs/throttler'; + import { UsersService } from './users.service'; +import { CreateUserDto, LoginDto, NewsletterSubscribeDto, UpdateUserDto } from './users.dto'; +import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'; +import { AdminGuard } from 'src/auth/guards/admin.guard'; + + @Controller('users') + @Throttle({ default: { limit: 30, ttl: 60000 } }) // 🛡️ Basis: 30 Requests/Minute + export class UsersController { + constructor(private readonly usersService: UsersService) {} + + // 🛡️ STRENG: 3 Registrierungen pro Stunde + @Throttle({ default: { limit: 3, ttl: 3600000 } }) + @Post('register') + @HttpCode(HttpStatus.CREATED) + async register(@Body() dto: CreateUserDto) { + return this.usersService.create(dto); + } + + // 🛡️ STRENG: 5 Login-Versuche pro Minute + @Throttle({ default: { limit: 5, ttl: 60000 } }) + @Post('login') + @HttpCode(HttpStatus.OK) + async login(@Body() dto: LoginDto) { + const user = await this.usersService.login(dto); + return { + message: 'Login successful', + user, + }; + } + + // 🛡️ STRENG: 3 Newsletter-Anmeldungen pro Stunde + @Throttle({ default: { limit: 3, ttl: 3600000 } }) + @Post('newsletter/subscribe') + @HttpCode(HttpStatus.OK) + async subscribeNewsletter(@Body() dto: NewsletterSubscribeDto) { + return this.usersService.subscribeNewsletter(dto); + } + + @Post('newsletter/unsubscribe') + @HttpCode(HttpStatus.OK) + async unsubscribeNewsletter(@Body() body: { email: string }) { + return this.usersService.unsubscribeNewsletter(body.email); + } + + // Admin Routen + @UseGuards(JwtAuthGuard, AdminGuard) + @Get('newsletter/subscribers') + async getNewsletterSubscribers() { + return this.usersService.getNewsletterSubscribers(); + } + + @UseGuards(JwtAuthGuard, AdminGuard) + @Get('stats') + async getStats() { + const totalUsers = await this.usersService.count(); + const newsletterSubscribers = await this.usersService.countNewsletterSubscribers(); + + return { + totalUsers, + newsletterSubscribers, + subscriberRate: totalUsers > 0 + ? Math.round((newsletterSubscribers / totalUsers) * 100) + : 0, + }; + } + + // User Management Routen + // Später: nur Admin oder der User selbst darf zugreifen + @UseGuards(JwtAuthGuard, AdminGuard) + @Get() + async findAll() { + return this.usersService.findAll(); + } + + @UseGuards(JwtAuthGuard, AdminGuard) + @Get(':id') + async findOne(@Param('id') id: string) { + return this.usersService.findOne(id); + } + + @UseGuards(JwtAuthGuard, AdminGuard) + @Patch(':id') + async update( + @Param('id') id: string, + @Body() dto: UpdateUserDto + ) { + return this.usersService.update(id, dto); + } + + @UseGuards(JwtAuthGuard, AdminGuard) + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + async delete(@Param('id') id: string) { + return this.usersService.delete(id); + } + + // Eigenes Profil abrufen (wenn eingeloggt) + // @UseGuards(JwtAuthGuard) + // @Get('me') + // async getProfile(@CurrentUser() user: User) { + // return this.usersService.findOne(user.id); + // } + } \ No newline at end of file diff --git a/apps/backend/src/users/users.dto.ts b/apps/backend/src/users/users.dto.ts new file mode 100644 index 0000000..362995d --- /dev/null +++ b/apps/backend/src/users/users.dto.ts @@ -0,0 +1,51 @@ +import { IsEmail, IsString, IsBoolean, IsOptional, MinLength, MaxLength, IsEnum } from 'class-validator'; +import { UserRole } from './users.entity'; + +export class CreateUserDto { + @IsEmail() + email: string; + + @IsString() + @MinLength(2) + @MaxLength(100) + name: string; + + @IsString() + @MinLength(8, { message: 'Passwort muss mindestens 8 Zeichen lang sein' }) + password: string; + + @IsBoolean() + @IsOptional() + wantsNewsletter?: boolean; +} + +export class UpdateUserDto { + @IsString() + @IsOptional() + name?: string; + + @IsBoolean() + @IsOptional() + wantsNewsletter?: boolean; + + @IsEnum(UserRole) + @IsOptional() + role?: UserRole; +} + +export class LoginDto { + @IsEmail() + email: string; + + @IsString() + password: string; +} + +export class NewsletterSubscribeDto { + @IsEmail() + email: string; + + @IsString() + @IsOptional() + name?: string; +} \ No newline at end of file diff --git a/apps/backend/src/users/users.entity.ts b/apps/backend/src/users/users.entity.ts new file mode 100644 index 0000000..5138764 --- /dev/null +++ b/apps/backend/src/users/users.entity.ts @@ -0,0 +1,53 @@ +import { ContactRequest } from 'src/contact-requests/contact-requests.entity'; +import { + Entity, PrimaryGeneratedColumn, Column, + CreateDateColumn, UpdateDateColumn, OneToMany, Index +} from 'typeorm'; + +export enum UserRole { + USER = 'user', + ADMIN = 'admin', +} + +@Entity('users') +export class User { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ unique: true }) // in PG per Migration auf CITEXT umstellen + email: string; + + @Column() + name: string; + + @Column({ select: false }) // optional: schützt vor versehentlichem Auslesen + password: string; + + @Column({ + type: 'enum', + enum: UserRole, + enumName: 'user_role', + default: UserRole.USER, + }) + role: UserRole; + + @Index() + @Column({ default: false }) + wantsNewsletter: boolean; + + @Index() + @Column({ default: false }) + isVerified: boolean; + + @Column({ nullable: true }) + verificationToken: string | null; + + @CreateDateColumn({ type: 'timestamptz', default: () => 'now()' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz', default: () => 'now()' }) + updatedAt: Date; + + @OneToMany(() => ContactRequest, request => request.user) + contactRequests: ContactRequest[]; +} diff --git a/apps/backend/src/users/users.module.ts b/apps/backend/src/users/users.module.ts new file mode 100644 index 0000000..a7e3291 --- /dev/null +++ b/apps/backend/src/users/users.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { UsersController } from './users.controller'; +import { UsersService } from './users.service'; +import { User } from './users.entity'; + + +@Module({ + imports: [ + TypeOrmModule.forFeature([User]) + ], + controllers: [UsersController], + providers: [UsersService], + exports: [UsersService], +}) +export class UsersModule { } \ No newline at end of file diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts new file mode 100644 index 0000000..834af71 --- /dev/null +++ b/apps/backend/src/users/users.service.ts @@ -0,0 +1,160 @@ +import { Injectable, NotFoundException, ConflictException, UnauthorizedException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import * as bcrypt from 'bcrypt'; +import { User } from './users.entity'; +import { CreateUserDto, LoginDto, NewsletterSubscribeDto, UpdateUserDto } from './users.dto'; + +@Injectable() +export class UsersService { + constructor( + @InjectRepository(User) + private readonly userRepo: Repository, + ) { } + + async create(dto: CreateUserDto): Promise { + // Prüfe ob Email bereits existiert + const existingUser = await this.userRepo.findOne({ + where: { email: dto.email } + }); + + if (existingUser) { + throw new ConflictException('Email already exists'); + } + + // Passwort hashen + const hashedPassword = await bcrypt.hash(dto.password, 10); + + const user = this.userRepo.create({ + ...dto, + password: hashedPassword, + }); + + const savedUser = await this.userRepo.save(user); + + // Passwort aus Response entfernen + delete savedUser.password; + return savedUser; + } + + async findAll(): Promise { + const users = await this.userRepo.find({ + order: { createdAt: 'DESC' }, + select: ['id', 'email', 'name', 'wantsNewsletter', 'isVerified', 'createdAt', 'updatedAt', 'role',], + }); + return users; + } + + async findOne(id: string): Promise { + const user = await this.userRepo.findOne({ + where: { id }, + select: ['id', 'email', 'name', 'wantsNewsletter', 'isVerified', 'createdAt', 'updatedAt', 'role',], + relations: ['generatedPages', 'contactRequests'], + }); + + if (!user) { + throw new NotFoundException(`User with ID ${id} not found`); + } + + return user; + } + + async findByEmail(email: string): Promise { + return this.userRepo.findOne({ where: { email } }); + } + + async update(id: string, dto: UpdateUserDto): Promise { + const user = await this.userRepo.findOne({ where: { id } }); + + if (!user) { + throw new NotFoundException(`User with ID ${id} not found`); + } + + Object.assign(user, dto); + const updated = await this.userRepo.save(user); + delete updated.password; + return updated; + } + + async delete(id: string): Promise { + const result = await this.userRepo.delete(id); + if (result.affected === 0) { + throw new NotFoundException(`User with ID ${id} not found`); + } + } + + async validateUser(email: string, password: string): Promise { + const user = await this.userRepo.findOne({ where: { email } }); + + if (!user) { + throw new UnauthorizedException('Invalid credentials'); + } + + const isPasswordValid = await bcrypt.compare(password, user.password); + + if (!isPasswordValid) { + throw new UnauthorizedException('Invalid credentials'); + } + + delete user.password; + return user; + } + + async login(dto: LoginDto): Promise { + return this.validateUser(dto.email, dto.password); + } + + async subscribeNewsletter(dto: NewsletterSubscribeDto): Promise<{ message: string }> { + let user = await this.findByEmail(dto.email); + + if (user) { + // User existiert bereits, Newsletter-Flag updaten + if (user.wantsNewsletter) { + return { message: 'Already subscribed to newsletter' }; + } + + user.wantsNewsletter = true; + await this.userRepo.save(user); + return { message: 'Successfully subscribed to newsletter' }; + } + + // Neuer User nur für Newsletter + user = this.userRepo.create({ + email: dto.email, + name: dto.name || dto.email.split('@')[0], + password: await bcrypt.hash(Math.random().toString(36), 10), // Dummy Password + wantsNewsletter: true, + }); + + await this.userRepo.save(user); + return { message: 'Successfully subscribed to newsletter' }; + } + + async unsubscribeNewsletter(email: string): Promise<{ message: string }> { + const user = await this.findByEmail(email); + + if (!user) { + throw new NotFoundException('Email not found'); + } + + user.wantsNewsletter = false; + await this.userRepo.save(user); + + return { message: 'Successfully unsubscribed from newsletter' }; + } + + async getNewsletterSubscribers(): Promise { + return this.userRepo.find({ + where: { wantsNewsletter: true }, + select: ['id', 'email', 'name', 'createdAt'], + }); + } + + async count(): Promise { + return this.userRepo.count(); + } + + async countNewsletterSubscribers(): Promise { + return this.userRepo.count({ where: { wantsNewsletter: true } }); + } +} \ No newline at end of file diff --git a/apps/backend/test/app.e2e-spec.ts b/apps/backend/test/app.e2e-spec.ts new file mode 100644 index 0000000..50cda62 --- /dev/null +++ b/apps/backend/test/app.e2e-spec.ts @@ -0,0 +1,24 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { AppModule } from './../src/app.module'; + +describe('AppController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('/ (GET)', () => { + return request(app.getHttpServer()) + .get('/') + .expect(200) + .expect('Hello World!'); + }); +}); diff --git a/apps/backend/test/jest-e2e.json b/apps/backend/test/jest-e2e.json new file mode 100644 index 0000000..e9d912f --- /dev/null +++ b/apps/backend/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/apps/backend/tsconfig.build.json b/apps/backend/tsconfig.build.json new file mode 100644 index 0000000..64f86c6 --- /dev/null +++ b/apps/backend/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json new file mode 100644 index 0000000..95f5641 --- /dev/null +++ b/apps/backend/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": false, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false + } +} diff --git a/apps/frontend/.editorconfig b/apps/frontend/.editorconfig new file mode 100644 index 0000000..f166060 --- /dev/null +++ b/apps/frontend/.editorconfig @@ -0,0 +1,17 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single +ij_typescript_use_double_quotes = false + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/apps/frontend/.tsbuildinfo b/apps/frontend/.tsbuildinfo new file mode 100644 index 0000000..d747cdd --- /dev/null +++ b/apps/frontend/.tsbuildinfo @@ -0,0 +1 @@ +{"program":{"fileNames":["../../../../node_modules/typescript/lib/lib.es5.d.ts","../../../../node_modules/typescript/lib/lib.es2015.d.ts","../../../../node_modules/typescript/lib/lib.es2016.d.ts","../../../../node_modules/typescript/lib/lib.es2017.d.ts","../../../../node_modules/typescript/lib/lib.es2018.d.ts","../../../../node_modules/typescript/lib/lib.es2019.d.ts","../../../../node_modules/typescript/lib/lib.es2020.d.ts","../../../../node_modules/typescript/lib/lib.es2021.d.ts","../../../../node_modules/typescript/lib/lib.es2022.d.ts","../../../../node_modules/typescript/lib/lib.dom.d.ts","../../../../node_modules/typescript/lib/lib.es2015.core.d.ts","../../../../node_modules/typescript/lib/lib.es2015.collection.d.ts","../../../../node_modules/typescript/lib/lib.es2015.generator.d.ts","../../../../node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../../../node_modules/typescript/lib/lib.es2015.promise.d.ts","../../../../node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../../../node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../../../node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../../../node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../../../node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../../../node_modules/typescript/lib/lib.es2016.intl.d.ts","../../../../node_modules/typescript/lib/lib.es2017.date.d.ts","../../../../node_modules/typescript/lib/lib.es2017.object.d.ts","../../../../node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../../../node_modules/typescript/lib/lib.es2017.string.d.ts","../../../../node_modules/typescript/lib/lib.es2017.intl.d.ts","../../../../node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../../../node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../../../node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../../../node_modules/typescript/lib/lib.es2018.intl.d.ts","../../../../node_modules/typescript/lib/lib.es2018.promise.d.ts","../../../../node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../../../node_modules/typescript/lib/lib.es2019.array.d.ts","../../../../node_modules/typescript/lib/lib.es2019.object.d.ts","../../../../node_modules/typescript/lib/lib.es2019.string.d.ts","../../../../node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../../../node_modules/typescript/lib/lib.es2019.intl.d.ts","../../../../node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../../../node_modules/typescript/lib/lib.es2020.date.d.ts","../../../../node_modules/typescript/lib/lib.es2020.promise.d.ts","../../../../node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../../../node_modules/typescript/lib/lib.es2020.string.d.ts","../../../../node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../../../node_modules/typescript/lib/lib.es2020.intl.d.ts","../../../../node_modules/typescript/lib/lib.es2020.number.d.ts","../../../../node_modules/typescript/lib/lib.es2021.promise.d.ts","../../../../node_modules/typescript/lib/lib.es2021.string.d.ts","../../../../node_modules/typescript/lib/lib.es2021.weakref.d.ts","../../../../node_modules/typescript/lib/lib.es2021.intl.d.ts","../../../../node_modules/typescript/lib/lib.es2022.array.d.ts","../../../../node_modules/typescript/lib/lib.es2022.error.d.ts","../../../../node_modules/typescript/lib/lib.es2022.intl.d.ts","../../../../node_modules/typescript/lib/lib.es2022.object.d.ts","../../../../node_modules/typescript/lib/lib.es2022.sharedmemory.d.ts","../../../../node_modules/typescript/lib/lib.es2022.string.d.ts","../../../../node_modules/typescript/lib/lib.es2022.regexp.d.ts","../../../../node_modules/typescript/lib/lib.decorators.d.ts","../../../../node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../../../node_modules/tslib/tslib.d.ts","../../../../node_modules/tslib/modules/index.d.ts","../../../../src/main.ngtypecheck.ts","../../../../node_modules/rxjs/dist/types/internal/subscription.d.ts","../../../../node_modules/rxjs/dist/types/internal/subscriber.d.ts","../../../../node_modules/rxjs/dist/types/internal/operator.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable.d.ts","../../../../node_modules/rxjs/dist/types/internal/types.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/audit.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/audittime.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/buffer.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/buffercount.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/buffertime.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/buffertoggle.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/bufferwhen.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/catcherror.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/combinelatestall.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/combineall.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/combinelatest.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/combinelatestwith.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/concat.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/concatall.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/concatmap.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/concatmapto.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/concatwith.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/connect.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/count.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/debounce.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/debouncetime.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/defaultifempty.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/delay.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/delaywhen.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/dematerialize.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/distinct.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/distinctuntilchanged.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/distinctuntilkeychanged.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/elementat.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/endwith.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/every.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/exhaustall.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/exhaust.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/exhaustmap.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/expand.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/filter.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/finalize.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/find.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/findindex.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/first.d.ts","../../../../node_modules/rxjs/dist/types/internal/subject.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/groupby.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/ignoreelements.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/isempty.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/last.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/map.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/mapto.d.ts","../../../../node_modules/rxjs/dist/types/internal/notification.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/materialize.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/max.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/merge.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/mergeall.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/mergemap.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/flatmap.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/mergemapto.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/mergescan.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/mergewith.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/min.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable/connectableobservable.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/multicast.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/observeon.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/onerrorresumenextwith.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/pairwise.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/partition.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/pluck.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/publish.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/publishbehavior.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/publishlast.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/publishreplay.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/race.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/racewith.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/reduce.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/repeat.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/repeatwhen.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/retry.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/retrywhen.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/refcount.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/sample.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/sampletime.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/scan.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/sequenceequal.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/share.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/sharereplay.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/single.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/skip.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/skiplast.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/skipuntil.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/skipwhile.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/startwith.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/subscribeon.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/switchall.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/switchmap.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/switchmapto.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/switchscan.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/take.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/takelast.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/takeuntil.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/takewhile.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/tap.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/throttle.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/throttletime.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/throwifempty.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/timeinterval.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/timeout.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/timeoutwith.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/timestamp.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/toarray.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/window.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/windowcount.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/windowtime.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/windowtoggle.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/windowwhen.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/withlatestfrom.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/zip.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/zipall.d.ts","../../../../node_modules/rxjs/dist/types/internal/operators/zipwith.d.ts","../../../../node_modules/rxjs/dist/types/operators/index.d.ts","../../../../node_modules/rxjs/dist/types/internal/scheduler/action.d.ts","../../../../node_modules/rxjs/dist/types/internal/scheduler.d.ts","../../../../node_modules/rxjs/dist/types/internal/testing/testmessage.d.ts","../../../../node_modules/rxjs/dist/types/internal/testing/subscriptionlog.d.ts","../../../../node_modules/rxjs/dist/types/internal/testing/subscriptionloggable.d.ts","../../../../node_modules/rxjs/dist/types/internal/testing/coldobservable.d.ts","../../../../node_modules/rxjs/dist/types/internal/testing/hotobservable.d.ts","../../../../node_modules/rxjs/dist/types/internal/scheduler/asyncscheduler.d.ts","../../../../node_modules/rxjs/dist/types/internal/scheduler/timerhandle.d.ts","../../../../node_modules/rxjs/dist/types/internal/scheduler/asyncaction.d.ts","../../../../node_modules/rxjs/dist/types/internal/scheduler/virtualtimescheduler.d.ts","../../../../node_modules/rxjs/dist/types/internal/testing/testscheduler.d.ts","../../../../node_modules/rxjs/dist/types/testing/index.d.ts","../../../../node_modules/rxjs/dist/types/internal/symbol/observable.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable/dom/animationframes.d.ts","../../../../node_modules/rxjs/dist/types/internal/behaviorsubject.d.ts","../../../../node_modules/rxjs/dist/types/internal/replaysubject.d.ts","../../../../node_modules/rxjs/dist/types/internal/asyncsubject.d.ts","../../../../node_modules/rxjs/dist/types/internal/scheduler/asapscheduler.d.ts","../../../../node_modules/rxjs/dist/types/internal/scheduler/asap.d.ts","../../../../node_modules/rxjs/dist/types/internal/scheduler/async.d.ts","../../../../node_modules/rxjs/dist/types/internal/scheduler/queuescheduler.d.ts","../../../../node_modules/rxjs/dist/types/internal/scheduler/queue.d.ts","../../../../node_modules/rxjs/dist/types/internal/scheduler/animationframescheduler.d.ts","../../../../node_modules/rxjs/dist/types/internal/scheduler/animationframe.d.ts","../../../../node_modules/rxjs/dist/types/internal/util/identity.d.ts","../../../../node_modules/rxjs/dist/types/internal/util/pipe.d.ts","../../../../node_modules/rxjs/dist/types/internal/util/noop.d.ts","../../../../node_modules/rxjs/dist/types/internal/util/isobservable.d.ts","../../../../node_modules/rxjs/dist/types/internal/lastvaluefrom.d.ts","../../../../node_modules/rxjs/dist/types/internal/firstvaluefrom.d.ts","../../../../node_modules/rxjs/dist/types/internal/util/argumentoutofrangeerror.d.ts","../../../../node_modules/rxjs/dist/types/internal/util/emptyerror.d.ts","../../../../node_modules/rxjs/dist/types/internal/util/notfounderror.d.ts","../../../../node_modules/rxjs/dist/types/internal/util/objectunsubscribederror.d.ts","../../../../node_modules/rxjs/dist/types/internal/util/sequenceerror.d.ts","../../../../node_modules/rxjs/dist/types/internal/util/unsubscriptionerror.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable/bindcallback.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable/bindnodecallback.d.ts","../../../../node_modules/rxjs/dist/types/internal/anycatcher.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable/combinelatest.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable/concat.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable/connectable.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable/defer.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable/empty.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable/forkjoin.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable/from.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable/fromevent.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable/fromeventpattern.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable/generate.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable/iif.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable/interval.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable/merge.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable/never.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable/of.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable/onerrorresumenext.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable/pairs.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable/partition.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable/race.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable/range.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable/throwerror.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable/timer.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable/using.d.ts","../../../../node_modules/rxjs/dist/types/internal/observable/zip.d.ts","../../../../node_modules/rxjs/dist/types/internal/scheduled/scheduled.d.ts","../../../../node_modules/rxjs/dist/types/internal/config.d.ts","../../../../node_modules/rxjs/dist/types/index.d.ts","../../../../node_modules/@angular/core/primitives/event-dispatch/index.d.ts","../../../../node_modules/@angular/core/primitives/signals/index.d.ts","../../../../node_modules/@angular/core/index.d.ts","../../../../node_modules/@angular/common/index.d.ts","../../../../node_modules/@angular/common/http/index.d.ts","../../../../node_modules/@angular/platform-browser/index.d.ts","../../../../src/app/app.config.ngtypecheck.ts","../../../../node_modules/@angular/router/index.d.ts","../../../../src/app/app.routes.ngtypecheck.ts","../../../../src/app/components/home/home.component.ngtypecheck.ts","../../../../src/app/shared/icon/icon.component.ngtypecheck.ts","../../../../src/app/shared/icon/icon.component.ts","../../../../node_modules/@angular/forms/index.d.ts","../../../../src/app/components/home/home.component.ts","../../../../src/app/shared/page-title/page-title.component.ngtypecheck.ts","../../../../src/app/shared/page-title/page-title.component.ts","../../../../src/app/components/about/about.component.ngtypecheck.ts","../../../../src/app/components/about/about.component.ts","../../../../src/app/components/contact/contact.component.ngtypecheck.ts","../../../../src/app/shared/service-data.service.ngtypecheck.ts","../../../../src/app/shared/service-data.service.ts","../../../../src/app/api/api.service.ngtypecheck.ts","../../../../environments/environment.ngtypecheck.ts","../../../../environments/environment.ts","../../../../src/app/services/auth.service.ngtypecheck.ts","../../../../src/app/services/auth.service.ts","../../../../src/app/api/api.service.ts","../../../../src/app/shared/toasts/toast.service.ngtypecheck.ts","../../../../src/app/shared/toasts/toast.model.ngtypecheck.ts","../../../../src/app/shared/toasts/toast.model.ts","../../../../src/app/shared/toasts/toast.service.ts","../../../../src/app/components/contact/contact.component.ts","../../../../src/app/components/imprint/imprint.component.ngtypecheck.ts","../../../../src/app/components/imprint/imprint.component.ts","../../../../src/app/components/policy/policy.component.ngtypecheck.ts","../../../../src/app/components/policy/policy.component.ts","../../../../src/app/components/services/services.component.ngtypecheck.ts","../../../../src/app/components/services/services.component.ts","../../../../src/app/components/server-status/server-status.component.ngtypecheck.ts","../../../../src/app/components/server-status/server-status.component.ts","../../../../src/app/components/vorgehen/vorgehen.component.ngtypecheck.ts","../../../../src/app/components/vorgehen/vorgehen.component.ts","../../../../src/app/components/faq/faq.component.ngtypecheck.ts","../../../../src/app/components/faq/faq.component.ts","../../../../src/app/components/preview/main-container/main-container.component.ngtypecheck.ts","../../../../src/app/state/preview.service.ngtypecheck.ts","../../../../src/app/state/preview.service.ts","../../../../src/app/shared/confirmation/confirmation.service.ngtypecheck.ts","../../../../src/app/shared/confirmation/confirmation.component.ngtypecheck.ts","../../../../src/app/shared/confirmation/confirmation.component.ts","../../../../src/app/shared/confirmation/confirmation.service.ts","../../../../src/app/components/preview/main-container/main-container.component.ts","../../../../src/app/components/preview/input-form/input-form.component.ngtypecheck.ts","../../../../src/app/components/preview/input-form/input-form.component.ts","../../../../src/app/components/maintenance/maintenance.component.ngtypecheck.ts","../../../../src/app/components/maintenance/maintenance.component.ts","../../../../src/app/components/admin/admin-requests/admin-requests.component.ngtypecheck.ts","../../../../src/app/components/admin/admin-header/admin-header.component.ngtypecheck.ts","../../../../src/app/components/admin/admin-header/admin-header.component.ts","../../../../src/app/components/admin/admin-requests/admin-requests.component.ts","../../../../src/app/guards/admin.guard.ngtypecheck.ts","../../../../src/app/guards/admin.guard.ts","../../../../src/app/guards/auth.guard.ngtypecheck.ts","../../../../src/app/guards/auth.guard.ts","../../../../src/app/components/login/login.component.ngtypecheck.ts","../../../../src/app/components/login/login.component.ts","../../../../src/app/components/register/register.component.ngtypecheck.ts","../../../../src/app/components/register/register.component.ts","../../../../src/app/components/profile/profile.component.ngtypecheck.ts","../../../../src/app/components/profile/profile.component.ts","../../../../src/app/components/admin/admin-users/admin-users.component.ngtypecheck.ts","../../../../src/app/components/admin/admin-users/admin-users.component.ts","../../../../src/app/components/admin/admin-gen-pages/admin-gen-pages.component.ngtypecheck.ts","../../../../src/app/components/admin/admin-gen-pages/admin-gen-pages.component.ts","../../../../src/app/components/admin/admin-settings/admin-settings.component.ngtypecheck.ts","../../../../src/app/components/admin/admin-settings/admin-settings.component.ts","../../../../src/app/shared/generation-loading/generation-loading.component.ngtypecheck.ts","../../../../src/app/shared/generation-loading/generation-loading.component.ts","../../../../src/app/components/booking/booking.component.ngtypecheck.ts","../../../../src/app/components/booking/booking.component.ts","../../../../src/app/components/admin/admin-booking/admin-booking.component.ngtypecheck.ts","../../../../src/app/components/admin/admin-booking/admin-booking.component.ts","../../../../src/app/components/it-services/it-services.component.ngtypecheck.ts","../../../../src/app/components/it-services/it-services.component.ts","../../../../src/app/components/admin/admin-newsletter/admin-newsletter.component.ngtypecheck.ts","../../../../src/app/components/admin/admin-newsletter/admin-newsletter.component.ts","../../../../src/app/components/services/common-service/common-service.component.ngtypecheck.ts","../../../../src/app/components/services/common-service/service.interface.ngtypecheck.ts","../../../../src/app/components/services/common-service/service.interface.ts","../../../../src/app/components/services/common-service/service.config.ngtypecheck.ts","../../../../src/app/components/services/common-service/service.config.ts","../../../../src/app/components/services/common-service/common-service.component.ts","../../../../src/app/app.routes.ts","../../../../src/app/services/auth.interceptor.ngtypecheck.ts","../../../../src/app/services/auth.interceptor.ts","../../../../src/app/app.config.ts","../../../../src/app/app.component.ngtypecheck.ts","../../../../src/app/shared/header/header.component.ngtypecheck.ts","../../../../src/app/shared/header/header.component.ts","../../../../src/app/shared/footer/footer.component.ngtypecheck.ts","../../../../src/app/shared/footer/footer.component.ts","../../../../src/app/shared/seo.service.ngtypecheck.ts","../../../../src/app/shared/seo.service.ts","../../../../src/app/shared/toasts/toast-container.component.ngtypecheck.ts","../../../../src/app/shared/toasts/toast-container.component.ts","../../../../src/app/app.component.ts","../../../../src/main.ts"],"fileInfos":[{"version":"44e584d4f6444f58791784f1d530875970993129442a847597db702a073ca68c","affectsGlobalScope":true},"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","9a68c0c07ae2fa71b44384a839b7b8d81662a236d4b9ac30916718f7510b1b2d","5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","5514e54f17d6d74ecefedc73c504eadffdeda79c7ea205cf9febead32d45c4bc",{"version":"4af6b0c727b7a2896463d512fafd23634229adf69ac7c00e2ae15a09cb084fad","affectsGlobalScope":true},{"version":"6920e1448680767498a0b77c6a00a8e77d14d62c3da8967b171f1ddffa3c18e4","affectsGlobalScope":true},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true},{"version":"4443e68b35f3332f753eacc66a04ac1d2053b8b035a0e0ac1d455392b5e243b3","affectsGlobalScope":true},{"version":"bc47685641087c015972a3f072480889f0d6c65515f12bd85222f49a98952ed7","affectsGlobalScope":true},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true},{"version":"93495ff27b8746f55d19fcbcdbaccc99fd95f19d057aed1bd2c0cafe1335fbf0","affectsGlobalScope":true},{"version":"6fc23bb8c3965964be8c597310a2878b53a0306edb71d4b5a4dfe760186bcc01","affectsGlobalScope":true},{"version":"ea011c76963fb15ef1cdd7ce6a6808b46322c527de2077b6cfdf23ae6f5f9ec7","affectsGlobalScope":true},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true},{"version":"bb42a7797d996412ecdc5b2787720de477103a0b2e53058569069a0e2bae6c7e","affectsGlobalScope":true},{"version":"4738f2420687fd85629c9efb470793bb753709c2379e5f85bc1815d875ceadcd","affectsGlobalScope":true},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true},{"version":"9fc46429fbe091ac5ad2608c657201eb68b6f1b8341bd6d670047d32ed0a88fa","affectsGlobalScope":true},{"version":"61c37c1de663cf4171e1192466e52c7a382afa58da01b1dc75058f032ddf0839","affectsGlobalScope":true},{"version":"b541a838a13f9234aba650a825393ffc2292dc0fc87681a5d81ef0c96d281e7a","affectsGlobalScope":true},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true},{"version":"ae37d6ccd1560b0203ab88d46987393adaaa78c919e51acf32fb82c86502e98c","affectsGlobalScope":true},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true},{"version":"bf14a426dbbf1022d11bd08d6b8e709a2e9d246f0c6c1032f3b2edb9a902adbe","affectsGlobalScope":true},{"version":"5e07ed3809d48205d5b985642a59f2eba47c402374a7cf8006b686f79efadcbd","affectsGlobalScope":true},{"version":"2b72d528b2e2fe3c57889ca7baef5e13a56c957b946906d03767c642f386bbc3","affectsGlobalScope":true},{"version":"479553e3779be7d4f68e9f40cdb82d038e5ef7592010100410723ceced22a0f7","affectsGlobalScope":true},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true},{"version":"d3d7b04b45033f57351c8434f60b6be1ea71a2dfec2d0a0c3c83badbb0e3e693","affectsGlobalScope":true},{"version":"956d27abdea9652e8368ce029bb1e0b9174e9678a273529f426df4b3d90abd60","affectsGlobalScope":true},{"version":"4fa6ed14e98aa80b91f61b9805c653ee82af3502dc21c9da5268d3857772ca05","affectsGlobalScope":true},{"version":"e6633e05da3ff36e6da2ec170d0d03ccf33de50ca4dc6f5aeecb572cedd162fb","affectsGlobalScope":true},{"version":"d8670852241d4c6e03f2b89d67497a4bbefe29ecaa5a444e2c11a9b05e6fccc6","affectsGlobalScope":true},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true},{"version":"caccc56c72713969e1cfe5c3d44e5bab151544d9d2b373d7dbe5a1e4166652be","affectsGlobalScope":true},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true},{"version":"b4b67b1a91182421f5df999988c690f14d813b9850b40acd06ed44691f6727ad","affectsGlobalScope":true},{"version":"33358442698bb565130f52ba79bfd3d4d484ac85fe33f3cb1759c54d18201393","affectsGlobalScope":true},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true},"a6a5253138c5432c68a1510c70fe78a644fe2e632111ba778e1978010d6edfec","b8f34dd1757f68e03262b1ca3ddfa668a855b872f8bdd5224d6f993a7b37dc2c","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","073ca26c96184db9941b5ec0ddea6981c9b816156d9095747809e524fdd90e35","e41d17a2ec23306d953cda34e573ed62954ca6ea9b8c8b74e013d07a6886ce47","241bd4add06f06f0699dcd58f3b334718d85e3045d9e9d4fa556f11f4d1569c1","2ae3787e1498b20aad1b9c2ee9ea517ec30e89b70d242d8e3e52d1e091039695",{"version":"c7c72c4cffb1bc83617eefed71ed68cc89df73cab9e19507ccdecb3e72b4967e","affectsGlobalScope":true},"b8bff8a60af0173430b18d9c3e5c443eaa3c515617210c0c7b3d2e1743c19ecb","38b38db08e7121828294dec10957a7a9ff263e33e2a904b346516d4a4acca482","a76ebdf2579e68e4cfe618269c47e5a12a4e045c2805ed7f7ab37af8daa6b091","8a2aaea564939c22be05d665cc955996721bad6d43148f8fa21ae8f64afecd37","e59d36b7b6e8ba2dd36d032a5f5c279d2460968c8b4e691ca384f118fb09b52a","e96885c0684c9042ec72a9a43ef977f6b4b4a2728f4b9e737edcbaa0c74e5bf6","95950a187596e206d32d5d9c7b932901088c65ed8f9040e614aa8e321e0225ef","89e061244da3fc21b7330f4bd32f47c1813dd4d7f1dc3d0883d88943f035b993","e46558c2e04d06207b080138678020448e7fc201f3d69c2601b0d1456105f29a","71549375db52b1163411dba383b5f4618bdf35dc57fa327a1c7d135cf9bf67d1","7e6b2d61d6215a4e82ea75bc31a80ebb8ad0c2b37a60c10c70dd671e8d9d6d5d","78bea05df2896083cca28ed75784dde46d4b194984e8fc559123b56873580a23","5dd04ced37b7ea09f29d277db11f160df7fd73ba8b9dba86cb25552e0653a637","f74b81712e06605677ae1f061600201c425430151f95b5ef4d04387ad7617e6a","9a72847fcf4ac937e352d40810f7b7aec7422d9178451148296cf1aa19467620","3ae18f60e0b96fa1e025059b7d25b3247ba4dcb5f4372f6d6e67ce2adac74eac","2b9260f44a2e071450ae82c110f5dc8f330c9e5c3e85567ed97248330f2bf639","4f196e13684186bda6f5115fc4677a87cf84a0c9c4fc17b8f51e0984f3697b6d","61419f2c5822b28c1ea483258437c1faab87d00c6f84481aa22afb3380d8e9a4","64479aee03812264e421c0bf5104a953ca7b02740ba80090aead1330d0effe91","0521108c9f8ddb17654a0a54dae6ba9667c99eddccfd6af5748113e022d1c37a","c5570e504be103e255d80c60b56c367bf45d502ca52ee35c55dec882f6563b5c","ee764e6e9a7f2b987cc1a2c0a9afd7a8f4d5ebc4fdb66ad557a7f14a8c2bd320","0520b5093712c10c6ef23b5fea2f833bf5481771977112500045e5ea7e8e2b69","5c3cf26654cf762ac4d7fd7b83f09acfe08eef88d2d6983b9a5a423cb4004ca3","e60fa19cf7911c1623b891155d7eb6b7e844e9afdf5738e3b46f3b687730a2bd","b1fd72ff2bb0ba91bb588f3e5329f8fc884eb859794f1c4657a2bfa122ae54d0","6cf42a4f3cfec648545925d43afaa8bb364ac10a839ffed88249da109361b275","d7058e75920120b142a9d57be25562a3cd9a936269fd52908505f530105f2ec4","6df52b70d7f7702202f672541a5f4a424d478ee5be51a9d37b8ccbe1dbf3c0f2","0ca7f997e9a4d8985e842b7c882e521b6f63233c4086e9fe79dd7a9dc4742b5e","91046b5c6b55d3b194c81fd4df52f687736fad3095e9d103ead92bb64dc160ee","db5704fdad56c74dfc5941283c1182ed471bd17598209d3ac4a49faa72e43cfc","758e8e89559b02b81bc0f8fd395b17ad5aff75490c862cbe369bb1a3d1577c40","2ee64342c077b1868f1834c063f575063051edd6e2964257d34aad032d6b657c","6f6b4b3d670b6a5f0e24ea001c1b3d36453c539195e875687950a178f1730fa7","a472a1d3f25ce13a1d44911cd3983956ac040ce2018e155435ea34afb25f864c","b48b83a86dd9cfe36f8776b3ff52fcd45b0e043c0538dc4a4b149ba45fe367b9","792de5c062444bd2ee0413fb766e57e03cce7cdaebbfc52fc0c7c8e95069c96b","a79e3e81094c7a04a885bad9b049c519aace53300fb8a0fe4f26727cb5a746ce","93181bac0d90db185bb730c95214f6118ae997fe836a98a49664147fbcaf1988","8a4e89564d8ea66ad87ee3762e07540f9f0656a62043c910d819b4746fc429c5","b9011d99942889a0f95e120d06b698c628b0b6fdc3e6b7ecb459b97ed7d5bcc6","4d639cbbcc2f8f9ce6d55d5d503830d6c2556251df332dc5255d75af53c8a0e7","cdb48277f600ab5f429ecf1c5ea046683bc6b9f73f3deab9a100adac4b34969c","75be84956a29040a1afbe864c0a7a369dfdb739380072484eff153905ef867ee","b06b4adc2ae03331a92abd1b19af8eb91ec2bf8541747ee355887a167d53145e","c54166a85bd60f86d1ebb90ce0117c0ecb850b8a33b366691629fdf26f1bbbd8","0d417c15c5c635384d5f1819cc253a540fe786cc3fda32f6a2ae266671506a21","80f23f1d60fbed356f726b3b26f9d348dddbb34027926d10d59fad961e70a730","cb59317243a11379a101eb2f27b9df1022674c3df1df0727360a0a3f963f523b","cc20bb2227dd5de0aab0c8d697d1572f8000550e62c7bf5c92f212f657dd88c5","06b8a7d46195b6b3980e523ef59746702fd210b71681a83a5cf73799623621f9","860e4405959f646c101b8005a191298b2381af8f33716dc5f42097e4620608f8","f7e32adf714b8f25d3c1783473abec3f2e82d5724538d8dcf6f51baaaff1ca7a","d0da80c845999a16c24d0783033fb5366ada98df17867c98ad433ede05cd87fd","bfbf80f9cd4558af2d7b2006065340aaaced15947d590045253ded50aabb9bc5","fd9a991b51870325e46ebb0e6e18722d313f60cd8e596e645ec5ac15b96dbf4e","c3bd2b94e4298f81743d92945b80e9b56c1cdfb2bef43c149b7106a2491b1fc9","a246cce57f558f9ebaffd55c1e5673da44ea603b4da3b2b47eb88915d30a9181","d993eacc103c5a065227153c9aae8acea3a4322fe1a169ee7c70b77015bf0bb2","fc2b03d0c042aa1627406e753a26a1eaad01b3c496510a78016822ef8d456bb6","063c7ebbe756f0155a8b453f410ca6b76ffa1bbc1048735bcaf9c7c81a1ce35f","314e402cd481370d08f63051ae8b8c8e6370db5ee3b8820eeeaaf8d722a6dac6","9669075ac38ce36b638b290ba468233980d9f38bdc62f0519213b2fd3e2552ec","4d123de012c24e2f373925100be73d50517ac490f9ed3578ac82d0168bfbd303","656c9af789629aa36b39092bee3757034009620439d9a39912f587538033ce28","3ac3f4bdb8c0905d4c3035d6f7fb20118c21e8a17bee46d3735195b0c2a9f39f","1f453e6798ed29c86f703e9b41662640d4f2e61337007f27ac1c616f20093f69","af43b7871ff21c62bf1a54ec5c488e31a8d3408d5b51ff2e9f8581b6c55f2fc7","70550511d25cbb0b6a64dcac7fffc3c1397fd4cbeb6b23ccc7f9b794ab8a6954","af0fbf08386603a62f2a78c42d998c90353b1f1d22e05a384545f7accf881e0a","cefc20054d20b85b534206dbcedd509bb74f87f3d8bc45c58c7be3a76caa45e1","ad6eee4877d0f7e5244d34bc5026fd6e9cf8e66c5c79416b73f9f6ebf132f924","4888fd2bcfee9a0ce89d0df860d233e0cee8ee9c479b6bd5a5d5f9aae98342fe","f4749c102ced952aa6f40f0b579865429c4869f6d83df91000e98005476bee87","56654d2c5923598384e71cb808fac2818ca3f07dd23bb018988a39d5e64f268b","8b6719d3b9e65863da5390cb26994602c10a315aa16e7d70778a63fee6c4c079","05f56cd4b929977d18df8f3d08a4c929a2592ef5af083e79974b20a063f30940","547d3c406a21b30e2b78629ecc0b2ddaf652d9e0bdb2d59ceebce5612906df33","b3a4f9385279443c3a5568ec914a9492b59a723386161fd5ef0619d9f8982f97","3fe66aba4fbe0c3ba196a4f9ed2a776fe99dc4d1567a558fb11693e9fcc4e6ed","140eef237c7db06fc5adcb5df434ee21e81ee3a6fd57e1a75b8b3750aa2df2d8","0944ec553e4744efae790c68807a461720cff9f3977d4911ac0d918a17c9dd99","cb46b38d5e791acaa243bf342b8b5f8491639847463ac965b93896d4fb0af0d9","7c7d9e116fe51100ff766703e6b5e4424f51ad8977fe474ddd8d0959aa6de257","af70a2567e586be0083df3938b6a6792e6821363d8ef559ad8d721a33a5bcdaf","006cff3a8bcb92d77953f49a94cd7d5272fef4ab488b9052ef82b6a1260d870b","7d44bfdc8ee5e9af70738ff652c622ae3ad81815e63ab49bdc593d34cb3a68e5","339814517abd4dbc7b5f013dfd3b5e37ef0ea914a8bbe65413ecffd668792bc6","34d5bc0a6958967ec237c99f980155b5145b76e6eb927c9ffc57d8680326b5d8","9eae79b70c9d8288032cbe1b21d0941f6bd4f315e14786b2c1d10bccc634e897","18ce015ed308ea469b13b17f99ce53bbb97975855b2a09b86c052eefa4aa013a","5a931bc4106194e474be141e0bc1046629510dc95b9a0e4b02a3783847222965","5e5f371bf23d5ced2212a5ff56675aefbd0c9b3f4d4fdda1b6123ac6e28f058c","907c17ad5a05eecb29b42b36cc8fec6437be27cc4986bb3a218e4f74f606911c","ce60a562cd2a92f37a88f2ddd99a3abfbc5848d7baf38c48fb8d3243701fcb75","a726ad2d0a98bfffbe8bc1cd2d90b6d831638c0adc750ce73103a471eb9a891c","f44c0c8ce58d3dacac016607a1a90e5342d830ea84c48d2e571408087ae55894","75a315a098e630e734d9bc932d9841b64b30f7a349a20cf4717bf93044eff113","9131d95e32b3d4611d4046a613e022637348f6cebfe68230d4e81b691e4761a1","b03aa292cfdcd4edc3af00a7dbd71136dd067ec70a7536b655b82f4dd444e857","b6e2b0448ced813b8c207810d96551a26e7d7bb73255eea4b9701698f78846d6","8ae10cd85c1bd94d2f2d17c4cbd25c068a4b2471c70c2d96434239f97040747a","9ed5b799c50467b0c9f81ddf544b6bcda3e34d92076d6cab183c84511e45c39f","b4fa87cc1833839e51c49f20de71230e259c15b2c9c3e89e4814acc1d1ef10de","e90ac9e4ac0326faa1bc39f37af38ace0f9d4a655cd6d147713c653139cf4928","ea27110249d12e072956473a86fd1965df8e1be985f3b686b4e277afefdde584","8776a368617ce51129b74db7d55c3373dadcce5d0701e61d106e99998922a239","5666075052877fe2fdddd5b16de03168076cf0f03fbca5c1d4a3b8f43cba570c","9108ab5af05418f599ab48186193b1b07034c79a4a212a7f73535903ba4ca249","bb4e2cdcadf9c9e6ee2820af23cee6582d47c9c9c13b0dca1baaffe01fbbcb5f","6e30d0b5a1441d831d19fe02300ab3d83726abd5141cbcc0e2993fa0efd33db4","423f28126b2fc8d8d6fa558035309000a1297ed24473c595b7dec52e5c7ebae5","fb30734f82083d4790775dae393cd004924ebcbfde49849d9430bf0f0229dd16","2c92b04a7a4a1cd9501e1be338bf435738964130fb2ad5bd6c339ee41224ac4c","c5c5f0157b41833180419dacfbd2bcce78fb1a51c136bd4bcba5249864d8b9b5","02ae43d5bae42efcd5a00d3923e764895ce056bca005a9f4e623aa6b4797c8af","db6e01f17012a9d7b610ae764f94a1af850f5d98c9c826ad61747dca0fb800bd","8a44b424edee7bb17dc35a558cc15f92555f14a0441205613e0e50452ab3a602","24a00d0f98b799e6f628373249ece352b328089c3383b5606214357e9107e7d5","33637e3bc64edd2075d4071c55d60b32bdb0d243652977c66c964021b6fc8066","0f0ad9f14dedfdca37260931fac1edf0f6b951c629e84027255512f06a6ebc4c","16ad86c48bf950f5a480dc812b64225ca4a071827d3d18ffc5ec1ae176399e36","8cbf55a11ff59fd2b8e39a4aa08e25c5ddce46e3af0ed71fb51610607a13c505","d5bc4544938741f5daf8f3a339bfbf0d880da9e89e79f44a6383aaf056fe0159","97f9169882d393e6f303f570168ca86b5fe9aab556e9a43672dae7e6bb8e6495","7c9adb3fcd7851497818120b7e151465406e711d6a596a71b807f3a17853cb58","6752d402f9282dd6f6317c8c048aaaac27295739a166eed27e00391b358fed9a","9fd7466b77020847dbc9d2165829796bf7ea00895b2520ff3752ffdcff53564b","fbfc12d54a4488c2eb166ed63bab0fb34413e97069af273210cf39da5280c8d6","85a84240002b7cf577cec637167f0383409d086e3c4443852ca248fc6e16711e","84794e3abd045880e0fadcf062b648faf982aa80cfc56d28d80120e298178626","053d8b827286a16a669a36ffc8ccc8acdf8cc154c096610aa12348b8c493c7b8","3cce4ce031710970fe12d4f7834375f5fd455aa129af4c11eb787935923ff551","8f62cbd3afbd6a07bb8c934294b6bfbe437021b89e53a4da7de2648ecfc7af25","62c3621d34fb2567c17a2c4b89914ebefbfbd1b1b875b070391a7d4f722e55dc","c05ac811542e0b59cb9c2e8f60e983461f0b0e39cea93e320fad447ff8e474f3","8e7a5b8f867b99cc8763c0b024068fb58e09f7da2c4810c12833e1ca6eb11c4f","132351cbd8437a463757d3510258d0fa98fd3ebef336f56d6f359cf3e177a3ce","df877050b04c29b9f8409aa10278d586825f511f0841d1ec41b6554f8362092b","33d1888c3c27d3180b7fd20bac84e97ecad94b49830d5dd306f9e770213027d1","ee942c58036a0de88505ffd7c129f86125b783888288c2389330168677d6347f","a3f317d500c30ea56d41501632cdcc376dae6d24770563a5e59c039e1c2a08ec","eb21ddc3a8136a12e69176531197def71dc28ffaf357b74d4bf83407bd845991","0c1651a159995dfa784c57b4ea9944f16bdf8d924ed2d8b3db5c25d25749a343","aaa13958e03409d72e179b5d7f6ec5c6cc666b7be14773ae7b6b5ee4921e52db","0a86e049843ad02977a94bb9cdfec287a6c5a0a4b6b5391a6648b1a122072c5a","40f06693e2e3e58526b713c937895c02e113552dc8ba81ecd49cdd9596567ddb","4ed5e1992aedb174fb8f5aa8796aa6d4dcb8bd819b4af1b162a222b680a37fa0","d7f4bd46a8b97232ea6f8c28012b8d2b995e55e729d11405f159d3e00c51420a","d604d413aff031f4bfbdae1560e54ebf503d374464d76d50a2c6ded4df525712","e4f4f9cf1e3ac9fd91ada072e4d428ecbf0aa6dc57138fb797b8a0ca3a1d521c","12bfd290936824373edda13f48a4094adee93239b9a73432db603127881a300d","340ceb3ea308f8e98264988a663640e567c553b8d6dc7d5e43a8f3b64f780374","c5a769564e530fba3ec696d0a5cff1709b9095a0bdf5b0826d940d2fc9786413","7124ef724c3fc833a17896f2d994c368230a8d4b235baed39aa8037db31de54f","5de1c0759a76e7710f76899dcae601386424eab11fb2efaf190f2b0f09c3d3d3","9c5ee8f7e581f045b6be979f062a61bf076d362bf89c7f966b993a23424e8b0d","1a11df987948a86aa1ec4867907c59bdf431f13ed2270444bf47f788a5c7f92d","8018dd2e95e7ce6e613ddd81672a54532614dc745520a2f9e3860ff7fb1be0ca","b756781cd40d465da57d1fc6a442c34ae61fe8c802d752aace24f6a43fedacee","0fe76167c87289ea094e01616dcbab795c11b56bad23e1ef8aba9aa37e93432a","3a45029dba46b1f091e8dc4d784e7be970e209cd7d4ff02bd15270a98a9ba24b","032c1581f921f8874cf42966f27fd04afcabbb7878fa708a8251cac5415a2a06","69c68ed9652842ce4b8e495d63d2cd425862104c9fb7661f72e7aa8a9ef836f8","0e704ee6e9fd8b6a5a7167886f4d8915f4bc22ed79f19cb7b32bd28458f50643","06f62a14599a68bcde148d1efd60c2e52e8fa540cc7dcfa4477af132bb3de271","904a96f84b1bcee9a7f0f258d17f8692e6652a0390566515fe6741a5c6db8c1c","11f19ce32d21222419cecab448fa335017ebebf4f9e5457c4fa9df42fa2dcca7","2e8ee2cbb5e9159764e2189cf5547aebd0e6b0d9a64d479397bb051cd1991744","1b0471d75f5adb7f545c1a97c02a0f825851b95fe6e069ac6ecaa461b8bb321d","1d157c31a02b1e5cca9bc495b3d8d39f4b42b409da79f863fb953fbe3c7d4884","07baaceaec03d88a4b78cb0651b25f1ae0322ac1aa0b555ae3749a79a41cba86","619a132f634b4ebe5b4b4179ea5870f62f2cb09916a25957bff17b408de8b56d","f60fa446a397eb1aead9c4e568faf2df8068b4d0306ebc075fb4be16ed26b741","f3cb784be4d9e91f966a0b5052a098d9b53b0af0d341f690585b0cc05c6ca412","350f63439f8fe2e06c97368ddc7fb6d6c676d54f59520966f7dbbe6a4586014e","eba613b9b357ac8c50a925fa31dc7e65ff3b95a07efbaa684b624f143d8d34ba","45b74185005ed45bec3f07cac6e4d68eaf02ead9ff5a66721679fb28020e5e7c","0f6199602df09bdb12b95b5434f5d7474b1490d2cd8cc036364ab3ba6fd24263","c8ca7fd9ec7a3ec82185bfc8213e4a7f63ae748fd6fced931741d23ef4ea3c0f","5c6a8a3c2a8d059f0592d4eab59b062210a1c871117968b10797dee36d991ef7","ad77fd25ece8e09247040826a777dc181f974d28257c9cd5acb4921b51967bd8","2dca2e0e4e286242a6841f73970258dc85d77b8416a3e2e667b08b7610a7bf52","dc6851ed9e14bdd116b759e9992a54abeb9143849de9264f45524e034428ba89","81bdf7710817d9aead1d8d1e27d8939283606d1eb7047b5a2abfcf03e764a78d","b1ce382697e238f8c72aa33f198ceeccaca13ddba9f9d904e3b7f245fe4271bf","6f3ae7a910d6564e77744f2b7a52d0a2a9e38f84a4232bf0c8df6481b0c63410","4642d56744c9a2a7d11d141c5cc8d777ba92bc03b2fe544171eb26e7d1982a90","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","db51da097787c245478c2c1a9fafaa233c67f59fbe0b73b988161f592ac8081a","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93",{"version":"469e434106d97bfd933b8dedb0e0b10b8f278a968617257c06ef409a9592f45b","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},{"version":"d0094fb7e6b14736e20deec1c4b1b14201ba5105bc363f43fd692d9191e35a46","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"284d20bc3222c59f3e49c7bb4614aa987ace07fdca83074efe7b5cea818db5a8","462781c32243f8e1e0d2b45f95910d7a37b43fe50aa163e1a269eb4f0f857644","c20fcdf66dac5e7992d94af77afcf3569de3fc3e8413ebf0a73bfbc014681996",{"version":"a9100f34ece23a015a17d01f311b5b999fef2ab035fc1077d6eddc5d91a6ccbc","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"b0b5e6e9d4dd1364701d3d3dcf67e51faf73677d1cadab2772850d8b2df098d3",{"version":"fd9e27c4c7d7e00bf84390b4ef097efec248d6bda782cfe3b4f4fef217624729","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"7249f48e5c90a1bc60034bb725c99c164384c491e278ec84cef6d5d7673bfd4e",{"version":"9a33b65f78ebcbdc3bc8e203efa10ef093862975e9118459ae5c7a410192c335","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93",{"version":"7186c1930cfeeb33352757bded124981f1bcddfb7d68ee056a7933cb8cec7547","signature":"e2152b19013b05846ac8a59b5ecc7ab65878544e08fa496c029ffd29e2808768"},"ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","52733f535154ef2d8c8ea637bdc333d8709df0ca0d9af6f9ec1826dc6d18d0a8","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","4a1bd1c37242e1b21c84c4125f663c85ba82f73b0d8b6694fdeda89a64926229","1e1d7f8d74462b247db772fd60785edad2316279e5042f36d4d36c69c30f0130","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","dcef3c19c38995968ed8b166042a2eca57bcf144d61123915a2986c9396333a5","5dd5526e11d8b7dfadc58f27a1029e6a6ec2846853da2e9e6d849612f2381960","c8111cdca65f05c12aff325236a142ecc1d796e090f9acac2399b457ce975f82",{"version":"e82b1f4043180d80d5cdb6aa7242cc2c16128bcc14838df3a9e9dda058d32c4f","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"5715caf1bd66acb4ba1e853aa9c2ad64206a4cc19aab2aa40808d938fdd06529",{"version":"28d624142378a29ea33625fe2f911932b9fb07f07be8ed9973febdc9a3cfc5ff","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"c18e9d86ad55052d913ffa82d73ad73686a51239afe2092ba64288b52afdef3c",{"version":"30a9d7dcec1d860fbc32886259aa9494e9ff5966978d9b8c60e0637af9fb5d22","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"ce1cbfbd3061e769078eb895971ff55eec1970c3d22471383177434f239248b8",{"version":"3c597416e8e9c56d2f5293241f81fb3fe4a1d476cc1513cf94561c84cb8ae937","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"65a47d7f56eb0a61474280723b2eb3bf0d215bae01bf2286a67842565b10e29f",{"version":"8860ac374430ee3f6a62abcfc39fa3ca243bec05cd177bc17a0c292200e7a9b8","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"2fcec9510574e2d45cbd8e4346cf68599960cc0527e6cc1b93674701a2aaa2c2",{"version":"952e3971af9b4807faa41ae62d6ab876a11c2661afc872e42c2f852c48c7ba53","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"d8b98b88afee0c08b01e4df36a90e2765c1bd83eb6c315705058becd6a2b2349",{"version":"67a30cabd2a73cd0ba1643792820f572a7160d0284bfaf8b7ece4f1c98e2f62b","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","b5cebafe04e6d2d8c40a558af7e9cb5bd033580682dd255efb19bb15b60d47d9","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93",{"version":"f5a80cc8ceae83a0c2ec28b814ddfd36a3099b8dba9c30dcf7d709f36d5c94d0","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"f1c866c36e3278f0723b5bcb3688ef0e8a9786cb1cfc41b68559dfa26b5401f1","0cdaf4ac4a21b8a313261acfc51ebb4440fd483c89b0d4cb52b3546c5f96771a","9bb2b3f1e313e080a3c08e2ea425494b39e36905b39f668e5994bf429f7fd349",{"version":"7e846212bb1f1ab59dbdc82dffbf59a85bdff8746616947a0fe7c7b3d681bea5","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"967953d8cdad24070162359564e811da43f164123c3bd72dedb23ead6aea6edd",{"version":"6de299fae186a9489a0a2732e994cead665a5796f81812cf56130391b8e9b1b7","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"9800c194aeb75d4fd988bda97162e31d0c30439658fd2715126ec88bc50353a9",{"version":"36c4ea4a9a66a1bb7e99a4dbccc75d410e9b7cbfca9879444973bb2cc13157f9","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},{"version":"84748ca0228162582a4f4a288d2052501ba2c54408968c5836f3d5eb1639f42f","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"084cca4b7057b5c18305ebd520d7e335734899f704574df8fa8346f34787a786","8f2192bc07d89b12ff44016407f500f6ebb69749d736bd7b0f09eb9659f056dd","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","104e532fde612885b512e4fe7790bc00fe3b60c5f32d699afa8ea8beaa2973ab","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","17a3d7bd50644aa8df7f1f029ab43e69d29dcb0f18b48394cbfc60d3c4fb1e40",{"version":"fd31517c198fe86b337697731d93d6a88651d43933d4082f980474b41e62d34a","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"3f15e7b6ccedd18f9becf47f58b10f8e583ed8f9323eff2328d6eddd9dc09c1c",{"version":"9eb05f810eae0d91971fb7c7e8878e54b22aae685f679b5c2fdb827ba367d920","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"387b20d06048ac2c4839b9c7a65f7c8f7008cd5e5c8601b1c2f763e09448962b",{"version":"7e4b47b523626c213fc87ea76bce0b9ce85629bd5377cc765a2c6e3e042f7dda","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"01683fb32aba52b5bac74c894fa9ddf50030a655b938fc9756dcab01d7376587",{"version":"d98470c8c833bb1f643412138dde0c63519c76a9f3d9babdb1cb1b96748fef95","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"366bf38d4cd2a60c9cd898c1c36055e3803810c19ad7234465458e484526b0ae",{"version":"30408e434ab82db0bd013831b288605efe12f300884aba5203e7df8786acc969","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"30fc51fc071b840b82011f2c3774a763c338ccfbf207c81e2bb6cb37294a31e5",{"version":"2c2c8db76fb9aab9e2803ff71c10de612393115755a47f548726a1e52176f06e","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"d3f795a33e0caec8192f48faa3eda6ac715fb9134746de8b0a3dcf8a893c2d09",{"version":"c21686d47127e04f27d829c5f67041a184945962b188c3a2050662558e4c1b36","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"1e0b6a5126862caf5eb750f7bdda4598ffafed6eb323f708bcbbf4b30e5114e8",{"version":"6b0c21b943ac6d54c27eb1fbbfb36c94fb4c8117c86f910355f79bc3a6679c0e","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"db97f7a050697dc38e432e34321d0b08a549ba3f816f9a13f0c67665d1df07e5",{"version":"38ac1460d049ae75e4c7b807b5a749fe309d696ed998268b972c7a44c33cf446","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"a2deee877b3d0d7467652a8307bc194282b08802b68d710e42928feb99295d7f",{"version":"67c5f3bc610e0b9f9c5a880cd27d7be49fea614957ac5e5a8053c9197061d507","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"099e56d511fcb32fed5ecfbea20a2fb42dcbf1979d09986068f667fd79d29b07",{"version":"5eaef84458502e2a3593cb23c967f531d2d14c98bc311d477b02ca72d6d635a0","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"9a1f3ae99efb4ca99b009f72db8c9b3e085f6c5d45cb449b245f80a0e6bf2e9e",{"version":"824247b838e37a3f64455306ff110384bbcf00fee46f9fe0e177d70e5ca5f068","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","4a51b5d9c1bb74f5598c07f388b9964d502fe84bea9aaeb4984d2159b6026adb","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","fd87004d8e80d36772714e5340219f1c22a058a2529799f50f032a0a1a92f291","488ad1c525e0e9b802a00bf9a39d056bd9ce664580d82fc79cae86b470577b58","e47c7128ca5faf7f27b9126a938bc4767e991759ba319fa629eaeba197da5ea2","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93","340a7e6b89e2efeb63212c5bed7e284bc0c956bb7cbd5c2159b8822f3d5135f3","2a2a202c20b06d155a50872d295cf4ac0eea8e23dd0f12ced7e4b6a77329f4d1",{"version":"943569a957aa9e3be7e1226e219efa7c7fc487a68ad1b8c97e6c4f7d58469712","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},{"version":"f4da04f329d74993e294f83a2c62a4b939adf4e9068f4e58169bf65ca981605b","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"549144306542cb93d81cfafb696c24ac7a7df6d5157681258fdbdc6b585f8bec",{"version":"23977bdd66e33f2155d2a3b22b55839888e236e8da8fc5706fd57413b5f07442","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"b9de1a6c4973b422655642aa51135d7da1ee71d76a656a0776c6c4cbdd52df79","ddd578018a259d1c494c834bdd8707769d07d1eb64f87f5217560cd2181b9e93",{"version":"6c051992d1a62fe8ff910ae5d3d73bf1adfbd5731d855be6a390a0cc18553123","signature":"69b6739a6d79c708ed054c451793d5160b26305d9a367d5ec63d6211aa8f6073"},{"version":"ebdb779b892ebbd9acc274b49591b513182bac0ff9991c11e2f5acae66ada532","signature":"b52dcd199c97746007e4589749483d8b943e6bac0bbf6a90c0b9c7be86f9b793"},"301f6e805e19002a8d23112c16e5ae1b149f53071ec701d8078874e8882d22d8","27413281f68f19035cc143958d5a826cc529b2157533a0fbe4018b88bf2f1ee3","abed2fcecfc488c97265276567a7eaeac7acb0abf954ab6fd6ccfbab2243b3e5"],"root":[61,357],"options":{"declaration":false,"declarationMap":false,"esModuleInterop":true,"experimentalDecorators":true,"importHelpers":true,"inlineSourceMap":true,"inlineSources":true,"module":7,"noEmitOnError":false,"noFallthroughCasesInSwitch":true,"noImplicitOverride":true,"noImplicitReturns":true,"noPropertyAccessFromIndexSignature":true,"outDir":"../../../..","skipLibCheck":true,"strict":true,"target":9,"tsBuildInfoFile":"./.tsbuildinfo"},"fileIdsList":[[60],[60,273],[250,253,254],[250,253],[250,251,252,253],[253,254,255],[250,253,254,256,258],[62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,78,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,118,119,120,121,122,123,124,125,126,127,128,129,131,132,133,134,135,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,181,182,183,185,194,196,197,198,199,200,201,203,204,206,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249],[107],[63,66],[65],[65,66],[62,63,64,66],[63,65,66,223],[66],[62,65,107],[65,66,223],[65,231],[63,65,66],[75],[98],[119],[65,66,107],[66,114],[65,66,107,125],[65,66,125],[66,166],[66,107],[62,66,184],[62,66,185],[207],[191,193],[202],[191],[62,66,184,191,192],[184,185,193],[205],[62,66,191,192,193],[64,65,66],[62,66],[63,65,185,186,187,188],[107,185,186,187,188],[185,187],[65,186,187,189,190,194],[62,65],[66,209],[67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,108,109,110,111,112,113,115,116,117,118,119,120,121,122,123,124,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182],[195],[59],[60,250,253,255,272,274,276],[60,253,254,300,306,356],[60,183,253,254,258,263,277,281,300,301,306,347,349,351,353,355],[60,253,255,257,258,343,345],[60,258,259,264,268,282,284,286,288,290,292,294,302,304,306,310,312,314,316,318,320,322,324,326,328,330,332,334,336,342],[60,253,266,268],[60,253,258,266,267],[60,253,254,263,332],[60,250,253,254,263,277,301,309,331],[60,253,254,263,324],[60,253,254,256,263,277,301,309,323],[60,253,254,258,309],[60,250,253,254,258,308],[60,253,254,336],[60,253,254,277,309,335],[60,253,254,262,310],[60,253,254,262,277,301,307,309],[60,253,254,263,266,326],[60,253,254,258,263,266,277,281,309,325],[60,253,254,263,322],[60,253,254,263,276,277,301,309,321],[60,253,254,263,330],[60,250,253,254,258,263,277,329],[60,253,254,263,266,282],[60,250,253,254,258,263,266,269,271,277,281],[60,253,254,263,266,294],[60,253,254,258,263,266,293],[60,253,264],[60,253,254,258,260,262,263],[60,253,266,284],[60,253,266,283],[60,253,254,258,334],[60,253,254,258,333],[60,253,254,258,263,316],[60,253,254,258,263,266,276,281,315],[60,253,254,263,306],[60,253,254,258,263,277,305],[60,253,266,286],[60,253,266,285],[60,253,254,263,304],[60,253,254,258,263,276,277,301,303],[60,253,254,263,302],[60,253,254,258,263,276,277,295,297,301],[60,253,254,266,320],[60,253,254,258,266,276,281,319],[60,253,254,258,263,318],[60,253,254,258,263,266,276,281,317],[60,253,254,290],[60,183,250,253,254,289],[60,253,254,266,342],[60,253,254,258,266,337,339,341],[60,339,340],[60,338],[60,253,254,263,288],[60,253,254,258,263,271,287],[60,253,266,292],[60,253,254,258,262,266,291],[60,250,253,258,276,301,311],[60,253,258,276,313],[60,250,253,255,344],[60,250,253,255,258,274,275],[60,253,254,300],[60,253,254,263,299],[60,250,253,298,300],[60,253,258,351],[60,253,254,258,350],[60,253,328],[60,253,258,327],[60,253,254,258,349],[60,253,254,258,276,281,297,301,348],[60,253,254,262],[60,253,254,261],[60,253,254,266],[60,253,254,265],[60,253,254,256,352],[60,253,270],[60,253,254,355],[60,253,254,280,281,354],[60,279],[60,250,253,278,280],[60,253,277,296],[60,61,256,346,356]],"referencedMap":[[273,1],[274,2],[255,3],[254,4],[253,5],[263,4],[256,6],[258,7],[250,8],[201,9],[199,9],[249,10],[214,11],[213,11],[114,12],[65,13],[221,12],[222,12],[224,14],[225,12],[226,15],[125,16],[227,12],[198,12],[228,12],[229,17],[230,12],[231,11],[232,18],[233,12],[234,12],[235,12],[236,12],[237,11],[238,12],[239,12],[240,12],[241,12],[242,19],[243,12],[244,12],[245,12],[246,12],[247,12],[64,10],[67,15],[68,15],[69,15],[70,15],[71,15],[72,15],[73,15],[74,12],[76,20],[77,15],[75,15],[78,15],[79,15],[80,15],[81,15],[82,15],[83,15],[84,12],[85,15],[86,15],[87,15],[88,15],[89,15],[90,12],[91,15],[92,15],[93,15],[94,15],[95,15],[96,15],[97,12],[99,21],[98,15],[100,15],[101,15],[102,15],[103,15],[104,19],[105,12],[106,12],[120,22],[108,23],[109,15],[110,15],[111,12],[112,15],[113,15],[115,24],[116,15],[117,15],[118,15],[119,15],[121,15],[122,15],[123,15],[124,15],[126,25],[127,15],[128,15],[129,15],[130,12],[131,15],[132,26],[133,26],[134,26],[135,12],[136,15],[137,15],[138,15],[143,15],[139,15],[140,12],[141,15],[142,12],[144,15],[145,15],[146,15],[147,15],[148,15],[149,15],[150,12],[151,15],[152,15],[153,15],[154,15],[155,15],[156,15],[157,15],[158,15],[159,15],[160,15],[161,15],[162,15],[163,15],[164,15],[165,15],[166,15],[167,27],[168,15],[169,15],[170,15],[171,15],[172,15],[173,15],[174,12],[175,12],[176,12],[177,12],[178,12],[179,15],[180,15],[181,15],[182,15],[200,28],[248,12],[185,29],[184,30],[208,31],[207,32],[203,33],[202,32],[204,34],[193,35],[191,36],[206,37],[205,34],[194,38],[107,39],[63,40],[62,15],[189,41],[190,42],[188,43],[186,15],[195,44],[66,45],[212,11],[210,46],[183,47],[196,48],[60,49],[272,1],[277,50],[347,51],[356,52],[257,1],[346,53],[259,1],[343,54],[267,55],[268,56],[331,57],[332,58],[323,59],[324,60],[308,61],[309,62],[335,63],[336,64],[307,65],[310,66],[325,67],[326,68],[321,69],[322,70],[329,71],[330,72],[269,73],[282,74],[293,75],[294,76],[260,77],[264,78],[283,79],[284,80],[333,81],[334,82],[315,83],[316,84],[305,85],[306,86],[285,87],[286,88],[303,89],[304,90],[295,91],[302,92],[319,93],[320,94],[317,95],[318,96],[289,97],[290,98],[337,99],[342,100],[340,1],[341,101],[338,1],[339,102],[287,103],[288,104],[291,105],[292,106],[311,1],[312,107],[313,1],[314,108],[344,1],[345,109],[275,1],[276,110],[299,111],[300,112],[298,1],[301,113],[350,114],[351,115],[327,116],[328,117],[348,118],[349,119],[261,120],[262,121],[265,122],[266,123],[352,1],[353,124],[270,1],[271,125],[354,126],[355,127],[279,1],[280,128],[278,1],[281,129],[296,1],[297,130],[61,1],[357,131]],"semanticDiagnosticsPerFile":[61,257,259,260,261,265,267,269,270,272,273,275,278,279,283,285,287,289,291,293,295,296,298,299,303,305,307,308,310,311,313,315,317,319,321,323,325,327,329,331,333,335,337,338,340,343,344,346,347,348,350,352,354,357]},"version":"5.5.4"} \ No newline at end of file diff --git a/apps/frontend/.vscode/extensions.json b/apps/frontend/.vscode/extensions.json new file mode 100644 index 0000000..77b3745 --- /dev/null +++ b/apps/frontend/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 + "recommendations": ["angular.ng-template"] +} diff --git a/apps/frontend/.vscode/launch.json b/apps/frontend/.vscode/launch.json new file mode 100644 index 0000000..925af83 --- /dev/null +++ b/apps/frontend/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "ng serve", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: start", + "url": "http://localhost:4200/" + }, + { + "name": "ng test", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: test", + "url": "http://localhost:9876/debug.html" + } + ] +} diff --git a/apps/frontend/.vscode/tasks.json b/apps/frontend/.vscode/tasks.json new file mode 100644 index 0000000..a298b5b --- /dev/null +++ b/apps/frontend/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "start", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "(.*?)" + }, + "endsPattern": { + "regexp": "bundle generation complete" + } + } + } + }, + { + "type": "npm", + "script": "test", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "(.*?)" + }, + "endsPattern": { + "regexp": "bundle generation complete" + } + } + } + } + ] +} diff --git a/apps/frontend/README.md b/apps/frontend/README.md new file mode 100644 index 0000000..9e75bc4 --- /dev/null +++ b/apps/frontend/README.md @@ -0,0 +1,27 @@ +# WebsiteBaseV2 + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 18.2.8. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. diff --git a/apps/frontend/angular.json b/apps/frontend/angular.json new file mode 100644 index 0000000..367a713 --- /dev/null +++ b/apps/frontend/angular.json @@ -0,0 +1,115 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "WebsiteBaseV2": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": "dist/website-base-v2", + "index": "src/index.html", + "browser": "src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + { + "glob": "**/*", + "input": "public" + }, + { + "glob": "sitemap.xml", + "input": "src", + "output": "/" + }, + { + "glob": "robots.txt", + "input": "src", + "output": "/" + } + ], + "styles": [ + "src/styles.scss" + ], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "2MB", + "maximumError": "4MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "50kB", + "maximumError": "100kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "WebsiteBaseV2:build:production" + }, + "development": { + "buildTarget": "WebsiteBaseV2:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": [ + "zone.js", + "zone.js/testing" + ], + "tsConfig": "tsconfig.spec.json", + "inlineStyleLanguage": "scss", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.scss" + ], + "scripts": [] + } + } + } + } + }, + "cli": { + "analytics": false + } +} \ No newline at end of file diff --git a/apps/frontend/environments/environment.prod.ts b/apps/frontend/environments/environment.prod.ts new file mode 100644 index 0000000..1f48b7f --- /dev/null +++ b/apps/frontend/environments/environment.prod.ts @@ -0,0 +1,4 @@ +export const environment = { + production: true, + apiUrl: 'https://api.leonardsmedia.de' // Später deine Production URL +}; \ No newline at end of file diff --git a/apps/frontend/environments/environment.ts b/apps/frontend/environments/environment.ts new file mode 100644 index 0000000..3ef182a --- /dev/null +++ b/apps/frontend/environments/environment.ts @@ -0,0 +1,5 @@ +export const environment = { + production: false, + apiUrl: 'http://192.168.178.111:3000' + // apiUrl: 'https://api.leonardsmedia.de' +}; diff --git a/apps/frontend/index.html b/apps/frontend/index.html new file mode 100644 index 0000000..475a6f0 --- /dev/null +++ b/apps/frontend/index.html @@ -0,0 +1,64 @@ + + + + + Leonards & Brandenburger IT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/frontend/main.ts b/apps/frontend/main.ts new file mode 100644 index 0000000..35b00f3 --- /dev/null +++ b/apps/frontend/main.ts @@ -0,0 +1,6 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { AppComponent } from './app/app.component'; + +bootstrapApplication(AppComponent, appConfig) + .catch((err) => console.error(err)); diff --git a/apps/frontend/nginx.conf b/apps/frontend/nginx.conf new file mode 100644 index 0000000..2dce889 --- /dev/null +++ b/apps/frontend/nginx.conf @@ -0,0 +1,23 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Angular SPA Routing - alle Routen auf index.html umleiten + location / { + try_files $uri $uri/ /index.html; + } + + # Caching für statische Assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + # Gzip Kompression + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + gzip_min_length 1000; +} diff --git a/apps/frontend/package.json b/apps/frontend/package.json new file mode 100644 index 0000000..5cf6ce5 --- /dev/null +++ b/apps/frontend/package.json @@ -0,0 +1,41 @@ +{ + "name": "website-base-v2", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test", + "prebuild": "node tools/generate-seo-assets.mjs" + }, + "private": true, + "dependencies": { + "@angular/animations": "^18.2.0", + "@angular/common": "^18.2.0", + "@angular/compiler": "^18.2.0", + "@angular/core": "^18.2.0", + "@angular/forms": "^18.2.0", + "@angular/platform-browser": "^18.2.0", + "@angular/platform-browser-dynamic": "^18.2.0", + "@angular/router": "^18.2.0", + "@popperjs/core": "^2.11.8", + "jspdf": "^4.0.0", + "tslib": "^2.3.0", + "zone.js": "~0.14.10" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^18.2.8", + "@angular/cli": "^18.2.8", + "@angular/compiler-cli": "^18.2.0", + "@types/jasmine": "~5.1.0", + "@types/jspdf": "^1.3.3", + "jasmine-core": "~5.2.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "typescript": "~5.5.2" + } +} diff --git a/apps/frontend/public/assets/cards/1-min.png b/apps/frontend/public/assets/cards/1-min.png new file mode 100644 index 0000000..cbf553e Binary files /dev/null and b/apps/frontend/public/assets/cards/1-min.png differ diff --git a/apps/frontend/public/assets/cards/1.png b/apps/frontend/public/assets/cards/1.png new file mode 100644 index 0000000..b2a770e Binary files /dev/null and b/apps/frontend/public/assets/cards/1.png differ diff --git a/apps/frontend/public/assets/cards/2.png b/apps/frontend/public/assets/cards/2.png new file mode 100644 index 0000000..39956f4 Binary files /dev/null and b/apps/frontend/public/assets/cards/2.png differ diff --git a/apps/frontend/public/assets/cards/3.png b/apps/frontend/public/assets/cards/3.png new file mode 100644 index 0000000..51f33fb Binary files /dev/null and b/apps/frontend/public/assets/cards/3.png differ diff --git a/apps/frontend/public/assets/cards/4-min.png b/apps/frontend/public/assets/cards/4-min.png new file mode 100644 index 0000000..146e807 Binary files /dev/null and b/apps/frontend/public/assets/cards/4-min.png differ diff --git a/apps/frontend/public/assets/cards/4.png b/apps/frontend/public/assets/cards/4.png new file mode 100644 index 0000000..6231221 Binary files /dev/null and b/apps/frontend/public/assets/cards/4.png differ diff --git a/apps/frontend/public/assets/cards/5.png b/apps/frontend/public/assets/cards/5.png new file mode 100644 index 0000000..1ddd2a9 Binary files /dev/null and b/apps/frontend/public/assets/cards/5.png differ diff --git a/apps/frontend/public/assets/cards/individual-min.png b/apps/frontend/public/assets/cards/individual-min.png new file mode 100644 index 0000000..3de5cd0 Binary files /dev/null and b/apps/frontend/public/assets/cards/individual-min.png differ diff --git a/apps/frontend/public/assets/cards/simple-min.png b/apps/frontend/public/assets/cards/simple-min.png new file mode 100644 index 0000000..0ddd6b3 Binary files /dev/null and b/apps/frontend/public/assets/cards/simple-min.png differ diff --git a/apps/frontend/public/assets/cards/standard-min.png b/apps/frontend/public/assets/cards/standard-min.png new file mode 100644 index 0000000..7b5bb65 Binary files /dev/null and b/apps/frontend/public/assets/cards/standard-min.png differ diff --git a/apps/frontend/public/assets/company.png b/apps/frontend/public/assets/company.png new file mode 100644 index 0000000..230f2cb Binary files /dev/null and b/apps/frontend/public/assets/company.png differ diff --git a/apps/frontend/public/assets/icons/.gitkeep b/apps/frontend/public/assets/icons/.gitkeep new file mode 100644 index 0000000..e18451d --- /dev/null +++ b/apps/frontend/public/assets/icons/.gitkeep @@ -0,0 +1,13 @@ +# PWA Icons Ordner +# Füge hier die App-Icons in folgenden Größen hinzu: +# - icon-72x72.png +# - icon-96x96.png +# - icon-128x128.png +# - icon-144x144.png +# - icon-152x152.png +# - icon-192x192.png +# - icon-384x384.png +# - icon-512x512.png +# +# Tipp: Verwende https://www.pwabuilder.com/imageGenerator +# oder https://maskable.app/editor für maskable icons diff --git a/apps/frontend/public/assets/icons/100.png b/apps/frontend/public/assets/icons/100.png new file mode 100644 index 0000000..bbe9ff8 Binary files /dev/null and b/apps/frontend/public/assets/icons/100.png differ diff --git a/apps/frontend/public/assets/icons/1024.png b/apps/frontend/public/assets/icons/1024.png new file mode 100644 index 0000000..094bfe3 Binary files /dev/null and b/apps/frontend/public/assets/icons/1024.png differ diff --git a/apps/frontend/public/assets/icons/114.png b/apps/frontend/public/assets/icons/114.png new file mode 100644 index 0000000..9d550e9 Binary files /dev/null and b/apps/frontend/public/assets/icons/114.png differ diff --git a/apps/frontend/public/assets/icons/120.png b/apps/frontend/public/assets/icons/120.png new file mode 100644 index 0000000..efae103 Binary files /dev/null and b/apps/frontend/public/assets/icons/120.png differ diff --git a/apps/frontend/public/assets/icons/128.png b/apps/frontend/public/assets/icons/128.png new file mode 100644 index 0000000..103c89e Binary files /dev/null and b/apps/frontend/public/assets/icons/128.png differ diff --git a/apps/frontend/public/assets/icons/144.png b/apps/frontend/public/assets/icons/144.png new file mode 100644 index 0000000..9fd94c4 Binary files /dev/null and b/apps/frontend/public/assets/icons/144.png differ diff --git a/apps/frontend/public/assets/icons/152.png b/apps/frontend/public/assets/icons/152.png new file mode 100644 index 0000000..98708ef Binary files /dev/null and b/apps/frontend/public/assets/icons/152.png differ diff --git a/apps/frontend/public/assets/icons/16.png b/apps/frontend/public/assets/icons/16.png new file mode 100644 index 0000000..28bb71e Binary files /dev/null and b/apps/frontend/public/assets/icons/16.png differ diff --git a/apps/frontend/public/assets/icons/167.png b/apps/frontend/public/assets/icons/167.png new file mode 100644 index 0000000..5013a80 Binary files /dev/null and b/apps/frontend/public/assets/icons/167.png differ diff --git a/apps/frontend/public/assets/icons/180.png b/apps/frontend/public/assets/icons/180.png new file mode 100644 index 0000000..f793dfe Binary files /dev/null and b/apps/frontend/public/assets/icons/180.png differ diff --git a/apps/frontend/public/assets/icons/192.png b/apps/frontend/public/assets/icons/192.png new file mode 100644 index 0000000..65332aa Binary files /dev/null and b/apps/frontend/public/assets/icons/192.png differ diff --git a/apps/frontend/public/assets/icons/20.png b/apps/frontend/public/assets/icons/20.png new file mode 100644 index 0000000..6539b36 Binary files /dev/null and b/apps/frontend/public/assets/icons/20.png differ diff --git a/apps/frontend/public/assets/icons/256.png b/apps/frontend/public/assets/icons/256.png new file mode 100644 index 0000000..c2a7783 Binary files /dev/null and b/apps/frontend/public/assets/icons/256.png differ diff --git a/apps/frontend/public/assets/icons/29.png b/apps/frontend/public/assets/icons/29.png new file mode 100644 index 0000000..3dd0c2d Binary files /dev/null and b/apps/frontend/public/assets/icons/29.png differ diff --git a/apps/frontend/public/assets/icons/32.png b/apps/frontend/public/assets/icons/32.png new file mode 100644 index 0000000..7714d40 Binary files /dev/null and b/apps/frontend/public/assets/icons/32.png differ diff --git a/apps/frontend/public/assets/icons/40.png b/apps/frontend/public/assets/icons/40.png new file mode 100644 index 0000000..7c31696 Binary files /dev/null and b/apps/frontend/public/assets/icons/40.png differ diff --git a/apps/frontend/public/assets/icons/50.png b/apps/frontend/public/assets/icons/50.png new file mode 100644 index 0000000..c0af9a2 Binary files /dev/null and b/apps/frontend/public/assets/icons/50.png differ diff --git a/apps/frontend/public/assets/icons/512.png b/apps/frontend/public/assets/icons/512.png new file mode 100644 index 0000000..cacb5f6 Binary files /dev/null and b/apps/frontend/public/assets/icons/512.png differ diff --git a/apps/frontend/public/assets/icons/57.png b/apps/frontend/public/assets/icons/57.png new file mode 100644 index 0000000..2636242 Binary files /dev/null and b/apps/frontend/public/assets/icons/57.png differ diff --git a/apps/frontend/public/assets/icons/58.png b/apps/frontend/public/assets/icons/58.png new file mode 100644 index 0000000..2ec91d6 Binary files /dev/null and b/apps/frontend/public/assets/icons/58.png differ diff --git a/apps/frontend/public/assets/icons/60.png b/apps/frontend/public/assets/icons/60.png new file mode 100644 index 0000000..4c2b96a Binary files /dev/null and b/apps/frontend/public/assets/icons/60.png differ diff --git a/apps/frontend/public/assets/icons/64.png b/apps/frontend/public/assets/icons/64.png new file mode 100644 index 0000000..770566a Binary files /dev/null and b/apps/frontend/public/assets/icons/64.png differ diff --git a/apps/frontend/public/assets/icons/72.png b/apps/frontend/public/assets/icons/72.png new file mode 100644 index 0000000..1000b8c Binary files /dev/null and b/apps/frontend/public/assets/icons/72.png differ diff --git a/apps/frontend/public/assets/icons/76.png b/apps/frontend/public/assets/icons/76.png new file mode 100644 index 0000000..f8b9984 Binary files /dev/null and b/apps/frontend/public/assets/icons/76.png differ diff --git a/apps/frontend/public/assets/icons/80.png b/apps/frontend/public/assets/icons/80.png new file mode 100644 index 0000000..1fd5bb7 Binary files /dev/null and b/apps/frontend/public/assets/icons/80.png differ diff --git a/apps/frontend/public/assets/icons/87.png b/apps/frontend/public/assets/icons/87.png new file mode 100644 index 0000000..1850f36 Binary files /dev/null and b/apps/frontend/public/assets/icons/87.png differ diff --git a/apps/frontend/public/assets/icons/LargeTile.scale-100.png b/apps/frontend/public/assets/icons/LargeTile.scale-100.png new file mode 100644 index 0000000..2fba282 Binary files /dev/null and b/apps/frontend/public/assets/icons/LargeTile.scale-100.png differ diff --git a/apps/frontend/public/assets/icons/LargeTile.scale-125.png b/apps/frontend/public/assets/icons/LargeTile.scale-125.png new file mode 100644 index 0000000..b22fc23 Binary files /dev/null and b/apps/frontend/public/assets/icons/LargeTile.scale-125.png differ diff --git a/apps/frontend/public/assets/icons/LargeTile.scale-150.png b/apps/frontend/public/assets/icons/LargeTile.scale-150.png new file mode 100644 index 0000000..f50efb1 Binary files /dev/null and b/apps/frontend/public/assets/icons/LargeTile.scale-150.png differ diff --git a/apps/frontend/public/assets/icons/LargeTile.scale-200.png b/apps/frontend/public/assets/icons/LargeTile.scale-200.png new file mode 100644 index 0000000..ea1df4b Binary files /dev/null and b/apps/frontend/public/assets/icons/LargeTile.scale-200.png differ diff --git a/apps/frontend/public/assets/icons/LargeTile.scale-400.png b/apps/frontend/public/assets/icons/LargeTile.scale-400.png new file mode 100644 index 0000000..516187f Binary files /dev/null and b/apps/frontend/public/assets/icons/LargeTile.scale-400.png differ diff --git a/apps/frontend/public/assets/icons/README.md b/apps/frontend/public/assets/icons/README.md new file mode 100644 index 0000000..f94cffe --- /dev/null +++ b/apps/frontend/public/assets/icons/README.md @@ -0,0 +1,30 @@ +# PWA Icons + +Diese SVG-Icons wurden automatisch generiert und dienen als Placeholder. + +## Für die Produktion: + +1. Erstelle ein quadratisches Logo (mindestens 512x512px) +2. Gehe zu https://www.pwabuilder.com/imageGenerator +3. Lade dein Logo hoch +4. Lade die generierten Icons herunter +5. Ersetze die SVG-Dateien hier mit den PNG-Dateien + +## Benötigte Größen: +- icon-72x72.png +- icon-96x96.png +- icon-128x128.png +- icon-144x144.png +- icon-152x152.png +- icon-192x192.png +- icon-384x384.png +- icon-512x512.png + +## Icon-Tipps: +- Verwende PNG mit Transparenz +- Halte wichtige Elemente in der "Safe Zone" (innere 80%) +- Teste mit https://maskable.app/editor + +## Schnelle Alternative: +Die SVG-Icons funktionieren auch, aber PNGs sind kompatibler. +Ändere in manifest.webmanifest die Endungen von .png zu .svg. diff --git a/apps/frontend/public/assets/icons/SmallTile.scale-100.png b/apps/frontend/public/assets/icons/SmallTile.scale-100.png new file mode 100644 index 0000000..d29c154 Binary files /dev/null and b/apps/frontend/public/assets/icons/SmallTile.scale-100.png differ diff --git a/apps/frontend/public/assets/icons/SmallTile.scale-125.png b/apps/frontend/public/assets/icons/SmallTile.scale-125.png new file mode 100644 index 0000000..9521cc4 Binary files /dev/null and b/apps/frontend/public/assets/icons/SmallTile.scale-125.png differ diff --git a/apps/frontend/public/assets/icons/SmallTile.scale-150.png b/apps/frontend/public/assets/icons/SmallTile.scale-150.png new file mode 100644 index 0000000..0e61e9e Binary files /dev/null and b/apps/frontend/public/assets/icons/SmallTile.scale-150.png differ diff --git a/apps/frontend/public/assets/icons/SmallTile.scale-200.png b/apps/frontend/public/assets/icons/SmallTile.scale-200.png new file mode 100644 index 0000000..b6ad8ef Binary files /dev/null and b/apps/frontend/public/assets/icons/SmallTile.scale-200.png differ diff --git a/apps/frontend/public/assets/icons/SmallTile.scale-400.png b/apps/frontend/public/assets/icons/SmallTile.scale-400.png new file mode 100644 index 0000000..438dc31 Binary files /dev/null and b/apps/frontend/public/assets/icons/SmallTile.scale-400.png differ diff --git a/apps/frontend/public/assets/icons/SplashScreen.scale-100.png b/apps/frontend/public/assets/icons/SplashScreen.scale-100.png new file mode 100644 index 0000000..c9ea43a Binary files /dev/null and b/apps/frontend/public/assets/icons/SplashScreen.scale-100.png differ diff --git a/apps/frontend/public/assets/icons/SplashScreen.scale-125.png b/apps/frontend/public/assets/icons/SplashScreen.scale-125.png new file mode 100644 index 0000000..38c92f4 Binary files /dev/null and b/apps/frontend/public/assets/icons/SplashScreen.scale-125.png differ diff --git a/apps/frontend/public/assets/icons/SplashScreen.scale-150.png b/apps/frontend/public/assets/icons/SplashScreen.scale-150.png new file mode 100644 index 0000000..7d7a254 Binary files /dev/null and b/apps/frontend/public/assets/icons/SplashScreen.scale-150.png differ diff --git a/apps/frontend/public/assets/icons/SplashScreen.scale-200.png b/apps/frontend/public/assets/icons/SplashScreen.scale-200.png new file mode 100644 index 0000000..1ec3ac9 Binary files /dev/null and b/apps/frontend/public/assets/icons/SplashScreen.scale-200.png differ diff --git a/apps/frontend/public/assets/icons/SplashScreen.scale-400.png b/apps/frontend/public/assets/icons/SplashScreen.scale-400.png new file mode 100644 index 0000000..3b4a52e Binary files /dev/null and b/apps/frontend/public/assets/icons/SplashScreen.scale-400.png differ diff --git a/apps/frontend/public/assets/icons/Square150x150Logo.scale-100.png b/apps/frontend/public/assets/icons/Square150x150Logo.scale-100.png new file mode 100644 index 0000000..9f18990 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square150x150Logo.scale-100.png differ diff --git a/apps/frontend/public/assets/icons/Square150x150Logo.scale-125.png b/apps/frontend/public/assets/icons/Square150x150Logo.scale-125.png new file mode 100644 index 0000000..d0aeb63 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square150x150Logo.scale-125.png differ diff --git a/apps/frontend/public/assets/icons/Square150x150Logo.scale-150.png b/apps/frontend/public/assets/icons/Square150x150Logo.scale-150.png new file mode 100644 index 0000000..97e72c6 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square150x150Logo.scale-150.png differ diff --git a/apps/frontend/public/assets/icons/Square150x150Logo.scale-200.png b/apps/frontend/public/assets/icons/Square150x150Logo.scale-200.png new file mode 100644 index 0000000..bfc8e77 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square150x150Logo.scale-200.png differ diff --git a/apps/frontend/public/assets/icons/Square150x150Logo.scale-400.png b/apps/frontend/public/assets/icons/Square150x150Logo.scale-400.png new file mode 100644 index 0000000..31e99db Binary files /dev/null and b/apps/frontend/public/assets/icons/Square150x150Logo.scale-400.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-16.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-16.png new file mode 100644 index 0000000..a50b673 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-16.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-20.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-20.png new file mode 100644 index 0000000..e5fc224 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-20.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-24.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-24.png new file mode 100644 index 0000000..5f36e1e Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-24.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-256.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-256.png new file mode 100644 index 0000000..a8733ee Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-256.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-30.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-30.png new file mode 100644 index 0000000..9c56d23 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-30.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-32.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-32.png new file mode 100644 index 0000000..d91449a Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-32.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-36.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-36.png new file mode 100644 index 0000000..7aed7d2 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-36.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-40.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-40.png new file mode 100644 index 0000000..346f5b2 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-40.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-44.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-44.png new file mode 100644 index 0000000..68091cb Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-44.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-48.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-48.png new file mode 100644 index 0000000..f6f339e Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-48.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-60.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-60.png new file mode 100644 index 0000000..ffb287e Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-60.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-64.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-64.png new file mode 100644 index 0000000..bcafca1 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-64.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-72.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-72.png new file mode 100644 index 0000000..ad9bc29 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-72.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-80.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-80.png new file mode 100644 index 0000000..05a93d3 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-80.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-96.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-96.png new file mode 100644 index 0000000..28fae59 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-lightunplated_targetsize-96.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-16.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-16.png new file mode 100644 index 0000000..a50b673 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-16.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-20.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-20.png new file mode 100644 index 0000000..e5fc224 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-20.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-24.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-24.png new file mode 100644 index 0000000..5f36e1e Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-24.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-256.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-256.png new file mode 100644 index 0000000..a8733ee Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-256.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-30.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-30.png new file mode 100644 index 0000000..9c56d23 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-30.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-32.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-32.png new file mode 100644 index 0000000..d91449a Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-32.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-36.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-36.png new file mode 100644 index 0000000..7aed7d2 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-36.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-40.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-40.png new file mode 100644 index 0000000..346f5b2 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-40.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-44.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-44.png new file mode 100644 index 0000000..68091cb Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-44.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-48.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-48.png new file mode 100644 index 0000000..f6f339e Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-48.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-60.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-60.png new file mode 100644 index 0000000..ffb287e Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-60.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-64.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-64.png new file mode 100644 index 0000000..bcafca1 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-64.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-72.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-72.png new file mode 100644 index 0000000..ad9bc29 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-72.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-80.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-80.png new file mode 100644 index 0000000..05a93d3 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-80.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-96.png b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-96.png new file mode 100644 index 0000000..28fae59 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.altform-unplated_targetsize-96.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.scale-100.png b/apps/frontend/public/assets/icons/Square44x44Logo.scale-100.png new file mode 100644 index 0000000..68091cb Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.scale-100.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.scale-125.png b/apps/frontend/public/assets/icons/Square44x44Logo.scale-125.png new file mode 100644 index 0000000..4ca60b4 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.scale-125.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.scale-150.png b/apps/frontend/public/assets/icons/Square44x44Logo.scale-150.png new file mode 100644 index 0000000..1aa846a Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.scale-150.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.scale-200.png b/apps/frontend/public/assets/icons/Square44x44Logo.scale-200.png new file mode 100644 index 0000000..d8e87d0 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.scale-200.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.scale-400.png b/apps/frontend/public/assets/icons/Square44x44Logo.scale-400.png new file mode 100644 index 0000000..e2ce960 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.scale-400.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-16.png b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-16.png new file mode 100644 index 0000000..a50b673 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-16.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-20.png b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-20.png new file mode 100644 index 0000000..e5fc224 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-20.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-24.png b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-24.png new file mode 100644 index 0000000..5f36e1e Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-24.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-256.png b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-256.png new file mode 100644 index 0000000..a8733ee Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-256.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-30.png b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-30.png new file mode 100644 index 0000000..9c56d23 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-30.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-32.png b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-32.png new file mode 100644 index 0000000..d91449a Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-32.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-36.png b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-36.png new file mode 100644 index 0000000..7aed7d2 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-36.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-40.png b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-40.png new file mode 100644 index 0000000..346f5b2 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-40.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-44.png b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-44.png new file mode 100644 index 0000000..68091cb Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-44.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-48.png b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-48.png new file mode 100644 index 0000000..f6f339e Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-48.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-60.png b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-60.png new file mode 100644 index 0000000..ffb287e Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-60.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-64.png b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-64.png new file mode 100644 index 0000000..bcafca1 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-64.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-72.png b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-72.png new file mode 100644 index 0000000..ad9bc29 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-72.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-80.png b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-80.png new file mode 100644 index 0000000..05a93d3 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-80.png differ diff --git a/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-96.png b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-96.png new file mode 100644 index 0000000..28fae59 Binary files /dev/null and b/apps/frontend/public/assets/icons/Square44x44Logo.targetsize-96.png differ diff --git a/apps/frontend/public/assets/icons/StoreLogo.scale-100.png b/apps/frontend/public/assets/icons/StoreLogo.scale-100.png new file mode 100644 index 0000000..c0af9a2 Binary files /dev/null and b/apps/frontend/public/assets/icons/StoreLogo.scale-100.png differ diff --git a/apps/frontend/public/assets/icons/StoreLogo.scale-125.png b/apps/frontend/public/assets/icons/StoreLogo.scale-125.png new file mode 100644 index 0000000..a100b34 Binary files /dev/null and b/apps/frontend/public/assets/icons/StoreLogo.scale-125.png differ diff --git a/apps/frontend/public/assets/icons/StoreLogo.scale-150.png b/apps/frontend/public/assets/icons/StoreLogo.scale-150.png new file mode 100644 index 0000000..5a1e633 Binary files /dev/null and b/apps/frontend/public/assets/icons/StoreLogo.scale-150.png differ diff --git a/apps/frontend/public/assets/icons/StoreLogo.scale-200.png b/apps/frontend/public/assets/icons/StoreLogo.scale-200.png new file mode 100644 index 0000000..bbe9ff8 Binary files /dev/null and b/apps/frontend/public/assets/icons/StoreLogo.scale-200.png differ diff --git a/apps/frontend/public/assets/icons/StoreLogo.scale-400.png b/apps/frontend/public/assets/icons/StoreLogo.scale-400.png new file mode 100644 index 0000000..2cbe5c5 Binary files /dev/null and b/apps/frontend/public/assets/icons/StoreLogo.scale-400.png differ diff --git a/apps/frontend/public/assets/icons/Wide310x150Logo.scale-100.png b/apps/frontend/public/assets/icons/Wide310x150Logo.scale-100.png new file mode 100644 index 0000000..f327781 Binary files /dev/null and b/apps/frontend/public/assets/icons/Wide310x150Logo.scale-100.png differ diff --git a/apps/frontend/public/assets/icons/Wide310x150Logo.scale-125.png b/apps/frontend/public/assets/icons/Wide310x150Logo.scale-125.png new file mode 100644 index 0000000..66508ff Binary files /dev/null and b/apps/frontend/public/assets/icons/Wide310x150Logo.scale-125.png differ diff --git a/apps/frontend/public/assets/icons/Wide310x150Logo.scale-150.png b/apps/frontend/public/assets/icons/Wide310x150Logo.scale-150.png new file mode 100644 index 0000000..3ddd9e1 Binary files /dev/null and b/apps/frontend/public/assets/icons/Wide310x150Logo.scale-150.png differ diff --git a/apps/frontend/public/assets/icons/Wide310x150Logo.scale-200.png b/apps/frontend/public/assets/icons/Wide310x150Logo.scale-200.png new file mode 100644 index 0000000..c9ea43a Binary files /dev/null and b/apps/frontend/public/assets/icons/Wide310x150Logo.scale-200.png differ diff --git a/apps/frontend/public/assets/icons/Wide310x150Logo.scale-400.png b/apps/frontend/public/assets/icons/Wide310x150Logo.scale-400.png new file mode 100644 index 0000000..1ec3ac9 Binary files /dev/null and b/apps/frontend/public/assets/icons/Wide310x150Logo.scale-400.png differ diff --git a/apps/frontend/public/assets/icons/android-launchericon-144-144.png b/apps/frontend/public/assets/icons/android-launchericon-144-144.png new file mode 100644 index 0000000..9fd94c4 Binary files /dev/null and b/apps/frontend/public/assets/icons/android-launchericon-144-144.png differ diff --git a/apps/frontend/public/assets/icons/android-launchericon-192-192.png b/apps/frontend/public/assets/icons/android-launchericon-192-192.png new file mode 100644 index 0000000..65332aa Binary files /dev/null and b/apps/frontend/public/assets/icons/android-launchericon-192-192.png differ diff --git a/apps/frontend/public/assets/icons/android-launchericon-48-48.png b/apps/frontend/public/assets/icons/android-launchericon-48-48.png new file mode 100644 index 0000000..00f3701 Binary files /dev/null and b/apps/frontend/public/assets/icons/android-launchericon-48-48.png differ diff --git a/apps/frontend/public/assets/icons/android-launchericon-512-512.png b/apps/frontend/public/assets/icons/android-launchericon-512-512.png new file mode 100644 index 0000000..cacb5f6 Binary files /dev/null and b/apps/frontend/public/assets/icons/android-launchericon-512-512.png differ diff --git a/apps/frontend/public/assets/icons/android-launchericon-72-72.png b/apps/frontend/public/assets/icons/android-launchericon-72-72.png new file mode 100644 index 0000000..1000b8c Binary files /dev/null and b/apps/frontend/public/assets/icons/android-launchericon-72-72.png differ diff --git a/apps/frontend/public/assets/icons/android-launchericon-96-96.png b/apps/frontend/public/assets/icons/android-launchericon-96-96.png new file mode 100644 index 0000000..4a5c085 Binary files /dev/null and b/apps/frontend/public/assets/icons/android-launchericon-96-96.png differ diff --git a/apps/frontend/public/assets/logos/blue_logo_transparent.svg b/apps/frontend/public/assets/logos/blue_logo_transparent.svg new file mode 100644 index 0000000..53489de --- /dev/null +++ b/apps/frontend/public/assets/logos/blue_logo_transparent.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/frontend/public/assets/logos/blue_logo_transparent_name.svg b/apps/frontend/public/assets/logos/blue_logo_transparent_name.svg new file mode 100644 index 0000000..813060d --- /dev/null +++ b/apps/frontend/public/assets/logos/blue_logo_transparent_name.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/frontend/public/assets/logos/blue_logo_transparent_name_big.png b/apps/frontend/public/assets/logos/blue_logo_transparent_name_big.png new file mode 100644 index 0000000..2457baa Binary files /dev/null and b/apps/frontend/public/assets/logos/blue_logo_transparent_name_big.png differ diff --git a/apps/frontend/public/assets/logos/dark_logo_transparent.svg b/apps/frontend/public/assets/logos/dark_logo_transparent.svg new file mode 100644 index 0000000..6941975 --- /dev/null +++ b/apps/frontend/public/assets/logos/dark_logo_transparent.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/frontend/public/assets/logos/dark_logo_transparent_name.svg b/apps/frontend/public/assets/logos/dark_logo_transparent_name.svg new file mode 100644 index 0000000..af5f02e --- /dev/null +++ b/apps/frontend/public/assets/logos/dark_logo_transparent_name.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/frontend/public/assets/screenshots/.gitkeep b/apps/frontend/public/assets/screenshots/.gitkeep new file mode 100644 index 0000000..1b5e766 --- /dev/null +++ b/apps/frontend/public/assets/screenshots/.gitkeep @@ -0,0 +1,4 @@ +# PWA Screenshots Ordner +# Füge hier Screenshots für den Install-Dialog hinzu: +# - desktop.png (1280x720 oder ähnlich, wide) +# - mobile.png (390x844 oder ähnlich, narrow) diff --git a/apps/frontend/public/config.json b/apps/frontend/public/config.json new file mode 100644 index 0000000..382f0db --- /dev/null +++ b/apps/frontend/public/config.json @@ -0,0 +1,3 @@ +{ + "apiUrl": "http://192.168.178.111:3000" +} diff --git a/apps/frontend/public/config.production.json b/apps/frontend/public/config.production.json new file mode 100644 index 0000000..58f9dc0 --- /dev/null +++ b/apps/frontend/public/config.production.json @@ -0,0 +1,3 @@ +{ + "apiUrl": "http://192.168.178.25:13000" +} \ No newline at end of file diff --git a/apps/frontend/public/favicon.ico b/apps/frontend/public/favicon.ico new file mode 100644 index 0000000..efb1d49 Binary files /dev/null and b/apps/frontend/public/favicon.ico differ diff --git a/apps/frontend/public/manifest.webmanifest b/apps/frontend/public/manifest.webmanifest new file mode 100644 index 0000000..8d344417 --- /dev/null +++ b/apps/frontend/public/manifest.webmanifest @@ -0,0 +1,80 @@ +{ + "name": "Leonards & Brandenburger IT", + "short_name": "L&B IT", + "description": "IT-Dienstleistungen, Webentwicklung und SEO aus einer Hand.", + "start_url": "/", + "scope": "/", + "display": "standalone", + "orientation": "portrait-primary", + "background_color": "#ffffff", + "theme_color": "#C2410C", + "categories": ["business", "productivity"], + "lang": "de", + "dir": "ltr", + "icons": [ + { + "src": "/assets/icons/icon-72x72.svg", + "sizes": "72x72", + "type": "image/svg+xml", + "purpose": "maskable any" + }, + { + "src": "/assets/icons/icon-96x96.svg", + "sizes": "96x96", + "type": "image/svg+xml", + "purpose": "maskable any" + }, + { + "src": "/assets/icons/icon-128x128.svg", + "sizes": "128x128", + "type": "image/svg+xml", + "purpose": "maskable any" + }, + { + "src": "/assets/icons/icon-144x144.svg", + "sizes": "144x144", + "type": "image/svg+xml", + "purpose": "maskable any" + }, + { + "src": "/assets/icons/icon-152x152.svg", + "sizes": "152x152", + "type": "image/svg+xml", + "purpose": "maskable any" + }, + { + "src": "/assets/icons/icon-192x192.svg", + "sizes": "192x192", + "type": "image/svg+xml", + "purpose": "maskable any" + }, + { + "src": "/assets/icons/icon-384x384.svg", + "sizes": "384x384", + "type": "image/svg+xml", + "purpose": "maskable any" + }, + { + "src": "/assets/icons/icon-512x512.svg", + "sizes": "512x512", + "type": "image/svg+xml", + "purpose": "maskable any" + } + ], + "shortcuts": [ + { + "name": "Kontakt", + "short_name": "Kontakt", + "description": "Kontaktformular öffnen", + "url": "/kontakt", + "icons": [{ "src": "/assets/icons/icon-96x96.svg", "sizes": "96x96" }] + }, + { + "name": "Services", + "short_name": "Services", + "description": "Unsere Dienstleistungen", + "url": "/services", + "icons": [{ "src": "/assets/icons/icon-96x96.svg", "sizes": "96x96" }] + } + ] +} diff --git a/apps/frontend/public/robots.txt b/apps/frontend/public/robots.txt new file mode 100644 index 0000000..e3fe7dc --- /dev/null +++ b/apps/frontend/public/robots.txt @@ -0,0 +1,6 @@ +# robots.txt +# Update the Sitemap URL to your production domain if needed. +User-agent: * +Allow: / + +Sitemap: /sitemap.xml diff --git a/apps/frontend/public/sitemap.xml b/apps/frontend/public/sitemap.xml new file mode 100644 index 0000000..ca790cc --- /dev/null +++ b/apps/frontend/public/sitemap.xml @@ -0,0 +1,69 @@ + + + + / + weekly + 1.0 + 2025-09-27 + + + /services + weekly + 0.8 + 2025-09-27 + + + /about + monthly + 0.6 + 2025-09-27 + + + /contact + monthly + 0.6 + 2025-09-27 + + + /imprint + yearly + 0.3 + 2025-09-27 + + + /policy + yearly + 0.3 + 2025-09-27 + + + /services/one-pager + monthly + 0.7 + 2025-09-27 + + + /services/all-in-one + monthly + 0.7 + 2025-09-27 + + + /services/large-website + monthly + 0.7 + 2025-09-27 + + + /services/seo-optimization + monthly + 0.7 + 2025-09-27 + + + /services/full-stack-development + monthly + 0.7 + 2025-09-27 + + diff --git a/apps/frontend/robots.txt b/apps/frontend/robots.txt new file mode 100644 index 0000000..7ce14f6 --- /dev/null +++ b/apps/frontend/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://Leonards & Brandenburger IT.de/sitemap.xml \ No newline at end of file diff --git a/apps/frontend/sitemap.xml b/apps/frontend/sitemap.xml new file mode 100644 index 0000000..2f8e00f --- /dev/null +++ b/apps/frontend/sitemap.xml @@ -0,0 +1,75 @@ + + + + https://leonardsmedia.de/ + weekly + 1.0 + 2025-10-20 + + + https://leonardsmedia.de/about + monthly + 0.6 + 2025-10-20 + + + https://leonardsmedia.de/booking + monthly + 0.6 + 2025-10-20 + + + https://leonardsmedia.de/contact + monthly + 0.6 + 2025-10-20 + + + https://leonardsmedia.de/faq + monthly + 0.6 + 2025-10-20 + + + https://leonardsmedia.de/imprint + monthly + 0.6 + 2025-10-20 + + + https://leonardsmedia.de/it-services + monthly + 0.6 + 2025-10-20 + + + https://leonardsmedia.de/policy + monthly + 0.6 + 2025-10-20 + + + https://leonardsmedia.de/process + monthly + 0.6 + 2025-10-20 + + + https://leonardsmedia.de/server-status + monthly + 0.6 + 2025-10-20 + + + https://leonardsmedia.de/services + weekly + 0.8 + 2025-10-20 + + + https://leonardsmedia.de/survey + monthly + 0.6 + 2025-10-20 + + \ No newline at end of file diff --git a/apps/frontend/src/app/api/api.service.ts b/apps/frontend/src/app/api/api.service.ts new file mode 100644 index 0000000..ba03206 --- /dev/null +++ b/apps/frontend/src/app/api/api.service.ts @@ -0,0 +1,1050 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { ConfigService } from '../services/config.service'; +import { User, UserRole } from '../services/auth.service'; + + +// ==================== INTERFACES ==================== + +export interface CreateUserDto { + email: string; + name: string; + password: string; + wantsNewsletter?: boolean; +} + +export interface LoginDto { + email: string; + password: string; +} + +export interface LoginResponse { + message: string; + user: User; + token?: string; // Später wenn JWT implementiert ist +} + +export interface NewsletterSubscribeDto { + email: string; + name?: string; +} + +export interface UpdateUserDto { + name?: string; + wantsNewsletter?: boolean; + role?: UserRole; +} + +export interface UserStats { + totalUsers: number; + newsletterSubscribers: number; + subscriberRate: number; +} + +// Contact Requests +export interface ContactRequest { + id: string; + name: string; + email: string; + serviceType: string; // Slug aus dem Services-Katalog + message: string; + prefersCallback: boolean; + phoneNumber?: string; + isProcessed: boolean; + notes?: string; + userId?: string; + createdAt: Date; +} + +export interface CreateContactRequestDto { + name: string; + email: string; + serviceType: string; // Slug aus dem Services-Katalog + message: string; + prefersCallback?: boolean; + phoneNumber?: string; + userId?: string; +} + +export interface UpdateContactRequestDto { + isProcessed?: boolean; + notes?: string; +} + +export interface BookingSlot { + id: string; + date: string; // YYYY-MM-DD + timeFrom: string; // HH:MM + timeTo: string; // HH:MM + isAvailable: boolean; + maxBookings: number; + currentBookings: number; + createdAt: Date; + updatedAt: Date; + googleEventId?: string; + meetLink?: string; +} + +export interface CreateBookingSlotDto { + date: string; + timeFrom: string; + timeTo: string; + maxBookings?: number; + isAvailable?: boolean; +} + +export interface UpdateBookingSlotDto { + date?: string; + timeFrom?: string; + timeTo?: string; + isAvailable?: boolean; + maxBookings?: number; +} + +export enum BookingStatus { + PENDING = 'pending', + CONFIRMED = 'confirmed', + CANCELLED = 'cancelled', + COMPLETED = 'completed', +} + +export interface Booking { + id: string; + name: string; + email: string; + phone: string | null; + message: string | null; + slotId: string; + slot?: BookingSlot; + status: BookingStatus; + adminNotes: string | null; + createdAt: Date; +} + +export interface CreateBookingDto { + name: string; + email: string; + phone?: string; + message?: string; + slotId: string; +} + +export interface UpdateBookingDto { + status?: BookingStatus; + adminNotes?: string; +} + +// Hilfs-Interface für das Frontend +export interface DayWithSlots { + date: string; + dayName: string; + dayNumber: number; + available: boolean; + slots: BookingSlot[]; + isPast: boolean; +} + +export interface NewsletterSubscriber { + id: string; + email: string; + isActive: boolean; + subscribedAt: Date; +} + +// ==================== FAQ INTERFACES ==================== + +export interface Faq { + id: string; + slug: string; + question: string; + answers: string[]; + listItems: string[] | null; + sortOrder: number; + isPublished: boolean; + category: string | null; + createdAt: Date; + updatedAt: Date; +} + +export interface CreateFaqDto { + slug: string; + question: string; + answers: string[]; + listItems?: string[]; + sortOrder?: number; + isPublished?: boolean; + category?: string; +} + +export interface UpdateFaqDto { + slug?: string; + question?: string; + answers?: string[]; + listItems?: string[]; + sortOrder?: number; + isPublished?: boolean; + category?: string; +} + +export interface BulkImportFaqDto { + faqs: CreateFaqDto[]; + overwriteExisting?: boolean; +} + +export interface ImportFaqResultDto { + imported: number; + updated: number; + skipped: number; + errors: string[]; +} + +// ==================== SETTINGS INTERFACES ==================== + +export interface Settings { + id: string; + isUnderConstruction: boolean; + maintenanceMessage?: string; + maintenancePassword?: string; + allowRegistration: boolean; + allowNewsletter: boolean; + siteTitle?: string; + siteDescription?: string; + contactEmail?: string; + contactPhone?: string; + createdAt: Date; + updatedAt: Date; +} + +export interface UpdateSettingsDto { + isUnderConstruction?: boolean; + maintenanceMessage?: string; + maintenancePassword?: string; + allowRegistration?: boolean; + allowNewsletter?: boolean; + siteTitle?: string; + siteDescription?: string; + contactEmail?: string; + contactPhone?: string; +} + +export interface PublicSettings { + isUnderConstruction: boolean; + maintenanceMessage?: string; + siteTitle?: string; + siteDescription?: string; + allowRegistration?: boolean; + allowNewsletter?: boolean; +} + +// ==================== SERVICES CATALOG INTERFACES ==================== + +export interface ServiceItem { + id: string; + slug: string; + icon: string; + title: string; + description: string; + longDescription: string; + tags: string[]; + keywords: string; + sortOrder: number; + isPublished: boolean; + categoryId: string; + category?: ServiceCategory; + createdAt: Date; + updatedAt: Date; +} + +export interface ServiceCategory { + id: string; + slug: string; + name: string; + subtitle: string; + materialIcon: string; + sortOrder: number; + isPublished: boolean; + services: ServiceItem[]; + createdAt: Date; + updatedAt: Date; +} + +export interface CreateServiceCategoryDto { + slug: string; + name: string; + subtitle: string; + materialIcon: string; + sortOrder?: number; + isPublished?: boolean; +} + +export interface UpdateServiceCategoryDto { + slug?: string; + name?: string; + subtitle?: string; + materialIcon?: string; + sortOrder?: number; + isPublished?: boolean; +} + +export interface CreateServiceDto { + slug: string; + icon: string; + title: string; + description: string; + longDescription: string; + tags: string[]; + keywords: string; + categoryId: string; + sortOrder?: number; + isPublished?: boolean; +} + +export interface UpdateServiceDto { + slug?: string; + icon?: string; + title?: string; + description?: string; + longDescription?: string; + tags?: string[]; + keywords?: string; + categoryId?: string; + sortOrder?: number; + isPublished?: boolean; +} + +export interface ImportServiceItemDto { + slug: string; + icon: string; + title: string; + description: string; + longDescription: string; + tags: string[]; + keywords: string; + sortOrder?: number; +} + +export interface ImportCategoryDto { + slug: string; + name: string; + subtitle: string; + materialIcon: string; + sortOrder?: number; + services: ImportServiceItemDto[]; +} + +export interface BulkImportServicesCatalogDto { + categories: ImportCategoryDto[]; + overwriteExisting?: boolean; +} + +export interface ImportServicesCatalogResultDto { + success: boolean; + categoriesCreated: number; + categoriesUpdated: number; + servicesCreated: number; + servicesUpdated: number; + errors: string[]; +} + + +// ==================== SERVICE ==================== + +@Injectable({ + providedIn: 'root' +}) +export class ApiService { + private get apiUrl(): string { + return this.configService.apiUrl; + } + + constructor( + private http: HttpClient, + private configService: ConfigService + ) { } + + private getHeaders(): HttpHeaders { + let headers = new HttpHeaders({ + 'Content-Type': 'application/json' + }); + + // Später: JWT Token aus LocalStorage holen und hinzufügen + // const token = localStorage.getItem('auth_token'); + // if (token) { + // headers = headers.set('Authorization', `Bearer ${token}`); + // } + + return headers; + } + + // ==================== USER ENDPOINTS ==================== + + /** + * User registrieren + */ + register(dto: CreateUserDto): Observable { + return this.http.post(`${this.apiUrl}/users/register`, dto, { + headers: this.getHeaders() + }); + } + + /** + * User login + */ + login(dto: LoginDto): Observable { + return this.http.post(`${this.apiUrl}/users/login`, dto, { + headers: this.getHeaders() + }); + } + + /** + * Alle User abrufen (Admin) + */ + getAllUsers(): Observable { + return this.http.get(`${this.apiUrl}/users`, { + headers: this.getHeaders() + }); + } + + /** + * Einzelnen User abrufen + */ + getUser(id: string): Observable { + return this.http.get(`${this.apiUrl}/users/${id}`, { + headers: this.getHeaders() + }); + } + + /** + * User aktualisieren + */ + updateUser(id: string, dto: UpdateUserDto): Observable { + return this.http.patch(`${this.apiUrl}/users/${id}`, dto, { + headers: this.getHeaders() + }); + } + + /** + * User löschen + */ + deleteUser(id: string): Observable { + return this.http.delete(`${this.apiUrl}/users/${id}`, { + headers: this.getHeaders() + }); + } + + /** + * User-Statistiken abrufen + */ + getUserStats(): Observable { + return this.http.get(`${this.apiUrl}/users/stats`, { + headers: this.getHeaders() + }); + } + + // ==================== NEWSLETTER ENDPOINTS ==================== + + /** + * Newsletter abonnieren + */ + subscribeNewsletterUser(dto: NewsletterSubscribeDto): Observable<{ message: string }> { + return this.http.post<{ message: string }>( + `${this.apiUrl}/users/newsletter/subscribe`, + dto, + { headers: this.getHeaders() } + ); + } + + /** + * Newsletter abmelden + */ + unsubscribeNewsletterUser(email: string): Observable<{ message: string }> { + return this.http.post<{ message: string }>( + `${this.apiUrl}/users/newsletter/unsubscribe`, + { email }, + { headers: this.getHeaders() } + ); + } + + /** + * Alle Newsletter-Abonnenten abrufen (Admin) + */ + getNewsletterUserSubscribers(): Observable { + return this.http.get(`${this.apiUrl}/users/newsletter/subscribers`, { + headers: this.getHeaders() + }); + } + + // ==================== CONTACT REQUEST ENDPOINTS ==================== + + /** + * Kontaktanfrage senden (öffentlich) + */ + createContactRequest(dto: CreateContactRequestDto): Observable { + return this.http.post(`${this.apiUrl}/contact-requests`, dto, { + headers: this.getHeaders() + }); + } + + /** + * Alle Kontaktanfragen abrufen (Admin) + */ + getAllContactRequests(): Observable { + return this.http.get(`${this.apiUrl}/contact-requests`, { + headers: this.getHeaders() + }); + } + + /** + * Unbearbeitete Kontaktanfragen abrufen (Admin) + */ + getUnprocessedContactRequests(): Observable { + return this.http.get( + `${this.apiUrl}/contact-requests/unprocessed`, + { headers: this.getHeaders() } + ); + } + + /** + * Einzelne Kontaktanfrage abrufen + */ + getContactRequest(id: string): Observable { + return this.http.get(`${this.apiUrl}/contact-requests/${id}`, { + headers: this.getHeaders() + }); + } + + /** + * Kontaktanfrage aktualisieren (Admin) + */ + updateContactRequest( + id: string, + dto: UpdateContactRequestDto + ): Observable { + return this.http.patch( + `${this.apiUrl}/contact-requests/${id}`, + dto, + { headers: this.getHeaders() } + ); + } + + /** + * Kontaktanfrage als bearbeitet markieren (Admin) + */ + markContactRequestAsProcessed(id: string): Observable { + return this.http.patch( + `${this.apiUrl}/contact-requests/${id}/process`, + {}, + { headers: this.getHeaders() } + ); + } + + /** + * Kontaktanfrage löschen (Admin) + */ + deleteContactRequest(id: string): Observable { + return this.http.delete(`${this.apiUrl}/contact-requests/${id}`, { + headers: this.getHeaders() + }); + } + + // ==================== BOOKING SLOTS ENDPOINTS ==================== + + /** + * Verfügbare Slots abrufen (öffentlich) + */ + getAvailableBookingSlots(fromDate?: string): Observable { + let params = new HttpParams(); + if (fromDate) { + params = params.set('fromDate', fromDate); + } + return this.http.get(`${this.apiUrl}/bookings/slots/available`, { params }); + } + + /** + * Slots für ein bestimmtes Datum abrufen (öffentlich) + */ + getBookingSlotsByDate(date: string): Observable { + return this.http.get(`${this.apiUrl}/bookings/slots/date/${date}`); + } + + /** + * Alle Slots abrufen (Admin) + */ + getAllBookingSlots(): Observable { + return this.http.get(`${this.apiUrl}/bookings/slots`, { + headers: this.getHeaders() + }); + } + + /** + * Einzelnen Slot erstellen (Admin) + */ + createBookingSlot(dto: CreateBookingSlotDto): Observable { + return this.http.post(`${this.apiUrl}/bookings/slots`, dto, { + headers: this.getHeaders() + }); + } + + /** + * Mehrere Slots auf einmal erstellen (Admin) + */ + createMultipleBookingSlots(slots: CreateBookingSlotDto[]): Observable { + return this.http.post( + `${this.apiUrl}/bookings/slots/bulk`, + { slots }, + { headers: this.getHeaders() } + ); + } + + /** + * Slot aktualisieren (Admin) + */ + updateBookingSlot(id: string, dto: UpdateBookingSlotDto): Observable { + return this.http.patch(`${this.apiUrl}/bookings/slots/${id}`, dto, { + headers: this.getHeaders() + }); + } + + /** + * Slot löschen (Admin) + */ + deleteBookingSlot(id: string): Observable { + return this.http.delete(`${this.apiUrl}/bookings/slots/${id}`, { + headers: this.getHeaders() + }); + } + + // ==================== BOOKINGS ENDPOINTS ==================== + + /** + * Booking erstellen (öffentlich) + */ + createBooking(dto: CreateBookingDto): Observable { + return this.http.post(`${this.apiUrl}/bookings`, dto); + } + + /** + * Alle Bookings abrufen (Admin) + */ + getAllBookings(): Observable { + return this.http.get(`${this.apiUrl}/bookings`, { + headers: this.getHeaders() + }); + } + + /** + * Einzelne Booking abrufen (Admin) + */ + getBooking(id: string): Observable { + return this.http.get(`${this.apiUrl}/bookings/${id}`, { + headers: this.getHeaders() + }); + } + + /** + * Booking aktualisieren (Admin) + */ + updateBooking(id: string, dto: UpdateBookingDto): Observable { + return this.http.patch(`${this.apiUrl}/bookings/${id}`, dto, { + headers: this.getHeaders() + }); + } + + /** + * Booking stornieren (Admin) + */ + cancelBooking(id: string): Observable { + return this.http.patch(`${this.apiUrl}/bookings/${id}/cancel`, {}, { + headers: this.getHeaders() + }); + } + + /** + * Booking löschen (Admin) + */ + deleteBooking(id: string): Observable { + return this.http.delete(`${this.apiUrl}/bookings/${id}`, { + headers: this.getHeaders() + }); + } + + + // ==================== NEWSLETTER ENDPOINTS ==================== + + /** + * Newsletter abonnieren (öffentlich) + */ + subscribeNewsletter(email: string): Observable<{ success: boolean; message: string; email: string }> { + return this.http.post<{ success: boolean; message: string; email: string }>( + `${this.apiUrl}/newsletter/subscribe`, + { email } + ); + } + + /** + * Newsletter abmelden (öffentlich) + */ + unsubscribeNewsletter(email: string): Observable<{ success: boolean; message: string }> { + const params = new HttpParams().set('email', email); + return this.http.delete<{ success: boolean; message: string }>( + `${this.apiUrl}/newsletter/unsubscribe`, + { params } + ); + } + + /** + * Alle Newsletter-Abonnenten abrufen (Admin) + */ + getNewsletterSubscribers(): Observable<{ total: number; active: number; inactive: number; subscribers: NewsletterSubscriber[] }> { + return this.http.get<{ total: number; active: number; inactive: number; subscribers: NewsletterSubscriber[] }>( + `${this.apiUrl}/newsletter/subscribers`, + { headers: this.getHeaders() } + ); + } + + /** + * Newsletter Statistiken (Admin) + */ + getNewsletterStats(): Observable<{ total: number; active: number; inactive: number }> { + return this.http.get<{ total: number; active: number; inactive: number }>( + `${this.apiUrl}/newsletter/stats`, + { headers: this.getHeaders() } + ); + } + + /** + * Subscriber Status umschalten (Admin) + */ + toggleNewsletterSubscriber(id: string): Observable<{ success: boolean; message: string; subscriber: NewsletterSubscriber }> { + return this.http.patch<{ success: boolean; message: string; subscriber: NewsletterSubscriber }>( + `${this.apiUrl}/newsletter/subscribers/${id}/toggle`, + {}, + { headers: this.getHeaders() } + ); + } + + /** + * Subscriber löschen (Admin) + */ + deleteNewsletterSubscriber(id: string): Observable<{ success: boolean; message: string }> { + return this.http.delete<{ success: boolean; message: string }>( + `${this.apiUrl}/newsletter/subscribers/${id}`, + { headers: this.getHeaders() } + ); + } + + /** + * Registrierte User mit Newsletter abrufen (Admin) + */ + getUserNewsletterSubscribers(): Observable { + return this.http.get( + `${this.apiUrl}/users/newsletter/subscribers`, + { headers: this.getHeaders() } + ); + } + + getPublicSettings(): Observable { + return this.http.get(`${this.apiUrl}/settings/public`); +} + +/** + * Maintenance Password prüfen + */ +checkMaintenancePassword(password: string): Observable<{ valid: boolean }> { + return this.http.post<{ valid: boolean }>( + `${this.apiUrl}/settings/check-maintenance-password`, + { password } + ); +} + +/** + * Alle Settings abrufen (Admin) + */ +getSettings(): Observable { + return this.http.get(`${this.apiUrl}/settings`, { + headers: this.getHeaders() + }); +} + +/** + * Settings aktualisieren (Admin) + */ +updateSettings(dto: UpdateSettingsDto): Observable { + return this.http.patch(`${this.apiUrl}/settings`, dto, { + headers: this.getHeaders() + }); +} + +// ==================== FAQ ENDPOINTS ==================== + +/** + * Alle veröffentlichten FAQs abrufen (öffentlich) + */ +getPublishedFaqs(): Observable { + return this.http.get(`${this.apiUrl}/faq`); +} + +/** + * FAQ per Slug abrufen (öffentlich) + */ +getFaqBySlug(slug: string): Observable { + return this.http.get(`${this.apiUrl}/faq/slug/${slug}`); +} + +/** + * Alle FAQs abrufen inkl. unpublished (Admin) + */ +getAllFaqs(): Observable { + return this.http.get(`${this.apiUrl}/faq/admin/all`, { + headers: this.getHeaders() + }); +} + +/** + * FAQ per ID abrufen (Admin) + */ +getFaqById(id: string): Observable { + return this.http.get(`${this.apiUrl}/faq/admin/${id}`, { + headers: this.getHeaders() + }); +} + +/** + * Neues FAQ erstellen (Admin) + */ +createFaq(dto: CreateFaqDto): Observable { + return this.http.post(`${this.apiUrl}/faq/admin`, dto, { + headers: this.getHeaders() + }); +} + +/** + * FAQ aktualisieren (Admin) + */ +updateFaq(id: string, dto: UpdateFaqDto): Observable { + return this.http.patch(`${this.apiUrl}/faq/admin/${id}`, dto, { + headers: this.getHeaders() + }); +} + +/** + * FAQ löschen (Admin) + */ +deleteFaq(id: string): Observable { + return this.http.delete(`${this.apiUrl}/faq/admin/${id}`, { + headers: this.getHeaders() + }); +} + +/** + * FAQ publish status togglen (Admin) + */ +toggleFaqPublish(id: string): Observable { + return this.http.patch(`${this.apiUrl}/faq/admin/${id}/toggle-publish`, {}, { + headers: this.getHeaders() + }); +} + +/** + * FAQ Sortierung aktualisieren (Admin) + */ +updateFaqSortOrder(items: { id: string; sortOrder: number }[]): Observable<{ success: boolean }> { + return this.http.patch<{ success: boolean }>(`${this.apiUrl}/faq/admin/sort-order`, items, { + headers: this.getHeaders() + }); +} + +/** + * FAQs aus JSON importieren (Admin) + */ +importFaqs(dto: BulkImportFaqDto): Observable { + return this.http.post(`${this.apiUrl}/faq/admin/import`, dto, { + headers: this.getHeaders() + }); +} + +/** + * Alle FAQs als JSON exportieren (Admin) + */ +exportFaqs(): Observable { + return this.http.get(`${this.apiUrl}/faq/admin/export`, { + headers: this.getHeaders() + }); +} + +// ==================== SERVICES CATALOG ==================== + +/** + * Alle veröffentlichten Service-Kategorien mit Services abrufen (öffentlich) + */ +getServicesCatalog(): Observable { + return this.http.get(`${this.apiUrl}/services-catalog`); +} + +/** + * Service-Kategorie per Slug abrufen (öffentlich) + */ +getServiceCategoryBySlug(slug: string): Observable { + return this.http.get(`${this.apiUrl}/services-catalog/category/${slug}`); +} + +/** + * Service per Slug abrufen (öffentlich) + */ +getServiceBySlug(slug: string): Observable { + return this.http.get(`${this.apiUrl}/services-catalog/service/${slug}`); +} + +// ===== ADMIN: CATEGORIES ===== + +/** + * Alle Kategorien abrufen (Admin) + */ +getServiceCategoriesAdmin(): Observable { + return this.http.get(`${this.apiUrl}/services-catalog/admin/categories`, { + headers: this.getHeaders() + }); +} + +/** + * Kategorie per ID abrufen (Admin) + */ +getServiceCategoryById(id: string): Observable { + return this.http.get(`${this.apiUrl}/services-catalog/admin/categories/${id}`, { + headers: this.getHeaders() + }); +} + +/** + * Neue Kategorie erstellen (Admin) + */ +createServiceCategory(dto: CreateServiceCategoryDto): Observable { + return this.http.post(`${this.apiUrl}/services-catalog/admin/categories`, dto, { + headers: this.getHeaders() + }); +} + +/** + * Kategorie aktualisieren (Admin) + */ +updateServiceCategory(id: string, dto: UpdateServiceCategoryDto): Observable { + return this.http.patch(`${this.apiUrl}/services-catalog/admin/categories/${id}`, dto, { + headers: this.getHeaders() + }); +} + +/** + * Kategorie löschen (Admin) + */ +deleteServiceCategory(id: string): Observable { + return this.http.delete(`${this.apiUrl}/services-catalog/admin/categories/${id}`, { + headers: this.getHeaders() + }); +} + +/** + * Kategorie Publish-Status togglen (Admin) + */ +toggleServiceCategoryPublish(id: string): Observable { + return this.http.patch(`${this.apiUrl}/services-catalog/admin/categories/${id}/toggle-publish`, {}, { + headers: this.getHeaders() + }); +} + +/** + * Kategorien-Sortierung aktualisieren (Admin) + */ +updateServiceCategorySortOrder(items: { id: string; sortOrder: number }[]): Observable<{ success: boolean }> { + return this.http.patch<{ success: boolean }>(`${this.apiUrl}/services-catalog/admin/categories/sort-order`, items, { + headers: this.getHeaders() + }); +} + +// ===== ADMIN: SERVICES ===== + +/** + * Alle Services abrufen (Admin) + */ +getServicesAdmin(): Observable { + return this.http.get(`${this.apiUrl}/services-catalog/admin/services`, { + headers: this.getHeaders() + }); +} + +/** + * Service per ID abrufen (Admin) + */ +getServiceById(id: string): Observable { + return this.http.get(`${this.apiUrl}/services-catalog/admin/services/${id}`, { + headers: this.getHeaders() + }); +} + +/** + * Neuen Service erstellen (Admin) + */ +createService(dto: CreateServiceDto): Observable { + return this.http.post(`${this.apiUrl}/services-catalog/admin/services`, dto, { + headers: this.getHeaders() + }); +} + +/** + * Service aktualisieren (Admin) + */ +updateService(id: string, dto: UpdateServiceDto): Observable { + return this.http.patch(`${this.apiUrl}/services-catalog/admin/services/${id}`, dto, { + headers: this.getHeaders() + }); +} + +/** + * Service löschen (Admin) + */ +deleteService(id: string): Observable { + return this.http.delete(`${this.apiUrl}/services-catalog/admin/services/${id}`, { + headers: this.getHeaders() + }); +} + +/** + * Service Publish-Status togglen (Admin) + */ +toggleServicePublish(id: string): Observable { + return this.http.patch(`${this.apiUrl}/services-catalog/admin/services/${id}/toggle-publish`, {}, { + headers: this.getHeaders() + }); +} + +/** + * Services-Sortierung aktualisieren (Admin) + */ +updateServiceSortOrder(items: { id: string; sortOrder: number }[]): Observable<{ success: boolean }> { + return this.http.patch<{ success: boolean }>(`${this.apiUrl}/services-catalog/admin/services/sort-order`, items, { + headers: this.getHeaders() + }); +} + +// ===== ADMIN: IMPORT/EXPORT ===== + +/** + * Services-Katalog importieren (Admin) + */ +importServicesCatalog(dto: BulkImportServicesCatalogDto): Observable { + return this.http.post(`${this.apiUrl}/services-catalog/admin/import`, dto, { + headers: this.getHeaders() + }); +} + +/** + * Services-Katalog exportieren (Admin) + */ +exportServicesCatalog(): Observable<{ categories: ImportCategoryDto[] }> { + return this.http.get<{ categories: ImportCategoryDto[] }>(`${this.apiUrl}/services-catalog/admin/export`, { + headers: this.getHeaders() + }); +} + + +} \ No newline at end of file diff --git a/apps/frontend/src/app/api/invoices-api.service.ts b/apps/frontend/src/app/api/invoices-api.service.ts new file mode 100644 index 0000000..0c74e81 --- /dev/null +++ b/apps/frontend/src/app/api/invoices-api.service.ts @@ -0,0 +1,91 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { ConfigService } from '../services/config.service'; + +export interface InvoiceItem { + id: string; + description: string; + quantity: number; + unit: string; + unitPrice: number; +} + +export interface Invoice { + id: string; + invoiceNumber: string; + date: string; + dueDate: string; + status: 'draft' | 'sent' | 'paid' | 'overdue'; + customerName: string; + customerEmail: string; + customerAddress: string; + customerCity: string; + customerZip: string; + items: InvoiceItem[]; + taxRate: number; + notes: string; + totalNet: number; + totalGross: number; + createdAt: Date; + updatedAt: Date; +} + +export interface InvoiceStats { + total: number; + draft: number; + sent: number; + paid: number; + overdue: number; + totalRevenue: number; +} + +@Injectable({ + providedIn: 'root' +}) +export class InvoicesApiService { + private get apiUrl(): string { + return `${this.configService.apiUrl}/invoices`; + } + + constructor( + private http: HttpClient, + private configService: ConfigService + ) {} + + getAll(): Observable { + return this.http.get(this.apiUrl); + } + + getOne(id: string): Observable { + return this.http.get(`${this.apiUrl}/${id}`); + } + + getStats(): Observable { + return this.http.get(`${this.apiUrl}/stats`); + } + + generateNumber(): Observable { + return this.http.get(`${this.apiUrl}/generate-number`, { responseType: 'text' }); + } + + create(invoice: Partial): Observable { + return this.http.post(this.apiUrl, invoice); + } + + update(id: string, invoice: Partial): Observable { + return this.http.put(`${this.apiUrl}/${id}`, invoice); + } + + updateStatus(id: string, status: Invoice['status']): Observable { + return this.http.patch(`${this.apiUrl}/${id}/status`, { status }); + } + + duplicate(id: string): Observable { + return this.http.post(`${this.apiUrl}/${id}/duplicate`, {}); + } + + delete(id: string): Observable { + return this.http.delete(`${this.apiUrl}/${id}`); + } +} diff --git a/apps/frontend/src/app/app.component.html b/apps/frontend/src/app/app.component.html new file mode 100644 index 0000000..2706be9 --- /dev/null +++ b/apps/frontend/src/app/app.component.html @@ -0,0 +1,33 @@ + +
+ +
+ + +
+ + +
+
+ +
+
+ + + + + +
+ + + + + + + + + \ No newline at end of file diff --git a/apps/frontend/src/app/app.component.scss b/apps/frontend/src/app/app.component.scss new file mode 100644 index 0000000..bdcf70d --- /dev/null +++ b/apps/frontend/src/app/app.component.scss @@ -0,0 +1,243 @@ +/* app.component.scss - Modern Glassmorphism Theme */ + +@import './utils/shared-styles.scss'; + +:host { + display: block; + position: relative; +} + +/* ===== PAGE LAYOUT ===== */ + +.page { + min-height: 100svh; + min-height: 100vh; + display: grid; + grid-template-rows: auto 1fr auto; + background: $gradient-bg; + background-attachment: fixed; + color: $color-text-primary; + position: relative; + overflow-x: hidden; + + // Subtiler Ambient Background Effect + &::before { + content: ''; + position: fixed; + inset: 0; + background: + radial-gradient(circle at 20% 20%, rgba(37, 99, 235, 0.04) 0%, transparent 50%), + radial-gradient(circle at 80% 80%, rgba(59, 130, 246, 0.03) 0%, transparent 50%); + pointer-events: none; + z-index: 0; + } + + // Maintenance Mode + &--maintenance { + grid-template-rows: 1fr; + background: linear-gradient(135deg, $color-gray-900 0%, $color-gray-800 100%); + + &::before { + display: none; + } + } +} + +/* ===== HEADER ===== */ + +.site-header { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + backdrop-filter: blur(20px) saturate(160%); + background: rgba(255, 255, 255, 0.85); + border-bottom: 1px solid rgba(0, 0, 0, 0.06); + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.04), + 0 0 0 1px rgba(255, 255, 255, 0.5) inset; + transition: all 0.3s ease; + animation: slideDown 0.4s ease-out; + + // Scrolled State + &.scrolled { + background: rgba(255, 255, 255, 0.95); + box-shadow: + 0 8px 24px rgba(0, 0, 0, 0.08), + 0 0 0 1px rgba(255, 255, 255, 0.8) inset; + } +} + +/* ===== MAIN CONTENT ===== */ + +.site-main { + position: relative; + z-index: 1; + padding-top: 57px; // Header Height Offset + + .main-content { + position: relative; + + // Container wird in jeder Section individuell gesetzt + // damit manche Sections full-width sein können + } +} + +/* ===== FOOTER ===== */ + +.site-footer { + position: relative; + z-index: 1; + margin-top: auto; +} + +/* ===== SCROLL TO TOP BUTTON ===== */ + +.scroll-to-top { + position: fixed; + bottom: 2rem; + right: 2rem; + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + background: $gradient-primary; + color: white; + border: none; + border-radius: 50%; + cursor: pointer; + box-shadow: $shadow-brand; + transition: all 0.3s ease; + transform: translateY(100px); + opacity: 0; + pointer-events: none; + z-index: 999; + + &.visible { + transform: translateY(0); + opacity: 1; + pointer-events: all; + } + + &:hover { + box-shadow: $shadow-brand-hover; + transform: translateY(-4px); + } + + &:active { + transform: translateY(0); + } + + &:focus-visible { + outline: none; + box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.2); + } + + .material-symbols-outlined { + font-size: 24px; + } + + // Mobile: Kleinerer Button und andere Position + @media (max-width: 768px) { + width: 44px; + height: 44px; + bottom: 1.5rem; + right: 1.5rem; + + .material-symbols-outlined { + font-size: 20px; + } + } +} + +/* ===== SAFE AREA INSETS (für iOS Notch etc.) ===== */ + +.page, +.site-header, +.site-footer { + padding-left: max(env(safe-area-inset-left), 0); + padding-right: max(env(safe-area-inset-right), 0); +} + +.site-header { + padding-top: env(safe-area-inset-top, 0); +} + +.site-footer { + padding-bottom: env(safe-area-inset-bottom, 0); +} + +/* ===== ANIMATIONS ===== */ + +@keyframes slideDown { + from { + transform: translateY(-100%); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +/* ===== LOADING OVERLAY (optional für später) ===== */ + +.page-loading { + position: fixed; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: $glass-bg; + backdrop-filter: blur(10px); + z-index: 9999; + + .spinner { + width: 48px; + height: 48px; + border: 4px solid $color-gray-200; + border-top-color: $color-brand-primary; + border-radius: 50%; + animation: spin 0.8s linear infinite; + } +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* ===== MOTION PREFERENCES ===== */ + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } + + .site-header { + animation: none; + } + + .scroll-to-top { + transition: opacity 0.2s ease, visibility 0.2s ease; + } +} + +/* ===== PREVIEW MODE (kein Header/Footer) ===== */ + +.page:has(router-outlet:only-child) { + // Wenn nur router-outlet sichtbar (Preview Mode) + grid-template-rows: 1fr; + + .site-main { + padding-block: 0; + padding-top: 0; + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/app.component.ts b/apps/frontend/src/app/app.component.ts new file mode 100644 index 0000000..3eaf948 --- /dev/null +++ b/apps/frontend/src/app/app.component.ts @@ -0,0 +1,198 @@ +import { Component, Inject, OnInit, HostListener } from '@angular/core'; +import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router'; +import { HeaderComponent } from "./shared/header/header.component"; +import { FooterComponent } from "./shared/footer/footer.component"; +import { SeoService } from './shared/seo.service'; +import { CommonModule, DOCUMENT } from '@angular/common'; +import { filter } from 'rxjs/operators'; +import { FormsModule } from '@angular/forms'; +import { MaintenanceComponent } from "./components/maintenance/maintenance.component"; +import { ToastContainerComponent } from "./shared/toasts/toast-container.component"; +import { ToastService } from './shared/toasts/toast.service'; +import { ConfirmationComponent } from "./shared/confirmation/confirmation.component"; +import { ConfirmationService } from './shared/confirmation/confirmation.service'; +import { ApiService } from './api/api.service'; +import { CookieBannerComponent } from './shared/cookie-banner/cookie-banner.component'; +import { AnalyticsService } from './services/analytics.service'; + +@Component({ + selector: 'app-root', + standalone: true, + imports: [ + RouterOutlet, + HeaderComponent, + FooterComponent, + CommonModule, + FormsModule, + MaintenanceComponent, + ToastContainerComponent, + ConfirmationComponent, + CookieBannerComponent + ], + templateUrl: './app.component.html', + styleUrl: './app.component.scss' +}) +export class AppComponent implements OnInit { + title = 'WebsiteBaseV2'; + isUnderConstruction = false; + maintenanceMessage = 'Die Seite wird gerade gewartet. Bitte versuchen Sie es später erneut.'; + + // Neue Properties für Scroll-Handling + isScrolled: boolean = false; + showScrollTop: boolean = false; + + // Admin-Route Check - Footer ausblenden + isAdminRoute: boolean = false; + + defaultConfig = { + title: 'Bestätigung', + message: 'Möchtest du fortfahren?', + type: 'info' as const + }; + + // WICHTIG: Erst nach defaultConfig deklarieren! + get confirmationState$() { + return this.confirmationService.state$; + } + + constructor( + public router: Router, + private route: ActivatedRoute, + private seo: SeoService, + @Inject(DOCUMENT) private doc: Document, + private toasts: ToastService, + private confirmationService: ConfirmationService, + private api: ApiService, + private analytics: AnalyticsService // Initialisiert automatisch Pageview-Tracking + ) { } + + ngOnInit(): void { + // Access Control für Under Construction Mode + this.route.queryParams.subscribe(params => { + const access = params['pw']; + if (access) { + // Prüfe Passwort gegen Backend + this.checkMaintenancePassword(access); + } + }); + + // SEO-Update bei Navigation + this.router.events + .pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd)) + .subscribe((event) => { + const deepest = this.getDeepest(this.route); + const routeTitle: any = deepest.snapshot.routeConfig && (deepest.snapshot.routeConfig as any).title; + const description = deepest.snapshot.data && deepest.snapshot.data['description']; + const title = typeof routeTitle === 'string' ? routeTitle : 'Leonards & Brandenburger IT'; + const url = this.doc.location.href; + + this.seo.update({ title, description, url }); + + // Admin-Route Check für Footer + this.isAdminRoute = event.urlAfterRedirects.startsWith('/admin'); + + // Scroll to top bei Route-Change (nicht bei Admin) + if (!this.isAdminRoute) { + window.scrollTo(0, 0); + } + }); + + // Initial Admin-Route Check + this.isAdminRoute = this.router.url.startsWith('/admin'); + + // Wartungsmodus-Check bei Routen-Änderungen + this.router.events + .pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd)) + .subscribe((event) => { + this.checkMaintenanceStatus(event.urlAfterRedirects); + }); + + // Initial check + this.checkMaintenanceStatus(this.router.url); + } + + private checkMaintenanceStatus(url: string): void { + // Admin-Bereich ist immer zugänglich (damit man Settings ändern kann) + if (url.startsWith('/admin')) { + this.isUnderConstruction = false; + return; + } + + // Wenn Zugang mit Passwort gewährt wurde (nicht für normale Logins!) + if (sessionStorage.getItem('maintenanceBypass') === 'true') { + this.isUnderConstruction = false; + return; + } + + this.api.getPublicSettings().subscribe({ + next: (settings) => { + this.isUnderConstruction = settings.isUnderConstruction; + if (settings.maintenanceMessage) { + this.maintenanceMessage = settings.maintenanceMessage; + } + }, + error: (error) => { + console.error('Fehler beim Laden der Settings:', error); + // Im Fehlerfall: Seite normal anzeigen + this.isUnderConstruction = false; + } + }); + } + + private checkMaintenancePassword(password: string): void { + this.api.checkMaintenancePassword(password).subscribe({ + next: (response) => { + if (response.valid) { + this.isUnderConstruction = false; + this.router.navigate([], { + queryParams: { pw: null }, + queryParamsHandling: 'merge' + }); + sessionStorage.setItem('maintenanceBypass', 'true'); + this.toasts.success('Zugang gewährt'); + } else { + this.toasts.error('Ungültiges Passwort'); + } + }, + error: (error) => { + console.error('Fehler bei Passwort-Prüfung:', error); + this.toasts.error('Fehler bei der Überprüfung'); + } + }); + } + + onConfirmed(): void { + this.confirmationService.handleConfirm(); + } + + onCancelled(): void { + this.confirmationService.handleCancel(); + } + + // Listen to scroll events + @HostListener('window:scroll', []) + onWindowScroll(): void { + const scrollPosition = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0; + + // Header scrolled state + this.isScrolled = scrollPosition > 50; + + // Show scroll-to-top button + this.showScrollTop = scrollPosition > 300; + } + + // Scroll to top smoothly + scrollToTop(): void { + window.scrollTo({ + top: 0, + behavior: 'smooth' + }); + } + + // Helper: Tiefste Route finden (für SEO) + private getDeepest(route: ActivatedRoute): ActivatedRoute { + let current = route; + while (current.firstChild) current = current.firstChild; + return current; + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/app.config.ts b/apps/frontend/src/app/app.config.ts new file mode 100644 index 0000000..b786a48 --- /dev/null +++ b/apps/frontend/src/app/app.config.ts @@ -0,0 +1,32 @@ +import { ApplicationConfig, APP_INITIALIZER, provideZoneChangeDetection } from '@angular/core'; +import { provideRouter, withInMemoryScrolling } from '@angular/router'; +import { provideAnimations } from '@angular/platform-browser/animations'; + +import { routes } from './app.routes'; +import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { AuthInterceptor } from './services/auth.interceptor'; +import { ConfigService } from './services/config.service'; + +export function initializeApp(configService: ConfigService) { + return () => configService.loadConfig(); +} + +export const appConfig: ApplicationConfig = { + providers: [ + provideZoneChangeDetection({ eventCoalescing: true }), + provideAnimations(), + provideHttpClient(withInterceptorsFromDi()), + { + provide: APP_INITIALIZER, + useFactory: initializeApp, + deps: [ConfigService], + multi: true + }, + { + provide: HTTP_INTERCEPTORS, + useClass: AuthInterceptor, + multi: true + }, + provideRouter(routes, withInMemoryScrolling({ scrollPositionRestoration: 'top' })), + ] +}; diff --git a/apps/frontend/src/app/app.routes.ts b/apps/frontend/src/app/app.routes.ts new file mode 100644 index 0000000..cd9754d --- /dev/null +++ b/apps/frontend/src/app/app.routes.ts @@ -0,0 +1,59 @@ +import { Routes } from '@angular/router'; +import { HomeComponent } from './components/home/home.component'; +import { AboutComponent } from './components/about/about.component'; +import { ContactComponent } from './components/contact/contact.component'; +import { ImprintComponent } from './components/imprint/imprint.component'; +import { PolicyComponent } from './components/policy/policy.component'; +import { ServicesComponent } from './components/services/services.component'; +import { ServerStatusComponent } from './components/server-status/server-status.component'; +import { VorgehenComponent } from './components/vorgehen/vorgehen.component'; +import { MaintenanceComponent } from './components/maintenance/maintenance.component'; +import { AdminRequestsComponent } from './components/admin/admin-requests/admin-requests.component'; +import { adminGuard } from './guards/admin.guard'; +import { authGuard } from './guards/auth.guard'; +import { LoginComponent } from './components/login/login.component'; +import { RegisterComponent } from './components/register/register.component'; +import { ProfileComponent } from './components/profile/profile.component'; +import { AdminUsersComponent } from './components/admin/admin-users/admin-users.component'; +import { AdminSettingsComponent } from './components/admin/admin-settings/admin-settings.component'; +import { BookingComponent } from './components/booking/booking.component'; +import { AdminBookingComponent } from './components/admin/admin-booking/admin-booking.component'; +import { ItServicesComponent } from './components/it-services/it-services.component'; +import { AdminNewsletterComponent } from './components/admin/admin-newsletter/admin-newsletter.component'; +import { AdminFaqComponent } from './components/admin/admin-faq/admin-faq.component'; +import { AdminServicesComponent } from './components/admin/admin-services/admin-services.component'; +import { AdminInvoicesComponent } from './components/admin/admin-invoices/admin-invoices.component'; +import { AdminDashboardComponent } from './components/admin/admin-dashboard/admin-dashboard.component'; +import { AdminAnalyticsComponent } from './components/admin/admin-analytics/admin-analytics.component'; +import { FaqComponent } from './components/faq/faq.component'; +import { RemoteSupportComponent } from './components/remote-support/remote-support.component'; + +const pageMainName = 'Leonards & Brandenburger IT'; +export const routes: Routes = [ + { path: '', component: HomeComponent, title: pageMainName, data: { description: 'IT-Dienstleistungen, Webentwicklung und SEO – pragmatisch, transparent und zuverlässig. Leonards & Brandenburger IT hilft Ihnen bei Konzeption, Entwicklung und Betrieb.' } }, + { path: 'services', component: ServicesComponent, title: pageMainName + ' | Dienstleistungen', data: { description: 'Übersicht unserer Leistungen: Websites, All-in-One-Pakete, Full-Stack-Entwicklung und SEO-Optimierung. Klar strukturiert und wirkungsorientiert.' } }, + { path: 'about', component: AboutComponent, title: pageMainName + ' | Über uns', data: { description: 'Erfahren Sie mehr über Leonards & Brandenburger IT: Werte, Arbeitsweise und warum wir Technologie pragmatisch und zielorientiert einsetzen.' } }, + { path: 'contact', component: ContactComponent, title: pageMainName + ' | Kontakt', data: { description: 'Kontaktieren Sie Leonards & Brandenburger IT für ein unverbindliches Erstgespräch. Schnelle Einschätzung ohne Sales-Druck.' } }, + { path: 'imprint', component: ImprintComponent, title: pageMainName + ' | Impressum', data: { description: 'Impressum von Leonards & Brandenburger IT.' } }, + { path: 'server-status', component: ServerStatusComponent, title: pageMainName + ' | Systemstatus', data: { description: 'Systemstatus von Leonards & Brandenburger IT.' } }, + { path: 'process', component: VorgehenComponent, title: pageMainName + ' | Vorgehen', data: { description: 'Vorgehen von Leonards & Brandenburger IT.' } }, + { path: 'faq', component: FaqComponent, title: pageMainName + ' | FAQ', data: { description: 'FAQ von Leonards & Brandenburger IT.' } }, + { path: 'policy', component: PolicyComponent, title: pageMainName + ' | Datenschutz', data: { description: 'Datenschutzerklärung von Leonards & Brandenburger IT.' } }, + { path: 'booking', component: BookingComponent, title: pageMainName + ' | Buchung', data: { description: 'Buchungsseite von Leonards & Brandenburger IT.' } }, + { path: 'login', component: LoginComponent, title: pageMainName + ' | Login', data: { description: 'Login von Leonards & Brandenburger IT.' } }, + { path: 'profile', component: ProfileComponent, title: pageMainName + ' | Profil', data: { description: 'Profil von Leonards & Brandenburger IT.' } }, + { path: 'register', component: RegisterComponent, title: pageMainName + ' | Register', data: { description: 'Register von Leonards & Brandenburger IT.' } }, + { path: 'it-services', component: ItServicesComponent, title: pageMainName + ' | IT-Services', data: { description: 'IT-Services von Leonards & Brandenburger IT.' } }, + { path: 'remote-support', component: RemoteSupportComponent, title: pageMainName + ' | Remote Support', data: { description: 'AnyDesk Remote-Verbindung für schnellen IT-Support. Laden Sie hier die Software herunter.' } }, + + { path: 'admin', component: AdminDashboardComponent, canActivate: [authGuard, adminGuard], title: pageMainName + ' | Admin Dashboard', data: { description: 'Admin Dashboard von Leonards & Brandenburger IT.' } }, + { path: 'admin/requests', component: AdminRequestsComponent, canActivate: [authGuard, adminGuard], title: pageMainName + ' | Admin Requests', data: { description: 'Admin Requests von Leonards & Brandenburger IT.' } }, + { path: 'admin/users', component: AdminUsersComponent, canActivate: [authGuard, adminGuard], title: pageMainName + ' | Admin Users', data: { description: 'Admin Users von Leonards & Brandenburger IT.' } }, + { path: 'admin/booking', component: AdminBookingComponent, canActivate: [authGuard, adminGuard], title: pageMainName + ' | Admin Booking', data: { description: 'Admin Booking von Leonards & Brandenburger IT.' } }, + { path: 'admin/settings', component: AdminSettingsComponent, canActivate: [authGuard, adminGuard], title: pageMainName + ' | Admin Settings', data: { description: 'Admin Settings von Leonards & Brandenburger IT.' } }, + { path: 'admin/newsletter', component: AdminNewsletterComponent, canActivate: [authGuard, adminGuard], title: pageMainName + ' | Admin Newsletter', data: { description: 'Admin Newsletter von Leonards & Brandenburger IT.' } }, + { path: 'admin/faq', component: AdminFaqComponent, canActivate: [authGuard, adminGuard], title: pageMainName + ' | Admin FAQ', data: { description: 'Admin FAQ Verwaltung von Leonards & Brandenburger IT.' } }, + { path: 'admin/services', component: AdminServicesComponent, canActivate: [authGuard, adminGuard], title: pageMainName + ' | Admin Services', data: { description: 'Admin Services Verwaltung von Leonards & Brandenburger IT.' } }, + { path: 'admin/invoices', component: AdminInvoicesComponent, canActivate: [authGuard, adminGuard], title: pageMainName + ' | Admin Rechnungen', data: { description: 'Admin Rechnungsverwaltung von Leonards & Brandenburger IT.' } }, + { path: 'admin/analytics', component: AdminAnalyticsComponent, canActivate: [authGuard, adminGuard], title: pageMainName + ' | Admin Analytics', data: { description: 'Analytics Dashboard von Leonards & Brandenburger IT.' } }, +]; \ No newline at end of file diff --git a/apps/frontend/src/app/components/about/about.component.html b/apps/frontend/src/app/components/about/about.component.html new file mode 100644 index 0000000..a866553 --- /dev/null +++ b/apps/frontend/src/app/components/about/about.component.html @@ -0,0 +1,147 @@ + + +
+
+ +
+
+ Portrait von Tom Leonards +
+
+ +
+

Tom Leonards

+

Full-Stack Webentwickler mit Fokus auf Qualität und Performance

+ +
+

+ Ich entwickle leistungsstarke und skalierbare Webanwendungen – mit einem klaren Anspruch an sauberen Code, durchdachte Architektur und nachhaltige technische Lösungen. +

+ +

+ Neben Kundenprojekten arbeite ich kontinuierlich an eigenen Tools und Anwendungen, um neue Technologien zu evaluieren, Prozesse zu optimieren und meine Entwicklungskompetenz stetig auszubauen. +

+
+ +
+

Arbeitsweise

+
+
+
🎯
+ Strukturiert & zielorientiert +

Jedes Projekt beginnt mit einem klaren Konzept und einem Fokus auf messbare Ergebnisse.

+
+
+
💬
+ Transparent & partnerschaftlich +

Offene Kommunikation und enge Zusammenarbeit – für effiziente Abstimmungen und reibungslose Abläufe.

+
+
+
+ Effizient & zukunftssicher +

Moderne Technologien, optimierte Prozesse und wartbarer Code für langfristigen Projekterfolg.

+
+
+
+ +
+

Technologie-Stack

+
+
+ Frontend: + Angular, TypeScript, HTML, SCSS +
+
+ Backend: + Node.js, NestJS, REST APIs +
+
+ Datenbank: + PostgreSQL, TypeORM +
+
+ DevOps: + Docker, Git, CI/CD +
+
+
+ +
+

Hintergrund

+
    +
  • + Ausbildung + Fachinformatiker für Anwendungsentwicklung + Fachabitur Informatik +
  • +
  • + Alter + 21 Jahre +
  • +
  • + Schwerpunkt + Full-Stack Webentwicklung (Frontend & Backend) +
  • +
  • + Standort + Remote (Deutschland) +
  • +
+
+ + +
+ + +
+
+ + +
+
+

Warum mit mir arbeiten?

+ +
+
+

🚀 Keine langen Wartezeiten

+

Als Einzelentwickler ohne Agentur-Struktur kann ich schnell reagieren und umsetzen. Deine Anfrage landet + direkt bei mir - nicht in einer Warteschlange.

+
+ +
+

💰 Faire Preise

+

Kein Overhead durch große Teams oder Büros. Du zahlst für Entwicklung, nicht für Projektmanager, + Account-Manager und Vertriebler.

+
+ +
+

🎨 Full-Stack Know-how

+

Von Design bis Deployment - alles aus einer Hand. Keine Abstimmung zwischen Frontend-, Backend- und + DevOps-Teams nötig.

+
+ +
+

📚 Stetig am Lernen

+

Durch eigene Projekte und kontinuierliches Lernen bleibe ich auf dem neuesten Stand. Neue Technologien und + Best Practices fließen direkt in meine Arbeit ein.

+
+ +
+

🤝 Direkter Draht

+

Du arbeitest mit mir persönlich zusammen - nicht mit wechselnden Ansprechpartnern. Feedback landet sofort + beim Entwickler.

+
+ +
+

✅ Ehrliche Beratung

+

Wenn etwas nicht passt oder zu komplex wird, sag ich das offen. Lieber ehrlich ablehnen als ein schlechtes + Projekt durchziehen.

+
+
+
+
\ No newline at end of file diff --git a/apps/frontend/src/app/components/about/about.component.scss b/apps/frontend/src/app/components/about/about.component.scss new file mode 100644 index 0000000..93629c4 --- /dev/null +++ b/apps/frontend/src/app/components/about/about.component.scss @@ -0,0 +1,366 @@ +@import '../../utils/shared-styles.scss'; + +.container { + width: 100%; + max-width: 1200px; + margin-inline: auto; + padding-inline: clamp(1rem, 4vw, 2rem); +} + +section { + display: flex; + flex-direction: column; + align-items: center; +} + +/* ===== ABOUT SECTION ===== */ + +.about--single { + padding-block: 2rem; + + @media (min-width: 768px) { + padding-block: 3rem; + } + + .profile { + display: grid; + grid-template-columns: 1fr; + gap: 2.5rem; + align-items: start; + margin-bottom: 2rem; + + @media (min-width: 900px) { + grid-template-columns: 0.85fr 1.15fr; + gap: 3rem; + } + } + + /* Profile Media */ + .profile__media { + justify-self: center; + width: 100%; + max-width: 320px; + + @media (min-width: 900px) { + justify-self: start; + position: sticky; + top: 2rem; + } + } + + .avatar { + margin: 0; + + img { + display: block; + width: 100%; + height: auto; + aspect-ratio: 1 / 1; + object-fit: cover; + border-radius: $radius-xl; + border: 1px solid $glass-border; + box-shadow: $shadow-glass; + transition: all 0.3s ease; + + &:hover { + box-shadow: $shadow-glass-hover; + transform: translateY(-4px); + } + } + } + + /* Profile Content */ + .profile__content { + display: grid; + gap: 2rem; + } + + .profile__name { + font-size: clamp(1.75rem, 3vw + 1rem, 2.25rem); + font-weight: 800; + color: $color-text-primary; + margin: 0; + line-height: 1.2; + letter-spacing: -0.02em; + } + + .profile__tagline { + font-size: 1.125rem; + color: $color-text-secondary; + margin: -1.25rem 0 0; + font-weight: 500; + line-height: 1.5; + } + + /* Profile Story */ + .profile__story { + display: grid; + gap: 1rem; + + p { + margin: 0; + color: $color-text-secondary; + line-height: 1.7; + font-size: 1rem; + + strong { + color: $color-text-primary; + font-weight: 600; + } + } + } + + /* Profile Approach */ + .profile__approach { + background: $glass-bg; + backdrop-filter: blur(10px); + border: 1px solid $glass-border; + border-radius: $radius-xl; + padding: 1.75rem; + box-shadow: $shadow-glass; + + h3 { + font-size: 1.25rem; + font-weight: 700; + color: $color-text-primary; + margin: 0 0 1.25rem; + } + + .approach__grid { + display: grid; + gap: 1rem; + grid-template-columns: 1fr; + + @media (min-width: 640px) { + grid-template-columns: repeat(3, 1fr); + } + } + + .approach__item { + background: $color-white; + border: 1px solid $glass-border; + border-radius: $radius-lg; + padding: 1.25rem; + text-align: center; + transition: all 0.3s ease; + + &:hover { + transform: translateY(-4px); + box-shadow: $shadow-md; + border-color: rgba(37, 99, 235, 0.2); + } + + .approach__icon { + font-size: 2rem; + margin-bottom: 0.75rem; + display: block; + } + + strong { + display: block; + color: $color-text-primary; + margin-bottom: 0.5rem; + font-size: 1rem; + font-weight: 700; + } + + p { + margin: 0; + color: $color-text-secondary; + font-size: 0.9375rem; + line-height: 1.6; + } + } + } + + /* Profile Tech Stack */ + .profile__tech { + background: $gradient-subtle; + border: 1px solid rgba(37, 99, 235, 0.15); + border-radius: $radius-xl; + padding: 1.75rem; + + h3 { + font-size: 1.25rem; + font-weight: 700; + color: $color-text-primary; + margin: 0 0 1.25rem; + } + + .tech__categories { + display: grid; + gap: 1rem; + } + + .tech__category { + display: grid; + grid-template-columns: 110px 1fr; + gap: 1rem; + align-items: baseline; + + @media (max-width: 639px) { + grid-template-columns: 1fr; + gap: 0.25rem; + } + + .tech__label { + color: $color-text-secondary; + font-size: 0.9375rem; + font-weight: 600; + } + + .tech__values { + color: $color-text-primary; + font-size: 0.9375rem; + font-weight: 500; + } + } + } + + /* Profile Facts */ + .profile__facts { + background: $glass-bg; + backdrop-filter: blur(10px); + border: 1px solid $glass-border; + border-radius: $radius-xl; + padding: 1.75rem; + box-shadow: $shadow-glass; + + h3 { + font-size: 1.25rem; + font-weight: 700; + color: $color-text-primary; + margin: 0 0 1.25rem; + } + + .facts { + list-style: none; + padding: 0; + margin: 0; + display: grid; + gap: 1rem; + + li { + display: grid; + grid-template-columns: 140px 1fr; + gap: 1rem; + align-items: start; + padding-bottom: 1rem; + border-bottom: 1px solid $color-gray-200; + + &:last-child { + border-bottom: none; + padding-bottom: 0; + } + + @media (max-width: 639px) { + grid-template-columns: 1fr; + gap: 0.375rem; + } + + .k { + color: $color-text-secondary; + font-size: 0.9375rem; + font-weight: 600; + } + + .v { + color: $color-text-primary; + font-weight: 500; + font-size: 0.9375rem; + line-height: 1.6; + } + } + } + } + + /* Profile Actions */ + .profile__actions { + display: flex; + gap: 1rem; + flex-wrap: wrap; + padding-top: 0.5rem; + + @media (max-width: 639px) { + flex-direction: column; + + .btn { + width: 100%; + } + } + } +} + +/* ===== WHY ME SECTION ===== */ + +.why-me { + background: $color-background-main; + padding-block: 3rem; + + @media (min-width: 768px) { + padding-block: 1rem; + } + + h2 { + text-align: center; + font-size: clamp(1.75rem, 3vw + 1rem, 2.25rem); + font-weight: 800; + color: $color-text-primary; + margin: 0 0 2.5rem; + letter-spacing: -0.02em; + } + + .why-me__grid { + display: grid; + gap: 1.25rem; + grid-template-columns: 1fr; + + @media (min-width: 640px) { + grid-template-columns: repeat(2, 1fr); + } + + @media (min-width: 1024px) { + grid-template-columns: repeat(3, 1fr); + } + } + + .why-me__item { + background: $glass-bg; + backdrop-filter: blur(20px); + border: 1px solid $glass-border; + border-radius: $radius-lg; + padding: 1.5rem; + box-shadow: $shadow-glass; + transition: all 0.3s ease; + + &:hover { + transform: translateY(-4px); + box-shadow: $shadow-glass-hover; + border-color: rgba(37, 99, 235, 0.3); + } + + h4 { + margin: 0 0 0.75rem; + color: $color-text-primary; + font-size: 1.0625rem; + font-weight: 700; + line-height: 1.3; + } + + p { + margin: 0; + color: $color-text-secondary; + font-size: 0.9375rem; + line-height: 1.6; + } + } +} + +/* ===== MOTION PREFERENCES ===== */ + +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/components/about/about.component.spec.ts b/apps/frontend/src/app/components/about/about.component.spec.ts new file mode 100644 index 0000000..74d6d9e --- /dev/null +++ b/apps/frontend/src/app/components/about/about.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AboutComponent } from './about.component'; + +describe('AboutComponent', () => { + let component: AboutComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AboutComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AboutComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/frontend/src/app/components/about/about.component.ts b/apps/frontend/src/app/components/about/about.component.ts new file mode 100644 index 0000000..0ef7eed --- /dev/null +++ b/apps/frontend/src/app/components/about/about.component.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; +import { PageTitleComponent } from "../../shared/page-title/page-title.component"; +import { Router } from '@angular/router'; + +@Component({ + selector: 'app-about', + standalone: true, + imports: [PageTitleComponent], + templateUrl: './about.component.html', + styleUrl: './about.component.scss' +}) +export class AboutComponent { + + constructor(public router: Router) {} + + navigateToContact() { + this.router.navigate(['/contact']); + } +} diff --git a/apps/frontend/src/app/components/admin/admin-analytics/admin-analytics.component.html b/apps/frontend/src/app/components/admin/admin-analytics/admin-analytics.component.html new file mode 100644 index 0000000..e6a43b0 --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-analytics/admin-analytics.component.html @@ -0,0 +1,268 @@ + +
+
+ + +
+
+

Analytics

+
+
+
+
+ +
+
+ {{ countdown }}s +
+
+
+
+
+ + +
+
+ + +
+
+

Daten werden geladen...

+
+ + +
+ error +

Fehler beim Laden

+

Analytics konnten nicht geladen werden.

+ +
+ + + + + +
+ +
+ + +
+
+
+ visibility +
+
+ {{ currentStats?.pageviews || 0 }} + Seitenaufrufe +
+
+ +
+
+ person +
+
+ {{ currentStats?.uniqueSessions || 0 }} + Besucher +
+
+ +
+
+ check_circle +
+
+ {{ currentStats?.conversions || 0 }} + Conversions +
+
+ +
+
+ trending_up +
+
+ + {{ getPagesPerVisit() | number:'1.1-1' }} + + Seiten/Besuch +
+
+
+ + +
+ + +
+

+ show_chart + Seitenaufrufe (30 Tage) +

+
+
+ {{ point.pageviews }} +
+
+
+ {{ getFirstDate() }} + {{ getLastDate() }} +
+
+ + +
+

+ article + Top Seiten +

+
+
+ {{ i + 1 }} + {{ page.page }} + {{ page.views }} +
+
+ inbox +

Noch keine Daten

+
+
+
+ + +
+

+ link + Referrer +

+
+
+ {{ i + 1 }} + {{ ref.referrer }} + {{ ref.count }} +
+
+ inbox +

Noch keine Daten

+
+
+
+ + +
+

+ devices + Geräte +

+
+
+ computer + Desktop +
+
+
+ {{ getDevicePercent(data.devices.desktop) }}% +
+
+ smartphone + Mobile +
+
+
+ {{ getDevicePercent(data.devices.mobile) }}% +
+
+ tablet + Tablet +
+
+
+ {{ getDevicePercent(data.devices.tablet) }}% +
+
+
+ + +
+

+ timeline + Letzte Events +

+
+
+
+ {{ getEventIcon(event.type) }} +
+
+
+ {{ getEventLabel(event.type) }} + + {{ getDeviceIcon(event.deviceType) }} + +
+ {{ event.page }} +
+ + +
+
+ aspect_ratio + {{ event.screenSize }} +
+
+ {{ formatEventTime(event.timestamp) }} +
+
+ inbox +

Noch keine Events

+
+
+
+ +
+ + +
+ security +

Alle Daten sind DSGVO-konform anonymisiert. IPs werden gekürzt, keine Profile erstellt.

+
+ +
+ +
+
+
diff --git a/apps/frontend/src/app/components/admin/admin-analytics/admin-analytics.component.scss b/apps/frontend/src/app/components/admin/admin-analytics/admin-analytics.component.scss new file mode 100644 index 0000000..bb52493 --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-analytics/admin-analytics.component.scss @@ -0,0 +1,827 @@ +// ============================================ +// ADMIN ANALYTICS - Mobile-First Compact Design +// ============================================ +@import '../admin-shared.scss'; + +// ============================================ +// LOADING & ERROR STATES +// ============================================ +.loading-state, +.error-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: $admin-space-2xl; + background: $glass-bg; + border: 1px solid $glass-border; + border-radius: $radius-md; + text-align: center; + animation: fadeIn 0.3s ease-out; + + .loading-spinner { + width: 40px; + height: 40px; + border: 3px solid rgba($color-brand-primary, 0.2); + border-top-color: $color-brand-primary; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: $admin-space-md; + } + + .material-symbols-outlined { + font-size: 2.5rem; + color: $accent-red; + margin-bottom: $admin-space-sm; + } + + h3 { + margin: 0 0 $admin-space-2xs; + font-size: 1rem; + font-weight: 700; + color: $color-text-primary; + } + + p { + color: $color-text-tertiary; + font-size: 0.875rem; + margin: 0 0 $admin-space-md; + } +} + +// ============================================ +// AUTO-RELOAD & COUNTDOWN +// ============================================ +.auto-reload-container { + display: flex; + align-items: center; + gap: $admin-space-sm; +} + +.auto-reload-toggle { + .toggle-label { + display: flex; + align-items: center; + gap: $admin-space-xs; + cursor: pointer; + font-size: 0.8125rem; + color: $color-text-secondary; + } + + .toggle-input { + position: absolute; + opacity: 0; + pointer-events: none; + + &:checked + .toggle-slider { + background: $color-brand-primary; + + &::before { + transform: translateX(16px); + } + } + } + + .toggle-slider { + position: relative; + width: 36px; + height: 20px; + background: $color-gray-300; + border-radius: $radius-full; + transition: background $transition-fast; + + &::before { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: 16px; + height: 16px; + background: white; + border-radius: 50%; + transition: transform $transition-fast; + box-shadow: $shadow-sm; + } + } + + .toggle-text { + font-weight: 500; + white-space: nowrap; + } +} + +.countdown-timer { + display: flex; + align-items: center; + gap: $admin-space-xs; + padding: $admin-space-2xs $admin-space-sm; + background: rgba($color-brand-primary, 0.1); + border-radius: $radius-full; +} + +.countdown-text { + font-size: 0.75rem; + font-weight: 600; + color: $color-brand-primary; + min-width: 24px; +} + +.countdown-progress { + width: 40px; + height: 4px; + background: rgba($color-brand-primary, 0.2); + border-radius: 2px; + overflow: hidden; +} + +.countdown-bar { + height: 100%; + background: $color-brand-primary; + border-radius: 2px; + transition: width 1s linear; +} + +// ============================================ +// PERIOD SELECTOR +// ============================================ +.period-selector { + display: flex; + gap: $admin-space-2xs; + margin-bottom: $admin-space-md; + overflow-x: auto; + padding-bottom: $admin-space-2xs; + scrollbar-width: none; + animation: fadeIn 0.3s ease-out; + + &::-webkit-scrollbar { + display: none; + } +} + +.period-btn { + padding: $admin-space-xs $admin-space-sm; + background: $glass-bg; + border: 1px solid $glass-border; + border-radius: $radius-sm; + font-size: 0.75rem; + font-weight: 600; + color: $color-text-secondary; + cursor: pointer; + transition: all $transition-fast; + white-space: nowrap; + + &:hover { + border-color: $color-brand-primary; + color: $color-brand-primary; + } + + &.active { + background: $gradient-primary; + border-color: transparent; + color: white; + } +} + +// ============================================ +// STAT CARDS +// ============================================ +.stat-cards { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: $admin-space-xs; + margin-bottom: $admin-space-md; + animation: fadeIn 0.3s ease-out 0.05s backwards; + + @media (min-width: 640px) { + grid-template-columns: repeat(4, 1fr); + } +} + +.stat-card { + display: flex; + align-items: center; + gap: $admin-space-sm; + padding: $admin-space-sm; + background: $glass-bg; + border: 1px solid $glass-border; + border-radius: $radius-md; + transition: all $transition-fast; + animation: staggerFade 0.3s ease-out backwards; + + @for $i from 1 through 4 { + &:nth-child(#{$i}) { + animation-delay: #{$i * 50}ms; + } + } + + &:hover { + box-shadow: $shadow-sm; + transform: translateY(-2px); + border-color: rgba($color-brand-primary, 0.3); + } +} + +.stat-icon { + width: 40px; + height: 40px; + border-radius: $radius-sm; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + .material-symbols-outlined { + font-size: 1.25rem; + color: white; + } + + &--blue { background: linear-gradient(135deg, $accent-blue, darken($accent-blue, 10%)); } + &--purple { background: linear-gradient(135deg, $accent-purple, darken($accent-purple, 10%)); } + &--green { background: linear-gradient(135deg, $accent-green, darken($accent-green, 10%)); } + &--orange { background: linear-gradient(135deg, $accent-orange, darken($accent-orange, 10%)); } + + @media (min-width: 768px) { + width: 44px; + height: 44px; + + .material-symbols-outlined { + font-size: 1.375rem; + } + } +} + +.stat-content { + display: flex; + flex-direction: column; + min-width: 0; +} + +.stat-value { + font-size: 1.25rem; + font-weight: 800; + color: $color-text-primary; + line-height: 1.2; + + @media (min-width: 768px) { + font-size: 1.5rem; + } +} + +.stat-label { + font-size: 0.625rem; + font-weight: 600; + color: $color-text-tertiary; + text-transform: uppercase; + letter-spacing: 0.03em; + + @media (min-width: 768px) { + font-size: 0.6875rem; + } +} + +// ============================================ +// ANALYTICS GRID +// ============================================ +.analytics-grid { + display: grid; + gap: $admin-space-sm; + grid-template-columns: 1fr; + animation: fadeIn 0.3s ease-out 0.1s backwards; + + @media (min-width: 768px) { + grid-template-columns: repeat(2, 1fr); + } + + @media (min-width: 1024px) { + grid-template-columns: repeat(3, 1fr); + } +} + +// ============================================ +// ANALYTICS CARD +// ============================================ +.analytics-card { + background: $glass-bg; + border: 1px solid $glass-border; + border-radius: $radius-md; + overflow: hidden; + animation: staggerFade 0.3s ease-out backwards; + transition: all $transition-fast; + + &:hover { + border-color: rgba($color-brand-primary, 0.2); + box-shadow: $shadow-sm; + } + + @for $i from 1 through 6 { + &:nth-child(#{$i}) { + animation-delay: #{100 + ($i * 50)}ms; + } + } + + // Card variants for grid placement + &.chart-card { + @media (min-width: 768px) { + grid-column: span 2; + } + } + + &.events-card { + @media (min-width: 1024px) { + grid-row: span 2; + } + } + + // Card header (h3 direct child) + > h3 { + display: flex; + align-items: center; + gap: $admin-space-xs; + padding: $admin-space-sm $admin-space-md; + margin: 0; + font-size: 0.875rem; + font-weight: 700; + color: $color-text-primary; + border-bottom: 1px solid $border-default; + background: rgba($color-gray-50, 0.5); + + .material-symbols-outlined { + font-size: 1.125rem; + color: $color-brand-primary; + } + } +} + +// ============================================ +// SIMPLE CHART (Bar Chart) +// ============================================ +.simple-chart { + display: flex; + align-items: flex-end; + gap: 2px; + height: 120px; + padding: $admin-space-md; + padding-bottom: 0; + + @media (min-width: 768px) { + height: 160px; + } +} + +.chart-bar { + flex: 1; + min-width: 4px; + background: linear-gradient(180deg, $color-brand-primary, rgba($color-brand-primary, 0.6)); + border-radius: 2px 2px 0 0; + transition: all $transition-fast; + position: relative; + cursor: pointer; + + &:hover { + background: linear-gradient(180deg, $accent-purple, rgba($accent-purple, 0.6)); + transform: scaleY(1.02); + } + + &:hover .chart-tooltip { + opacity: 1; + visibility: visible; + transform: translateX(-50%) translateY(-4px); + } +} + +.chart-tooltip { + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%) translateY(0); + padding: $admin-space-2xs $admin-space-xs; + background: $color-gray-900; + color: white; + font-size: 0.625rem; + font-weight: 600; + border-radius: $radius-sm; + white-space: nowrap; + opacity: 0; + visibility: hidden; + transition: all $transition-fast; + pointer-events: none; + z-index: 10; + + &::after { + content: ''; + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + border: 4px solid transparent; + border-top-color: $color-gray-900; + } +} + +.chart-labels { + display: flex; + justify-content: space-between; + padding: $admin-space-xs $admin-space-md $admin-space-md; + font-size: 0.625rem; + color: $color-text-tertiary; +} + +// ============================================ +// LIST CONTENT (Top Pages, Referrers) +// ============================================ +.list-content { + display: flex; + flex-direction: column; + gap: $admin-space-2xs; + padding: $admin-space-sm; + max-height: 200px; + overflow-y: auto; + + &::-webkit-scrollbar { + width: 4px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: $color-gray-300; + border-radius: 2px; + } +} + +.list-item { + display: flex; + align-items: center; + gap: $admin-space-xs; + padding: $admin-space-xs; + background: $color-gray-50; + border-radius: $radius-sm; + transition: all $transition-fast; + + &:hover { + background: $color-gray-100; + } +} + +.list-rank { + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + background: $color-brand-primary; + color: white; + font-size: 0.625rem; + font-weight: 700; + border-radius: $radius-sm; + flex-shrink: 0; +} + +.list-name { + flex: 1; + font-size: 0.75rem; + color: $color-text-primary; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; +} + +.list-value { + font-size: 0.75rem; + font-weight: 700; + color: $color-brand-primary; + padding: $admin-space-2xs $admin-space-xs; + background: rgba($color-brand-primary, 0.1); + border-radius: $radius-sm; +} + +// ============================================ +// EMPTY STATE +// ============================================ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: $admin-space-xl; + text-align: center; + color: $color-text-tertiary; + + .material-symbols-outlined { + font-size: 2rem; + margin-bottom: $admin-space-xs; + opacity: 0.4; + } + + p { + font-size: 0.8125rem; + margin: 0; + } +} + +// ============================================ +// DEVICES SECTION +// ============================================ +.devices-content { + display: flex; + flex-direction: column; + gap: $admin-space-sm; + padding: $admin-space-sm; +} + +.device-row { + display: flex; + align-items: center; + gap: $admin-space-xs; +} + +.device-icon { + font-size: 1.25rem; + width: 32px; + color: $color-text-tertiary; +} + +.device-label { + font-size: 0.75rem; + font-weight: 600; + color: $color-text-secondary; + width: 60px; +} + +.device-bar { + flex: 1; + height: 8px; + background: $color-gray-200; + border-radius: $radius-full; + overflow: hidden; +} + +.device-fill { + height: 100%; + border-radius: $radius-full; + transition: width 0.5s ease-out; + + &.desktop { + background: linear-gradient(90deg, $accent-blue, lighten($accent-blue, 10%)); + } + + &.mobile { + background: linear-gradient(90deg, $accent-green, lighten($accent-green, 10%)); + } + + &.tablet { + background: linear-gradient(90deg, $accent-purple, lighten($accent-purple, 10%)); + } +} + +.device-percent { + font-size: 0.75rem; + font-weight: 700; + color: $color-text-primary; + width: 40px; + text-align: right; +} + +// ============================================ +// EVENTS SECTION +// ============================================ +.events-list { + display: flex; + flex-direction: column; + gap: $admin-space-2xs; + padding: $admin-space-sm; + max-height: 280px; + overflow-y: auto; + + @media (min-width: 1024px) { + max-height: 400px; + } + + &::-webkit-scrollbar { + width: 4px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: $color-gray-300; + border-radius: 2px; + } +} + +.event-item { + display: flex; + gap: $admin-space-xs; + padding: $admin-space-xs; + background: $color-gray-50; + border-radius: $radius-sm; + transition: all $transition-fast; + + &:hover { + background: $color-gray-100; + } + + &.conversion-event { + background: rgba($accent-green, 0.08); + border-left: 3px solid $accent-green; + + &:hover { + background: rgba($accent-green, 0.12); + } + } +} + +.event-icon { + width: 28px; + height: 28px; + border-radius: $radius-sm; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + .material-symbols-outlined { + font-size: 0.875rem; + color: white; + } + + &[data-type="pageview"] { background: $accent-blue; } + &[data-type="conversion"] { background: $accent-green; } + &[data-type="click"] { background: $accent-purple; } + &[data-type="scroll"] { background: $accent-orange; } + &[data-type="error"] { background: $accent-red; } +} + +.event-content { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; +} + +.event-header { + display: flex; + align-items: center; + gap: $admin-space-xs; +} + +.event-type { + font-size: 0.6875rem; + font-weight: 700; + color: $color-text-primary; + text-transform: uppercase; + letter-spacing: 0.02em; +} + +.event-device { + .material-symbols-outlined { + font-size: 0.875rem; + color: $color-text-tertiary; + } +} + +.event-page { + font-size: 0.75rem; + color: $color-text-secondary; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.event-details { + display: flex; + flex-wrap: wrap; + gap: $admin-space-2xs; + margin-top: 2px; +} + +.event-metadata { + display: inline-flex; + align-items: center; + gap: 2px; + padding: 2px $admin-space-2xs; + background: rgba($color-brand-primary, 0.1); + border-radius: $radius-sm; + font-size: 0.625rem; + color: $color-brand-primary; + + .material-symbols-outlined { + font-size: 0.75rem; + } +} + +.event-session { + display: flex; + align-items: center; + gap: 2px; + font-size: 0.625rem; + color: $color-text-tertiary; + + .material-symbols-outlined { + font-size: 0.75rem; + } +} + +.event-time { + font-size: 0.625rem; + font-weight: 500; + color: $color-text-tertiary; + white-space: nowrap; + flex-shrink: 0; +} + +// ============================================ +// PRIVACY INFO +// ============================================ +.privacy-info { + display: flex; + align-items: center; + gap: $admin-space-sm; + margin-top: $admin-space-md; + padding: $admin-space-sm $admin-space-md; + background: rgba($accent-green, 0.08); + border: 1px solid rgba($accent-green, 0.2); + border-radius: $radius-md; + animation: fadeIn 0.3s ease-out 0.3s backwards; + + .material-symbols-outlined { + font-size: 1.25rem; + color: $accent-green; + flex-shrink: 0; + } + + p { + margin: 0; + font-size: 0.75rem; + color: $color-text-secondary; + line-height: 1.4; + } +} + +// ============================================ +// RESPONSIVE ADJUSTMENTS +// ============================================ +@media (max-width: 640px) { + .admin-header-actions { + flex-direction: column; + align-items: stretch; + gap: $admin-space-xs; + } + + .auto-reload-container { + justify-content: space-between; + flex-wrap: wrap; + } + + .stat-cards { + gap: $admin-space-2xs; + } + + .stat-card { + padding: $admin-space-xs; + gap: $admin-space-xs; + } + + .stat-icon { + width: 32px; + height: 32px; + + .material-symbols-outlined { + font-size: 1rem; + } + } + + .stat-value { + font-size: 1rem; + } + + .simple-chart { + height: 100px; + } + + .privacy-info { + flex-direction: column; + text-align: center; + } +} + +// ============================================ +// ANIMATIONS +// ============================================ +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} diff --git a/apps/frontend/src/app/components/admin/admin-analytics/admin-analytics.component.ts b/apps/frontend/src/app/components/admin/admin-analytics/admin-analytics.component.ts new file mode 100644 index 0000000..369022b --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-analytics/admin-analytics.component.ts @@ -0,0 +1,295 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { HttpClient } from '@angular/common/http'; +import { interval, Subscription } from 'rxjs'; +import { AdminLayoutComponent } from '../admin-layout/admin-layout.component'; +import { ConfigService } from '../../../services/config.service'; +import { ConfirmationService } from '../../../shared/confirmation/confirmation.service'; + +interface OverviewStats { + today: { pageviews: number; uniqueSessions: number; conversions: number }; + yesterday: { pageviews: number; uniqueSessions: number; conversions: number }; + last7Days: { pageviews: number; uniqueSessions: number; conversions: number }; + last30Days: { pageviews: number; uniqueSessions: number; conversions: number }; +} + +interface TimeSeriesPoint { + date: string; + pageviews: number; + uniqueSessions: number; + conversions: number; +} + +interface PageStats { + page: string; + views: number; + uniqueSessions: number; +} + +interface ReferrerStats { + referrer: string; + count: number; +} + +interface DeviceStats { + desktop: number; + mobile: number; + tablet: number; +} + +interface RecentEvent { + type: string; + page: string; + timestamp: Date; + sessionId: string; + userAgent?: string; + screenSize?: string; + referrer?: string; + metadata?: Record; + deviceType?: 'desktop' | 'mobile' | 'tablet'; +} + +interface DashboardData { + overview: OverviewStats; + timeSeries: TimeSeriesPoint[]; + topPages: PageStats[]; + referrers: ReferrerStats[]; + devices: DeviceStats; + recentEvents: RecentEvent[]; +} + +@Component({ + selector: 'app-admin-analytics', + standalone: true, + imports: [CommonModule, AdminLayoutComponent], + templateUrl: './admin-analytics.component.html', + styleUrls: ['./admin-analytics.component.scss'] +}) +export class AdminAnalyticsComponent implements OnInit, OnDestroy { + private get API_URL(): string { + return `${this.configService.apiUrl}/analytics`; + } + + loading = true; + error = false; + autoReload = true; + countdown = 15; + + data: DashboardData | null = null; + selectedPeriod: 'today' | 'yesterday' | 'last7Days' | 'last30Days' = 'today'; + periods: ('today' | 'yesterday' | 'last7Days' | 'last30Days')[] = ['today', 'yesterday', 'last7Days', 'last30Days']; + + private refreshSub?: Subscription; + private countdownSub?: Subscription; + + constructor( + private http: HttpClient, + private confirmationService: ConfirmationService, + private configService: ConfigService + ) {} + + ngOnInit(): void { + this.loadDashboard(); + this.startAutoReload(); + } + + ngOnDestroy(): void { + this.refreshSub?.unsubscribe(); + this.countdownSub?.unsubscribe(); + } + + startAutoReload(): void { + this.stopAutoReload(); + if (this.autoReload) { + this.countdown = 15; + + // Countdown Timer (every second) + this.countdownSub = interval(1000).subscribe(() => { + this.countdown--; + if (this.countdown <= 0) { + this.countdown = 15; + this.loadDashboard(false); + } + }); + + // Backup: Auto-Refresh alle 15 Sekunden (fallback) + this.refreshSub = interval(15000).subscribe(() => { + this.countdown = 15; + this.loadDashboard(false); + }); + } + } + + stopAutoReload(): void { + this.refreshSub?.unsubscribe(); + this.refreshSub = undefined; + this.countdownSub?.unsubscribe(); + this.countdownSub = undefined; + this.countdown = 60; + } + + toggleAutoReload(): void { + this.autoReload = !this.autoReload; + if (this.autoReload) { + this.startAutoReload(); + } else { + this.stopAutoReload(); + } + } + + loadDashboard(showLoading = true): void { + if (showLoading) this.loading = true; + this.error = false; + + this.http.get(`${this.API_URL}/dashboard`).subscribe({ + next: (data) => { + this.data = data; + this.loading = false; + }, + error: () => { + this.error = true; + this.loading = false; + } + }); + } + + get currentStats() { + if (!this.data) return null; + return this.data.overview[this.selectedPeriod]; + } + + get previousStats() { + if (!this.data) return null; + // Vergleich mit vorheriger Periode + if (this.selectedPeriod === 'today') return this.data.overview.yesterday; + if (this.selectedPeriod === 'yesterday') return this.data.overview.yesterday; // Same + if (this.selectedPeriod === 'last7Days') return this.data.overview.last7Days; + return this.data.overview.last30Days; + } + + setPeriod(period: 'today' | 'yesterday' | 'last7Days' | 'last30Days'): void { + this.selectedPeriod = period; + } + + getPagesPerVisit(): number { + const stats = this.currentStats; + if (!stats || !stats.uniqueSessions) return 0; + return stats.pageviews / stats.uniqueSessions; + } + + getFirstDate(): string { + if (!this.data?.timeSeries?.length) return ''; + return this.formatDate(this.data.timeSeries[0].date); + } + + getLastDate(): string { + if (!this.data?.timeSeries?.length) return ''; + return this.formatDate(this.data.timeSeries[this.data.timeSeries.length - 1].date); + } + + getChange(current: number, previous: number): number { + if (previous === 0) return current > 0 ? 100 : 0; + return Math.round(((current - previous) / previous) * 100); + } + + get totalDevices(): number { + if (!this.data) return 0; + const d = this.data.devices; + return d.desktop + d.mobile + d.tablet; + } + + getDevicePercent(count: number): number { + if (this.totalDevices === 0) return 0; + return Math.round((count / this.totalDevices) * 100); + } + + getMaxPageviews(): number { + if (!this.data?.timeSeries.length) return 1; + return Math.max(...this.data.timeSeries.map(p => p.pageviews), 1); + } + + formatDate(date: string): string { + const d = new Date(date); + return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); + } + + formatEventTime(timestamp: Date): string { + const d = new Date(timestamp); + return d.toLocaleString('de-DE', { + day: '2-digit', + month: '2-digit', + hour: '2-digit', + minute: '2-digit' + }); + } + + getEventIcon(type: string): string { + const icons: Record = { + 'pageview': 'visibility', + 'click': 'touch_app', + 'conversion': 'check_circle', + 'error': 'error', + 'custom': 'code', + 'form_submit': 'send', + 'scroll': 'swap_vert' + }; + return icons[type] || 'analytics'; + } + + getEventLabel(type: string): string { + const labels: Record = { + 'pageview': 'Seitenaufruf', + 'click': 'Klick', + 'conversion': 'Conversion', + 'error': 'Fehler', + 'custom': 'Event', + 'form_submit': 'Formular', + 'scroll': 'Scroll' + }; + return labels[type] || type; + } + + getDeviceIcon(deviceType?: 'desktop' | 'mobile' | 'tablet'): string { + const icons: Record = { + 'desktop': 'computer', + 'mobile': 'smartphone', + 'tablet': 'tablet' + }; + return icons[deviceType || 'desktop'] || 'devices'; + } + + async cleanupOldData(): Promise { + const confirmed = await this.confirmationService.confirm({ + title: 'Analytics bereinigen', + message: 'Möchtest du alte Analytics-Daten (älter als 90 Tage) wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.', + confirmText: 'Ja, löschen', + cancelText: 'Abbrechen', + type: 'danger', + icon: 'delete_sweep' + }); + + if (!confirmed) return; + + this.http.delete<{ deleted: number }>(`${this.API_URL}/cleanup`).subscribe({ + next: (result) => { + this.confirmationService.confirm({ + title: 'Erfolgreich bereinigt', + message: `${result.deleted} Analytics-Events wurden erfolgreich gelöscht.`, + confirmText: 'OK', + type: 'success', + icon: 'check_circle' + }); + this.loadDashboard(); + }, + error: () => { + this.confirmationService.confirm({ + title: 'Fehler beim Löschen', + message: 'Die Analytics-Daten konnten nicht gelöscht werden. Bitte versuche es später erneut.', + confirmText: 'OK', + type: 'danger', + icon: 'error' + }); + } + }); + } +} diff --git a/apps/frontend/src/app/components/admin/admin-booking/admin-booking.component.html b/apps/frontend/src/app/components/admin/admin-booking/admin-booking.component.html new file mode 100644 index 0000000..ffb2efa --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-booking/admin-booking.component.html @@ -0,0 +1,501 @@ + +
+
+ + +
+
+
+

Terminverwaltung

+

{{ stats.total }} Slots · {{ stats.booked }} gebucht

+
+
+ + +
+
+ + +
+
+ + {{ stats.total }} + Gesamt +
+
+ + {{ stats.available }} + Frei +
+
+ + {{ stats.booked }} + Gebucht +
+
+ + {{ stats.upcoming }} + Bevorstehend +
+
+
+ + +
+ + + +
+ + +
+
+ + + + +
+ + +
+ + +
+
+
{{ day }}
+
+ +
+
+ +
+ {{ day.date.getDate() }} + + {{ day.slots.length }} + +
+ +
+
+ {{ formatTime(slot.timeFrom) }} + +
+ +
+ + +
+
+
+ + +
+
+

Lade Kalender...

+
+ +
+ + +
+
+ + +
+ + +
+
+ +
+
+ {{ formatDateLong(slot.date) }} + {{ formatTime(slot.timeFrom) }} - {{ formatTime(slot.timeTo) }} +
+ + + +
+ +
+
+ + {{ slot.currentBookings || 0 }}/{{ slot.maxBookings }} +
+
+ + {{ getSlotBookings(slot).length }} Buchung(en) +
+
+ +
+
+ {{ booking.name }} + + {{ getStatusLabel(booking.status) }} + +
+
+
+ +
+ +

Keine anstehenden Termine

+

Erstelle neue Slots im Kalender

+
+
+ + +
+
+ +
+
+ {{ formatDateLong(slot.date) }} + {{ formatTime(slot.timeFrom) }} - {{ formatTime(slot.timeTo) }} +
+ + +
+ +
+
+ + {{ slot.currentBookings || 0 }}/{{ slot.maxBookings }} +
+
+
+
+
+
+ + +
+
+
+

+ + Slot-Serie erstellen +

+ +
+ +
+ +
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + {{ getPreviewCount() }} Slots werden erstellt +
+
+ + +
+
+ + +
+
+
+

+ + Slot Details +

+ +
+ +
+
+
+ {{ formatDateLong(selectedSlot.date) }} + {{ formatTime(selectedSlot.timeFrom) }} - {{ formatTime(selectedSlot.timeTo) }} +
+
+ + + {{ getSlotStatusText(selectedSlot) }} + +
+
+ +
+
+ + {{ selectedSlot.maxBookings }} +
+
+ + {{ selectedSlot.currentBookings || 0 }} +
+
+ + + + Öffnen + +
+
+ + +
+

+ + Buchungen ({{ selectedSlotBookings.length }}) +

+ +
+
+
+ {{ booking.name }} + + {{ getStatusLabel(booking.status) }} + +
+ +
+
+ + {{ booking.email }} +
+
+ + {{ booking.phone }} +
+
+ + {{ booking.message }} +
+
+ + +
+
+
+ +
+ +

Keine Buchungen vorhanden

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

+ + {{ formatDateLongFromDate(selectedDay.date) }} +

+ +
+ +
+
+
+ +
+ {{ formatTime(slot.timeFrom) }} - {{ formatTime(slot.timeTo) }} +
+ +
+ + + {{ slot.currentBookings || 0 }}/{{ slot.maxBookings }} gebucht +
+ +
+ Buchungen: +
{{ getBookingNames(slot) }}
+
+
+
+
+ + +
+
+
\ No newline at end of file diff --git a/apps/frontend/src/app/components/admin/admin-booking/admin-booking.component.scss b/apps/frontend/src/app/components/admin/admin-booking/admin-booking.component.scss new file mode 100644 index 0000000..905ad89 --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-booking/admin-booking.component.scss @@ -0,0 +1,620 @@ +// ============================================ +// ADMIN BOOKING - Component-Specific Styles +// ============================================ +@import '../admin-shared.scss'; + +// ============================================ +// HEADER EXTENSIONS +// ============================================ +.header-top { + display: flex; + justify-content: space-between; + align-items: center; + gap: $admin-space-sm; + + @media (max-width: 640px) { + flex-direction: column; + align-items: stretch; + } +} + +.header-content { + min-width: 0; +} + +// ============================================ +// CALENDAR NAVIGATION +// ============================================ +.calendar-nav { + display: flex; + align-items: center; + gap: $admin-space-xs; + padding: $admin-space-sm; + background: $glass-bg; + border: 1px solid $glass-border; + border-radius: $radius-md; + margin-bottom: $admin-space-md; + animation: fadeIn 0.3s ease-out 0.1s backwards; + + @media (max-width: 480px) { + flex-wrap: wrap; + justify-content: center; + } +} + +.nav-center { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: $admin-space-sm; + + h2 { + font-size: 0.9375rem; + font-weight: 700; + color: $color-text-primary; + margin: 0; + + @media (min-width: 768px) { + font-size: 1rem; + } + } +} + +// ============================================ +// CALENDAR GRID +// ============================================ +.calendar-container { + background: $glass-bg; + border: 1px solid $glass-border; + border-radius: $radius-md; + overflow: hidden; + animation: fadeIn 0.3s ease-out 0.15s backwards; + + @media (max-width: 768px) { + display: none; + } +} + +.calendar-header { + display: grid; + grid-template-columns: repeat(7, 1fr); + background: rgba($color-gray-50, 0.8); + border-bottom: 1px solid $border-default; +} + +.weekday { + padding: $admin-space-xs; + text-align: center; + font-size: 0.6875rem; + font-weight: 700; + color: $color-text-tertiary; + text-transform: uppercase; + letter-spacing: 0.03em; + + @media (max-width: 480px) { + padding: 6px 2px; + font-size: 0.5625rem; + } +} + +.calendar-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); +} + +.calendar-day { + min-height: 90px; + padding: $admin-space-xs; + border-right: 1px solid $border-default; + border-bottom: 1px solid $border-default; + background: white; + transition: all $transition-fast; + position: relative; + + &:nth-child(7n) { + border-right: none; + } + + &:hover { + background: rgba($color-brand-primary, 0.02); + } + + &.other-month { + background: $color-gray-50; + opacity: 0.5; + } + + &.today { + background: rgba($color-brand-primary, 0.05); + + .day-number { + background: $color-brand-primary; + color: white; + } + } + + &.has-slots { + .btn-add-slot { + display: none; + } + } + + @media (max-width: 640px) { + min-height: 60px; + padding: 4px; + } +} + +.day-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 4px; +} + +.day-number { + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + font-weight: 600; + color: $color-text-primary; + border-radius: 50%; + transition: all $transition-fast; + + @media (max-width: 480px) { + width: 20px; + height: 20px; + font-size: 0.625rem; + } +} + +.slot-count { + font-size: 0.625rem; + font-weight: 700; + color: $color-brand-primary; + background: rgba($color-brand-primary, 0.1); + padding: 2px 6px; + border-radius: $radius-full; +} + +.day-slots { + display: flex; + flex-direction: column; + gap: 2px; +} + +.slot-preview { + display: flex; + align-items: center; + justify-content: space-between; + padding: 3px 6px; + border-radius: $radius-sm; + font-size: 0.625rem; + cursor: pointer; + transition: all $transition-fast; + + &.available { + background: rgba($accent-green, 0.12); + color: darken($accent-green, 10%); + + &:hover { + background: rgba($accent-green, 0.2); + } + } + + &.booked { + background: rgba($accent-orange, 0.12); + color: darken($accent-orange, 10%); + + &:hover { + background: rgba($accent-orange, 0.2); + } + } + + &.unavailable { + background: rgba($color-gray-400, 0.12); + color: $color-gray-500; + + &:hover { + background: rgba($color-gray-400, 0.2); + } + } +} + +.slot-time { + font-weight: 600; +} + +.slot-status-icon { + font-size: 0.75rem; + opacity: 0.8; +} + +.more-slots { + padding: 2px 6px; + font-size: 0.5625rem; + font-weight: 600; + color: $color-brand-primary; + background: rgba($color-brand-primary, 0.08); + border: none; + border-radius: $radius-sm; + cursor: pointer; + transition: all $transition-fast; + + &:hover { + background: rgba($color-brand-primary, 0.15); + } +} + +.btn-add-slot { + position: absolute; + bottom: 4px; + right: 4px; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: 1px dashed $border-default; + border-radius: $radius-sm; + color: $color-text-tertiary; + cursor: pointer; + opacity: 0; + transition: all $transition-fast; + + app-icon { + font-size: 0.875rem; + } + + .calendar-day:hover & { + opacity: 1; + } + + &:hover { + background: rgba($color-brand-primary, 0.08); + border-color: $color-brand-primary; + color: $color-brand-primary; + } +} + +// ============================================ +// MOBILE LIST VIEW +// ============================================ +.mobile-list-view { + display: none; + padding: 0 $admin-space-md $admin-space-md; + + @media (max-width: 768px) { + display: block; + } +} + +.mobile-slot-card { + cursor: pointer; + transition: all $transition-fast; + + &:hover { + transform: translateY(-2px); + box-shadow: $shadow-md; + } + + &.available { + border-left: 3px solid $accent-green; + } + + &.booked { + border-left: 3px solid $accent-orange; + } + + &.unavailable { + border-left: 3px solid $color-gray-400; + opacity: 0.7; + } +} + +.mobile-slot-date { + display: flex; + flex-direction: column; + gap: 2px; + + strong { + font-size: 0.8125rem; + color: $color-text-primary; + } + + span { + font-size: 0.6875rem; + color: $color-text-tertiary; + } +} + +.mobile-slot-bookings { + margin-top: $admin-space-xs; + padding-top: $admin-space-xs; + border-top: 1px solid $border-default; +} + +.booking-preview { + display: flex; + justify-content: space-between; + align-items: center; + padding: $admin-space-2xs 0; + font-size: 0.75rem; + + strong { + color: $color-text-primary; + } + + span { + font-size: 0.6875rem; + font-weight: 600; + } +} + +// ============================================ +// MODAL EXTENSIONS +// ============================================ +.pattern-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: $admin-space-xs; + + @media (min-width: 480px) { + grid-template-columns: repeat(4, 1fr); + } + + .admin-chip { + flex-direction: column; + padding: $admin-space-sm; + gap: $admin-space-2xs; + + app-icon { + font-size: 1.25rem; + } + + small { + font-size: 0.625rem; + } + } +} + +.slot-detail-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: $admin-space-sm; + padding-bottom: $admin-space-sm; + border-bottom: 1px solid $border-default; + margin-bottom: $admin-space-sm; +} + +.detail-date { + display: flex; + flex-direction: column; + gap: 2px; + + strong { + font-size: 0.9375rem; + color: $color-text-primary; + } + + span { + font-size: 0.8125rem; + color: $color-brand-primary; + font-weight: 600; + } +} + +.detail-status { + flex-shrink: 0; +} + +.slot-info-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: $admin-space-sm; + margin-bottom: $admin-space-md; + + @media (min-width: 480px) { + grid-template-columns: repeat(3, 1fr); + } +} + +.info-item { + display: flex; + flex-direction: column; + gap: 2px; + padding: $admin-space-xs; + background: $color-gray-50; + border-radius: $radius-sm; + + label { + font-size: 0.625rem; + font-weight: 600; + color: $color-text-tertiary; + text-transform: uppercase; + letter-spacing: 0.02em; + } + + strong { + font-size: 0.875rem; + color: $color-text-primary; + } +} + +.meet-link { + display: inline-flex; + align-items: center; + gap: 4px; + color: $color-brand-primary; + font-size: 0.75rem; + font-weight: 600; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + + app-icon { + font-size: 1rem; + } +} + +.bookings-section { + margin-top: $admin-space-md; + padding-top: $admin-space-md; + border-top: 1px solid $border-default; + + > h3 { + display: flex; + align-items: center; + gap: $admin-space-xs; + font-size: 0.875rem; + font-weight: 700; + color: $color-text-primary; + margin: 0 0 $admin-space-sm; + + app-icon { + font-size: 1rem; + color: $color-brand-primary; + } + } +} + +.booking-detail { + display: flex; + align-items: center; + gap: $admin-space-xs; + font-size: 0.75rem; + color: $color-text-secondary; + padding: $admin-space-2xs 0; + + app-icon { + font-size: 0.875rem; + color: $color-text-tertiary; + } + + &.booking-message { + background: $color-gray-50; + padding: $admin-space-xs; + border-radius: $radius-sm; + margin-top: $admin-space-2xs; + } +} + +// Day Modal specific +.slot-time-range { + font-size: 0.875rem; + + strong { + color: $color-brand-primary; + } +} + +.slot-status-info { + display: flex; + align-items: center; + gap: $admin-space-xs; + font-size: 0.75rem; + color: $color-text-secondary; +} + +.slot-booking-preview { + margin-top: $admin-space-xs; + padding-top: $admin-space-xs; + border-top: 1px solid $border-default; + + small { + font-size: 0.625rem; + color: $color-text-tertiary; + text-transform: uppercase; + font-weight: 600; + } +} + +.booking-names { + font-size: 0.75rem; + color: $color-text-primary; + margin-top: 2px; +} + +// ============================================ +// SEARCH BOX EXTENSION +// ============================================ +.search-input { + flex: 1; + border: none; + background: transparent; + font-size: 0.8125rem; + color: $color-text-primary; + outline: none; + + &::placeholder { + color: $color-text-tertiary; + } +} + +.search-icon { + color: $color-text-tertiary; + font-size: 1rem; +} + +.clear-btn { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + background: transparent; + border: none; + border-radius: $radius-sm; + color: $color-text-tertiary; + cursor: pointer; + transition: all $transition-fast; + + app-icon { + font-size: 0.875rem; + } + + &:hover { + background: $color-gray-200; + color: $color-text-primary; + } +} + +// ============================================ +// UTILITIES +// ============================================ +.spin { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +// ============================================ +// RESPONSIVE +// ============================================ +@media (max-width: 640px) { + .calendar-nav { + padding: $admin-space-xs; + } + + .nav-center h2 { + font-size: 0.8125rem; + } +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} diff --git a/apps/frontend/src/app/components/admin/admin-booking/admin-booking.component.ts b/apps/frontend/src/app/components/admin/admin-booking/admin-booking.component.ts new file mode 100644 index 0000000..1536443 --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-booking/admin-booking.component.ts @@ -0,0 +1,599 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { AdminLayoutComponent } from '../admin-layout/admin-layout.component'; +import { IconComponent } from '../../../shared/icon/icon.component'; +import { + ApiService, + BookingSlot, + CreateBookingSlotDto, + Booking, + BookingStatus +} from '../../../api/api.service'; +import { ConfirmationService } from '../../../shared/confirmation/confirmation.service'; +import { NotificationRefreshService } from '../../../shared/admin-notification-center/notification-refresh.service'; +import { firstValueFrom } from 'rxjs'; + +interface CalendarDay { + date: Date; + isCurrentMonth: boolean; + slots: BookingSlot[]; +} + +interface SeriesPattern { + id: 'daily' | 'weekly' | 'workweek' | 'custom'; + name: string; + icon: string; +} + +@Component({ + selector: 'app-admin-bookings', + standalone: true, + imports: [CommonModule, FormsModule, AdminLayoutComponent, IconComponent], + templateUrl: './admin-booking.component.html', + styleUrls: ['./admin-booking.component.scss'] +}) +export class AdminBookingComponent implements OnInit { + // Data + slots: BookingSlot[] = []; + allBookings: Booking[] = []; + calendarDays: CalendarDay[] = []; + + mobileView: 'upcoming' | 'all' = 'upcoming'; + + // UI State + loading = false; + loadingCreate = false; + currentDate = new Date(); + showCreateModal = false; + selectedSlot: BookingSlot | null = null; + selectedSlotBookings: Booking[] = []; + selectedDay: CalendarDay | null = null; + + // Constants (für Template zugänglich) + BookingStatus = BookingStatus; + weekDays = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']; + + // Filter State + filters = { + status: 'all' as 'all' | 'available' | 'booked' | 'unavailable', + searchText: '' + }; + + // Stats + stats = { + total: 0, + available: 0, + booked: 0, + upcoming: 0 + }; + + // Series Creation + seriesPatterns: SeriesPattern[] = [ + { id: 'daily', name: 'Täglich', icon: 'today' }, + { id: 'weekly', name: 'Wöchentlich', icon: 'date_range' }, + { id: 'workweek', name: 'Mo-Fr', icon: 'work' }, + { id: 'custom', name: 'Custom', icon: 'tune' } + ]; + + quickCreate = { + pattern: 'daily' as SeriesPattern['id'], + startDate: new Date().toISOString().split('T')[0], + endDate: this.getDatePlusWeeks(2), + startTime: '09:00', + endTime: '17:00', + slotDuration: 30, + breakDuration: 0, + breakAfter: 4, + customDays: [1, 2, 3, 4, 5] as number[] + }; + + constructor( + private api: ApiService, + private confirmationService: ConfirmationService, + private route: ActivatedRoute, + private router: Router, + private notificationRefresh: NotificationRefreshService + ) { } + + ngOnInit(): void { + this.loadSlots().then(() => { + // Check for bookingId query param to open booking details + this.route.queryParams.subscribe(params => { + if (params['bookingId']) { + this.openBookingById(params['bookingId']); + } + }); + }); + } + + // Open a specific booking by ID (from query param) + openBookingById(bookingId: string): void { + const booking = this.allBookings.find(b => b.id === bookingId); + if (booking && booking.slotId) { + const slot = this.slots.find(s => s.id === booking.slotId); + if (slot) { + this.openSlotModal(slot); + // Clear the query param after opening + this.router.navigate([], { + relativeTo: this.route, + queryParams: {}, + replaceUrl: true + }); + } + } + } + + // ===== Data Laden & Aufbereiten ===== + async loadSlots(): Promise { + this.loading = true; + try { + const [slots, bookings] = await Promise.all([ + firstValueFrom(this.api.getAllBookingSlots()), + firstValueFrom(this.api.getAllBookings()) + ]); + + this.slots = slots ?? []; + this.allBookings = bookings ?? []; + this.generateCalendar(); + this.calculateStats(); + } catch (err) { + console.error('Error loading data:', err); + } finally { + this.loading = false; + } + } + + generateCalendar(): void { + const year = this.currentDate.getFullYear(); + const month = this.currentDate.getMonth(); + + const firstDay = new Date(year, month, 1); + const lastDay = new Date(year, month + 1, 0); + + const startDate = new Date(firstDay); + const dowStart = startDate.getDay(); + const toSubtract = dowStart === 0 ? 6 : dowStart - 1; + startDate.setDate(startDate.getDate() - toSubtract); + + const endDate = new Date(lastDay); + const dowEnd = endDate.getDay(); + const toAdd = dowEnd === 0 ? 0 : 7 - dowEnd; + endDate.setDate(endDate.getDate() + toAdd); + + this.calendarDays = []; + const cur = new Date(startDate); + while (cur <= endDate) { + const dateStr = this.toLocalYMD(cur); + const daySlots = this.getFilteredSlotsForDate(dateStr); + + this.calendarDays.push({ + date: new Date(cur), + isCurrentMonth: cur.getMonth() === month, + slots: daySlots + }); + + cur.setDate(cur.getDate() + 1); + } + } + + getFilteredSlotsForDate(date: string): BookingSlot[] { + let daySlots = this.slots.filter((s) => s.date === date); + + if (this.filters.status !== 'all') { + daySlots = daySlots.filter((slot) => { + if (this.filters.status === 'available') { + return slot.isAvailable && (slot.currentBookings || 0) === 0; + } else if (this.filters.status === 'booked') { + return (slot.currentBookings || 0) > 0; + } else if (this.filters.status === 'unavailable') { + return !slot.isAvailable; + } + return true; + }); + } + + if (this.filters.searchText.trim()) { + const search = this.filters.searchText.toLowerCase(); + daySlots = daySlots.filter((slot) => { + const slotBookings = this.allBookings.filter((b) => b.slotId === slot.id); + if (slotBookings.length === 0) return false; + return slotBookings.some( + (b) => + b.name.toLowerCase().includes(search) || + b.email.toLowerCase().includes(search) || + (!!b.phone && b.phone.toLowerCase().includes(search)) + ); + }); + } + + return daySlots.sort((a, b) => a.timeFrom.localeCompare(b.timeFrom)); + } + + getFilteredSlotsCount(): number { + return this.calendarDays.reduce((sum, d) => sum + d.slots.length, 0); + } + + calculateStats(): void { + const today = new Date(); + today.setHours(0, 0, 0, 0); + this.stats.total = this.slots.length; + this.stats.available = this.slots.filter(s => s.isAvailable && (s.currentBookings || 0) === 0).length; + this.stats.booked = this.slots.filter(s => (s.currentBookings || 0) > 0).length; + this.stats.upcoming = this.slots.filter(s => this.parseLocalYMD(s.date) >= today && s.isAvailable).length; + } + + formatDateLongFromDate(d: Date): string { + return d.toLocaleDateString('de-DE', { weekday: 'long', day: '2-digit', month: 'long', year: 'numeric' }); + } + + // ===== Navigation ===== + previousMonth(): void { + this.currentDate.setMonth(this.currentDate.getMonth() - 1); + this.currentDate = new Date(this.currentDate); + this.generateCalendar(); + } + + nextMonth(): void { + this.currentDate.setMonth(this.currentDate.getMonth() + 1); + this.currentDate = new Date(this.currentDate); + this.generateCalendar(); + } + + goToToday(): void { + this.currentDate = new Date(); + this.generateCalendar(); + } + + getCurrentMonthYear(): string { + return this.currentDate.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' }); + } + + isToday(date: Date): boolean { + const t = new Date(); + return ( + date.getDate() === t.getDate() && + date.getMonth() === t.getMonth() && + date.getFullYear() === t.getFullYear() + ); + } + + // ===== Modals ===== + openCreateModal(): void { + this.showCreateModal = true; + } + + closeCreateModal(): void { + this.showCreateModal = false; + } + + openSlotModal(slot: BookingSlot): void { + this.selectedSlot = slot; + this.selectedSlotBookings = this.allBookings.filter((b) => b.slotId === slot.id); + } + + closeSlotModal(): void { + this.selectedSlot = null; + this.selectedSlotBookings = []; + } + + openDayModal(day: CalendarDay): void { + this.selectedDay = day; + } + + closeDayModal(): void { + this.selectedDay = null; + } + + openQuickCreateForDay(date: Date): void { + const dateStr = this.toLocalYMD(date); + this.quickCreate.startDate = dateStr; + this.quickCreate.endDate = dateStr; + this.openCreateModal(); + } + + // ===== Filter ===== + onFilterChange(): void { + this.generateCalendar(); + } + + clearFilters(): void { + this.filters = { status: 'all', searchText: '' }; + this.generateCalendar(); + } + + // ===== Slot-Serien-Erstellung ===== + getDatePlusWeeks(weeks: number): string { + const date = new Date(); + date.setDate(date.getDate() + weeks * 7); + return this.toLocalYMD(date); + } + + generateSeriesSlots(): CreateBookingSlotDto[] { + const slots: CreateBookingSlotDto[] = []; + const { pattern, startDate, endDate, startTime, endTime, slotDuration, breakDuration, breakAfter } = this.quickCreate; + + const start = this.parseLocalYMD(startDate); + const end = this.parseLocalYMD(endDate); + + for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) { + const dayOfWeek = d.getDay(); + let shouldInclude = false; + if (pattern === 'daily') shouldInclude = true; + else if (pattern === 'workweek') shouldInclude = dayOfWeek >= 1 && dayOfWeek <= 5; + else if (pattern === 'weekly') shouldInclude = d.getDay() === start.getDay(); + else if (pattern === 'custom') shouldInclude = this.quickCreate.customDays.includes(dayOfWeek); + if (!shouldInclude) continue; + + const dateStr = this.toLocalYMD(d); + const daySlots = this.generateTimeSlotsForDay(dateStr, startTime, endTime, slotDuration, breakDuration, breakAfter); + slots.push(...daySlots); + } + return slots; + } + + generateTimeSlotsForDay( + date: string, + startTime: string, + endTime: string, + slotDuration: number, + breakDuration: number, + breakAfter: number + ): CreateBookingSlotDto[] { + const slots: CreateBookingSlotDto[] = []; + const [startH, startM] = startTime.split(':').map(Number); + const [endH, endM] = endTime.split(':').map(Number); + + let currentMinutes = startH * 60 + startM; + const endMinutes = endH * 60 + endM; + let slotCount = 0; + + while (currentMinutes + slotDuration <= endMinutes) { + slots.push({ + date, + timeFrom: this.minutesToTime(currentMinutes), + timeTo: this.minutesToTime(currentMinutes + slotDuration), + maxBookings: 1, + isAvailable: true + }); + + currentMinutes += slotDuration; + slotCount++; + + if (breakAfter > 0 && slotCount % breakAfter === 0 && breakDuration > 0) { + currentMinutes += breakDuration; + } + } + + return slots; + } + + minutesToTime(minutes: number): string { + const h = Math.floor(minutes / 60); + const m = minutes % 60; + return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`; + } + + toggleCustomDay(day: number): void { + const idx = this.quickCreate.customDays.indexOf(day); + if (idx > -1) { + this.quickCreate.customDays.splice(idx, 1); + } else { + this.quickCreate.customDays.push(day); + this.quickCreate.customDays.sort(); + } + } + + isCustomDaySelected(day: number): boolean { + return this.quickCreate.customDays.includes(day); + } + + getPreviewCount(): number { + return this.generateSeriesSlots().length; + } + + // NEU: Mit Confirmation Service + async handleCreateSeries(): Promise { + const slots = this.generateSeriesSlots(); + + if (slots.length === 0) { + await this.confirmationService.confirm({ + title: 'Keine Slots', + message: 'Es wurden keine Slots zum Erstellen gefunden. Bitte überprüfe deine Einstellungen.', + confirmText: 'OK', + type: 'warning', + icon: 'warning' + }); + return; + } + + if (slots.length > 200) { + const confirmed = await this.confirmationService.confirm({ + title: 'Viele Slots erstellen', + message: `Das würde ${slots.length} Slots erstellen. Möchtest du wirklich fortfahren?`, + confirmText: 'Ja, erstellen', + cancelText: 'Abbrechen', + type: 'warning', + icon: 'warning' + }); + if (!confirmed) return; + } + + this.loadingCreate = true; + try { + await firstValueFrom(this.api.createMultipleBookingSlots(slots)); + await this.loadSlots(); + this.closeCreateModal(); + + await this.confirmationService.confirm({ + title: 'Erfolgreich!', + message: `${slots.length} Slots wurden erfolgreich erstellt.`, + confirmText: 'OK', + type: 'success', + icon: 'check_circle' + }); + } catch (err) { + console.error(err); + await this.confirmationService.confirm({ + title: 'Fehler', + message: 'Beim Erstellen der Slots ist ein Fehler aufgetreten.', + confirmText: 'OK', + type: 'danger', + icon: 'error' + }); + } finally { + this.loadingCreate = false; + } + } + + // NEU: Mit Confirmation Service + async handleDeleteSlot(id?: string): Promise { + if (!id) return; + + const confirmed = await this.confirmationService.confirm({ + title: 'Slot löschen', + message: 'Möchtest du diesen Slot wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.', + confirmText: 'Ja, löschen', + cancelText: 'Abbrechen', + type: 'danger', + icon: 'delete' + }); + + if (!confirmed) return; + + try { + await firstValueFrom(this.api.deleteBookingSlot(id)); + await this.loadSlots(); + this.closeSlotModal(); + } catch (err) { + console.error(err); + await this.confirmationService.confirm({ + title: 'Fehler', + message: 'Beim Löschen des Slots ist ein Fehler aufgetreten.', + confirmText: 'OK', + type: 'danger', + icon: 'error' + }); + } + } + + getUpcomingSlots(): BookingSlot[] { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + return this.slots + .filter(s => this.parseLocalYMD(s.date) >= today) + .sort((a, b) => { + const dateCompare = a.date.localeCompare(b.date); + if (dateCompare !== 0) return dateCompare; + return a.timeFrom.localeCompare(b.timeFrom); + }) + .slice(0, 20); + } + + async updateBookingStatus(bookingId: string, status: BookingStatus): Promise { + try { + await firstValueFrom(this.api.updateBooking(bookingId, { status })); + await this.loadSlots(); + + if (this.selectedSlot) { + this.selectedSlotBookings = this.allBookings.filter( + (b) => b.slotId === this.selectedSlot!.id + ); + } + + this.notificationRefresh.triggerRefresh(); + } catch (err) { + console.error(err); + await this.confirmationService.confirm({ + title: 'Fehler', + message: 'Beim Aktualisieren der Buchung ist ein Fehler aufgetreten.', + confirmText: 'OK', + type: 'danger', + icon: 'error' + }); + } + } + + // ===== Helper fürs Template ===== + formatTime(time: string): string { + const [h, m] = time.split(':').map(Number); + return `${String(h ?? 0).padStart(2, '0')}:${String(m ?? 0).padStart(2, '0')}`; + } + + formatDateLong(dateStr: string): string { + const [y, m, d] = dateStr.split('-').map(Number); + const date = new Date(y, (m ?? 1) - 1, d ?? 1); + return date.toLocaleDateString('de-DE', { + weekday: 'long', + day: '2-digit', + month: 'long', + year: 'numeric' + }); + } + + getStatusColor(status: BookingStatus): string { + switch (status) { + case BookingStatus.PENDING: + return '#f59e0b'; + case BookingStatus.CONFIRMED: + return '#10b981'; + case BookingStatus.CANCELLED: + return '#ef4444'; + case BookingStatus.COMPLETED: + return '#3b82f6'; + default: + return '#6b7280'; + } + } + + getStatusLabel(status: BookingStatus): string { + switch (status) { + case BookingStatus.PENDING: + return 'Ausstehend'; + case BookingStatus.CONFIRMED: + return 'Bestätigt'; + case BookingStatus.CANCELLED: + return 'Storniert'; + case BookingStatus.COMPLETED: + return 'Abgeschlossen'; + default: + return 'Unbekannt'; + } + } + + toLocalYMD(d: Date): string { + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + return `${y}-${m}-${day}`; + } + + parseLocalYMD(ymd: string): Date { + const [y, m, d] = ymd.split('-').map(Number); + return new Date(y, (m - 1), d, 0, 0, 0, 0); + } + + getSlotStatusIcon(slot: BookingSlot): string { + if (!slot.isAvailable) return 'lock'; + if ((slot.currentBookings || 0) > 0) return 'event_busy'; + return 'check_circle'; + } + + getSlotStatusText(slot: BookingSlot): string { + if (!slot.isAvailable) return 'Gesperrt'; + if ((slot.currentBookings || 0) >= slot.maxBookings) return 'Ausgebucht'; + if ((slot.currentBookings || 0) > 0) return 'Teilweise gebucht'; + return 'Verfügbar'; + } + + getSlotBookings(slot: BookingSlot): Booking[] { + return this.allBookings.filter((b) => b.slotId === slot.id); + } + + getBookingNames(slot: BookingSlot): string { + const bookings = this.getSlotBookings(slot); + return bookings.map((b) => b.name).join(', '); + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/components/admin/admin-dashboard/admin-dashboard.component.html b/apps/frontend/src/app/components/admin/admin-dashboard/admin-dashboard.component.html new file mode 100644 index 0000000..744a91a --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-dashboard/admin-dashboard.component.html @@ -0,0 +1,177 @@ + +
+
+ + +
+
+

{{ getGreeting() }} 👋

+

+ {{ formatDate(currentTime) }} + · + {{ formatTime(currentTime) }} Uhr +

+
+ +
+ + +
+
+

Dashboard wird geladen...

+
+ + +
+ +

Fehler aufgetreten

+

{{ error }}

+ +
+ + +
+ + + + + + + + +
+
+ + System: {{ settings?.isUnderConstruction ? 'Wartungsmodus' : 'Online' }} +
+
+ + Registrierung: {{ settings?.allowRegistration ? 'Aktiviert' : 'Deaktiviert' }} +
+
+ + Newsletter: {{ settings?.allowNewsletter ? 'Aktiviert' : 'Deaktiviert' }} +
+
+ +
+ +
+
+
diff --git a/apps/frontend/src/app/components/admin/admin-dashboard/admin-dashboard.component.scss b/apps/frontend/src/app/components/admin/admin-dashboard/admin-dashboard.component.scss new file mode 100644 index 0000000..04a0bf6 --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-dashboard/admin-dashboard.component.scss @@ -0,0 +1,586 @@ +@import '../admin-shared.scss'; + +// ===== DASHBOARD - Compact & Mobile-First ===== + +.dashboard { + min-height: calc(100vh - 72px); + padding: $admin-space-lg; + background: $gradient-bg; + animation: pageEnter 0.4s ease-out; + + @media (max-width: $admin-container-md) { + padding: $admin-space-md; + } +} + +.dashboard-container { + max-width: $admin-container-2xl; + margin: 0 auto; +} + +// ===== HEADER ===== +.dashboard-header { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: $admin-space-md; + padding: $admin-space-lg; + background: $glass-bg; + backdrop-filter: blur(20px); + border: 1px solid $glass-border; + border-radius: $radius-lg; + box-shadow: $shadow-glass; + margin-bottom: $admin-space-lg; + animation: slideInLeft 0.4s ease-out; + + @media (max-width: $admin-container-sm) { + flex-direction: column; + align-items: flex-start; + padding: $admin-space-md; + } +} + +.greeting { + h1 { + margin: 0 0 0.25rem 0; + font-size: 1.5rem; + font-weight: 800; + background: $gradient-primary; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + + @media (max-width: $admin-container-sm) { + font-size: 1.25rem; + } + } +} + +.date-time { + color: $color-text-secondary; + font-size: 0.8125rem; + display: flex; + align-items: center; + gap: 0.5rem; + + .separator { opacity: 0.5; } + .time { font-weight: 500; } +} + +.refresh-btn { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.875rem; + background: white; + border: 1px solid $glass-border; + border-radius: $radius-md; + color: $color-text-primary; + font-size: 0.8125rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + + &:hover:not(:disabled) { + background: rgba($color-brand-primary, 0.06); + border-color: rgba($color-brand-primary, 0.3); + color: $color-brand-primary; + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } + + app-icon.spin { + animation: spin 1s linear infinite; + } + + @media (max-width: $admin-container-sm) { + width: 100%; + justify-content: center; + } +} + +// ===== LOADING & ERROR ===== +.loading-state, +.error-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: $admin-space-2xl $admin-space-lg; + text-align: center; + background: $glass-bg; + backdrop-filter: blur(20px); + border: 1px solid $glass-border; + border-radius: $radius-lg; + animation: scaleIn 0.3s ease-out; + + p { + color: $color-text-secondary; + margin-top: $admin-space-md; + } +} + +.spinner { + width: 32px; + height: 32px; + border: 3px solid $glass-border; + border-top-color: $color-brand-primary; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +.error-state { + app-icon { color: $color-error; } + + h3 { + margin: $admin-space-md 0 $admin-space-sm; + color: $color-text-primary; + } + + .btn { margin-top: $admin-space-md; } +} + +// ===== STATS CARDS ===== +.stats-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: $admin-space-md; + margin-bottom: $admin-space-lg; + + @media (max-width: 1200px) { + grid-template-columns: repeat(2, 1fr); + } + + @media (max-width: $admin-container-sm) { + grid-template-columns: 1fr; + gap: $admin-space-sm; + } +} + +.stat-card { + display: flex; + align-items: center; + gap: $admin-space-md; + padding: $admin-space-md; + background: $glass-bg; + backdrop-filter: blur(20px); + border: 1px solid $glass-border; + border-radius: $radius-lg; + text-decoration: none; + transition: all 0.2s ease; + position: relative; + overflow: hidden; + animation: staggerFade 0.3s ease-out both; + + @for $i from 1 through 4 { + &:nth-child(#{$i}) { + animation-delay: #{$i * 0.05}s; + } + } + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 3px; + height: 100%; + background: $color-brand-primary; + transition: width 0.2s ease; + } + + &:hover { + transform: translateY(-2px); + box-shadow: $shadow-glass-hover; + border-color: rgba($color-brand-primary, 0.2); + + &::before { width: 5px; } + + .stat-card__arrow { + transform: translateX(4px); + opacity: 1; + } + } + + // Color variants + &--success::before { background: $color-success; } + &--warning::before { background: $color-warning; } + &--danger::before { background: $color-error; } + &--info::before { background: $color-brand-primary; } + &--primary::before { background: $color-brand-primary; } + + &__icon { + display: flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + background: rgba($color-brand-primary, 0.1); + border-radius: $radius-md; + color: $color-brand-primary; + flex-shrink: 0; + } + + &--success &__icon { background: rgba($color-success, 0.1); color: $color-success; } + &--warning &__icon { background: rgba($color-warning, 0.1); color: $color-warning; } + &--danger &__icon { background: rgba($color-error, 0.1); color: $color-error; } + + &__content { + display: flex; + flex-direction: column; + flex: 1; + min-width: 0; + } + + &__value { + font-size: 1.5rem; + font-weight: 800; + color: $color-text-primary; + line-height: 1.2; + } + + &__title { + font-size: 0.8125rem; + font-weight: 600; + color: $color-text-primary; + } + + &__subtitle { + font-size: 0.6875rem; + color: $color-text-secondary; + } + + &__arrow { + color: $color-text-tertiary; + opacity: 0; + transition: all 0.2s ease; + } +} + +// ===== DASHBOARD GRID ===== +.dashboard-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: $admin-space-lg; + + @media (max-width: $admin-container-lg) { + grid-template-columns: 1fr; + } +} + +.dashboard-section { + background: $glass-bg; + backdrop-filter: blur(20px); + border: 1px solid $glass-border; + border-radius: $radius-lg; + padding: $admin-space-lg; + box-shadow: $shadow-glass; + animation: scaleIn 0.4s ease-out both; + + &:nth-child(1) { animation-delay: 0.1s; } + &:nth-child(2) { animation-delay: 0.15s; } +} + +.section-header { + margin-bottom: $admin-space-md; + + h2 { + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0; + font-size: 1rem; + font-weight: 700; + color: $color-text-primary; + + app-icon { color: $color-brand-primary; } + } +} + +// ===== QUICK ACTIONS ===== +.quick-actions-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: $admin-space-sm; + + @media (max-width: $admin-container-sm) { + grid-template-columns: 1fr; + } +} + +.quick-action { + display: flex; + align-items: center; + gap: $admin-space-sm; + padding: $admin-space-sm $admin-space-md; + background: rgba(255, 255, 255, 0.5); + border: 1px solid transparent; + border-radius: $radius-md; + text-decoration: none; + transition: all 0.2s ease; + animation: staggerFade 0.25s ease-out both; + + @for $i from 1 through 8 { + &:nth-child(#{$i}) { + animation-delay: #{0.2 + $i * 0.03}s; + } + } + + &:hover { + background: rgba($color-brand-primary, 0.06); + border-color: rgba($color-brand-primary, 0.15); + transform: translateX(4px); + + .quick-action__arrow { + opacity: 1; + transform: translateX(2px); + } + } + + &__icon { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + background: rgba($color-brand-primary, 0.1); + border-radius: $radius-sm; + color: $color-brand-primary; + flex-shrink: 0; + transition: background 0.2s ease; + } + + &__content { + display: flex; + flex-direction: column; + flex: 1; + min-width: 0; + } + + &__label { + font-size: 0.8125rem; + font-weight: 600; + color: $color-text-primary; + } + + &__desc { + font-size: 0.6875rem; + color: $color-text-secondary; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__arrow { + color: $color-text-tertiary; + opacity: 0; + transition: all 0.2s ease; + } +} + +// ===== ACTIVITY ===== +.activity-group { + &:not(:last-child) { + margin-bottom: $admin-space-lg; + padding-bottom: $admin-space-lg; + border-bottom: 1px solid $glass-border; + } + + &__title { + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0 0 $admin-space-sm 0; + font-size: 0.6875rem; + font-weight: 600; + color: $color-text-secondary; + text-transform: uppercase; + letter-spacing: 0.5px; + + app-icon { color: $color-text-tertiary; } + } +} + +.activity-list { + display: flex; + flex-direction: column; + gap: $admin-space-xs; +} + +.activity-item { + display: flex; + align-items: center; + gap: $admin-space-sm; + padding: $admin-space-sm; + background: rgba(255, 255, 255, 0.3); + border-radius: $radius-md; + text-decoration: none; + transition: all 0.2s ease; + animation: staggerFade 0.2s ease-out both; + + @for $i from 1 through 5 { + &:nth-child(#{$i}) { + animation-delay: #{0.25 + $i * 0.03}s; + } + } + + &:hover { + background: rgba($color-brand-primary, 0.06); + transform: translateX(4px); + } + + &__avatar { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + background: rgba($color-brand-primary, 0.1); + border-radius: 50%; + color: $color-brand-primary; + flex-shrink: 0; + + &.booking { + background: rgba($color-warning, 0.1); + color: $color-warning; + } + } + + &__content { + display: flex; + flex-direction: column; + flex: 1; + min-width: 0; + } + + &__name { + font-size: 0.8125rem; + font-weight: 600; + color: $color-text-primary; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__meta { + font-size: 0.6875rem; + color: $color-text-secondary; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__time { + font-size: 0.625rem; + color: $color-text-tertiary; + white-space: nowrap; + } + + &__badge { + font-size: 0.5625rem; + font-weight: 600; + padding: 0.125rem 0.375rem; + border-radius: 999px; + text-transform: uppercase; + letter-spacing: 0.3px; + + &.pending { + background: rgba($color-warning, 0.15); + color: $color-warning; + } + } +} + +.view-all-link { + display: inline-flex; + align-items: center; + gap: 0.25rem; + margin-top: $admin-space-sm; + font-size: 0.75rem; + font-weight: 500; + color: $color-brand-primary; + text-decoration: none; + transition: gap 0.2s ease; + + &:hover { gap: 0.5rem; } +} + +.empty-activity { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: $admin-space-xl; + text-align: center; + + app-icon { + color: $color-success; + opacity: 0.7; + } + + h3 { + margin: $admin-space-sm 0 0.25rem; + font-size: 0.9375rem; + color: $color-text-primary; + } + + p { + margin: 0; + font-size: 0.8125rem; + color: $color-text-secondary; + } +} + +// ===== SYSTEM INFO ===== +.system-info { + display: flex; + flex-wrap: wrap; + gap: $admin-space-sm $admin-space-lg; + margin-top: $admin-space-lg; + padding: $admin-space-sm $admin-space-md; + background: $glass-bg; + backdrop-filter: blur(20px); + border: 1px solid $glass-border; + border-radius: $radius-md; + + &__item { + display: flex; + align-items: center; + gap: 0.375rem; + font-size: 0.6875rem; + color: $color-text-secondary; + + app-icon { color: $color-text-tertiary; } + } +} + +// ===== BUTTONS ===== +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + font-size: 0.8125rem; + font-weight: 600; + border-radius: $radius-md; + border: none; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; + + &--primary { + background: $gradient-primary; + color: white; + box-shadow: $shadow-brand; + + &:hover { + transform: translateY(-1px); + box-shadow: $shadow-brand-hover; + } + } +} diff --git a/apps/frontend/src/app/components/admin/admin-dashboard/admin-dashboard.component.ts b/apps/frontend/src/app/components/admin/admin-dashboard/admin-dashboard.component.ts new file mode 100644 index 0000000..90692b3 --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-dashboard/admin-dashboard.component.ts @@ -0,0 +1,207 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; +import { forkJoin, Subject, of } from 'rxjs'; +import { takeUntil, catchError } from 'rxjs/operators'; +import { AdminLayoutComponent } from '../admin-layout/admin-layout.component'; +import { IconComponent } from '../../../shared/icon/icon.component'; +import { ApiService, ContactRequest, Booking, BookingStatus, Settings } from '../../../api/api.service'; + +interface DashboardCard { + title: string; + value: number | string; + subtitle: string; + icon: string; + route: string; + color: 'primary' | 'success' | 'warning' | 'danger' | 'info'; + trend?: { value: number; label: string }; +} + +interface QuickAction { + label: string; + icon: string; + route: string; + description: string; +} + +@Component({ + selector: 'app-admin-dashboard', + standalone: true, + imports: [CommonModule, RouterModule, AdminLayoutComponent, IconComponent], + templateUrl: './admin-dashboard.component.html', + styleUrls: ['./admin-dashboard.component.scss'] +}) +export class AdminDashboardComponent implements OnInit, OnDestroy { + private destroy$ = new Subject(); + + loading = true; + error = ''; + currentTime = new Date(); + private timeInterval: any; + + // Stats + unprocessedContacts: ContactRequest[] = []; + pendingBookings: Booking[] = []; + allBookings: Booking[] = []; + settings: Settings | null = null; + + // Dashboard cards + cards: DashboardCard[] = []; + + // Quick actions + quickActions: QuickAction[] = [ + { label: 'Neue Anfragen', icon: 'mail', route: '/admin/requests', description: 'Kontaktanfragen bearbeiten' }, + { label: 'Buchungen', icon: 'calendar_today', route: '/admin/booking', description: 'Termine verwalten' }, + { label: 'Newsletter', icon: 'newspaper', route: '/admin/newsletter', description: 'Abonnenten verwalten' }, + { label: 'FAQ verwalten', icon: 'quiz', route: '/admin/faq', description: 'Häufige Fragen bearbeiten' }, + { label: 'Services', icon: 'build', route: '/admin/services', description: 'Dienstleistungen pflegen' }, + { label: 'Rechnungen', icon: 'receipt_long', route: '/admin/invoices', description: 'Rechnungen erstellen' }, + { label: 'Benutzer', icon: 'group', route: '/admin/users', description: 'Nutzer verwalten' }, + { label: 'Einstellungen', icon: 'settings', route: '/admin/settings', description: 'System konfigurieren' } + ]; + + // Recent activity + recentContacts: ContactRequest[] = []; + recentBookings: Booking[] = []; + + constructor(private api: ApiService) {} + + ngOnInit(): void { + this.loadDashboardData(); + + // Update time every minute + this.timeInterval = setInterval(() => { + this.currentTime = new Date(); + }, 60000); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + if (this.timeInterval) { + clearInterval(this.timeInterval); + } + } + + loadDashboardData(): void { + this.loading = true; + this.error = ''; + + forkJoin({ + contacts: this.api.getUnprocessedContactRequests().pipe(catchError(() => of([]))), + allContacts: this.api.getAllContactRequests().pipe(catchError(() => of([]))), + bookings: this.api.getAllBookings().pipe(catchError(() => of([]))), + settings: this.api.getSettings().pipe(catchError(() => of(null))) + }) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: ({ contacts, allContacts, bookings, settings }) => { + this.unprocessedContacts = contacts; + this.allBookings = bookings; + this.pendingBookings = bookings.filter(b => b.status === BookingStatus.PENDING); + this.settings = settings; + + // Recent items (last 5) + this.recentContacts = contacts.slice(0, 5); + this.recentBookings = this.pendingBookings.slice(0, 5); + + this.buildCards(allContacts); + this.loading = false; + }, + error: (err) => { + console.error('Dashboard load error:', err); + this.error = 'Fehler beim Laden der Dashboard-Daten'; + this.loading = false; + } + }); + } + + private buildCards(allContacts: ContactRequest[]): void { + const confirmedBookings = this.allBookings.filter(b => b.status === BookingStatus.CONFIRMED); + const todayBookings = this.allBookings.filter(b => { + if (!b.slot?.date) return false; + const bookingDate = new Date(b.slot.date); + const today = new Date(); + return bookingDate.toDateString() === today.toDateString(); + }); + + this.cards = [ + { + title: 'Offene Anfragen', + value: this.unprocessedContacts.length, + subtitle: `von ${allContacts.length} gesamt`, + icon: 'mail', + route: '/admin/requests', + color: this.unprocessedContacts.length > 0 ? 'warning' : 'success' + }, + { + title: 'Ausstehende Buchungen', + value: this.pendingBookings.length, + subtitle: `${confirmedBookings.length} bestätigt`, + icon: 'pending_actions', + route: '/admin/booking', + color: this.pendingBookings.length > 0 ? 'warning' : 'success' + }, + { + title: 'Termine heute', + value: todayBookings.length, + subtitle: 'für heute geplant', + icon: 'today', + route: '/admin/booking', + color: todayBookings.length > 0 ? 'info' : 'primary' + }, + { + title: 'System-Status', + value: this.settings?.isUnderConstruction ? 'Wartung' : 'Online', + subtitle: this.settings?.isUnderConstruction ? 'Wartungsmodus aktiv' : 'Alles funktioniert', + icon: this.settings?.isUnderConstruction ? 'construction' : 'check_circle', + route: '/admin/settings', + color: this.settings?.isUnderConstruction ? 'danger' : 'success' + } + ]; + } + + getGreeting(): string { + const hour = this.currentTime.getHours(); + if (hour < 12) return 'Guten Morgen'; + if (hour < 18) return 'Guten Tag'; + return 'Guten Abend'; + } + + formatTime(date: Date): string { + return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); + } + + formatDate(date: Date): string { + return date.toLocaleDateString('de-DE', { + weekday: 'long', + day: 'numeric', + month: 'long', + year: 'numeric' + }); + } + + getRelativeTime(date: Date | string): string { + const d = typeof date === 'string' ? new Date(date) : date; + const now = new Date(); + const diffMs = now.getTime() - d.getTime(); + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMins / 60); + const diffDays = Math.floor(diffHours / 24); + + if (diffMins < 1) return 'Gerade eben'; + if (diffMins < 60) return `vor ${diffMins} Min`; + if (diffHours < 24) return `vor ${diffHours} Std`; + if (diffDays === 1) return 'Gestern'; + if (diffDays < 7) return `vor ${diffDays} Tagen`; + return d.toLocaleDateString('de-DE'); + } + + trackByRoute(_: number, item: QuickAction): string { + return item.route; + } + + trackById(_: number, item: ContactRequest | Booking): string { + return item.id; + } +} diff --git a/apps/frontend/src/app/components/admin/admin-faq/admin-faq.component.html b/apps/frontend/src/app/components/admin/admin-faq/admin-faq.component.html new file mode 100644 index 0000000..365293b --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-faq/admin-faq.component.html @@ -0,0 +1,346 @@ + +
+
+ + +
+
+ {{ faqs.length }} FAQs + + {{ publishedCount }} veröffentlicht + +
+
+ + + +
+
+ + +
+
+

FAQs werden geladen...

+
+ + +
+ quiz +

Keine FAQs vorhanden

+

Erstelle deine erste FAQ oder importiere bestehende Daten.

+
+ + +
+
+ + +
+
+ +
+
+ + {{ faq.isPublished ? 'Veröffentlicht' : 'Entwurf' }} +
+ {{ faq.slug }} +
+ +
+

{{ faq.question }}

+ +
+

{{ answer }}

+ + +{{ faq.answers.length - 2 }} weitere Antworten + +
+ +
+ list + {{ faq.listItems.length }} Listenpunkte +
+
+ + +
+
+ + +
+
+
+

{{ editingFaq ? 'FAQ bearbeiten' : 'Neue FAQ erstellen' }}

+ +
+ +
+
+ + +
+

Grunddaten

+ +
+ + +
+ +
+ + + Eindeutiger Identifier (URL-freundlich) +
+ +
+
+ + +
+ +
+ + +
+
+ +
+ +
+
+ + +
+

Antworten *

+

Mindestens eine Antwort erforderlich. Jeder Eintrag wird als Absatz angezeigt.

+ +
+
+
+ {{ i + 1 }} + {{ answer }} +
+ + + +
+
+
+ +
+ + +
+
+
+ + +
+

Aufzählungspunkte (optional)

+

Werden als Bullet-Liste unter den Antworten angezeigt.

+ +
+
+
+ + {{ item }} + +
+
+ +
+ + +
+
+
+
+
+ + +
+
+ + +
+
+
+

FAQs importieren

+ +
+ +
+

+ Füge hier JSON ein. Unterstützte Formate: +

+
    +
  • [{{ '{' }} "slug": "...", "question": "...", "answers": [...] {{ '}' }}]
  • +
  • {{ '{' }} "faqs": [...] {{ '}' }}
  • +
  • Legacy: [{{ '{' }} "id": "...", "q": "...", "a": [...] {{ '}' }}]
  • +
+ +
+ + +
+ +
+ +
+
+ + +
+
+ +
+
+
\ No newline at end of file diff --git a/apps/frontend/src/app/components/admin/admin-faq/admin-faq.component.scss b/apps/frontend/src/app/components/admin/admin-faq/admin-faq.component.scss new file mode 100644 index 0000000..e30d30f --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-faq/admin-faq.component.scss @@ -0,0 +1,603 @@ +// ============================================ +// ADMIN FAQ - Mobile-First Compact Design +// ============================================ +@import '../admin-shared.scss'; + +.admin-faq { + min-height: 100%; + padding: $admin-space-md; + padding-bottom: calc($admin-space-md + 80px); + animation: pageEnter 0.4s ease-out; + + @media (min-width: 768px) { + padding: $admin-space-lg; + padding-bottom: $admin-space-lg; + } + + .container { + max-width: $admin-container-lg; + margin: 0 auto; + } +} + +// ============================================ +// HEADER +// ============================================ +.admin-faq__header { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: $admin-space-sm; + margin-bottom: $admin-space-md; + animation: fadeIn 0.3s ease-out; + + @media (max-width: 480px) { + flex-direction: column; + align-items: stretch; + } +} + +.header-info { + display: flex; + gap: $admin-space-2xs; + flex-wrap: wrap; +} + +.badge { + padding: 4px $admin-space-xs; + border-radius: $radius-full; + font-size: 0.6875rem; + font-weight: 600; + background: $color-gray-100; + color: $color-text-secondary; + + &--success { + background: rgba($accent-green, 0.1); + color: $accent-green; + } +} + +.header-actions { + display: flex; + gap: $admin-space-2xs; + flex-wrap: wrap; +} + +// ============================================ +// STATES +// ============================================ +.loading-state, +.empty-state { + text-align: center; + padding: $admin-space-2xl; + background: $glass-bg; + border-radius: $radius-md; + animation: fadeIn 0.3s ease-out; + + .material-symbols-outlined, + app-icon { + font-size: 2.5rem; + color: $color-gray-300; + margin-bottom: $admin-space-sm; + } + + h3 { + margin: 0 0 $admin-space-2xs; + font-size: 1rem; + color: $color-text-primary; + } + + p { + color: $color-text-tertiary; + font-size: 0.8125rem; + margin-bottom: $admin-space-sm; + } +} + +.empty-actions { + display: flex; + gap: $admin-space-xs; + justify-content: center; + flex-wrap: wrap; +} + +// ============================================ +// FAQ LIST +// ============================================ +.faq-list { + display: grid; + gap: $admin-space-xs; +} + +.faq-card { + background: $glass-bg; + border: 1px solid $glass-border; + border-radius: $radius-md; + padding: $admin-space-sm; + transition: all 0.2s ease; + animation: staggerFade 0.3s ease-out backwards; + + @for $i from 1 through 30 { + &:nth-child(#{$i}) { + animation-delay: #{$i * 30}ms; + } + } + + &:hover { + box-shadow: $shadow-sm; + border-color: rgba($color-brand-primary, 0.2); + } + + &--editing { + border-color: $color-brand-primary; + box-shadow: 0 0 0 3px rgba($color-brand-primary, 0.1); + } + + &--inactive { + opacity: 0.6; + } +} + +.faq-card__header { + display: flex; + align-items: flex-start; + gap: $admin-space-sm; + margin-bottom: $admin-space-xs; + + @media (max-width: 480px) { + flex-direction: column; + gap: $admin-space-xs; + } +} + +.faq-card__drag { + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + color: $color-text-tertiary; + cursor: grab; + flex-shrink: 0; + + &:active { + cursor: grabbing; + } + + app-icon { + font-size: 1rem; + } +} + +.faq-card__content { + flex: 1; + min-width: 0; +} + +.faq-card__question { + font-size: 0.875rem; + font-weight: 600; + color: $color-text-primary; + margin: 0 0 $admin-space-2xs; + line-height: 1.4; +} + +.faq-card__answer { + font-size: 0.75rem; + color: $color-text-secondary; + margin: 0; + line-height: 1.5; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.faq-card__meta { + display: flex; + align-items: center; + gap: $admin-space-xs; + margin-top: $admin-space-xs; + flex-wrap: wrap; +} + +.faq-card__category { + padding: 3px 6px; + background: rgba($color-brand-primary, 0.1); + border-radius: $radius-sm; + font-size: 0.625rem; + font-weight: 600; + color: $color-brand-primary; +} + +.faq-card__status { + padding: 3px 6px; + border-radius: $radius-sm; + font-size: 0.625rem; + font-weight: 600; + + &--active { + background: rgba($accent-green, 0.1); + color: $accent-green; + } + + &--inactive { + background: rgba($color-gray-400, 0.1); + color: $color-gray-500; + } +} + +.faq-card__actions { + display: flex; + gap: $admin-space-2xs; + flex-shrink: 0; + + @media (max-width: 480px) { + width: 100%; + padding-top: $admin-space-xs; + border-top: 1px solid $border-default; + justify-content: flex-end; + } +} + +// ============================================ +// ACTION BUTTONS +// ============================================ +.btn-icon { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + border-radius: $radius-sm; + color: $color-text-tertiary; + cursor: pointer; + transition: all 0.15s ease; + + app-icon { + font-size: 0.9375rem; + } + + &:hover { + background: $color-gray-100; + color: $color-text-primary; + } + + &--edit:hover { + background: rgba($accent-blue, 0.1); + color: $accent-blue; + } + + &--delete:hover { + background: rgba($accent-red, 0.1); + color: $accent-red; + } + + &:disabled { + opacity: 0.4; + cursor: not-allowed; + } +} + +.btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: $admin-space-xs $admin-space-sm; + border-radius: $radius-sm; + font-weight: 600; + font-size: 0.75rem; + border: none; + cursor: pointer; + transition: all 0.15s ease; + + app-icon { + font-size: 0.9375rem; + } + + &--primary { + background: $gradient-primary; + color: white; + + &:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba($color-brand-primary, 0.3); + } + } + + &--secondary { + background: $color-gray-100; + color: $color-text-secondary; + + &:hover { + background: $color-gray-200; + } + } + + &--ghost { + background: transparent; + color: $color-brand-primary; + + &:hover { + background: rgba($color-brand-primary, 0.08); + } + } + + &--success { + background: $accent-green; + color: white; + + &:hover { + background: darken($accent-green, 6%); + } + } + + &--danger { + background: rgba($accent-red, 0.1); + color: $accent-red; + + &:hover { + background: rgba($accent-red, 0.15); + } + } +} + +// ============================================ +// EDIT FORM +// ============================================ +.faq-edit-form { + display: flex; + flex-direction: column; + gap: $admin-space-sm; +} + +.form-group { + label { + display: block; + font-size: 0.6875rem; + font-weight: 600; + color: $color-text-secondary; + text-transform: uppercase; + letter-spacing: 0.02em; + margin-bottom: 4px; + } +} + +.form-input, +.form-textarea, +.form-select { + width: 100%; + padding: $admin-space-xs $admin-space-sm; + background: white; + border: 1px solid $border-default; + border-radius: $radius-sm; + font-size: 0.8125rem; + color: $color-text-primary; + transition: all 0.15s ease; + + &:focus { + outline: none; + border-color: $color-brand-primary; + box-shadow: 0 0 0 3px rgba($color-brand-primary, 0.1); + } +} + +.form-textarea { + min-height: 80px; + resize: vertical; +} + +.form-row { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: $admin-space-sm; + + @media (max-width: 480px) { + grid-template-columns: 1fr; + } +} + +.edit-actions { + display: flex; + gap: $admin-space-xs; + justify-content: flex-end; + padding-top: $admin-space-xs; + border-top: 1px solid $border-default; +} + +// ============================================ +// ADD NEW CARD +// ============================================ +.add-card { + background: rgba($color-brand-primary, 0.03); + border: 2px dashed rgba($color-brand-primary, 0.2); + border-radius: $radius-md; + padding: $admin-space-md; + animation: fadeIn 0.3s ease-out; +} + +.add-card__header { + display: flex; + align-items: center; + gap: $admin-space-xs; + margin-bottom: $admin-space-sm; + + app-icon { + font-size: 1.25rem; + color: $color-brand-primary; + } + + h3 { + font-size: 0.9375rem; + font-weight: 600; + color: $color-text-primary; + margin: 0; + } +} + +// ============================================ +// CATEGORY FILTER +// ============================================ +.category-filter { + display: flex; + gap: $admin-space-2xs; + margin-bottom: $admin-space-md; + overflow-x: auto; + padding-bottom: $admin-space-2xs; + scrollbar-width: none; + animation: fadeIn 0.3s ease-out 0.05s backwards; + + &::-webkit-scrollbar { + display: none; + } +} + +.filter-btn { + padding: $admin-space-xs $admin-space-sm; + background: $glass-bg; + border: 1px solid $glass-border; + border-radius: $radius-sm; + font-size: 0.75rem; + font-weight: 600; + color: $color-text-secondary; + cursor: pointer; + transition: all 0.15s ease; + white-space: nowrap; + + &:hover { + border-color: $color-brand-primary; + color: $color-brand-primary; + } + + &--active { + background: $gradient-primary; + border-color: transparent; + color: white; + } +} + +// ============================================ +// MODAL +// ============================================ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(black, 0.5); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: $admin-space-md; + animation: fadeIn 0.2s ease-out; +} + +.modal { + width: 100%; + max-width: 500px; + max-height: 90vh; + background: white; + border-radius: $radius-lg; + box-shadow: $shadow-xl; + overflow: hidden; + animation: scaleIn 0.25s ease-out; + + &__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: $admin-space-sm $admin-space-md; + border-bottom: 1px solid $border-default; + + h2 { + font-size: 1rem; + font-weight: 700; + color: $color-text-primary; + margin: 0; + } + } + + &__body { + padding: $admin-space-md; + overflow-y: auto; + max-height: calc(90vh - 120px); + } + + &__footer { + display: flex; + gap: $admin-space-xs; + padding: $admin-space-sm $admin-space-md; + border-top: 1px solid $border-default; + justify-content: flex-end; + } + + &__close { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + border-radius: $radius-sm; + color: $color-text-tertiary; + cursor: pointer; + transition: all 0.15s ease; + + &:hover { + background: $color-gray-100; + color: $color-text-primary; + } + } +} + +// ============================================ +// TOAST +// ============================================ +.toast { + position: fixed; + bottom: 5rem; + left: 50%; + transform: translateX(-50%); + padding: $admin-space-sm $admin-space-md; + background: $color-gray-900; + color: white; + border-radius: $radius-md; + font-size: 0.8125rem; + font-weight: 500; + box-shadow: $shadow-lg; + z-index: 1000; + animation: popIn 0.3s ease-out; + + &--success { + background: $accent-green; + } + + &--error { + background: $accent-red; + } + + @media (min-width: 768px) { + bottom: $admin-space-lg; + } +} + +// ============================================ +// RESPONSIVE +// ============================================ +@media (max-width: 640px) { + .admin-faq { + padding: $admin-space-sm; + padding-bottom: calc($admin-space-sm + 80px); + } +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} diff --git a/apps/frontend/src/app/components/admin/admin-faq/admin-faq.component.ts b/apps/frontend/src/app/components/admin/admin-faq/admin-faq.component.ts new file mode 100644 index 0000000..1ea27aa --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-faq/admin-faq.component.ts @@ -0,0 +1,372 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { AdminLayoutComponent } from '../admin-layout/admin-layout.component'; +import { ToastService } from '../../../shared/toasts/toast.service'; +import { ConfirmationService } from '../../../shared/confirmation/confirmation.service'; +import { + ApiService, + Faq, + CreateFaqDto, + UpdateFaqDto, + ImportFaqResultDto +} from '../../../api/api.service'; + +interface FaqFormModel { + slug: string; + question: string; + answers: string[]; + listItems: string[]; + sortOrder: number; + isPublished: boolean; + category: string; +} + +@Component({ + selector: 'app-admin-faq', + standalone: true, + imports: [ + CommonModule, + FormsModule, + AdminLayoutComponent + ], + templateUrl: './admin-faq.component.html', + styleUrl: './admin-faq.component.scss' +}) +export class AdminFaqComponent implements OnInit { + faqs: Faq[] = []; + isLoading = true; + isSaving = false; + + // Editor State + showEditor = false; + editingFaq: Faq | null = null; + + // Form Model + formModel: FaqFormModel = this.getEmptyForm(); + + // Temp fields for adding answers/list items + newAnswer = ''; + newListItem = ''; + + // Import/Export + showImportModal = false; + importJson = ''; + importOverwrite = false; + + constructor( + private api: ApiService, + private toasts: ToastService, + private confirmationService: ConfirmationService + ) {} + + // Getter für die Anzahl veröffentlichter FAQs + get publishedCount(): number { + return this.faqs.filter(f => f.isPublished).length; + } + + ngOnInit(): void { + this.loadFaqs(); + } + + // ===== DATA LOADING ===== + + loadFaqs(): void { + this.isLoading = true; + this.api.getAllFaqs().subscribe({ + next: (data) => { + this.faqs = data; + this.isLoading = false; + }, + error: (err) => { + console.error('Fehler beim Laden der FAQs:', err); + this.toasts.error('Fehler beim Laden der FAQs'); + this.isLoading = false; + } + }); + } + + // ===== EDITOR ===== + + openCreateEditor(): void { + this.editingFaq = null; + this.formModel = this.getEmptyForm(); + this.showEditor = true; + } + + openEditEditor(faq: Faq): void { + this.editingFaq = faq; + this.formModel = { + slug: faq.slug, + question: faq.question, + answers: [...faq.answers], + listItems: faq.listItems ? [...faq.listItems] : [], + sortOrder: faq.sortOrder, + isPublished: faq.isPublished, + category: faq.category || '' + }; + this.showEditor = true; + } + + closeEditor(): void { + this.showEditor = false; + this.editingFaq = null; + this.formModel = this.getEmptyForm(); + this.newAnswer = ''; + this.newListItem = ''; + } + + // ===== FORM HELPERS ===== + + private getEmptyForm(): FaqFormModel { + return { + slug: '', + question: '', + answers: [], + listItems: [], + sortOrder: 0, + isPublished: true, + category: '' + }; + } + + generateSlug(): void { + if (this.formModel.question && !this.editingFaq) { + this.formModel.slug = this.formModel.question + .toLowerCase() + .replace(/[äÄ]/g, 'ae') + .replace(/[öÖ]/g, 'oe') + .replace(/[üÜ]/g, 'ue') + .replace(/ß/g, 'ss') + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .substring(0, 50); + } + } + + // Answers Management + addAnswer(): void { + if (this.newAnswer.trim()) { + this.formModel.answers.push(this.newAnswer.trim()); + this.newAnswer = ''; + } + } + + removeAnswer(index: number): void { + this.formModel.answers.splice(index, 1); + } + + moveAnswerUp(index: number): void { + if (index > 0) { + [this.formModel.answers[index - 1], this.formModel.answers[index]] = + [this.formModel.answers[index], this.formModel.answers[index - 1]]; + } + } + + moveAnswerDown(index: number): void { + if (index < this.formModel.answers.length - 1) { + [this.formModel.answers[index], this.formModel.answers[index + 1]] = + [this.formModel.answers[index + 1], this.formModel.answers[index]]; + } + } + + // List Items Management + addListItem(): void { + if (this.newListItem.trim()) { + this.formModel.listItems.push(this.newListItem.trim()); + this.newListItem = ''; + } + } + + removeListItem(index: number): void { + this.formModel.listItems.splice(index, 1); + } + + // ===== CRUD OPERATIONS ===== + + saveFaq(): void { + if (!this.formModel.slug || !this.formModel.question || this.formModel.answers.length === 0) { + this.toasts.error('Bitte fülle alle Pflichtfelder aus (Slug, Frage, mind. 1 Antwort)'); + return; + } + + this.isSaving = true; + + const dto: CreateFaqDto | UpdateFaqDto = { + slug: this.formModel.slug, + question: this.formModel.question, + answers: this.formModel.answers, + listItems: this.formModel.listItems.length > 0 ? this.formModel.listItems : undefined, + sortOrder: this.formModel.sortOrder, + isPublished: this.formModel.isPublished, + category: this.formModel.category || undefined + }; + + if (this.editingFaq) { + // Update + this.api.updateFaq(this.editingFaq.id, dto).subscribe({ + next: () => { + this.toasts.success('FAQ erfolgreich aktualisiert'); + this.closeEditor(); + this.loadFaqs(); + this.isSaving = false; + }, + error: (err) => { + console.error('Fehler beim Aktualisieren:', err); + this.toasts.error(err.error?.message || 'Fehler beim Aktualisieren'); + this.isSaving = false; + } + }); + } else { + // Create + this.api.createFaq(dto as CreateFaqDto).subscribe({ + next: () => { + this.toasts.success('FAQ erfolgreich erstellt'); + this.closeEditor(); + this.loadFaqs(); + this.isSaving = false; + }, + error: (err) => { + console.error('Fehler beim Erstellen:', err); + this.toasts.error(err.error?.message || 'Fehler beim Erstellen'); + this.isSaving = false; + } + }); + } + } + + async deleteFaq(faq: Faq): Promise { + const confirmed = await this.confirmationService.confirm({ + title: 'FAQ löschen', + message: `Möchtest du "${faq.question}" wirklich löschen?`, + type: 'danger', + confirmText: 'Löschen', + cancelText: 'Abbrechen' + }); + + if (confirmed) { + this.api.deleteFaq(faq.id).subscribe({ + next: () => { + this.toasts.success('FAQ gelöscht'); + this.loadFaqs(); + }, + error: (err) => { + console.error('Fehler beim Löschen:', err); + this.toasts.error('Fehler beim Löschen'); + } + }); + } + } + + togglePublish(faq: Faq): void { + this.api.toggleFaqPublish(faq.id).subscribe({ + next: (updated) => { + const index = this.faqs.findIndex(f => f.id === faq.id); + if (index !== -1) { + this.faqs[index] = updated; + } + this.toasts.success(updated.isPublished ? 'FAQ veröffentlicht' : 'FAQ versteckt'); + }, + error: (err) => { + console.error('Fehler:', err); + this.toasts.error('Fehler beim Ändern des Status'); + } + }); + } + + // ===== IMPORT / EXPORT ===== + + openImportModal(): void { + this.importJson = ''; + this.importOverwrite = false; + this.showImportModal = true; + } + + closeImportModal(): void { + this.showImportModal = false; + this.importJson = ''; + } + + importFaqs(): void { + if (!this.importJson.trim()) { + this.toasts.error('Bitte JSON eingeben'); + return; + } + + let parsed: any; + try { + parsed = JSON.parse(this.importJson); + } catch (e) { + this.toasts.error('Ungültiges JSON-Format'); + return; + } + + // Support both array and object with faqs property + const faqsArray = Array.isArray(parsed) ? parsed : parsed.faqs; + + if (!Array.isArray(faqsArray)) { + this.toasts.error('JSON muss ein Array von FAQs sein oder ein Objekt mit "faqs" Property'); + return; + } + + // Transform to match backend format if needed + const transformedFaqs = faqsArray.map((f: any) => ({ + slug: f.slug || f.id, + question: f.question || f.q, + answers: f.answers || f.a, + listItems: f.listItems || f.list, + sortOrder: f.sortOrder ?? 0, + isPublished: f.isPublished ?? true, + category: f.category + })); + + this.isSaving = true; + this.api.importFaqs({ + faqs: transformedFaqs, + overwriteExisting: this.importOverwrite + }).subscribe({ + next: (result: ImportFaqResultDto) => { + this.toasts.success( + `Import abgeschlossen: ${result.imported} neu, ${result.updated} aktualisiert, ${result.skipped} übersprungen` + ); + if (result.errors.length > 0) { + console.warn('Import Fehler:', result.errors); + } + this.closeImportModal(); + this.loadFaqs(); + this.isSaving = false; + }, + error: (err) => { + console.error('Import Fehler:', err); + this.toasts.error('Fehler beim Import'); + this.isSaving = false; + } + }); + } + + exportFaqs(): void { + this.api.exportFaqs().subscribe({ + next: (data) => { + const json = JSON.stringify(data, null, 2); + const blob = new Blob([json], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `faqs-export-${new Date().toISOString().split('T')[0]}.json`; + a.click(); + URL.revokeObjectURL(url); + this.toasts.success('Export erfolgreich'); + }, + error: (err) => { + console.error('Export Fehler:', err); + this.toasts.error('Fehler beim Export'); + } + }); + } + + // ===== HELPERS ===== + + trackById(_: number, faq: Faq): string { + return faq.id; + } +} diff --git a/apps/frontend/src/app/components/admin/admin-header/admin-header.component.html b/apps/frontend/src/app/components/admin/admin-header/admin-header.component.html new file mode 100644 index 0000000..d8ea291 --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-header/admin-header.component.html @@ -0,0 +1,29 @@ + \ No newline at end of file diff --git a/apps/frontend/src/app/components/admin/admin-header/admin-header.component.scss b/apps/frontend/src/app/components/admin/admin-header/admin-header.component.scss new file mode 100644 index 0000000..8d796d4 --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-header/admin-header.component.scss @@ -0,0 +1,215 @@ +@import '../../..//utils/shared-styles.scss'; + +/* =============================== + ADMIN HEADER + Sitzt unter dem normalen Header + =============================== */ + +.admin-header { + position: sticky; + top: 72px; + /* Platz für deinen Page-Header */ + z-index: 1000; + backdrop-filter: blur(20px) saturate(160%); + background: rgba(255, 255, 255, 0.85); + border-bottom: 1px solid rgba(0, 0, 0, 0.06); + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.04), + 0 0 0 1px rgba(255, 255, 255, 0.5) inset; + transition: all 0.3s ease; + padding-top: env(safe-area-inset-top); + + .header__inner { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + min-height: 60px; + max-width: 1200px; + margin-inline: auto; + padding-inline: clamp(1rem, 4vw, 2rem); + position: relative; // wichtig für das mobile Menü + } +} + +/* ===== BRAND (optional Icon/Titel links) ===== */ +.brand { + display: inline-flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; + user-select: none; + font-weight: 700; + color: #9a3412; + + .material-symbols-outlined { + font-size: 22px; + } + + &:hover { + opacity: 0.85; + } +} + +/* ===== DESKTOP NAVIGATION ===== */ +.admin-nav { + display: none; + gap: 0.5rem; + + @media (min-width: 900px) { + display: flex; + align-items: center; + margin-left: auto; + } + + a { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.625rem 1rem; + border-radius: $radius-md; + font-size: 0.9375rem; + font-weight: 600; + color: #c2410c; + text-decoration: none; + border: 2px solid rgba(234, 88, 12, 0.2); + background: #fff; + transition: transform 0.2s ease, box-shadow 0.2s ease, + background 0.3s ease, border-color 0.3s ease, color 0.2s ease; + min-height: 44px; + min-width: 44px; + + .material-symbols-outlined { + font-size: 20px; + } + + &:hover { + background: linear-gradient(135deg, + rgba(234, 88, 12, 0.12), + rgba(249, 115, 22, 0.16)); + border-color: rgba(234, 88, 12, 0.4); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(234, 88, 12, 0.2); + color: #9a3412; + } + + &.nav-link--active { + background: linear-gradient(135deg, + rgba(234, 88, 12, 0.15), + rgba(249, 115, 22, 0.2)); + border-color: #ea580c; + color: #9a3412; + box-shadow: + 0 4px 12px rgba(234, 88, 12, 0.25), + 0 0 0 1px rgba(234, 88, 12, 0.1) inset; + } + + &:focus-visible { + outline: none; + box-shadow: 0 0 0 4px rgba(234, 88, 12, 0.28); + } + } + + /* ===== MOBILE BURGER MENÜ ===== */ + &.open { + display: flex; + flex-direction: column; + position: absolute; + top: 100%; + /* direkt unter dem Admin-Header */ + left: 0; + right: 0; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(20px); + border-bottom: 1px solid rgba(0, 0, 0, 0.06); + padding: 1rem; + gap: 0.5rem; + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.08); + animation: slideDown 0.25s ease; + + a { + width: 100%; + justify-content: flex-start; + padding: 0.875rem 1rem; + font-size: 1rem; + } + } +} + +/* ===== BURGER BUTTON ===== */ +.nav-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + border-radius: $radius-md; + background: transparent; + border: 2px solid $color-gray-200; + cursor: pointer; + transition: all 0.2s ease; + + .material-symbols-outlined { + font-size: 24px; + } + + @media (min-width: 900px) { + display: none; + } + + &:hover { + border-color: #ea580c; + background: rgba(234, 88, 12, 0.06); + } + + &:focus-visible { + outline: none; + box-shadow: 0 0 0 3px rgba(234, 88, 12, 0.2); + } +} + +/* ===== BACKDROP ===== */ +.backdrop { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.45); + backdrop-filter: blur(4px); + animation: fadeIn 0.25s ease; + cursor: pointer; + z-index: -1 + /* unter Admin-Header, über Content */ +} + +/* ===== ANIMATIONS ===== */ +@keyframes slideDown { + from { + transform: translateY(-10px); + opacity: 0; + } + + to { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +/* ===== MOTION REDUCE ===== */ +@media (prefers-reduced-motion: reduce) { + + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/components/admin/admin-header/admin-header.component.spec.ts b/apps/frontend/src/app/components/admin/admin-header/admin-header.component.spec.ts new file mode 100644 index 0000000..ae3ee55 --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-header/admin-header.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AdminHeaderComponent } from './admin-header.component'; + +describe('AdminHeaderComponent', () => { + let component: AdminHeaderComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AdminHeaderComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AdminHeaderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/frontend/src/app/components/admin/admin-header/admin-header.component.ts b/apps/frontend/src/app/components/admin/admin-header/admin-header.component.ts new file mode 100644 index 0000000..d1b364e --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-header/admin-header.component.ts @@ -0,0 +1,134 @@ +import { CommonModule } from '@angular/common'; +import { Component, HostListener, inject, OnDestroy, OnInit } from '@angular/core'; +import { Router, NavigationEnd, RouterLink, RouterLinkActive } from '@angular/router'; +import { filter, Subscription } from 'rxjs'; + +@Component({ + selector: 'app-admin-header', + standalone: true, + imports: [CommonModule, RouterLink, RouterLinkActive], + templateUrl: './admin-header.component.html', + styleUrls: ['./admin-header.component.scss'] +}) +export class AdminHeaderComponent implements OnInit, OnDestroy { + open = false; + isMobile = false; + private sub?: Subscription; + private mq?: MediaQueryList; + private mqListener = (e: MediaQueryListEvent) => { + this.isMobile = e.matches; + if (!this.isMobile) this.close(); // falls von mobile -> desktop + }; + + constructor(private router: Router) { } + + routes = [ + { + path: 'admin/requests', + label: 'Anfragen', + icon: 'mail' + }, + { + path: 'admin/booking', + label: 'Buchungen', + icon: 'calendar_today' + }, + { + path: 'admin/newsletter', + label: 'Newsletter', + icon: 'newspaper' + }, + { + path: 'admin/faq', + label: 'FAQ', + icon: 'quiz' + }, + { + path: 'admin/services', + label: 'Services', + icon: 'build' + }, + { + path: 'admin/users', + label: 'User', + icon: 'group' + }, + { + path: 'admin/invoices', + label: 'Rechnungen', + icon: 'receipt_long' + }, + { + path: 'admin/settings', + label: 'Einstellungen', + icon: 'settings' + }, + ]; + + getCurrentRouteLabel() { + const currentPath = this.router.url.split('?')[0]; + const route = this.routes.find(r => this.normalize(r.path) === this.normalize(currentPath)); + return route ? route.label : 'Admin Bereich'; + } + + ngOnInit() { + // Media Query initial + listener + this.mq = window.matchMedia('(max-width: 899px)'); + this.isMobile = this.mq.matches; + this.mq.addEventListener?.('change', this.mqListener); + + // Close on route change + this.sub = this.router.events.pipe(filter(e => e instanceof NavigationEnd)).subscribe(() => { + this.close(); + }); + } + + ngOnDestroy() { + this.sub?.unsubscribe(); + this.mq?.removeEventListener?.('change', this.mqListener); + this.unlockScroll(); + } + + toggle() { + this.open = !this.open; + this.open ? this.lockScroll() : this.unlockScroll(); + } + close() { + if (!this.open) return; + this.open = false; + this.unlockScroll(); + } + + onNavClick() { + // Mobile: Linkklick schließt Menü (Desktop egal) + if (this.isMobile) this.close(); + } + + normalize(path: string) { + // immer absolut machen, damit NG04002 durch relative Segmente nicht passiert + return path.startsWith('/') ? path : '/' + path; + } + + // ESC schließt + @HostListener('document:keydown.escape', ['$event']) + onEsc(e: Event) { + if (this.open) { + e.preventDefault(); + this.close(); + } + } + + // Body Scroll Lock (ohne Lib) + private lockScroll() { + document.body.style.overflow = 'hidden'; + document.body.style.touchAction = 'none'; + } + private unlockScroll() { + document.body.style.overflow = ''; + document.body.style.touchAction = ''; + } + + routeTo(path: string) { + this.router.navigateByUrl(this.normalize(path)); + } +} diff --git a/apps/frontend/src/app/components/admin/admin-invoices/admin-invoices.component.html b/apps/frontend/src/app/components/admin/admin-invoices/admin-invoices.component.html new file mode 100644 index 0000000..28241e1 --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-invoices/admin-invoices.component.html @@ -0,0 +1,426 @@ + +
+ +
+
+ + {{ stats.total }} + Rechnungen +
+
+ + {{ stats.draft }} + Entwürfe +
+
+ + {{ stats.sent }} + Gesendet +
+
+ + {{ stats.paid }} + Bezahlt +
+
+ + {{ formatCurrency(stats.totalRevenue) }} + Umsatz +
+
+ + +
+

Rechnungen

+ +
+ + +
+ @if (loading) { +
+
+ Lade Rechnungen... +
+ } @else if (invoices.length === 0) { +
+ +

Keine Rechnungen

+

Erstelle deine erste Rechnung um loszulegen

+ +
+ } @else { + +
+
+
+ Nr. +
+
+ Kunde +
+
+ Datum +
+
+ Status +
+
+ Betrag +
+
Aktionen
+
+ + + @for (invoice of invoices; track invoice.id) { +
+ +
+
+ +
+
+ {{ invoice.invoiceNumber }} +
+
+ {{ invoice.customerName }} +
+
+ {{ formatDate(invoice.date) }} +
+
+ + +
+ {{ getStatusLabel(invoice.status) }} +
+
+
+
+ {{ formatCurrency(invoice.totalGross || calculateTotal(invoice.items, invoice.taxRate).gross) }} +
+
+ + + +
+
+ + + @if (isExpanded(invoice.id)) { +
+
+
+

Kunde

+

{{ invoice.customerName }}

+

{{ invoice.customerAddress }}

+

{{ invoice.customerZip }} {{ invoice.customerCity }}

+ @if (invoice.customerEmail) { +

{{ invoice.customerEmail }}

+ } +
+ +
+

Daten

+

Erstellt: {{ formatDate(invoice.date) }}

+

+ Fällig: {{ formatDate(invoice.dueDate) }} +

+
+ +
+

Positionen ({{ invoice.items.length }})

+ @for (item of invoice.items; track item.id) { +
+ {{ item.description }} + {{ item.quantity }} {{ item.unit }} + {{ formatCurrency(item.quantity * item.unitPrice) }} +
+ } +
+
Netto: {{ formatCurrency(calculateTotal(invoice.items, invoice.taxRate).net) }}
+
MwSt. ({{ invoice.taxRate }}%): {{ formatCurrency(calculateTotal(invoice.items, invoice.taxRate).tax) }}
+
Gesamt: {{ formatCurrency(calculateTotal(invoice.items, invoice.taxRate).gross) }}
+
+
+
+ +
+
+ @if (invoice.status === 'draft') { + + } + @if (invoice.status === 'sent' || invoice.status === 'overdue') { + + } + @if (invoice.status === 'sent') { + + } +
+
+ + +
+
+
+ } +
+ } + } +
+
+ + +@if (showEditor) { +
+
+
+

{{ editingInvoice ? 'Rechnung bearbeiten' : 'Neue Rechnung' }}

+ +
+ +
+ +
+

Rechnungsdetails

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

Kundendaten

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

Positionen

+ +
+ +
+
+ Beschreibung + Menge + Einheit + Einzelpreis + Gesamt + +
+ + @for (item of form.items; track item.id; let i = $index) { +
+ + + + + {{ formatCurrency(item.quantity * item.unitPrice) }} + +
+ } +
+ + +
+
+ + +
+
+
+ Netto: + {{ formatCurrency(formTotals.net) }} +
+
+ MwSt. ({{ form.taxRate }}%): + {{ formatCurrency(formTotals.tax) }} +
+
+ Gesamt: + {{ formatCurrency(formTotals.gross) }} +
+
+
+
+ + +
+

Hinweise

+ +
+
+ + +
+
+} + + +@if (showPreview) { +
+
+
+

Rechnungsvorschau

+
+ + +
+
+ +
+
+
+
+

RECHNUNG

+ Leonards & Brandenburger IT +
+
+
Nr. {{ form.invoiceNumber }}
+
Datum: {{ formatDate(form.date || '') }}
+
Fällig: {{ formatDate(form.dueDate || '') }}
+
+
+ +
+ Leonards & Brandenburger IT · Musterstraße 1 · 12345 Musterstadt +
+ {{ form.customerName }}
+ {{ form.customerAddress }}
+ {{ form.customerZip }} {{ form.customerCity }}
+ @if (form.customerEmail) { + + } +
+
+ + + + + + + + + + + + + @for (item of form.items; track item.id) { + + + + + + + + } + +
BeschreibungMengeEinheitEinzelpreisGesamt
{{ item.description }}{{ item.quantity }}{{ item.unit }}{{ formatCurrency(item.unitPrice) }}{{ formatCurrency(item.quantity * item.unitPrice) }}
+ +
+
Netto:{{ formatCurrency(formTotals.net) }}
+
MwSt. ({{ form.taxRate }}%):{{ formatCurrency(formTotals.tax) }}
+
Gesamt:{{ formatCurrency(formTotals.gross) }}
+
+ + @if (form.notes) { +
+ Hinweise: +

{{ form.notes }}

+
+ } + + +
+
+
+
+} +
\ No newline at end of file diff --git a/apps/frontend/src/app/components/admin/admin-invoices/admin-invoices.component.scss b/apps/frontend/src/app/components/admin/admin-invoices/admin-invoices.component.scss new file mode 100644 index 0000000..af671a6 --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-invoices/admin-invoices.component.scss @@ -0,0 +1,815 @@ +// ============================================ +// ADMIN INVOICES - Component Styles +// Matching HTML classes exactly +// ============================================ +@import '../admin-shared.scss'; + +// ============================================ +// TABLE CONTAINER & STRUCTURE +// ============================================ +.admin-table-container { + background: $glass-bg; + backdrop-filter: blur(20px); + border-radius: $radius-lg; + border: 1px solid $glass-border; + box-shadow: $shadow-sm; + overflow: hidden; + animation: fadeIn 0.3s ease-out 0.1s backwards; +} + +.admin-table-header { + display: grid; + gap: $admin-space-xs; + padding: $admin-space-sm $admin-space-md; + background: rgba($color-gray-50, 0.8); + font-size: 0.6875rem; + font-weight: 700; + color: $color-text-tertiary; + text-transform: uppercase; + letter-spacing: 0.03em; + border-bottom: 1px solid $border-default; + + @media (max-width: 900px) { + display: none; + } + + .sortable { + cursor: pointer; + display: flex; + align-items: center; + gap: 4px; + transition: color 0.15s ease; + + app-icon { + font-size: 0.875rem; + opacity: 0.5; + transition: opacity 0.15s ease; + } + + &:hover { + color: $color-brand-primary; + app-icon { opacity: 1; } + } + } +} + +.table-row-wrapper { + border-bottom: 1px solid rgba($border-default, 0.5); + transition: background 0.2s ease; + + &:last-child { border-bottom: none; } + + &.expanded { + background: rgba($color-brand-primary, 0.02); + } +} + +.admin-table-row { + display: grid; + gap: $admin-space-xs; + padding: $admin-space-sm $admin-space-md; + align-items: center; + font-size: 0.8125rem; + cursor: pointer; + transition: background 0.15s ease; + + &:hover { + background: rgba($color-brand-primary, 0.03); + } + + // Status-based left border accent + &.status-paid { + border-left: 3px solid $accent-green; + } + &.status-sent { + border-left: 3px solid $accent-blue; + } + &.status-draft { + border-left: 3px solid $color-gray-300; + } + &.status-overdue { + border-left: 3px solid $accent-red; + } + + // Mobile: Card-Layout + @media (max-width: 900px) { + display: flex; + flex-wrap: wrap; + gap: $admin-space-sm; + padding: $admin-space-md; + } +} + +// ============================================ +// TABLE COLUMNS +// ============================================ +.col-expand { + display: flex; + align-items: center; + justify-content: center; + + app-icon { + font-size: 1.25rem; + color: $color-text-tertiary; + transition: transform 0.2s ease; + } + + .expanded & app-icon { + transform: rotate(180deg); + } + + @media (max-width: 900px) { + display: none; + } +} + +.col-number { + .number { + font-weight: 700; + color: $color-brand-primary; + font-size: 0.8125rem; + } + + @media (max-width: 900px) { + order: 1; + flex: 0 0 auto; + } +} + +.col-customer { + min-width: 0; + + .customer-name { + font-weight: 600; + color: $color-text-primary; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: block; + } + + @media (max-width: 900px) { + order: 2; + flex: 1; + } +} + +.col-date { + font-size: 0.75rem; + color: $color-text-secondary; + + @media (max-width: 900px) { + order: 5; + flex: 0 0 50%; + font-size: 0.6875rem; + color: $color-text-tertiary; + } +} + +.col-status { + @media (max-width: 900px) { + order: 3; + flex: 0 0 auto; + } +} + +.col-amount { + text-align: right; + + strong { + font-weight: 700; + color: $color-text-primary; + font-size: 0.875rem; + } + + @media (max-width: 900px) { + order: 4; + flex: 0 0 auto; + margin-left: auto; + } +} + +.col-actions { + display: flex; + gap: 4px; + justify-content: flex-end; + + @media (max-width: 900px) { + order: 6; + flex: 0 0 100%; + justify-content: flex-start; + padding-top: $admin-space-xs; + margin-top: $admin-space-xs; + border-top: 1px solid $border-default; + } +} + +// ============================================ +// EXPANDED ROW DETAILS +// ============================================ +.row-details { + padding: $admin-space-md; + padding-top: 0; + animation: slideDown 0.25s ease-out; +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.details-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: $admin-space-md; + padding: $admin-space-md; + background: rgba($color-gray-50, 0.5); + border-radius: $radius-md; + margin-bottom: $admin-space-sm; + + @media (max-width: 640px) { + grid-template-columns: 1fr; + gap: $admin-space-sm; + padding: $admin-space-sm; + } +} + +.detail-section { + h4 { + display: flex; + align-items: center; + gap: $admin-space-2xs; + font-size: 0.75rem; + font-weight: 700; + color: $color-text-secondary; + margin: 0 0 $admin-space-xs; + text-transform: uppercase; + letter-spacing: 0.02em; + + app-icon { + font-size: 1rem; + color: $color-brand-primary; + } + } + + p { + font-size: 0.8125rem; + color: $color-text-primary; + margin: 0 0 $admin-space-2xs; + line-height: 1.5; + + &.muted { + color: $color-text-tertiary; + font-size: 0.75rem; + } + + &.overdue { + color: $accent-red; + font-weight: 600; + } + } +} + +.items-section { + grid-column: 1 / -1; +} + +// Item lines in details +.item-line { + display: grid; + grid-template-columns: 1fr auto auto; + gap: $admin-space-sm; + padding: $admin-space-2xs 0; + font-size: 0.8125rem; + border-bottom: 1px dashed rgba($border-default, 0.5); + + &:last-of-type { + border-bottom: none; + } + + .item-desc { + color: $color-text-primary; + } + + .item-qty { + color: $color-text-tertiary; + font-size: 0.75rem; + } + + .item-price { + font-weight: 600; + color: $color-text-primary; + text-align: right; + } +} + +.item-totals { + margin-top: $admin-space-sm; + padding-top: $admin-space-sm; + border-top: 1px solid $border-default; + + > div { + display: flex; + justify-content: space-between; + font-size: 0.8125rem; + padding: $admin-space-2xs 0; + color: $color-text-secondary; + + span:last-child { + font-weight: 500; + } + + &.gross { + font-weight: 700; + font-size: 0.9375rem; + color: $color-brand-primary; + padding-top: $admin-space-xs; + margin-top: $admin-space-2xs; + border-top: 1px solid $border-default; + } + } +} + +// Details actions +.details-actions { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + gap: $admin-space-sm; + padding-top: $admin-space-sm; + + @media (max-width: 640px) { + flex-direction: column; + } +} + +.status-actions, +.main-actions { + display: flex; + gap: $admin-space-xs; + flex-wrap: wrap; +} + +// ============================================ +// MODAL EXTENSIONS +// ============================================ +.close-btn { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + border-radius: $radius-sm; + color: $color-text-tertiary; + cursor: pointer; + transition: all 0.15s ease; + + &:hover { + background: $color-gray-100; + color: $color-text-primary; + } +} + +// Form sections +.form-section { + margin-bottom: $admin-space-lg; + padding-bottom: $admin-space-md; + border-bottom: 1px solid $border-default; + + &:last-child { + margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; + } + + h3 { + display: flex; + align-items: center; + gap: $admin-space-xs; + font-size: 0.875rem; + font-weight: 700; + color: $color-text-primary; + margin: 0 0 $admin-space-md; + + app-icon { + font-size: 1.125rem; + color: $color-brand-primary; + } + } +} + +// Form row layouts +.form-row { + display: grid; + gap: $admin-space-md; + + &.two-col { + grid-template-columns: repeat(2, 1fr); + } + + &.three-col { + grid-template-columns: repeat(3, 1fr); + } + + @media (max-width: 640px) { + &.two-col, + &.three-col { + grid-template-columns: 1fr; + } + } +} + +// ============================================ +// ITEMS TABLE (Editor Modal) +// ============================================ +.items-table { + width: 100%; + border: 1px solid $border-default; + border-radius: $radius-md; + overflow: hidden; +} + +.items-header { + display: grid; + grid-template-columns: 1fr 70px 80px 100px 90px 40px; + gap: $admin-space-xs; + padding: $admin-space-xs $admin-space-sm; + background: $color-gray-50; + font-size: 0.625rem; + font-weight: 700; + color: $color-text-tertiary; + text-transform: uppercase; + letter-spacing: 0.03em; + + @media (max-width: 768px) { + display: none; + } +} + +.item-row { + display: grid; + grid-template-columns: 1fr 70px 80px 100px 90px 40px; + gap: $admin-space-xs; + padding: $admin-space-xs $admin-space-sm; + border-top: 1px solid $border-default; + align-items: center; + + @media (max-width: 768px) { + grid-template-columns: 1fr auto; + gap: $admin-space-sm; + + .col-desc { + grid-column: 1 / -1; + } + + .col-unit { + grid-column: span 2; + } + } + + .col-total { + font-weight: 600; + color: $color-text-primary; + text-align: right; + font-size: 0.8125rem; + } + + .admin-input, + .admin-select { + padding: $admin-space-2xs $admin-space-xs; + font-size: 0.8125rem; + } +} + +// ============================================ +// TOTALS SECTION (Editor Modal) +// ============================================ +.totals { + display: flex; + justify-content: space-between; + align-items: flex-start; + flex-wrap: wrap; + gap: $admin-space-md; + margin-top: $admin-space-md; + padding-top: $admin-space-md; + border-top: 1px solid $border-default; + + @media (max-width: 640px) { + flex-direction: column; + } +} + +.tax-rate { + display: flex; + align-items: center; + gap: $admin-space-xs; + + label { + font-size: 0.8125rem; + font-weight: 600; + color: $color-text-secondary; + } + + .admin-select { + width: auto; + min-width: 80px; + } +} + +.totals-breakdown { + display: flex; + flex-direction: column; + gap: $admin-space-2xs; + min-width: 200px; + text-align: right; + + .total-row { + display: flex; + justify-content: space-between; + gap: $admin-space-md; + font-size: 0.8125rem; + color: $color-text-secondary; + + span:last-child { + font-weight: 600; + color: $color-text-primary; + min-width: 80px; + } + + &.gross { + margin-top: $admin-space-xs; + padding-top: $admin-space-xs; + border-top: 1px solid $border-default; + font-size: 1rem; + font-weight: 700; + + span:last-child { + color: $color-brand-primary; + } + } + } +} + +// ============================================ +// PREVIEW MODAL +// ============================================ +.preview-modal { + .admin-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + + .preview-actions { + display: flex; + align-items: center; + gap: $admin-space-sm; + } + } +} + +.preview-body { + background: $color-gray-100; + padding: $admin-space-md !important; +} + +.invoice-preview { + background: white; + border-radius: $radius-md; + box-shadow: $shadow-md; + padding: $admin-space-lg; + max-width: 800px; + margin: 0 auto; + font-size: 0.875rem; + + @media (max-width: 640px) { + padding: $admin-space-md; + font-size: 0.8125rem; + } +} + +.preview-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: $admin-space-lg; + padding-bottom: $admin-space-md; + border-bottom: 2px solid $color-brand-primary; + + @media (max-width: 640px) { + flex-direction: column; + gap: $admin-space-md; + } + + .brand { + h1 { + font-size: 1.5rem; + font-weight: 800; + color: $color-brand-primary; + margin: 0; + letter-spacing: 0.05em; + } + + .company { + font-size: 0.75rem; + color: $color-text-tertiary; + } + } + + .meta { + text-align: right; + font-size: 0.8125rem; + color: $color-text-secondary; + + @media (max-width: 640px) { + text-align: left; + } + + div { + margin-bottom: $admin-space-2xs; + } + + strong { + color: $color-text-primary; + } + } +} + +.preview-customer { + margin-bottom: $admin-space-lg; + + small { + display: block; + font-size: 0.6875rem; + color: $color-text-tertiary; + margin-bottom: $admin-space-xs; + } + + .customer-block { + font-size: 0.875rem; + line-height: 1.6; + + strong { + font-size: 1rem; + } + + .email { + color: $color-brand-primary; + } + } +} + +.preview-table { + width: 100%; + border-collapse: collapse; + margin-bottom: $admin-space-md; + + th, td { + padding: $admin-space-xs $admin-space-sm; + text-align: left; + border-bottom: 1px solid $border-default; + } + + th { + background: $color-gray-50; + font-size: 0.6875rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.03em; + color: $color-text-tertiary; + } + + td { + font-size: 0.8125rem; + } + + td:last-child, + th:last-child { + text-align: right; + } +} + +.preview-totals { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: $admin-space-2xs; + margin-bottom: $admin-space-lg; + + .row { + display: flex; + justify-content: space-between; + gap: $admin-space-lg; + font-size: 0.875rem; + color: $color-text-secondary; + min-width: 200px; + + span:last-child { + font-weight: 500; + } + + &.gross { + font-size: 1.125rem; + font-weight: 700; + color: $color-brand-primary; + padding-top: $admin-space-xs; + margin-top: $admin-space-2xs; + border-top: 2px solid $color-brand-primary; + } + } +} + +.preview-notes { + margin-bottom: $admin-space-lg; + padding: $admin-space-sm; + background: $color-gray-50; + border-radius: $radius-sm; + font-size: 0.8125rem; + + strong { + display: block; + font-size: 0.75rem; + color: $color-text-secondary; + margin-bottom: $admin-space-2xs; + } + + p { + margin: 0; + color: $color-text-secondary; + } +} + +.preview-footer { + display: flex; + justify-content: space-between; + padding-top: $admin-space-md; + border-top: 1px solid $border-default; + font-size: 0.75rem; + color: $color-text-tertiary; + + @media (max-width: 640px) { + flex-direction: column; + gap: $admin-space-2xs; + } +} + +// ============================================ +// EMPTY STATE ICON +// ============================================ +.state-icon { + font-size: 3rem !important; + display: block; + margin-bottom: $admin-space-sm; +} + +// ============================================ +// RESPONSIVE IMPROVEMENTS +// ============================================ +@media (max-width: 900px) { + // Better table row cards on tablet/mobile + .admin-table-row { + border-radius: $radius-sm; + margin: $admin-space-2xs $admin-space-xs; + background: white; + border: 1px solid $glass-border; + + &:hover { + box-shadow: $shadow-sm; + } + } + + .table-row-wrapper { + border-bottom: none; + } +} + +@media (max-width: 640px) { + // Stack modal actions on small screens + .admin-modal-footer { + flex-direction: column; + + .admin-btn { + width: 100%; + justify-content: center; + } + } +} + +// ============================================ +// ANIMATIONS +// ============================================ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} diff --git a/apps/frontend/src/app/components/admin/admin-invoices/admin-invoices.component.ts b/apps/frontend/src/app/components/admin/admin-invoices/admin-invoices.component.ts new file mode 100644 index 0000000..1602043 --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-invoices/admin-invoices.component.ts @@ -0,0 +1,513 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { AdminLayoutComponent } from '../admin-layout/admin-layout.component'; +import { IconComponent } from '../../../shared/icon/icon.component'; +import { ToastService } from '../../../shared/toasts/toast.service'; +import { ConfirmationService } from '../../../shared/confirmation/confirmation.service'; +import { InvoicesApiService, Invoice, InvoiceItem, InvoiceStats } from '../../../api/invoices-api.service'; + +@Component({ + selector: 'app-admin-invoices', + standalone: true, + imports: [CommonModule, FormsModule, AdminLayoutComponent, IconComponent], + templateUrl: './admin-invoices.component.html', + styleUrls: ['./admin-invoices.component.scss'] +}) +export class AdminInvoicesComponent implements OnInit { + invoices: Invoice[] = []; + loading = false; + + // Modal State + showEditor = false; + editingInvoice: Invoice | null = null; + + // Form Model + form: Partial = this.getEmptyInvoice(); + + // PDF Preview + showPreview = false; + + // Sorting + sortField: keyof Invoice = 'createdAt'; + sortDirection: 'asc' | 'desc' = 'desc'; + + // Expanded rows + expandedIds = new Set(); + + // Stats + stats: InvoiceStats = { + total: 0, + draft: 0, + sent: 0, + paid: 0, + overdue: 0, + totalRevenue: 0 + }; + + constructor( + private toasts: ToastService, + private invoicesApi: InvoicesApiService, + private confirmationService: ConfirmationService + ) {} + + ngOnInit(): void { + this.loadInvoices(); + } + + loadInvoices(): void { + this.loading = true; + this.invoicesApi.getAll().subscribe({ + next: (invoices) => { + this.invoices = invoices; + this.sortInvoices(); + this.loading = false; + this.loadStats(); + }, + error: (err) => { + console.error('Error loading invoices', err); + this.toasts.error('Fehler beim Laden der Rechnungen'); + this.loading = false; + } + }); + } + + loadStats(): void { + this.invoicesApi.getStats().subscribe({ + next: (stats) => this.stats = stats, + error: () => { + this.stats = { + total: this.invoices.length, + draft: this.invoices.filter(i => i.status === 'draft').length, + sent: this.invoices.filter(i => i.status === 'sent').length, + paid: this.invoices.filter(i => i.status === 'paid').length, + overdue: this.invoices.filter(i => i.status === 'overdue').length, + totalRevenue: this.invoices + .filter(i => i.status === 'paid') + .reduce((sum, i) => sum + Number(i.totalGross || 0), 0) + }; + } + }); + } + + // ===== SORTING ===== + + sortBy(field: keyof Invoice): void { + if (this.sortField === field) { + this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; + } else { + this.sortField = field; + this.sortDirection = 'desc'; + } + this.sortInvoices(); + } + + sortInvoices(): void { + this.invoices.sort((a, b) => { + let aVal = a[this.sortField]; + let bVal = b[this.sortField]; + + if (aVal == null) aVal = '' as any; + if (bVal == null) bVal = '' as any; + + let comparison = 0; + if (typeof aVal === 'string' && typeof bVal === 'string') { + comparison = aVal.localeCompare(bVal); + } else if (typeof aVal === 'number' && typeof bVal === 'number') { + comparison = aVal - bVal; + } else { + comparison = String(aVal).localeCompare(String(bVal)); + } + + return this.sortDirection === 'asc' ? comparison : -comparison; + }); + } + + getSortIcon(field: keyof Invoice): string { + if (this.sortField !== field) return 'unfold_more'; + return this.sortDirection === 'asc' ? 'expand_less' : 'expand_more'; + } + + // ===== EXPAND/COLLAPSE ===== + + toggleExpand(id: string): void { + if (this.expandedIds.has(id)) { + this.expandedIds.delete(id); + } else { + this.expandedIds.add(id); + } + } + + isExpanded(id: string): boolean { + return this.expandedIds.has(id); + } + + // ===== CRUD ===== + + openCreateModal(): void { + this.editingInvoice = null; + this.form = this.getEmptyInvoice(); + + this.invoicesApi.generateNumber().subscribe({ + next: (num) => this.form.invoiceNumber = num, + error: () => this.form.invoiceNumber = this.generateLocalInvoiceNumber() + }); + + this.showEditor = true; + } + + openEditModal(invoice: Invoice, event: Event): void { + event.stopPropagation(); + this.editingInvoice = invoice; + this.form = JSON.parse(JSON.stringify(invoice)); + this.showEditor = true; + } + + closeEditor(): void { + this.showEditor = false; + this.editingInvoice = null; + } + + saveInvoice(): void { + if (!this.validateForm()) { + this.toasts.error('Bitte fülle alle Pflichtfelder aus'); + return; + } + + if (this.editingInvoice) { + this.invoicesApi.update(this.editingInvoice.id, this.form).subscribe({ + next: () => { + this.toasts.success('Rechnung aktualisiert'); + this.loadInvoices(); + this.closeEditor(); + }, + error: () => this.toasts.error('Fehler beim Speichern') + }); + } else { + this.invoicesApi.create(this.form).subscribe({ + next: () => { + this.toasts.success('Rechnung erstellt'); + this.loadInvoices(); + this.closeEditor(); + }, + error: () => this.toasts.error('Fehler beim Erstellen') + }); + } + } + + async deleteInvoice(invoice: Invoice, event: Event): Promise { + event.stopPropagation(); + + const confirmed = await this.confirmationService.confirm({ + title: 'Rechnung löschen', + message: 'Möchtest du diese Rechnung wirklich löschen?', + type: 'danger', + confirmText: 'Löschen', + cancelText: 'Abbrechen' + }); + + if (!confirmed) return; + + this.invoicesApi.delete(invoice.id).subscribe({ + next: () => { + this.toasts.success('Rechnung gelöscht'); + this.loadInvoices(); + }, + error: () => this.toasts.error('Fehler beim Löschen') + }); + } + + duplicateInvoice(invoice: Invoice, event: Event): void { + event.stopPropagation(); + this.invoicesApi.duplicate(invoice.id).subscribe({ + next: () => { + this.toasts.success('Rechnung dupliziert'); + this.loadInvoices(); + }, + error: () => this.toasts.error('Fehler beim Duplizieren') + }); + } + + // ===== ITEMS ===== + + addItem(): void { + if (!this.form.items) this.form.items = []; + this.form.items.push({ + id: crypto.randomUUID(), + description: '', + quantity: 1, + unit: 'Stk.', + unitPrice: 0 + }); + } + + removeItem(index: number): void { + this.form.items?.splice(index, 1); + } + + // ===== CALCULATIONS ===== + + calculateTotal(items: InvoiceItem[], taxRate: number) { + const net = items.reduce((sum, item) => sum + (item.quantity * item.unitPrice), 0); + const tax = net * (taxRate / 100); + const gross = net + tax; + return { net, tax, gross }; + } + + get formTotals() { + return this.calculateTotal(this.form.items || [], this.form.taxRate || 19); + } + + // ===== PDF EXPORT ===== + + async exportPdf(invoice: Invoice | Partial, event?: Event): Promise { + event?.stopPropagation(); + this.toasts.info('PDF wird erstellt...'); + + const jspdfModule = await import('jspdf'); + const jsPDF = jspdfModule.default; + + const doc = new jsPDF({ + orientation: 'portrait', + unit: 'mm', + format: 'a4' + }); + + const pageWidth = doc.internal.pageSize.getWidth(); + const margin = 20; + let y = 20; + + doc.setFillColor(37, 99, 235); + doc.rect(0, 0, pageWidth, 45, 'F'); + + doc.setTextColor(255, 255, 255); + doc.setFontSize(24); + doc.setFont('helvetica', 'bold'); + doc.text('RECHNUNG', margin, 28); + + doc.setFontSize(11); + doc.setFont('helvetica', 'normal'); + doc.text(`Nr. ${invoice.invoiceNumber}`, pageWidth - margin, 20, { align: 'right' }); + doc.text(`Datum: ${this.formatDate(invoice.date || '')}`, pageWidth - margin, 27, { align: 'right' }); + doc.text(`Fällig: ${this.formatDate(invoice.dueDate || '')}`, pageWidth - margin, 34, { align: 'right' }); + + y = 60; + doc.setTextColor(30, 41, 59); + + doc.setFontSize(9); + doc.setTextColor(100, 116, 139); + doc.text('Leonards & Brandenburger IT · Musterstraße 1 · 12345 Musterstadt', margin, y); + + y += 12; + + doc.setFontSize(11); + doc.setTextColor(30, 41, 59); + doc.setFont('helvetica', 'bold'); + doc.text(invoice.customerName || '', margin, y); + doc.setFont('helvetica', 'normal'); + y += 6; + doc.text(invoice.customerAddress || '', margin, y); + y += 5; + doc.text(`${invoice.customerZip || ''} ${invoice.customerCity || ''}`, margin, y); + if (invoice.customerEmail) { + y += 5; + doc.setTextColor(100, 116, 139); + doc.text(invoice.customerEmail, margin, y); + } + + y += 20; + + doc.setFillColor(248, 250, 252); + doc.rect(margin, y - 5, pageWidth - 2 * margin, 10, 'F'); + + doc.setFontSize(9); + doc.setFont('helvetica', 'bold'); + doc.setTextColor(71, 85, 105); + doc.text('Beschreibung', margin + 3, y + 1); + doc.text('Menge', margin + 95, y + 1); + doc.text('Einheit', margin + 115, y + 1); + doc.text('Preis', margin + 135, y + 1); + doc.text('Gesamt', pageWidth - margin - 3, y + 1, { align: 'right' }); + + y += 10; + doc.setFont('helvetica', 'normal'); + doc.setTextColor(30, 41, 59); + + for (const item of (invoice.items || [])) { + doc.setFontSize(10); + doc.text(item.description || '-', margin + 3, y + 1); + doc.text(item.quantity.toString(), margin + 95, y + 1); + doc.text(item.unit, margin + 115, y + 1); + doc.text(this.formatCurrency(item.unitPrice), margin + 135, y + 1); + doc.text(this.formatCurrency(item.quantity * item.unitPrice), pageWidth - margin - 3, y + 1, { align: 'right' }); + + doc.setDrawColor(226, 232, 240); + doc.line(margin, y + 4, pageWidth - margin, y + 4); + + y += 10; + } + + y += 10; + + const totals = this.calculateTotal(invoice.items || [], invoice.taxRate || 19); + const sumX = pageWidth - margin - 60; + + doc.setFontSize(10); + doc.text('Netto:', sumX, y); + doc.text(this.formatCurrency(totals.net), pageWidth - margin, y, { align: 'right' }); + + y += 7; + doc.text(`MwSt. (${invoice.taxRate || 19}%):`, sumX, y); + doc.text(this.formatCurrency(totals.tax), pageWidth - margin, y, { align: 'right' }); + + y += 3; + doc.setDrawColor(37, 99, 235); + doc.setLineWidth(0.5); + doc.line(sumX, y, pageWidth - margin, y); + + y += 8; + doc.setFontSize(12); + doc.setFont('helvetica', 'bold'); + doc.text('Gesamt:', sumX, y); + doc.setTextColor(37, 99, 235); + doc.text(this.formatCurrency(totals.gross), pageWidth - margin, y, { align: 'right' }); + + if (invoice.notes) { + y += 20; + doc.setFontSize(9); + doc.setFont('helvetica', 'normal'); + doc.setTextColor(100, 116, 139); + doc.text('Hinweise:', margin, y); + y += 5; + doc.setTextColor(71, 85, 105); + const splitNotes = doc.splitTextToSize(invoice.notes, pageWidth - 2 * margin); + doc.text(splitNotes, margin, y); + } + + const footerY = doc.internal.pageSize.getHeight() - 20; + doc.setFontSize(8); + doc.setTextColor(148, 163, 184); + doc.text('Leonards & Brandenburger IT', margin, footerY); + doc.text('IBAN: DE12 3456 7890 1234 5678 90 · BIC: DEUTDEDB', pageWidth / 2, footerY, { align: 'center' }); + doc.text(`Seite 1 von 1`, pageWidth - margin, footerY, { align: 'right' }); + + doc.save(`Rechnung_${invoice.invoiceNumber}.pdf`); + this.toasts.success('PDF wurde heruntergeladen'); + } + + // ===== PREVIEW ===== + + openPreview(invoice: Invoice, event: Event): void { + event.stopPropagation(); + this.form = JSON.parse(JSON.stringify(invoice)); + this.showPreview = true; + } + + closePreview(): void { + this.showPreview = false; + } + + // ===== STATUS ===== + + setStatus(invoice: Invoice, status: Invoice['status'], event: Event): void { + event.stopPropagation(); + this.invoicesApi.updateStatus(invoice.id, status).subscribe({ + next: () => { + this.toasts.success(`Status auf "${this.getStatusLabel(status)}" geändert`); + this.loadInvoices(); + }, + error: () => this.toasts.error('Fehler beim Ändern des Status') + }); + } + + getStatusLabel(status: string): string { + const labels: Record = { + draft: 'Entwurf', + sent: 'Gesendet', + paid: 'Bezahlt', + overdue: 'Überfällig' + }; + return labels[status] || status; + } + + getStatusIcon(status: string): string { + const icons: Record = { + draft: 'edit_note', + sent: 'send', + paid: 'check_circle', + overdue: 'warning' + }; + return icons[status] || 'help'; + } + + // ===== HELPERS ===== + + private getEmptyInvoice(): Partial { + return { + invoiceNumber: '', + date: new Date().toISOString().split('T')[0], + dueDate: this.getDefaultDueDate(), + status: 'draft', + customerName: '', + customerEmail: '', + customerAddress: '', + customerCity: '', + customerZip: '', + items: [ + { + id: crypto.randomUUID(), + description: '', + quantity: 1, + unit: 'Stk.', + unitPrice: 0 + } + ], + taxRate: 19, + notes: 'Zahlbar innerhalb von 14 Tagen ohne Abzug.' + }; + } + + private generateLocalInvoiceNumber(): string { + const year = new Date().getFullYear(); + const count = this.invoices.filter(i => + i.invoiceNumber.startsWith(`RE-${year}`) + ).length + 1; + return `RE-${year}-${count.toString().padStart(4, '0')}`; + } + + private getDefaultDueDate(): string { + const date = new Date(); + date.setDate(date.getDate() + 14); + return date.toISOString().split('T')[0]; + } + + private validateForm(): boolean { + return !!( + this.form.customerName && + this.form.invoiceNumber && + this.form.items && + this.form.items.length > 0 && + this.form.items.every(i => i.description && i.quantity > 0) + ); + } + + formatDate(dateStr: string): string { + if (!dateStr) return ''; + return new Date(dateStr).toLocaleDateString('de-DE'); + } + + formatCurrency(amount: number): string { + return new Intl.NumberFormat('de-DE', { + style: 'currency', + currency: 'EUR' + }).format(amount); + } + + trackByItemId(_: number, item: InvoiceItem): string { + return item.id; + } + + trackByInvoiceId(_: number, invoice: Invoice): string { + return invoice.id; + } +} diff --git a/apps/frontend/src/app/components/admin/admin-layout/admin-layout.component.html b/apps/frontend/src/app/components/admin/admin-layout/admin-layout.component.html new file mode 100644 index 0000000..2d23f40 --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-layout/admin-layout.component.html @@ -0,0 +1,61 @@ +
+ + + + + + + +
+
+ +
+
+ + + +
diff --git a/apps/frontend/src/app/components/admin/admin-layout/admin-layout.component.scss b/apps/frontend/src/app/components/admin/admin-layout/admin-layout.component.scss new file mode 100644 index 0000000..3e8bab5 --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-layout/admin-layout.component.scss @@ -0,0 +1,344 @@ +@import '../../../utils/shared-styles.scss'; + +// ===== LAYOUT VARIABLES ===== +$sidebar-width: 220px; +$mobile-nav-height: 64px; +$transition-sidebar: 0.3s cubic-bezier(0.4, 0, 0.2, 1); + +// ===== BASE LAYOUT ===== +.admin-layout { + display: flex; + min-height: 100vh; + background: $gradient-bg; +} + +// ===== SIDEBAR ===== +.sidebar { + position: fixed; + top: 72px; + left: 0; + bottom: 0; + width: $sidebar-width; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(20px); + border-right: 1px solid $glass-border; + display: flex; + flex-direction: column; + z-index: 1001; + transition: transform $transition-sidebar, opacity $transition-sidebar; + box-shadow: $shadow-glass; + + @media (max-width: 1024px) { + width: 280px; + transform: translateX(-100%); + opacity: 0; + top: 72px; + bottom: $mobile-nav-height; + + &.open { + transform: translateX(0); + opacity: 1; + } + } +} + +.sidebar-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem; + border-bottom: 1px solid rgba($color-gray-200, 0.5); + + .brand { + display: flex; + align-items: center; + gap: 0.5rem; + } + + .brand-icon { + font-size: 24px; + color: #C2410C; + } + + .brand-title { + font-size: 1rem; + font-weight: 800; + color: #C2410C; + } +} + +.sidebar-close { + display: none; + width: 32px; + height: 32px; + align-items: center; + justify-content: center; + background: transparent; + border: 1px solid $glass-border; + border-radius: $radius-md; + cursor: pointer; + color: $color-text-secondary; + transition: all 0.2s; + + .material-symbols-outlined { + font-size: 18px; + } + + &:hover { + background: rgba($color-brand-primary, 0.08); + color: $color-brand-primary; + border-color: $color-brand-primary; + } + + @media (max-width: 1024px) { + display: flex; + } +} + +.sidebar-nav { + flex: 1; + padding: 0.75rem 0.5rem; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 2px; +} + +.nav-item { + display: flex; + align-items: center; + gap: 0.625rem; + padding: 0.625rem 0.75rem; + border-radius: $radius-md; + text-decoration: none; + color: $color-text-secondary; + font-weight: 600; + font-size: 0.8125rem; + transition: all 0.2s ease; + border: 1px solid transparent; + position: relative; + overflow: hidden; + animation: slideInNav 0.3s ease-out both; + + @keyframes slideInNav { + from { + opacity: 0; + transform: translateX(-10px); + } + to { + opacity: 1; + transform: translateX(0); + } + } + + .nav-item-indicator { + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 3px; + height: 0; + background: $gradient-primary; + border-radius: 0 2px 2px 0; + transition: height 0.2s ease; + } + + .nav-icon { + font-size: 20px; + transition: transform 0.2s, color 0.2s; + } + + &:hover { + background: rgba($color-brand-primary, 0.06); + color: $color-brand-primary; + + .nav-icon { + transform: scale(1.1); + } + + .nav-item-indicator { + height: 20px; + } + } + + &--active { + background: linear-gradient(135deg, rgba($color-brand-primary, 0.1), rgba($color-brand-primary, 0.05)); + color: $color-brand-primary; + font-weight: 700; + + .nav-item-indicator { + height: 24px; + } + + .nav-icon { + color: $color-brand-primary; + } + + &:hover { + background: linear-gradient(135deg, rgba($color-brand-primary, 0.12), rgba($color-brand-primary, 0.08)); + } + } +} + +.sidebar-footer { + padding: 0.5rem; + border-top: 1px solid rgba($color-gray-200, 0.5); + + .nav-item--home { + color: $color-text-tertiary; + + &:hover { + background: rgba($color-gray-500, 0.08); + color: $color-text-primary; + } + } +} + +// ===== BACKDROP ===== +.backdrop { + position: fixed; + inset: 0; + top: 72px; + bottom: $mobile-nav-height; + background: rgba(15, 23, 42, 0.5); + backdrop-filter: blur(4px); + z-index: 1000; + animation: fadeIn 0.25s ease; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +// ===== MAIN CONTENT ===== +.admin-main { + flex: 1; + margin-left: $sidebar-width; + min-height: calc(100vh - 72px); + transition: margin-left $transition-sidebar; + display: flex; + flex-direction: column; + + @media (max-width: 1024px) { + margin-left: 0; + padding-bottom: $mobile-nav-height; + } +} + +.admin-content { + flex: 1; + position: relative; +} + +// ===== MOBILE BOTTOM NAVIGATION ===== +// Positioned outside admin-main to prevent route animation interference +.mobile-nav { + display: none; + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: $mobile-nav-height; + background: rgba(255, 255, 255, 0.98); + backdrop-filter: blur(20px); + border-top: 1px solid $glass-border; + z-index: 1002; + padding: 0.5rem 0.25rem; + padding-bottom: calc(0.5rem + env(safe-area-inset-bottom)); + // Prevent any inherited animations + transform: translateZ(0); + will-change: auto; + + @media (max-width: 1024px) { + display: flex; + justify-content: space-around; + align-items: center; + } +} + +.mobile-nav-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 2px; + padding: 0.375rem 0.75rem; + border-radius: $radius-md; + text-decoration: none; + color: $color-text-tertiary; + background: transparent; + border: none; + cursor: pointer; + transition: all 0.2s ease; + min-width: 56px; + + .material-symbols-outlined { + font-size: 22px; + transition: transform 0.2s; + } + + .mobile-nav-label { + font-size: 0.625rem; + font-weight: 600; + letter-spacing: 0.2px; + } + + &:hover, &:active { + color: $color-brand-primary; + + .material-symbols-outlined { + transform: scale(1.1); + } + } + + &.active { + color: $color-brand-primary; + background: rgba($color-brand-primary, 0.1); + + .material-symbols-outlined { + transform: scale(1.05); + } + } + + &.mobile-nav-more { + color: $color-text-secondary; + + &:hover { + color: $color-brand-primary; + } + } +} + +// ===== FAB MENU (Fallback) ===== +.fab-menu { + display: none; + position: fixed; + bottom: calc($mobile-nav-height + 1rem); + right: 1rem; + width: 48px; + height: 48px; + border-radius: 50%; + background: $gradient-primary; + color: white; + border: none; + box-shadow: $shadow-brand, 0 4px 16px rgba($color-brand-primary, 0.3); + cursor: pointer; + z-index: 999; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + + .material-symbols-outlined { + font-size: 24px; + } + + &:hover { + transform: scale(1.05); + box-shadow: $shadow-brand, 0 6px 20px rgba($color-brand-primary, 0.4); + } + + &:active { + transform: scale(0.95); + } +} diff --git a/apps/frontend/src/app/components/admin/admin-layout/admin-layout.component.ts b/apps/frontend/src/app/components/admin/admin-layout/admin-layout.component.ts new file mode 100644 index 0000000..495033a --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-layout/admin-layout.component.ts @@ -0,0 +1,140 @@ +import { CommonModule } from '@angular/common'; +import { Component, HostListener, OnDestroy, OnInit } from '@angular/core'; +import { Router, NavigationEnd, RouterLink, RouterLinkActive, RouterOutlet, ChildrenOutletContexts } from '@angular/router'; +import { filter, Subscription } from 'rxjs'; +import { trigger, transition, style, animate, query, group } from '@angular/animations'; + +interface NavRoute { + path: string; + label: string; + icon: string; +} + +// Page transition animations +const routeAnimations = trigger('routeAnimations', [ + transition('* <=> *', [ + style({ position: 'relative' }), + query(':enter, :leave', [ + style({ + position: 'absolute', + top: 0, + left: 0, + width: '100%' + }) + ], { optional: true }), + query(':enter', [ + style({ opacity: 0, transform: 'translateY(15px)' }) + ], { optional: true }), + group([ + query(':leave', [ + animate('200ms ease-out', style({ opacity: 0, transform: 'translateY(-10px)' })) + ], { optional: true }), + query(':enter', [ + animate('300ms 100ms ease-out', style({ opacity: 1, transform: 'translateY(0)' })) + ], { optional: true }) + ]) + ]) +]); + +@Component({ + selector: 'app-admin-layout', + standalone: true, + imports: [CommonModule, RouterLink, RouterLinkActive], + templateUrl: './admin-layout.component.html', + styleUrls: ['./admin-layout.component.scss'], + animations: [routeAnimations] +}) +export class AdminLayoutComponent implements OnInit, OnDestroy { + sidebarOpen = false; + isMobile = false; + private sub?: Subscription; + private mq?: MediaQueryList; + private mqListener = (e: MediaQueryListEvent) => { + this.isMobile = e.matches; + if (!this.isMobile) { + this.sidebarOpen = false; + this.unlockScroll(); + } + }; + + routes: NavRoute[] = [ + { path: 'admin', label: 'Dashboard', icon: 'dashboard' }, + { path: 'admin/analytics', label: 'Analytics', icon: 'bar_chart' }, + { path: 'admin/requests', label: 'Anfragen', icon: 'mail' }, + { path: 'admin/booking', label: 'Buchungen', icon: 'calendar_today' }, + { path: 'admin/newsletter', label: 'Newsletter', icon: 'newspaper' }, + { path: 'admin/faq', label: 'FAQ', icon: 'quiz' }, + { path: 'admin/services', label: 'Services', icon: 'build' }, + { path: 'admin/users', label: 'User', icon: 'group' }, + { path: 'admin/invoices', label: 'Rechnungen', icon: 'receipt_long' }, + { path: 'admin/settings', label: 'Settings', icon: 'settings' }, + ]; + + // Mobile bottom nav - show only 4 most important + more button + mobileNavRoutes: NavRoute[] = [ + { path: 'admin', label: 'Home', icon: 'dashboard' }, + { path: 'admin/requests', label: 'Anfragen', icon: 'mail' }, + { path: 'admin/booking', label: 'Termine', icon: 'calendar_today' }, + { path: 'admin/analytics', label: 'Stats', icon: 'bar_chart' }, + ]; + + constructor(private router: Router, private contexts: ChildrenOutletContexts) {} + + ngOnInit() { + this.mq = window.matchMedia('(max-width: 1024px)'); + this.isMobile = this.mq.matches; + this.mq.addEventListener?.('change', this.mqListener); + + // Close sidebar on route change (mobile) + this.sub = this.router.events.pipe(filter(e => e instanceof NavigationEnd)).subscribe(() => { + if (this.isMobile) { + this.closeSidebar(); + } + }); + } + + ngOnDestroy() { + this.sub?.unsubscribe(); + this.mq?.removeEventListener?.('change', this.mqListener); + this.unlockScroll(); + } + + prepareRoute() { + return this.router.url; + } + + toggleSidebar() { + this.sidebarOpen = !this.sidebarOpen; + this.sidebarOpen ? this.lockScroll() : this.unlockScroll(); + } + + closeSidebar() { + this.sidebarOpen = false; + this.unlockScroll(); + } + + private lockScroll() { + document.body.style.overflow = 'hidden'; + } + + private unlockScroll() { + document.body.style.overflow = ''; + } + + normalize(path: string): string { + return '/' + path.replace(/^\//, ''); + } + + getCurrentRouteLabel(): string { + const currentPath = this.router.url.split('?')[0]; + const route = this.routes.find(r => this.normalize(r.path) === currentPath); + return route ? route.label : 'Admin'; + } + + @HostListener('document:keydown.escape') + onEscape() { + if (this.sidebarOpen) { + this.closeSidebar(); + } + } +} diff --git a/apps/frontend/src/app/components/admin/admin-newsletter/admin-newsletter.component.html b/apps/frontend/src/app/components/admin/admin-newsletter/admin-newsletter.component.html new file mode 100644 index 0000000..6e1a390 --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-newsletter/admin-newsletter.component.html @@ -0,0 +1,316 @@ + +
+
+ + +
+
+

Newsletter-Abonnenten

+

Verwalte E-Mail-Abonnenten und registrierte User

+
+
+ + +
+
+ + +
+ + +
+ + +
+ {{ toast.type === 'success' ? 'check_circle' : toast.type === 'error' ? 'error' : 'info' }} + {{ toast.message }} +
+ + +
+
+

Lade Abonnenten…

+
+ + +
+ + +
+
+ people +
+ {{ stats.total }} + Gesamt +
+
+
+ check_circle +
+ {{ stats.active }} + Aktiv +
+
+
+ cancel +
+ {{ stats.inactive }} + Inaktiv +
+
+
+ + +
+
+ +
+ search + + +
+ + +
+ + + +
+
+ +
+ + +
+
+ + +
+ error +

Fehler beim Laden

+

{{ error }}

+ +
+ + +
+ mail +

Noch keine E-Mail Abonnenten

+

Öffentliche Newsletter-Anmeldungen erscheinen hier.

+
+ + +
+ search_off +

Keine Treffer

+

Keine Abonnenten für "{{ searchTerm }}" gefunden.

+ +
+ + +
+
+ +
+ +
+ + schedule + {{ formatDate(s.subscribedAt) }} + +
+
+ +
+ + + mail + + + +
+ +
+
+ + +
+ {{ filteredSubscribers.length }} von {{ subscribers.length }} Abonnenten +
+
+ + +
+ + +
+
+ person +
+ {{ userSubscribers.length }} + Registrierte User +
+
+
+ verified +
+ 100% + Newsletter aktiv +
+
+
+ + +
+
+
+ search + + +
+
+ +
+ + +
+
+ + +
+ person_off +

Keine User mit Newsletter

+

Registrierte User mit aktivem Newsletter erscheinen hier.

+
+ + +
+ search_off +

Keine Treffer

+

Keine User für "{{ userSearchTerm }}" gefunden.

+ +
+ + +
+
+ +
+ +
+ + person + {{ u.name }} + + + schedule + {{ formatDate(u.createdAt) }} + +
+
+ +
+ + + mail + + + open_in_new + +
+ +
+
+ + +
+ {{ filteredUserSubscribers.length }} von {{ userSubscribers.length }} User +
+ + +
+ info +

Diese Liste zeigt alle registrierten User, die Newsletter in ihrem Profil aktiviert haben. + Die Einstellung kann nur vom User selbst geändert werden.

+
+
+ +
+
+ + + + +
\ No newline at end of file diff --git a/apps/frontend/src/app/components/admin/admin-newsletter/admin-newsletter.component.scss b/apps/frontend/src/app/components/admin/admin-newsletter/admin-newsletter.component.scss new file mode 100644 index 0000000..77a21f7 --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-newsletter/admin-newsletter.component.scss @@ -0,0 +1,630 @@ +// ============================================ +// ADMIN NEWSLETTER - Component-Specific Styles +// ============================================ +@import '../admin-shared.scss'; + +// ============================================ +// TAB NAVIGATION +// ============================================ +.tab-navigation { + display: flex; + gap: $admin-space-2xs; + margin-bottom: $admin-space-md; + padding: $admin-space-2xs; + background: $glass-bg; + backdrop-filter: blur(20px); + border: 1px solid $glass-border; + border-radius: $radius-md; + animation: fadeIn 0.3s ease-out; + + @media (max-width: 480px) { + flex-direction: column; + } +} + +.tab-btn { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: $admin-space-xs; + padding: $admin-space-xs $admin-space-sm; + background: transparent; + border: none; + border-radius: $radius-sm; + font-size: 0.8125rem; + font-weight: 600; + color: $color-text-secondary; + cursor: pointer; + transition: all $transition-fast; + + .material-symbols-outlined { + font-size: 1.125rem; + opacity: 0.7; + } + + .tab-label { + @media (max-width: 360px) { + display: none; + } + } + + .tab-count { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 1.25rem; + height: 1.125rem; + padding: 0 $admin-space-2xs; + background: $color-gray-100; + border-radius: $radius-full; + font-size: 0.625rem; + font-weight: 700; + color: $color-text-tertiary; + transition: all $transition-fast; + } + + &:hover:not(.active) { + background: $color-gray-50; + color: $color-text-primary; + + .material-symbols-outlined { + opacity: 1; + } + } + + &.active { + background: $gradient-primary; + color: white; + box-shadow: 0 2px 8px rgba($color-brand-primary, 0.25); + + .material-symbols-outlined { + opacity: 1; + } + + .tab-count { + background: rgba(white, 0.25); + color: white; + } + } +} + +// ============================================ +// TAB CONTENT +// ============================================ +.tab-content { + animation: fadeIn 0.25s ease-out; +} + +// ============================================ +// FILTER BAR +// ============================================ +.filter-bar { + display: flex; + flex-wrap: wrap; + gap: $admin-space-sm; + justify-content: space-between; + align-items: center; + margin-bottom: $admin-space-md; + padding: $admin-space-sm; + background: $glass-bg; + border: 1px solid $glass-border; + border-radius: $radius-md; + animation: fadeIn 0.3s ease-out 0.1s backwards; + + @media (max-width: 640px) { + flex-direction: column; + align-items: stretch; + } +} + +.filter-bar__left { + display: flex; + flex-wrap: wrap; + gap: $admin-space-sm; + align-items: center; + flex: 1; + + @media (max-width: 480px) { + flex-direction: column; + align-items: stretch; + } +} + +.filter-bar__right { + display: flex; + gap: $admin-space-xs; + + @media (max-width: 640px) { + justify-content: flex-end; + } + + @media (max-width: 480px) { + flex-wrap: wrap; + } +} + +// ============================================ +// SEARCH INPUT +// ============================================ +.search-input { + display: flex; + align-items: center; + gap: $admin-space-xs; + padding: $admin-space-xs $admin-space-sm; + background: white; + border: 1px solid $border-default; + border-radius: $radius-sm; + min-width: 200px; + transition: all $transition-fast; + + &:focus-within { + border-color: $color-brand-primary; + box-shadow: 0 0 0 3px rgba($color-brand-primary, 0.1); + } + + .material-symbols-outlined { + font-size: 1rem; + color: $color-text-tertiary; + } + + input { + flex: 1; + border: none; + background: transparent; + font-size: 0.8125rem; + color: $color-text-primary; + outline: none; + min-width: 0; + + &::placeholder { + color: $color-text-tertiary; + } + } + + .clear-btn { + display: flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + background: $color-gray-100; + border: none; + border-radius: $radius-sm; + color: $color-text-tertiary; + cursor: pointer; + transition: all $transition-fast; + + .material-symbols-outlined { + font-size: 0.875rem; + } + + &:hover { + background: $color-gray-200; + color: $color-text-primary; + } + } + + @media (max-width: 480px) { + min-width: 100%; + } +} + +// ============================================ +// FILTER CHIPS +// ============================================ +.filter-chips { + display: flex; + gap: $admin-space-2xs; + flex-wrap: wrap; +} + +.filter-chip { + display: inline-flex; + align-items: center; + gap: 4px; + padding: $admin-space-2xs $admin-space-xs; + background: white; + border: 1px solid $border-default; + border-radius: $radius-full; + font-size: 0.6875rem; + font-weight: 600; + color: $color-text-secondary; + cursor: pointer; + transition: all $transition-fast; + + &:hover { + border-color: $color-brand-primary; + color: $color-brand-primary; + } + + &.active { + background: $color-brand-primary; + border-color: $color-brand-primary; + color: white; + + .dot { + border-color: white; + } + } +} + +.dot { + width: 6px; + height: 6px; + border-radius: 50%; + border: 1.5px solid currentColor; + + &--success { + background: $accent-green; + border-color: $accent-green; + } + + &--muted { + background: $color-gray-400; + border-color: $color-gray-400; + } +} + +// ============================================ +// SUBSCRIBERS GRID +// ============================================ +.subscribers-grid { + display: grid; + gap: $admin-space-xs; + animation: fadeIn 0.3s ease-out 0.15s backwards; +} + +.subscriber-card { + display: flex; + align-items: center; + justify-content: space-between; + gap: $admin-space-sm; + padding: $admin-space-sm; + background: $glass-bg; + border: 1px solid $glass-border; + border-radius: $radius-md; + transition: all $transition-fast; + animation: staggerFade 0.3s ease-out backwards; + + @for $i from 1 through 30 { + &:nth-child(#{$i}) { + animation-delay: #{$i * 25}ms; + } + } + + &:hover { + border-color: rgba($color-brand-primary, 0.3); + box-shadow: $shadow-sm; + } + + &.inactive { + opacity: 0.6; + + .email { + color: $color-text-tertiary; + } + } + + &.user-card { + border-left: 3px solid $accent-blue; + } + + @media (max-width: 640px) { + flex-direction: column; + align-items: stretch; + } +} + +.card__main { + flex: 1; + min-width: 0; +} + +.card__email-row { + display: flex; + align-items: center; + gap: $admin-space-sm; + margin-bottom: $admin-space-2xs; + + @media (max-width: 480px) { + flex-direction: column; + align-items: flex-start; + gap: $admin-space-2xs; + } +} + +.email { + font-size: 0.875rem; + font-weight: 600; + color: $color-text-primary; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.status-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: $radius-full; + font-size: 0.625rem; + font-weight: 700; + text-transform: uppercase; + flex-shrink: 0; + + &.status--active { + background: rgba($accent-green, 0.12); + color: $accent-green; + } + + &.status--inactive { + background: rgba($color-gray-400, 0.12); + color: $color-gray-500; + } + + &.status--user { + background: rgba($accent-blue, 0.12); + color: $accent-blue; + + .material-symbols-outlined { + font-size: 0.75rem; + } + } +} + +.card__meta { + display: flex; + flex-wrap: wrap; + gap: $admin-space-sm; +} + +.meta-item { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 0.6875rem; + color: $color-text-tertiary; + + .material-symbols-outlined { + font-size: 0.875rem; + } +} + +.card__actions { + display: flex; + gap: $admin-space-2xs; + flex-shrink: 0; + + @media (max-width: 640px) { + padding-top: $admin-space-xs; + border-top: 1px solid $border-default; + justify-content: flex-end; + } +} + +.action-btn { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + background: transparent; + border: 1px solid $border-default; + border-radius: $radius-sm; + color: $color-text-tertiary; + cursor: pointer; + transition: all $transition-fast; + text-decoration: none; + + .material-symbols-outlined { + font-size: 1rem; + } + + &:hover { + background: rgba($color-brand-primary, 0.08); + border-color: $color-brand-primary; + color: $color-brand-primary; + } + + &.active { + color: $accent-green; + border-color: $accent-green; + + &:hover { + background: rgba($accent-green, 0.08); + } + } + + &--danger:hover { + background: rgba($accent-red, 0.08); + border-color: $accent-red; + color: $accent-red; + } +} + +// ============================================ +// RESULT COUNT +// ============================================ +.result-count { + margin-top: $admin-space-sm; + padding: $admin-space-xs; + text-align: center; + font-size: 0.75rem; + color: $color-text-tertiary; +} + +// ============================================ +// INFO BOX +// ============================================ +.info-box { + display: flex; + align-items: flex-start; + gap: $admin-space-sm; + margin-top: $admin-space-md; + padding: $admin-space-sm $admin-space-md; + background: rgba($accent-blue, 0.08); + border: 1px solid rgba($accent-blue, 0.2); + border-radius: $radius-md; + animation: fadeIn 0.3s ease-out 0.2s backwards; + + .material-symbols-outlined { + font-size: 1.25rem; + color: $accent-blue; + flex-shrink: 0; + } + + p { + margin: 0; + font-size: 0.75rem; + color: $color-text-secondary; + line-height: 1.5; + + strong { + color: $color-text-primary; + } + } +} + +// ============================================ +// LOADING STATE +// ============================================ +.loading-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: $admin-space-sm; + padding: $admin-space-2xl; + color: $color-text-secondary; + + .spinner { + width: 40px; + height: 40px; + border: 3px solid rgba($color-brand-primary, 0.2); + border-top-color: $color-brand-primary; + border-radius: 50%; + animation: spin 1s linear infinite; + } + + p { + font-size: 0.875rem; + margin: 0; + } +} + +// ============================================ +// EMPTY & ERROR STATES +// ============================================ +.empty-state, +.error-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: $admin-space-2xl; + text-align: center; + background: $glass-bg; + border: 1px solid $glass-border; + border-radius: $radius-md; + animation: fadeIn 0.3s ease-out; + + .material-symbols-outlined { + font-size: 2.5rem; + color: $color-gray-300; + margin-bottom: $admin-space-sm; + } + + h3 { + font-size: 1rem; + font-weight: 700; + color: $color-text-primary; + margin: 0 0 $admin-space-2xs; + } + + p { + font-size: 0.8125rem; + color: $color-text-tertiary; + margin: 0 0 $admin-space-md; + } +} + +.error-state { + .material-symbols-outlined { + color: $accent-red; + } +} + +// ============================================ +// TOAST +// ============================================ +.toast { + position: fixed; + bottom: 5rem; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + gap: $admin-space-xs; + padding: $admin-space-sm $admin-space-md; + background: $color-gray-900; + color: white; + border-radius: $radius-md; + font-size: 0.8125rem; + font-weight: 500; + box-shadow: $shadow-lg; + z-index: 1000; + animation: popIn 0.3s ease-out; + + .material-symbols-outlined { + font-size: 1.125rem; + } + + &--success { + background: $accent-green; + } + + &--error { + background: $accent-red; + } + + &--info { + background: $accent-blue; + } + + @media (min-width: 768px) { + bottom: $admin-space-lg; + } +} + +// ============================================ +// BUTTON LABEL (responsive) +// ============================================ +.btn-label { + @media (max-width: 480px) { + display: none; + } +} + +// ============================================ +// UTILITIES +// ============================================ +.spinning { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +// ============================================ +// RESPONSIVE +// ============================================ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} diff --git a/apps/frontend/src/app/components/admin/admin-newsletter/admin-newsletter.component.spec.ts b/apps/frontend/src/app/components/admin/admin-newsletter/admin-newsletter.component.spec.ts new file mode 100644 index 0000000..140ea7b --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-newsletter/admin-newsletter.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AdminNewsletterComponent } from './admin-newsletter.component'; + +describe('AdminNewsletterComponent', () => { + let component: AdminNewsletterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AdminNewsletterComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AdminNewsletterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/frontend/src/app/components/admin/admin-newsletter/admin-newsletter.component.ts b/apps/frontend/src/app/components/admin/admin-newsletter/admin-newsletter.component.ts new file mode 100644 index 0000000..d591371 --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-newsletter/admin-newsletter.component.ts @@ -0,0 +1,375 @@ +import { CommonModule } from '@angular/common'; +import { Component, OnInit } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; +import { trigger, transition, style, animate } from '@angular/animations'; +import { forkJoin } from 'rxjs'; +import { ApiService, NewsletterSubscriber } from '../../../api/api.service'; +import { User } from '../../../services/auth.service'; +import { AdminLayoutComponent } from '../admin-layout/admin-layout.component'; +import { ConfirmationComponent, ConfirmationConfig } from '../../../shared/confirmation/confirmation.component'; + +@Component({ + selector: 'app-admin-newsletter', + standalone: true, + imports: [CommonModule, FormsModule, RouterModule, AdminLayoutComponent, ConfirmationComponent], + templateUrl: './admin-newsletter.component.html', + styleUrl: './admin-newsletter.component.scss', + animations: [ + trigger('fadeSlide', [ + transition(':enter', [ + style({ opacity: 0, transform: 'translateY(-10px)' }), + animate('200ms ease-out', style({ opacity: 1, transform: 'translateY(0)' })) + ]), + transition(':leave', [ + animate('200ms ease-in', style({ opacity: 0, transform: 'translateY(-10px)' })) + ]) + ]) + ] +}) +export class AdminNewsletterComponent implements OnInit { + // Tab management + activeTab: 'emails' | 'users' = 'emails'; + + // Email Subscribers (öffentliche Anmeldungen) + subscribers: NewsletterSubscriber[] = []; + filteredSubscribers: NewsletterSubscriber[] = []; + + // User Subscribers (registrierte User mit Newsletter) + userSubscribers: User[] = []; + filteredUserSubscribers: User[] = []; + + loading = true; + error = ''; + copiedEmail = ''; + searchTerm = ''; + userSearchTerm = ''; + filterStatus: 'all' | 'active' | 'inactive' = 'all'; + + stats = { total: 0, active: 0, inactive: 0 }; + userStats = { total: 0 }; + + toast = { show: false, message: '', type: 'success' as 'success' | 'error' | 'info' }; + private toastTimeout: any; + + deleteModal = { + isOpen: false, + loading: false, + subscriber: null as NewsletterSubscriber | null, + config: { + title: 'Abonnent löschen', + message: 'Möchtest du diesen Abonnenten wirklich endgültig löschen?', + confirmText: 'Löschen', + cancelText: 'Abbrechen', + type: 'danger' as const, + icon: 'delete' + } as ConfirmationConfig + }; + + constructor(private apiService: ApiService) {} + + ngOnInit(): void { + this.loadAll(); + } + + trackById = (_: number, s: NewsletterSubscriber) => s.id; + trackByUserId = (_: number, u: User) => u.id; + + loadAll(): void { + this.loading = true; + this.error = ''; + + forkJoin({ + newsletter: this.apiService.getNewsletterSubscribers(), + users: this.apiService.getUserNewsletterSubscribers() + }).subscribe({ + next: (res) => { + // Email Subscribers + this.subscribers = res.newsletter.subscribers || []; + this.stats = { + total: res.newsletter.total, + active: res.newsletter.active, + inactive: res.newsletter.inactive + }; + this.filterSubscribers(); + + // User Subscribers + this.userSubscribers = res.users || []; + this.userStats.total = this.userSubscribers.length; + this.filterUserSubscribers(); + }, + error: (err) => { + console.error('Error loading newsletter data:', err); + this.error = 'Fehler beim Laden der Abonnenten'; + }, + complete: () => { + this.loading = false; + } + }); + } + + loadSubscribers(): void { + this.loadAll(); + } + + setTab(tab: 'emails' | 'users'): void { + this.activeTab = tab; + this.searchTerm = ''; + this.filterStatus = 'all'; + if (tab === 'emails') { + this.filterSubscribers(); + } else { + this.filterUserSubscribers(); + } + } + + filterSubscribers(): void { + let result = [...this.subscribers]; + + // Search filter + if (this.searchTerm.trim()) { + const term = this.searchTerm.toLowerCase(); + result = result.filter(s => s.email.toLowerCase().includes(term)); + } + + // Status filter + if (this.filterStatus === 'active') { + result = result.filter(s => s.isActive); + } else if (this.filterStatus === 'inactive') { + result = result.filter(s => !s.isActive); + } + + this.filteredSubscribers = result; + } + + filterUserSubscribers(): void { + let result = [...this.userSubscribers]; + + // Search filter + if (this.userSearchTerm.trim()) { + const term = this.userSearchTerm.toLowerCase(); + result = result.filter(u => + u.email.toLowerCase().includes(term) || + (u.name && u.name.toLowerCase().includes(term)) + ); + } + + this.filteredUserSubscribers = result; + } + + onSearchChange(): void { + if (this.activeTab === 'emails') { + this.filterSubscribers(); + } else { + this.filterUserSubscribers(); + } + } + + setFilter(status: 'all' | 'active' | 'inactive'): void { + this.filterStatus = status; + this.filterSubscribers(); + } + + toggleStatus(subscriber: NewsletterSubscriber): void { + this.apiService.toggleNewsletterSubscriber(subscriber.id).subscribe({ + next: (res) => { + // Update local state + const idx = this.subscribers.findIndex(s => s.id === subscriber.id); + if (idx !== -1) { + this.subscribers[idx] = res.subscriber; + } + // Update stats + if (res.subscriber.isActive) { + this.stats.active++; + this.stats.inactive--; + } else { + this.stats.active--; + this.stats.inactive++; + } + this.filterSubscribers(); + this.showToast(res.message, 'success'); + }, + error: (err) => { + console.error('Toggle error:', err); + this.showToast('Fehler beim Ändern des Status', 'error'); + } + }); + } + + confirmDelete(subscriber: NewsletterSubscriber): void { + this.deleteModal.subscriber = subscriber; + this.deleteModal.config.message = `Möchtest du "${subscriber.email}" wirklich endgültig löschen? Diese Aktion kann nicht rückgängig gemacht werden.`; + this.deleteModal.isOpen = true; + } + + executeDelete(): void { + if (!this.deleteModal.subscriber) return; + + this.deleteModal.loading = true; + const id = this.deleteModal.subscriber.id; + const email = this.deleteModal.subscriber.email; + const wasActive = this.deleteModal.subscriber.isActive; + + this.apiService.deleteNewsletterSubscriber(id).subscribe({ + next: () => { + // Remove from local state + this.subscribers = this.subscribers.filter(s => s.id !== id); + // Update stats + this.stats.total--; + if (wasActive) { + this.stats.active--; + } else { + this.stats.inactive--; + } + this.filterSubscribers(); + this.showToast(`"${email}" wurde gelöscht`, 'success'); + this.deleteModal.isOpen = false; + }, + error: (err) => { + console.error('Delete error:', err); + this.showToast('Fehler beim Löschen', 'error'); + }, + complete: () => { + this.deleteModal.loading = false; + } + }); + } + + copyEmail(email: string): void { + navigator.clipboard.writeText(email).then(() => { + this.copiedEmail = email; + this.showToast('E-Mail kopiert!', 'success'); + setTimeout(() => this.copiedEmail = '', 2000); + }); + } + + copyAllEmails(): void { + let emails: string; + + if (this.activeTab === 'emails') { + emails = this.filteredSubscribers + .filter(s => s.isActive) + .map(s => s.email) + .join('\n'); + } else { + emails = this.filteredUserSubscribers + .map(u => u.email) + .join('\n'); + } + + if (!emails) { + this.showToast('Keine E-Mails zum Kopieren', 'info'); + return; + } + + navigator.clipboard.writeText(emails).then(() => { + const count = emails.split('\n').length; + this.showToast(`${count} E-Mail${count > 1 ? 's' : ''} kopiert!`, 'success'); + }); + } + + copyAllCombined(): void { + const emailList = this.subscribers.filter(s => s.isActive).map(s => s.email); + const userList = this.userSubscribers.map(u => u.email); + + // Combine and deduplicate + const allEmails = [...new Set([...emailList, ...userList])].join('\n'); + + if (!allEmails) { + this.showToast('Keine E-Mails zum Kopieren', 'info'); + return; + } + + navigator.clipboard.writeText(allEmails).then(() => { + const count = allEmails.split('\n').length; + this.showToast(`${count} E-Mail${count > 1 ? 's' : ''} kopiert (kombiniert, ohne Duplikate)!`, 'success'); + }); + } + + exportToCSV(): void { + if (this.activeTab === 'emails') { + this.exportEmailsToCSV(); + } else { + this.exportUsersToCSV(); + } + } + + private exportEmailsToCSV(): void { + const headers = ['E-Mail', 'Status', 'Angemeldet am']; + const rows = this.filteredSubscribers.map(s => [ + s.email, + s.isActive ? 'Aktiv' : 'Inaktiv', + this.formatDateFull(s.subscribedAt) + ]); + + this.downloadCSV(headers, rows, 'newsletter-emails'); + } + + private exportUsersToCSV(): void { + const headers = ['E-Mail', 'Name', 'Registriert am']; + const rows = this.filteredUserSubscribers.map(u => [ + u.email, + u.name || '-', + this.formatDateFull(u.createdAt) + ]); + + this.downloadCSV(headers, rows, 'newsletter-users'); + } + + private downloadCSV(headers: string[], rows: string[][], filename: string): void { + const esc = (v: string) => { + const value = String(v ?? ''); + return /[;"\n\r]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value; + }; + + const csv = [ + headers.map(esc).join(';'), + ...rows.map(r => r.map(esc).join(';')) + ].join('\n'); + + const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${filename}-${new Date().toISOString().slice(0, 10)}.csv`; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); + + this.showToast('CSV exportiert!', 'success'); + } + + formatDate(date: Date | string): string { + const d = new Date(date); + const now = new Date(); + const diffMs = now.getTime() - d.getTime(); + const min = Math.floor(diffMs / 60000); + const h = Math.floor(diffMs / 3600000); + const days = Math.floor(diffMs / 86400000); + + if (min < 1) return 'Gerade eben'; + if (min < 60) return `Vor ${min} Min.`; + if (h < 24) return `Vor ${h} Std.`; + if (days < 7) return `Vor ${days} Tag${days > 1 ? 'en' : ''}`; + return d.toLocaleDateString('de-DE', { day: '2-digit', month: 'short', year: 'numeric' }); + } + + formatDateFull(date: Date | string): string { + return new Date(date).toLocaleDateString('de-DE', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + } + + private showToast(message: string, type: 'success' | 'error' | 'info'): void { + if (this.toastTimeout) clearTimeout(this.toastTimeout); + this.toast = { show: true, message, type }; + this.toastTimeout = setTimeout(() => { + this.toast.show = false; + }, 3000); + } +} diff --git a/apps/frontend/src/app/components/admin/admin-requests/admin-requests.component.html b/apps/frontend/src/app/components/admin/admin-requests/admin-requests.component.html new file mode 100644 index 0000000..55b815d --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-requests/admin-requests.component.html @@ -0,0 +1,194 @@ + +
+
+ + +
+
+

Anfragen

+

+ {{ unprocessedRequests.length }} offen · {{ processedRequests.length }} erledigt +

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

Lade Anfragen...

+
+ + +
+ +

Fehler aufgetreten

+

{{ error }}

+ +
+ + +
+ +

Alles erledigt! 🎉

+

Noch nichts erledigt

+

Keine offenen Anfragen vorhanden

+

Erledigte Anfragen erscheinen hier

+
+ + +
+ +
+ + +
+ + +
+
+ +
+
+
+ {{ request.name || 'Unbekannt' }} + + {{ getServiceLabel(request.serviceType) }} + + + + Rückruf + +
+
+ + + {{ formatDate(request.createdAt) }} + + {{ request.phoneNumber }} +
+
+
+ + +
+ + + + + + + + + + +
+
+ + +
+ + +
+
+ + Nachricht +
+
+

{{ request.message || 'Keine Nachricht hinterlassen.' }}

+
+
+ + +
+
+ + Details +
+
+
+
+ {{ field.key }} + + {{ field.isDate ? (field.value | date:'dd.MM.yyyy HH:mm') : field.value }} + +
+
+
+
+ + +
+ + + E-Mail schreiben + + + + {{ request.phoneNumber }} + +
+ + + +
+
+
+
+ +
+
+
\ No newline at end of file diff --git a/apps/frontend/src/app/components/admin/admin-requests/admin-requests.component.scss b/apps/frontend/src/app/components/admin/admin-requests/admin-requests.component.scss new file mode 100644 index 0000000..e47986e --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-requests/admin-requests.component.scss @@ -0,0 +1,606 @@ +// ============================================ +// ADMIN REQUESTS - Mobile-First Compact Design +// ============================================ +@import '../admin-shared.scss'; + +.admin-requests { + min-height: 100%; + padding: $admin-space-md; + padding-bottom: calc($admin-space-md + 80px); // Mobile nav space + animation: pageEnter 0.4s ease-out; + + @media (min-width: 768px) { + padding: $admin-space-lg; + padding-bottom: $admin-space-lg; + } +} + +// ============================================ +// PAGE HEADER - Compact Stats +// ============================================ +.page-header { + display: flex; + flex-direction: column; + gap: $admin-space-sm; + margin-bottom: $admin-space-md; + animation: fadeIn 0.3s ease-out; + + @media (min-width: 768px) { + flex-direction: row; + align-items: center; + justify-content: space-between; + } +} + +.header-left { + display: flex; + align-items: center; + gap: $admin-space-sm; + flex-wrap: wrap; +} + +.page-title { + font-size: 1.125rem; + font-weight: 700; + color: $color-text-primary; + margin: 0; + display: flex; + align-items: center; + gap: $admin-space-xs; + + @media (min-width: 768px) { + font-size: 1.25rem; + } +} + +.mini-stats { + display: flex; + gap: $admin-space-xs; + flex-wrap: wrap; +} + +.mini-stat { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + background: rgba($color-gray-100, 0.8); + border-radius: $radius-full; + font-size: 0.6875rem; + font-weight: 600; + color: $color-text-secondary; + + app-icon { + font-size: 0.75rem; + } + + strong { + color: $color-text-primary; + } + + &--open { + background: rgba($accent-blue, 0.1); + color: $accent-blue; + strong { color: $accent-blue; } + } + + &--done { + background: rgba($accent-green, 0.1); + color: $accent-green; + strong { color: $accent-green; } + } +} + +// ============================================ +// FILTER TABS +// ============================================ +.filter-bar { + display: flex; + gap: $admin-space-2xs; + padding: 3px; + background: rgba($color-gray-100, 0.6); + border-radius: $radius-md; + overflow-x: auto; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } +} + +.filter-tab { + display: flex; + align-items: center; + gap: 4px; + padding: $admin-space-xs $admin-space-sm; + background: transparent; + border: none; + border-radius: $radius-sm; + font-size: 0.75rem; + font-weight: 600; + color: $color-text-secondary; + cursor: pointer; + white-space: nowrap; + transition: all 0.15s ease; + + app-icon { + font-size: 0.875rem; + } + + .count { + padding: 2px 6px; + background: rgba($color-gray-200, 0.6); + border-radius: $radius-full; + font-size: 0.625rem; + font-weight: 700; + } + + &:hover { + color: $color-text-primary; + background: rgba(white, 0.6); + } + + &--active { + background: white; + color: $color-brand-primary; + box-shadow: 0 1px 3px rgba(black, 0.08); + + .count { + background: rgba($color-brand-primary, 0.15); + color: $color-brand-primary; + } + } + + @media (max-width: 480px) { + app-icon { display: none; } + } +} + +// ============================================ +// REQUESTS LIST +// ============================================ +.requests-list { + display: flex; + flex-direction: column; + gap: $admin-space-xs; +} + +.request-item { + background: $glass-bg; + border: 1px solid $glass-border; + border-radius: $radius-md; + overflow: hidden; + transition: all 0.2s ease; + animation: staggerFade 0.3s ease-out backwards; + + @for $i from 1 through 20 { + &:nth-child(#{$i}) { + animation-delay: #{$i * 30}ms; + } + } + + &:hover { + border-color: rgba($color-brand-primary, 0.2); + box-shadow: $shadow-sm; + } + + &--expanded { + border-color: rgba($color-brand-primary, 0.3); + box-shadow: $shadow-md; + } +} + +.request-item__main { + display: flex; + align-items: center; + gap: $admin-space-sm; + padding: $admin-space-sm; + cursor: pointer; + + @media (max-width: 640px) { + flex-wrap: wrap; + } +} + +// Status Indicator +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + + &--open { + background: $accent-yellow; + box-shadow: 0 0 0 3px rgba($accent-yellow, 0.2); + animation: pulse 2s ease-in-out infinite; + } + + &--done { + background: $accent-green; + } +} + +// Info Content +.info-content { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; +} + +.info-primary { + display: flex; + align-items: center; + gap: $admin-space-xs; + flex-wrap: wrap; +} + +.name { + font-size: 0.875rem; + font-weight: 600; + color: $color-text-primary; +} + +.service-tag { + padding: 2px 6px; + background: rgba($color-brand-primary, 0.1); + border-radius: $radius-sm; + font-size: 0.625rem; + font-weight: 600; + color: $color-brand-primary; +} + +.info-secondary { + display: flex; + align-items: center; + gap: $admin-space-xs; + font-size: 0.75rem; + color: $color-text-tertiary; + + .email { + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + @media (max-width: 640px) { + max-width: 150px; + } + } + + .separator { + opacity: 0.5; + } +} + +// Quick Actions +.quick-actions { + display: flex; + align-items: center; + gap: $admin-space-2xs; + + @media (max-width: 640px) { + width: 100%; + padding-top: $admin-space-xs; + border-top: 1px solid $border-default; + justify-content: flex-end; + } +} + +.action-btn { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + border-radius: $radius-sm; + color: $color-text-tertiary; + cursor: pointer; + transition: all 0.15s ease; + text-decoration: none; + + app-icon { + font-size: 1rem; + } + + &:hover { + background: $color-gray-100; + color: $color-text-primary; + } + + &--mail:hover { + background: rgba($accent-blue, 0.1); + color: $accent-blue; + } + + &--call:hover { + background: rgba($accent-green, 0.1); + color: $accent-green; + } + + &--done:hover { + background: rgba($accent-green, 0.1); + color: $accent-green; + } + + &--reopen:hover { + background: rgba($accent-yellow, 0.1); + color: $accent-yellow; + } + + &--delete:hover { + background: rgba($accent-red, 0.1); + color: $accent-red; + } +} + +.expand-toggle { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + border-radius: $radius-sm; + color: $color-text-tertiary; + cursor: pointer; + transition: all 0.15s ease; + + app-icon { + font-size: 1rem; + transition: transform 0.2s ease; + } + + &--open { + color: $color-brand-primary; + + app-icon { + transform: rotate(180deg); + } + } +} + +// ============================================ +// DETAIL PANEL +// ============================================ +.request-item__detail { + padding: $admin-space-sm; + padding-top: 0; + border-top: 1px solid $border-default; + animation: slideDown 0.2s ease-out; +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.detail-block { + background: $color-gray-50; + border-radius: $radius-sm; + overflow: hidden; + margin-bottom: $admin-space-xs; + + &:last-child { + margin-bottom: 0; + } + + &__header { + display: flex; + align-items: center; + gap: 6px; + padding: $admin-space-xs $admin-space-sm; + background: $color-gray-100; + font-size: 0.6875rem; + font-weight: 700; + color: $color-text-tertiary; + text-transform: uppercase; + letter-spacing: 0.03em; + + app-icon { + font-size: 0.875rem; + } + } + + &__content { + padding: $admin-space-sm; + } +} + +.message-text { + margin: 0; + font-size: 0.8125rem; + line-height: 1.6; + color: $color-text-primary; + white-space: pre-wrap; + word-break: break-word; +} + +.detail-grid { + display: grid; + gap: $admin-space-xs; + + @media (min-width: 480px) { + grid-template-columns: repeat(2, 1fr); + } +} + +.detail-item { + display: flex; + flex-direction: column; + gap: 2px; + + .label { + font-size: 0.6875rem; + font-weight: 600; + color: $color-text-tertiary; + text-transform: uppercase; + letter-spacing: 0.02em; + } + + .value { + font-size: 0.8125rem; + color: $color-text-primary; + } +} + +.detail-actions { + display: flex; + gap: $admin-space-xs; + padding-top: $admin-space-sm; + border-top: 1px solid $border-default; + margin-top: $admin-space-sm; + flex-wrap: wrap; + + @media (max-width: 480px) { + flex-direction: column; + + .btn { + width: 100%; + justify-content: center; + } + } +} + +// ============================================ +// BUTTONS (using admin-shared, extending) +// ============================================ +.btn { + @extend .admin-btn; +} + +.btn--primary { + @extend .admin-btn--primary; +} + +.btn--success { + background: $accent-green; + color: white; + box-shadow: 0 2px 8px rgba($accent-green, 0.25); + + &:hover { + background: darken($accent-green, 6%); + transform: translateY(-1px); + } +} + +.btn--danger { + background: rgba($accent-red, 0.1); + color: $accent-red; + + &:hover { + background: rgba($accent-red, 0.15); + } +} + +.btn--ghost { + @extend .admin-btn--ghost; +} + +// ============================================ +// EMPTY STATE +// ============================================ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: $admin-space-2xl $admin-space-md; + text-align: center; + animation: fadeIn 0.4s ease-out; + + app-icon { + font-size: 2.5rem; + color: $color-gray-300; + margin-bottom: $admin-space-sm; + } + + h3 { + font-size: 1rem; + font-weight: 600; + color: $color-text-secondary; + margin: 0 0 $admin-space-2xs; + } + + p { + font-size: 0.8125rem; + color: $color-text-tertiary; + margin: 0; + } +} + +// ============================================ +// LOADING STATE +// ============================================ +.loading-state { + display: flex; + align-items: center; + justify-content: center; + gap: $admin-space-sm; + padding: $admin-space-2xl; + color: $color-text-secondary; + font-size: 0.875rem; + + app-icon { + animation: spin 1s linear infinite; + } +} + +// ============================================ +// TOAST NOTIFICATION +// ============================================ +.toast { + position: fixed; + bottom: 5rem; // Above mobile nav + left: 50%; + transform: translateX(-50%); + padding: $admin-space-sm $admin-space-md; + background: $color-gray-900; + color: white; + border-radius: $radius-md; + font-size: 0.8125rem; + font-weight: 500; + box-shadow: $shadow-lg; + z-index: 1000; + animation: popIn 0.3s ease-out; + + @media (min-width: 768px) { + bottom: $admin-space-lg; + } +} + +// ============================================ +// RESPONSIVE TWEAKS +// ============================================ +@media (max-width: 640px) { + .admin-requests { + padding: $admin-space-sm; + padding-bottom: calc($admin-space-sm + 80px); + } + + .page-header { + gap: $admin-space-xs; + } + + .page-title { + font-size: 1rem; + } +} + +// Reduced motion +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} diff --git a/apps/frontend/src/app/components/admin/admin-requests/admin-requests.component.spec.ts b/apps/frontend/src/app/components/admin/admin-requests/admin-requests.component.spec.ts new file mode 100644 index 0000000..e7eaf14 --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-requests/admin-requests.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AdminRequestsComponent } from './admin-requests.component'; + +describe('AdminRequestsComponent', () => { + let component: AdminRequestsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AdminRequestsComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AdminRequestsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/frontend/src/app/components/admin/admin-requests/admin-requests.component.ts b/apps/frontend/src/app/components/admin/admin-requests/admin-requests.component.ts new file mode 100644 index 0000000..8d344419 --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-requests/admin-requests.component.ts @@ -0,0 +1,216 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { AdminLayoutComponent } from '../admin-layout/admin-layout.component'; +import { IconComponent } from '../../../shared/icon/icon.component'; +import { ConfirmationService } from '../../../shared/confirmation/confirmation.service'; +import { ContactRequest, ApiService, ServiceCategory } from '../../../api/api.service'; +import { NotificationRefreshService } from '../../../shared/admin-notification-center/notification-refresh.service'; + +type Tab = 'unprocessed' | 'processed'; + +@Component({ + selector: 'app-admin-requests', + standalone: true, + imports: [CommonModule, AdminLayoutComponent, IconComponent], + templateUrl: './admin-requests.component.html', + styleUrl: './admin-requests.component.scss' +}) +export class AdminRequestsComponent implements OnInit { + activeTab: Tab = 'unprocessed'; + unprocessedRequests: ContactRequest[] = []; + processedRequests: ContactRequest[] = []; + loading = true; + error = ''; + expandedId: string | null = null; + + // Services aus dem Katalog für Label-Mapping + private servicesMap = new Map(); + + constructor( + private api: ApiService, + private confirmationService: ConfirmationService, + private notificationRefresh: NotificationRefreshService + ) {} + + ngOnInit(): void { + this.loadServices(); + this.loadRequests(); + } + + private loadServices(): void { + this.api.getServicesCatalog().subscribe({ + next: (categories) => { + categories.forEach(cat => { + cat.services?.forEach(service => { + this.servicesMap.set(service.slug, service.title); + }); + }); + } + }); + } + + get currentRequests(): ContactRequest[] { + return this.activeTab === 'unprocessed' + ? this.unprocessedRequests + : this.processedRequests; + } + + trackById = (_: number, r: ContactRequest) => r.id; + + switchTab(tab: Tab): void { + this.activeTab = tab; + this.expandedId = null; + } + + toggleExpand(id: string): void { + this.expandedId = this.expandedId === id ? null : id; + } + + loadRequests(): void { + this.loading = true; + this.error = ''; + this.expandedId = null; + + Promise.all([ + this.api.getUnprocessedContactRequests().toPromise(), + this.api.getAllContactRequests().toPromise() + ]) + .then(([unprocessed, all]) => { + this.unprocessedRequests = unprocessed || []; + const unprocessedIds = new Set(this.unprocessedRequests.map(r => r.id)); + this.processedRequests = (all || []).filter(r => !unprocessedIds.has(r.id)); + }) + .catch((err) => { + console.error('Fehler beim Laden der Anfragen:', err); + this.error = 'Fehler beim Laden der Anfragen'; + }) + .finally(() => this.loading = false); + } + + markAsProcessed(id: string): void { + this.api.markContactRequestAsProcessed(id).subscribe({ + next: () => { + const idx = this.unprocessedRequests.findIndex(r => r.id === id); + if (idx > -1) { + const [request] = this.unprocessedRequests.splice(idx, 1); + this.processedRequests.unshift({ ...request, isProcessed: true }); + } + if (this.expandedId === id) this.expandedId = null; + this.notificationRefresh.triggerRefresh(); + }, + error: (err) => this.handleError('Markieren', err) + }); + } + + markAsUnprocessed(id: string): void { + this.api.updateContactRequest(id, { isProcessed: false }).subscribe({ + next: () => { + const idx = this.processedRequests.findIndex(r => r.id === id); + if (idx > -1) { + const [request] = this.processedRequests.splice(idx, 1); + this.unprocessedRequests.unshift({ ...request, isProcessed: false }); + } + if (this.expandedId === id) this.expandedId = null; + this.notificationRefresh.triggerRefresh(); + }, + error: (err) => this.handleError('Markieren', err) + }); + } + + async deleteRequest(id: string): Promise { + const confirmed = await this.confirmationService.confirm({ + title: 'Anfrage löschen', + message: 'Diese Aktion kann nicht rückgängig gemacht werden.', + confirmText: 'Löschen', + cancelText: 'Abbrechen', + type: 'danger', + icon: 'delete' + }); + + if (confirmed) { + this.api.deleteContactRequest(id).subscribe({ + next: () => { + this.unprocessedRequests = this.unprocessedRequests.filter(r => r.id !== id); + this.processedRequests = this.processedRequests.filter(r => r.id !== id); + if (this.expandedId === id) this.expandedId = null; + this.notificationRefresh.triggerRefresh(); + }, + error: (err) => this.handleError('Löschen', err) + }); + } + } + + private async handleError(action: string, err: any): Promise { + console.error(`Fehler beim ${action}:`, err); + await this.confirmationService.confirm({ + title: 'Fehler', + message: `Beim ${action} ist ein Fehler aufgetreten.`, + confirmText: 'OK', + type: 'danger', + icon: 'error' + }); + } + + getServiceLabel(slug: string): string { + // Aus Services-Katalog holen oder Slug formatieren + return this.servicesMap.get(slug) || this.formatSlug(slug); + } + + private formatSlug(slug: string): string { + // "pc-reparatur" → "Pc Reparatur" + return slug + .split('-') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + } + + formatDate(dateInput: Date | string): string { + if (!dateInput) return '—'; + + const date = new Date(dateInput); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + if (diffMins < 1) return 'Gerade eben'; + if (diffMins < 60) return `vor ${diffMins}m`; + if (diffHours < 24) return `vor ${diffHours}h`; + if (diffDays === 1) return 'Gestern'; + if (diffDays < 7) return `vor ${diffDays} Tagen`; + + return date.toLocaleDateString('de-DE', { + day: '2-digit', + month: '2-digit', + year: '2-digit' + }); + } + + getAdditionalFields(request: any): Array<{ key: string; value: any; isDate: boolean }> { + const excludedKeys = new Set([ + 'id', 'name', 'email', 'phoneNumber', 'serviceType', + 'message', 'createdAt', 'prefersCallback', 'isProcessed' + ]); + + return Object.entries(request || {}) + .filter(([key, value]) => + !excludedKeys.has(key) && + value !== null && + value !== undefined && + value !== '' + ) + .map(([key, value]) => ({ + key: this.formatFieldKey(key), + value: Array.isArray(value) ? value.join(', ') : value, + isDate: typeof value === 'string' && !isNaN(Date.parse(value)) + })); + } + + private formatFieldKey(key: string): string { + return key + .replace(/([A-Z])/g, ' $1') + .replace(/^./, str => str.toUpperCase()) + .trim(); + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/components/admin/admin-services/admin-services.component.html b/apps/frontend/src/app/components/admin/admin-services/admin-services.component.html new file mode 100644 index 0000000..0de0ce6 --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-services/admin-services.component.html @@ -0,0 +1,384 @@ + +
+
+ + +
+
+
+ {{ categories.length }} + Kategorien +
+
+ {{ publishedCategoriesCount }} + Veröffentlicht +
+
+ {{ totalServicesCount }} + Services +
+
+ {{ publishedServicesCount }} + Services veröff. +
+
+ +
+ + +
+
+ + +
+ + +
+ + +
+
+

Lade Daten...

+
+ + +
+
+

Kategorien

+ +
+ +
+ folder_off +

Keine Kategorien

+

Erstelle die erste Kategorie oder importiere Daten.

+
+ +
+
+
+
+ {{ cat.materialIcon }} +
+
+

{{ cat.name }}

+

{{ cat.subtitle }}

+
+ {{ cat.slug }} + {{ cat.services.length }} Services + + {{ cat.isPublished ? 'Veröffentlicht' : 'Versteckt' }} + +
+
+
+ +
+
+
+ + +
+
+

Services

+ +
+ +
+ build_circle +

Keine Services

+

Erstelle zuerst eine Kategorie.

+
+ + +
+
+
+
+ {{ cat.materialIcon }} + {{ cat.name }} + {{ cat.services.length }} +
+
+ + + expand_more + +
+
+ +
+
+

Keine Services in dieser Kategorie

+
+ +
+
+
{{ svc.icon }}
+
+

{{ svc.title }}

+

{{ svc.description }}

+
+ {{ svc.slug }} + + {{ tag }} + +{{ svc.tags.length - 3 }} + +
+
+
+ +
+
+
+
+
+ +
+
+ + +
+
+
+

{{ editingCategory ? 'Kategorie bearbeiten' : 'Neue Kategorie' }}

+ +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + + + Icons durchsuchen + + +
+ +
+
+ + +
+
+ +
+
+
+ + +
+
+ + +
+
+
+

{{ editingService ? 'Service bearbeiten' : 'Neuer Service' }}

+ +
+ +
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ + {{ tag }} + + +
+
+ + +
+
+
+ +
+
+ + +
+
+ +
+
+
+ + +
+
+ + +
+
+
+

Services importieren

+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+
+
\ No newline at end of file diff --git a/apps/frontend/src/app/components/admin/admin-services/admin-services.component.scss b/apps/frontend/src/app/components/admin/admin-services/admin-services.component.scss new file mode 100644 index 0000000..882495d --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-services/admin-services.component.scss @@ -0,0 +1,350 @@ +// ============================================ +// ADMIN SERVICES - Component Styles +// Matching HTML classes exactly +// ============================================ +@import '../admin-shared.scss'; + +// ============================================ +// TAB CONTENT +// ============================================ +.tab-content { + animation: fadeIn 0.3s ease-out; +} + +// ============================================ +// CATEGORIES TAB - Category Cards +// ============================================ +.category-info { + display: flex; + align-items: flex-start; + gap: $admin-space-md; + + @media (max-width: 640px) { + flex-direction: column; + gap: $admin-space-sm; + } +} + +.category-icon { + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, rgba($color-brand-primary, 0.15), rgba($accent-purple, 0.1)); + border-radius: $radius-md; + flex-shrink: 0; + transition: all 0.2s ease; + + .material-symbols-outlined { + font-size: 1.5rem; + color: $color-brand-primary; + } + + &.unpublished { + background: rgba($color-gray-400, 0.15); + + .material-symbols-outlined { + color: $color-gray-400; + } + } +} + +.category-details { + flex: 1; + min-width: 0; + + h3 { + font-size: 1rem; + font-weight: 600; + color: $color-text-primary; + margin: 0 0 $admin-space-2xs; + } + + p { + font-size: 0.8125rem; + color: $color-text-secondary; + margin: 0 0 $admin-space-xs; + } +} + +.category-meta { + display: flex; + flex-wrap: wrap; + gap: $admin-space-2xs; +} + +// ============================================ +// SERVICES TAB - Grouped by Category +// ============================================ +.services-by-category { + display: grid; + gap: $admin-space-sm; +} + +.category-group { + background: $glass-bg; + border: 1px solid $glass-border; + border-radius: $radius-lg; + overflow: hidden; + animation: staggerFade 0.3s ease-out backwards; + + @for $i from 1 through 10 { + &:nth-child(#{$i}) { + animation-delay: #{$i * 60}ms; + } + } +} + +.category-group-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: $admin-space-sm $admin-space-md; + background: rgba($color-brand-primary, 0.03); + cursor: pointer; + transition: all 0.2s ease; + user-select: none; + + &:hover { + background: rgba($color-brand-primary, 0.06); + } +} + +.category-group-info { + display: flex; + align-items: center; + gap: $admin-space-sm; + + .icon { + font-size: 1.25rem; + color: $color-brand-primary; + } + + .name { + font-size: 0.9375rem; + font-weight: 600; + color: $color-text-primary; + } +} + +.category-group-actions { + display: flex; + align-items: center; + gap: $admin-space-xs; + + .expand-icon { + font-size: 1.25rem; + color: $color-text-tertiary; + transition: transform 0.25s ease; + + &.expanded { + transform: rotate(180deg); + } + } +} + +// Services within category group +.category-group > .admin-list { + border-top: 1px solid $glass-border; + padding: $admin-space-xs; + background: rgba(white, 0.5); +} + +// ============================================ +// SERVICE CARDS +// ============================================ +.service-info { + display: flex; + align-items: flex-start; + gap: $admin-space-sm; + + @media (max-width: 640px) { + flex-direction: column; + } +} + +.service-icon { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.5rem; + background: rgba($color-brand-primary, 0.08); + border-radius: $radius-md; + flex-shrink: 0; +} + +.service-details { + flex: 1; + min-width: 0; + + h4 { + font-size: 0.9375rem; + font-weight: 600; + color: $color-text-primary; + margin: 0 0 $admin-space-2xs; + } + + p { + font-size: 0.8125rem; + color: $color-text-secondary; + margin: 0 0 $admin-space-xs; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + } +} + +.service-meta { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: $admin-space-xs; +} + +// Unpublished service card styling +.admin-card.unpublished { + opacity: 0.6; + border-style: dashed; + + &:hover { + opacity: 0.8; + } +} + +// ============================================ +// MODAL EXTENSIONS +// ============================================ +.close-btn { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + border-radius: $radius-sm; + color: $color-text-tertiary; + cursor: pointer; + transition: all 0.15s ease; + + &:hover { + background: $color-gray-100; + color: $color-text-primary; + } +} + +// Form row for side-by-side inputs +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: $admin-space-md; + + @media (max-width: 640px) { + grid-template-columns: 1fr; + } +} + +// Checkbox group styling +.checkbox-group { + display: flex; + align-items: center; + + label { + display: flex !important; + align-items: center; + gap: $admin-space-xs; + cursor: pointer; + font-size: 0.875rem !important; + text-transform: none !important; + letter-spacing: normal !important; + color: $color-text-primary !important; + + input[type="checkbox"] { + width: 18px; + height: 18px; + accent-color: $color-brand-primary; + cursor: pointer; + } + } +} + +// ============================================ +// TAGS EDITOR +// ============================================ +.tags-editor { + display: flex; + flex-direction: column; + gap: $admin-space-xs; +} + +.tag-input { + display: flex; + gap: $admin-space-xs; + + .admin-input { + flex: 1; + } +} + +// Tag remove button within chip +.admin-chip button { + background: none; + border: none; + color: inherit; + font-size: 1rem; + line-height: 1; + cursor: pointer; + padding: 0; + margin-left: $admin-space-2xs; + opacity: 0.7; + transition: opacity 0.15s ease; + + &:hover { + opacity: 1; + } +} + +// ============================================ +// EMPTY STATE ICON +// ============================================ +.state-icon { + font-size: 3rem !important; + display: block; + margin-bottom: $admin-space-sm; +} + +// ============================================ +// RESPONSIVE ADJUSTMENTS +// ============================================ +@media (max-width: 640px) { + .category-group-header { + padding: $admin-space-xs $admin-space-sm; + } + + .category-group-info { + .name { + font-size: 0.875rem; + } + } + + .admin-card__footer { + flex-wrap: wrap; + justify-content: center; + } +} + +// ============================================ +// ANIMATIONS +// ============================================ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} diff --git a/apps/frontend/src/app/components/admin/admin-services/admin-services.component.ts b/apps/frontend/src/app/components/admin/admin-services/admin-services.component.ts new file mode 100644 index 0000000..f16b1d0 --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-services/admin-services.component.ts @@ -0,0 +1,476 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { AdminLayoutComponent } from '../admin-layout/admin-layout.component'; +import { ToastService } from '../../../shared/toasts/toast.service'; +import { ConfirmationService } from '../../../shared/confirmation/confirmation.service'; +import { + ApiService, + ServiceCategory, + ServiceItem, + CreateServiceCategoryDto, + UpdateServiceCategoryDto, + CreateServiceDto, + UpdateServiceDto, + ImportServicesCatalogResultDto +} from '../../../api/api.service'; + +interface CategoryFormModel { + slug: string; + name: string; + subtitle: string; + materialIcon: string; + sortOrder: number; + isPublished: boolean; +} + +interface ServiceFormModel { + slug: string; + icon: string; + title: string; + description: string; + longDescription: string; + tags: string[]; + keywords: string; + categoryId: string; + sortOrder: number; + isPublished: boolean; +} + +@Component({ + selector: 'app-admin-services', + standalone: true, + imports: [ + CommonModule, + FormsModule, + AdminLayoutComponent + ], + templateUrl: './admin-services.component.html', + styleUrl: './admin-services.component.scss' +}) +export class AdminServicesComponent implements OnInit { + categories: ServiceCategory[] = []; + isLoading = true; + isSaving = false; + + // Tab-Ansicht: categories | services + activeTab: 'categories' | 'services' = 'categories'; + + // Kategorie Editor + showCategoryEditor = false; + editingCategory: ServiceCategory | null = null; + categoryForm: CategoryFormModel = this.getEmptyCategoryForm(); + + // Service Editor + showServiceEditor = false; + editingService: ServiceItem | null = null; + serviceForm: ServiceFormModel = this.getEmptyServiceForm(); + newTag = ''; + + // Import/Export + showImportModal = false; + importJson = ''; + importOverwrite = false; + + // Expanded Categories (für Services-Tab) + expandedCategories: Set = new Set(); + + constructor( + private api: ApiService, + private toasts: ToastService, + private confirmation: ConfirmationService + ) {} + + // ===== GETTERS ===== + + get publishedCategoriesCount(): number { + return this.categories.filter(c => c.isPublished).length; + } + + get totalServicesCount(): number { + return this.categories.reduce((sum, c) => sum + c.services.length, 0); + } + + get publishedServicesCount(): number { + return this.categories.reduce((sum, c) => + sum + c.services.filter(s => s.isPublished).length, 0); + } + + // ===== LIFECYCLE ===== + + ngOnInit(): void { + this.loadCategories(); + } + + // ===== DATA LOADING ===== + + loadCategories(): void { + this.isLoading = true; + this.api.getServiceCategoriesAdmin().subscribe({ + next: (data) => { + this.categories = data; + this.isLoading = false; + }, + error: (err) => { + console.error('Fehler beim Laden:', err); + this.toasts.error('Fehler beim Laden der Services'); + this.isLoading = false; + } + }); + } + + // ===== TABS ===== + + setTab(tab: 'categories' | 'services'): void { + this.activeTab = tab; + } + + toggleCategoryExpand(categoryId: string): void { + if (this.expandedCategories.has(categoryId)) { + this.expandedCategories.delete(categoryId); + } else { + this.expandedCategories.add(categoryId); + } + } + + isCategoryExpanded(categoryId: string): boolean { + return this.expandedCategories.has(categoryId); + } + + // ===== CATEGORY EDITOR ===== + + getEmptyCategoryForm(): CategoryFormModel { + return { + slug: '', + name: '', + subtitle: '', + materialIcon: 'category', + sortOrder: 0, + isPublished: true + }; + } + + openCreateCategory(): void { + this.editingCategory = null; + this.categoryForm = this.getEmptyCategoryForm(); + this.categoryForm.sortOrder = this.categories.length; + this.showCategoryEditor = true; + } + + openEditCategory(category: ServiceCategory): void { + this.editingCategory = category; + this.categoryForm = { + slug: category.slug, + name: category.name, + subtitle: category.subtitle, + materialIcon: category.materialIcon, + sortOrder: category.sortOrder, + isPublished: category.isPublished + }; + this.showCategoryEditor = true; + } + + closeCategoryEditor(): void { + this.showCategoryEditor = false; + this.editingCategory = null; + } + + saveCategory(): void { + if (!this.categoryForm.slug || !this.categoryForm.name) { + this.toasts.error('Slug und Name sind erforderlich'); + return; + } + + this.isSaving = true; + + if (this.editingCategory) { + // Update + const dto: UpdateServiceCategoryDto = { ...this.categoryForm }; + this.api.updateServiceCategory(this.editingCategory.id, dto).subscribe({ + next: () => { + this.toasts.success('Kategorie aktualisiert'); + this.isSaving = false; + this.closeCategoryEditor(); + this.loadCategories(); + }, + error: (err) => { + console.error(err); + this.toasts.error('Fehler beim Speichern'); + this.isSaving = false; + } + }); + } else { + // Create + const dto: CreateServiceCategoryDto = { ...this.categoryForm }; + this.api.createServiceCategory(dto).subscribe({ + next: () => { + this.toasts.success('Kategorie erstellt'); + this.isSaving = false; + this.closeCategoryEditor(); + this.loadCategories(); + }, + error: (err) => { + console.error(err); + this.toasts.error('Fehler beim Erstellen'); + this.isSaving = false; + } + }); + } + } + + toggleCategoryPublish(category: ServiceCategory): void { + this.api.toggleServiceCategoryPublish(category.id).subscribe({ + next: (updated) => { + category.isPublished = updated.isPublished; + this.toasts.success(updated.isPublished ? 'Veröffentlicht' : 'Versteckt'); + }, + error: () => this.toasts.error('Fehler') + }); + } + + async deleteCategory(category: ServiceCategory): Promise { + const confirmed = await this.confirmation.confirm({ + title: 'Kategorie löschen', + message: `Kategorie "${category.name}" und alle zugehörigen Services wirklich löschen?`, + type: 'danger', + confirmText: 'Löschen', + cancelText: 'Abbrechen' + }); + + if (!confirmed) return; + + this.api.deleteServiceCategory(category.id).subscribe({ + next: () => { + this.toasts.success('Kategorie gelöscht'); + this.loadCategories(); + }, + error: () => this.toasts.error('Fehler beim Löschen') + }); + } + + // ===== SERVICE EDITOR ===== + + getEmptyServiceForm(): ServiceFormModel { + return { + slug: '', + icon: '🔧', + title: '', + description: '', + longDescription: '', + tags: [], + keywords: '', + categoryId: '', + sortOrder: 0, + isPublished: true + }; + } + + openCreateService(categoryId?: string): void { + this.editingService = null; + this.serviceForm = this.getEmptyServiceForm(); + if (categoryId) { + this.serviceForm.categoryId = categoryId; + const cat = this.categories.find(c => c.id === categoryId); + if (cat) { + this.serviceForm.sortOrder = cat.services.length; + } + } + this.showServiceEditor = true; + } + + openEditService(service: ServiceItem): void { + this.editingService = service; + this.serviceForm = { + slug: service.slug, + icon: service.icon, + title: service.title, + description: service.description, + longDescription: service.longDescription, + tags: [...service.tags], + keywords: service.keywords, + categoryId: service.categoryId, + sortOrder: service.sortOrder, + isPublished: service.isPublished + }; + this.showServiceEditor = true; + } + + closeServiceEditor(): void { + this.showServiceEditor = false; + this.editingService = null; + this.newTag = ''; + } + + addTag(): void { + const tag = this.newTag.trim(); + if (tag && !this.serviceForm.tags.includes(tag)) { + this.serviceForm.tags.push(tag); + } + this.newTag = ''; + } + + removeTag(index: number): void { + this.serviceForm.tags.splice(index, 1); + } + + saveService(): void { + if (!this.serviceForm.slug || !this.serviceForm.title || !this.serviceForm.categoryId) { + this.toasts.error('Slug, Titel und Kategorie sind erforderlich'); + return; + } + + this.isSaving = true; + + if (this.editingService) { + // Update + const dto: UpdateServiceDto = { ...this.serviceForm }; + this.api.updateService(this.editingService.id, dto).subscribe({ + next: () => { + this.toasts.success('Service aktualisiert'); + this.isSaving = false; + this.closeServiceEditor(); + this.loadCategories(); + }, + error: (err) => { + console.error(err); + this.toasts.error('Fehler beim Speichern'); + this.isSaving = false; + } + }); + } else { + // Create + const dto: CreateServiceDto = { ...this.serviceForm }; + this.api.createService(dto).subscribe({ + next: () => { + this.toasts.success('Service erstellt'); + this.isSaving = false; + this.closeServiceEditor(); + this.loadCategories(); + }, + error: (err) => { + console.error(err); + this.toasts.error('Fehler beim Erstellen'); + this.isSaving = false; + } + }); + } + } + + toggleServicePublish(service: ServiceItem): void { + this.api.toggleServicePublish(service.id).subscribe({ + next: (updated) => { + service.isPublished = updated.isPublished; + this.toasts.success(updated.isPublished ? 'Veröffentlicht' : 'Versteckt'); + }, + error: () => this.toasts.error('Fehler') + }); + } + + async deleteService(service: ServiceItem): Promise { + const confirmed = await this.confirmation.confirm({ + title: 'Service löschen', + message: `Service "${service.title}" wirklich löschen?`, + type: 'danger', + confirmText: 'Löschen', + cancelText: 'Abbrechen' + }); + + if (!confirmed) return; + + this.api.deleteService(service.id).subscribe({ + next: () => { + this.toasts.success('Service gelöscht'); + this.loadCategories(); + }, + error: () => this.toasts.error('Fehler beim Löschen') + }); + } + + // ===== IMPORT/EXPORT ===== + + openImportModal(): void { + this.importJson = ''; + this.importOverwrite = false; + this.showImportModal = true; + } + + closeImportModal(): void { + this.showImportModal = false; + } + + doImport(): void { + let parsed: any; + try { + parsed = JSON.parse(this.importJson); + } catch { + this.toasts.error('Ungültiges JSON-Format'); + return; + } + + // Prüfen ob es ein Array oder ein Objekt mit categories ist + let categories = Array.isArray(parsed) ? parsed : parsed.categories; + if (!Array.isArray(categories)) { + this.toasts.error('JSON muss ein Array von Kategorien sein oder ein Objekt mit "categories"'); + return; + } + + this.isSaving = true; + this.api.importServicesCatalog({ categories, overwriteExisting: this.importOverwrite }).subscribe({ + next: (result: ImportServicesCatalogResultDto) => { + this.toasts.success( + `Import erfolgreich: ${result.categoriesCreated} Kategorien erstellt, ` + + `${result.servicesCreated} Services erstellt` + ); + if (result.errors.length > 0) { + console.warn('Import Fehler:', result.errors); + } + this.closeImportModal(); + this.loadCategories(); + this.isSaving = false; + }, + error: (err) => { + console.error(err); + this.toasts.error('Fehler beim Import'); + this.isSaving = false; + } + }); + } + + doExport(): void { + this.api.exportServicesCatalog().subscribe({ + next: (data) => { + const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'services-catalog-export.json'; + a.click(); + URL.revokeObjectURL(url); + this.toasts.success('Export heruntergeladen'); + }, + error: () => this.toasts.error('Fehler beim Export') + }); + } + + // ===== HELPERS ===== + + generateSlug(name: string): string { + return name + .toLowerCase() + .replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue').replace(/ß/g, 'ss') + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, ''); + } + + onCategoryNameChange(): void { + if (!this.editingCategory && this.categoryForm.name) { + this.categoryForm.slug = this.generateSlug(this.categoryForm.name); + } + } + + onServiceTitleChange(): void { + if (!this.editingService && this.serviceForm.title) { + this.serviceForm.slug = this.generateSlug(this.serviceForm.title); + } + } +} diff --git a/apps/frontend/src/app/components/admin/admin-settings/admin-settings.component.html b/apps/frontend/src/app/components/admin/admin-settings/admin-settings.component.html new file mode 100644 index 0000000..9eac53b --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-settings/admin-settings.component.html @@ -0,0 +1,188 @@ + +
+
+ +
+
+

Einstellungen werden geladen...

+
+ + +
+
+ + +
+
+

+ engineering + Wartungsmodus +

+
+ +
+
+ +
+ Wartungsmodus aktiviert + + {{ formData.isUnderConstruction ? 'Aktiv' : 'Inaktiv' }} + +
+
+ +
+ + +
+ +
+ + + Mit diesem Passwort kann die Seite trotz Wartungsmodus betreten werden +
+
+
+ + +
+
+

+ language + Allgemeine Einstellungen +

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

+ toggle_on + Funktionen +

+
+ +
+
+ +
+ Registrierung erlauben + Neue Benutzer können sich registrieren +
+
+ +
+ +
+ Newsletter-Anmeldung erlauben + Newsletter-Formular wird angezeigt +
+
+
+
+ + +
+ + +
+
+ + +
+
+ info +
+ Hinweis: Änderungen am Wartungsmodus werden sofort wirksam. + Besucher sehen dann die Wartungsseite, bis der Modus deaktiviert wird oder sie das Passwort eingeben. +
+
+
+
+
+
+
\ No newline at end of file diff --git a/apps/frontend/src/app/components/admin/admin-settings/admin-settings.component.scss b/apps/frontend/src/app/components/admin/admin-settings/admin-settings.component.scss new file mode 100644 index 0000000..3cbbda0 --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-settings/admin-settings.component.scss @@ -0,0 +1,597 @@ +// ============================================ +// ADMIN SETTINGS - Mobile-First Compact Design +// ============================================ +@import '../admin-shared.scss'; + +.admin-settings { + min-height: 100%; + padding: $admin-space-md; + padding-bottom: calc($admin-space-md + 80px); + animation: pageEnter 0.4s ease-out; + + @media (min-width: 768px) { + padding: $admin-space-lg; + padding-bottom: $admin-space-lg; + } + + .container { + display: grid; + gap: $admin-space-sm; + max-width: $admin-container-lg; + margin: 0 auto; + } +} + +// ============================================ +// LOADING STATE +// ============================================ +.loading-wrapper { + background: $glass-bg; + backdrop-filter: blur(20px); + border: 1px solid $glass-border; + border-radius: $radius-md; + padding: $admin-space-lg; + text-align: center; + animation: fadeIn 0.3s ease-out; + + .spinner { + width: 32px; + height: 32px; + margin: 0 auto $admin-space-sm; + border: 3px solid rgba($color-brand-primary, 0.15); + border-top-color: $color-brand-primary; + border-radius: 50%; + animation: spin 0.7s linear infinite; + } + + p { + margin: 0; + font-size: 0.8125rem; + color: $color-text-secondary; + font-weight: 600; + } +} + +// ============================================ +// SETTINGS CONTENT +// ============================================ +.settings-content { + display: grid; + gap: $admin-space-sm; +} + +.settings-form { + display: grid; + gap: $admin-space-sm; +} + +// ============================================ +// SETTINGS SECTIONS +// ============================================ +.settings-section { + background: $glass-bg; + backdrop-filter: blur(20px); + border: 1px solid $glass-border; + border-radius: $radius-md; + padding: $admin-space-sm; + transition: all 0.2s ease; + animation: staggerFade 0.3s ease-out backwards; + + @for $i from 1 through 10 { + &:nth-child(#{$i}) { + animation-delay: #{$i * 50}ms; + } + } + + &:hover { + box-shadow: $shadow-sm; + } + + @media (min-width: 768px) { + padding: $admin-space-md; + } +} + +.section-title { + display: flex; + align-items: center; + gap: $admin-space-xs; + margin: 0 0 $admin-space-sm; + font-size: 0.9375rem; + font-weight: 700; + background: $gradient-primary; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + + .material-symbols-outlined, + app-icon { + font-size: 1.25rem; + background: $gradient-primary; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + } +} + +// ============================================ +// FORM GROUPS +// ============================================ +.form-group { + margin-bottom: $admin-space-sm; + + &:last-child { + margin-bottom: 0; + } +} + +.form-label { + display: flex; + align-items: center; + gap: $admin-space-2xs; + margin-bottom: 4px; + font-size: 0.6875rem; + font-weight: 600; + color: $color-text-secondary; + text-transform: uppercase; + letter-spacing: 0.02em; + + .hint { + font-weight: 400; + color: $color-text-tertiary; + text-transform: none; + letter-spacing: 0; + } +} + +.form-input, +.form-select, +.form-textarea { + width: 100%; + padding: $admin-space-xs $admin-space-sm; + background: white; + border: 1px solid $border-default; + border-radius: $radius-sm; + font-size: 0.8125rem; + color: $color-text-primary; + transition: all 0.15s ease; + + &:focus { + outline: none; + border-color: $color-brand-primary; + box-shadow: 0 0 0 3px rgba($color-brand-primary, 0.1); + } + + &::placeholder { + color: $color-text-tertiary; + } + + &:disabled { + background: $color-gray-50; + cursor: not-allowed; + } +} + +.form-textarea { + min-height: 80px; + resize: vertical; +} + +.form-help { + margin-top: 4px; + font-size: 0.6875rem; + color: $color-text-tertiary; +} + +// ============================================ +// FORM ROW (2 Columns) +// ============================================ +.form-row { + display: grid; + gap: $admin-space-sm; + + @media (min-width: 480px) { + grid-template-columns: repeat(2, 1fr); + } +} + +// ============================================ +// TOGGLE SWITCH +// ============================================ +.toggle-group { + display: flex; + align-items: center; + justify-content: space-between; + padding: $admin-space-xs 0; + border-bottom: 1px solid rgba($border-default, 0.5); + + &:last-child { + border-bottom: none; + } +} + +.toggle-info { + flex: 1; + min-width: 0; + padding-right: $admin-space-sm; + + .toggle-label { + font-size: 0.8125rem; + font-weight: 600; + color: $color-text-primary; + } + + .toggle-description { + font-size: 0.6875rem; + color: $color-text-tertiary; + margin-top: 2px; + } +} + +.toggle-switch { + position: relative; + width: 44px; + height: 24px; + flex-shrink: 0; + + input { + opacity: 0; + width: 0; + height: 0; + + &:checked + .toggle-slider { + background: $color-brand-primary; + + &::before { + transform: translateX(20px); + } + } + + &:focus + .toggle-slider { + box-shadow: 0 0 0 3px rgba($color-brand-primary, 0.2); + } + } +} + +.toggle-slider { + position: absolute; + cursor: pointer; + inset: 0; + background: $color-gray-200; + border-radius: $radius-full; + transition: all 0.2s ease; + + &::before { + content: ''; + position: absolute; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background: white; + border-radius: 50%; + box-shadow: 0 1px 3px rgba(black, 0.15); + transition: all 0.2s ease; + } +} + +// ============================================ +// COLOR PICKER +// ============================================ +.color-input-group { + display: flex; + gap: $admin-space-xs; + align-items: center; +} + +.color-preview { + width: 32px; + height: 32px; + border-radius: $radius-sm; + border: 1px solid $border-default; + cursor: pointer; + transition: all 0.15s ease; + + &:hover { + transform: scale(1.05); + } +} + +.color-input { + flex: 1; +} + +// ============================================ +// API KEY / PASSWORD FIELD +// ============================================ +.password-input-group { + position: relative; + + .form-input { + padding-right: 40px; + } + + .toggle-visibility { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + border-radius: $radius-sm; + color: $color-text-tertiary; + cursor: pointer; + transition: all 0.15s ease; + + &:hover { + background: $color-gray-100; + color: $color-text-primary; + } + } +} + +// ============================================ +// BUTTONS +// ============================================ +.btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: $admin-space-xs $admin-space-sm; + border-radius: $radius-sm; + font-weight: 600; + font-size: 0.75rem; + border: none; + cursor: pointer; + transition: all 0.15s ease; + + app-icon, + .material-symbols-outlined { + font-size: 0.9375rem; + } + + &--primary { + background: $gradient-primary; + color: white; + + &:hover:not(:disabled) { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba($color-brand-primary, 0.3); + } + } + + &--secondary { + background: $color-gray-100; + color: $color-text-secondary; + + &:hover:not(:disabled) { + background: $color-gray-200; + } + } + + &--ghost { + background: transparent; + color: $color-brand-primary; + + &:hover:not(:disabled) { + background: rgba($color-brand-primary, 0.08); + } + } + + &--danger { + background: rgba($accent-red, 0.1); + color: $accent-red; + + &:hover:not(:disabled) { + background: rgba($accent-red, 0.15); + } + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +// ============================================ +// SAVE BAR +// ============================================ +.save-bar { + position: fixed; + bottom: 5rem; + left: 50%; + transform: translateX(-50%); + display: flex; + gap: $admin-space-xs; + padding: $admin-space-sm $admin-space-md; + background: white; + border-radius: $radius-md; + box-shadow: $shadow-xl; + z-index: 100; + animation: popIn 0.3s ease-out; + + @media (min-width: 768px) { + bottom: $admin-space-lg; + } + + .btn { + padding: $admin-space-xs $admin-space-md; + } +} + +// ============================================ +// DANGER ZONE +// ============================================ +.danger-zone { + background: rgba($accent-red, 0.03); + border-color: rgba($accent-red, 0.2); + + .section-title { + background: linear-gradient(135deg, $accent-red, darken($accent-red, 10%)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + + .material-symbols-outlined, + app-icon { + background: linear-gradient(135deg, $accent-red, darken($accent-red, 10%)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + } + } +} + +.danger-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: $admin-space-sm 0; + border-bottom: 1px solid rgba($accent-red, 0.1); + gap: $admin-space-sm; + + &:last-child { + border-bottom: none; + padding-bottom: 0; + } + + @media (max-width: 480px) { + flex-direction: column; + align-items: stretch; + } +} + +.danger-info { + flex: 1; + + h4 { + font-size: 0.8125rem; + font-weight: 600; + color: $color-text-primary; + margin: 0 0 2px; + } + + p { + font-size: 0.6875rem; + color: $color-text-tertiary; + margin: 0; + } +} + +// ============================================ +// TOAST +// ============================================ +.toast { + position: fixed; + bottom: 5rem; + left: 50%; + transform: translateX(-50%); + padding: $admin-space-sm $admin-space-md; + background: $color-gray-900; + color: white; + border-radius: $radius-md; + font-size: 0.8125rem; + font-weight: 500; + box-shadow: $shadow-lg; + z-index: 1000; + animation: popIn 0.3s ease-out; + + &--success { background: $accent-green; } + &--error { background: $accent-red; } + + @media (min-width: 768px) { + bottom: $admin-space-lg; + } +} + +// ============================================ +// CONFIRMATION MODAL +// ============================================ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(black, 0.5); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: $admin-space-md; + animation: fadeIn 0.2s ease-out; +} + +.modal { + width: 100%; + max-width: 400px; + background: white; + border-radius: $radius-lg; + box-shadow: $shadow-xl; + overflow: hidden; + animation: scaleIn 0.25s ease-out; + + &__header { + display: flex; + align-items: center; + gap: $admin-space-xs; + padding: $admin-space-sm $admin-space-md; + border-bottom: 1px solid $border-default; + + app-icon, + .material-symbols-outlined { + font-size: 1.25rem; + color: $accent-red; + } + + h3 { + font-size: 0.9375rem; + font-weight: 700; + color: $color-text-primary; + margin: 0; + } + } + + &__body { + padding: $admin-space-md; + + p { + font-size: 0.8125rem; + color: $color-text-secondary; + margin: 0; + line-height: 1.5; + } + } + + &__footer { + display: flex; + gap: $admin-space-xs; + padding: $admin-space-sm $admin-space-md; + border-top: 1px solid $border-default; + justify-content: flex-end; + } +} + +// ============================================ +// RESPONSIVE +// ============================================ +@media (max-width: 640px) { + .admin-settings { + padding: $admin-space-sm; + padding-bottom: calc($admin-space-sm + 80px); + } + + .settings-section { + padding: $admin-space-sm; + } +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} diff --git a/apps/frontend/src/app/components/admin/admin-settings/admin-settings.component.spec.ts b/apps/frontend/src/app/components/admin/admin-settings/admin-settings.component.spec.ts new file mode 100644 index 0000000..96445ba --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-settings/admin-settings.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AdminSettingsComponent } from './admin-settings.component'; + +describe('AdminSettingsComponent', () => { + let component: AdminSettingsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AdminSettingsComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AdminSettingsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/frontend/src/app/components/admin/admin-settings/admin-settings.component.ts b/apps/frontend/src/app/components/admin/admin-settings/admin-settings.component.ts new file mode 100644 index 0000000..9aceb5b --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-settings/admin-settings.component.ts @@ -0,0 +1,120 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { AdminLayoutComponent } from '../admin-layout/admin-layout.component'; +import { ToastService } from '../../../shared/toasts/toast.service'; +import { Router } from '@angular/router'; +import { ApiService, Settings, UpdateSettingsDto } from '../../../api/api.service'; + +@Component({ + selector: 'app-admin-settings', + standalone: true, + imports: [ + CommonModule, + FormsModule, + AdminLayoutComponent + ], + templateUrl: './admin-settings.component.html', + styleUrl: './admin-settings.component.scss' +}) +export class AdminSettingsComponent implements OnInit { + settings: Settings | null = null; + isLoading = true; + isSaving = false; + + // Form Model + formData: UpdateSettingsDto = { + isUnderConstruction: false, + maintenanceMessage: '', + maintenancePassword: '', + allowRegistration: true, + allowNewsletter: true, + siteTitle: '', + siteDescription: '', + contactEmail: '', + contactPhone: '' + }; + + constructor( + private api: ApiService, + private toasts: ToastService, + private router: Router + ) {} + + ngOnInit(): void { + this.loadSettings(); + } + + async loadSettings(): Promise { + try { + this.isLoading = true; + this.api.getSettings().subscribe({ + next: (settings) => { + this.settings = settings; + // Populate form + this.formData = { + isUnderConstruction: settings.isUnderConstruction, + maintenanceMessage: settings.maintenanceMessage || '', + maintenancePassword: settings.maintenancePassword || '', + allowRegistration: settings.allowRegistration, + allowNewsletter: settings.allowNewsletter, + siteTitle: settings.siteTitle || '', + siteDescription: settings.siteDescription || '', + contactEmail: settings.contactEmail || '', + contactPhone: settings.contactPhone || '' + }; + this.isLoading = false; + }, + error: (error) => { + console.error('Fehler beim Laden der Einstellungen:', error); + this.toasts.error('Fehler beim Laden der Einstellungen'); + this.isLoading = false; + } + }); + } catch (error) { + console.error('Fehler beim Laden der Einstellungen:', error); + this.toasts.error('Fehler beim Laden der Einstellungen'); + this.isLoading = false; + } + } + + async saveSettings(): Promise { + try { + this.isSaving = true; + + this.api.updateSettings(this.formData).subscribe({ + next: (updatedSettings) => { + this.settings = updatedSettings; + this.toasts.success('Einstellungen wurden erfolgreich gespeichert'); + this.isSaving = false; + }, + error: (error) => { + console.error('Fehler beim Speichern:', error); + this.toasts.error('Fehler beim Speichern der Einstellungen'); + this.isSaving = false; + } + }); + } catch (error) { + console.error('Fehler beim Speichern:', error); + this.toasts.error('Fehler beim Speichern der Einstellungen'); + this.isSaving = false; + } + } + + resetForm(): void { + if (this.settings) { + this.formData = { + isUnderConstruction: this.settings.isUnderConstruction, + maintenanceMessage: this.settings.maintenanceMessage || '', + maintenancePassword: this.settings.maintenancePassword || '', + allowRegistration: this.settings.allowRegistration, + allowNewsletter: this.settings.allowNewsletter, + siteTitle: this.settings.siteTitle || '', + siteDescription: this.settings.siteDescription || '', + contactEmail: this.settings.contactEmail || '', + contactPhone: this.settings.contactPhone || '' + }; + this.toasts.info('Formular wurde zurückgesetzt'); + } + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/components/admin/admin-shared.scss b/apps/frontend/src/app/components/admin/admin-shared.scss new file mode 100644 index 0000000..a403700 --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-shared.scss @@ -0,0 +1,1672 @@ +// ============================================ +// 🎨 ADMIN DESIGN SYSTEM v2.0 +// ============================================ +// Unified styles for all admin components +// Mobile-first, compact, animated, modern +// ============================================ + +@import '../../utils/shared-styles'; + +// ============================================ +// 🎯 DESIGN TOKENS +// ============================================ + +// Accent Colors (semantic) +$accent-blue: #3b82f6; +$accent-green: #22c55e; +$accent-red: #ef4444; +$accent-yellow: #eab308; +$accent-orange: #f97316; +$accent-purple: #8b5cf6; +$accent-cyan: #06b6d4; +$accent-pink: #ec4899; + +// Soft variants +$accent-green-soft: rgba($accent-green, 0.1); +$accent-red-soft: rgba($accent-red, 0.1); +$accent-yellow-soft: rgba($accent-yellow, 0.1); +$accent-blue-soft: rgba($accent-blue, 0.1); + +// Borders +$border-default: #e2e8f0; +$border-light: rgba(226, 232, 240, 0.6); + +// Legacy text variables (for compatibility) +$text-primary: $color-text-primary; +$text-secondary: $color-text-secondary; +$text-muted: $color-text-tertiary; + +// Spacing Scale (compact) +$admin-space-2xs: 0.125rem; // 2px +$admin-space-xs: 0.25rem; // 4px +$admin-space-sm: 0.5rem; // 8px +$admin-space-md: 0.75rem; // 12px +$admin-space-lg: 1rem; // 16px +$admin-space-xl: 1.5rem; // 24px +$admin-space-2xl: 2rem; // 32px + +// Container widths +$admin-container-sm: 640px; +$admin-container-md: 768px; +$admin-container-lg: 1024px; +$admin-container-xl: 1280px; +$admin-container-2xl: 1400px; + +// Transitions +$transition-fast: 0.15s cubic-bezier(0.4, 0, 0.2, 1); +$transition-normal: 0.25s cubic-bezier(0.4, 0, 0.2, 1); +$transition-slow: 0.35s cubic-bezier(0.4, 0, 0.2, 1); +$transition-bounce: 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); + +// ============================================ +// 🚀 ANIMATIONS +// ============================================ + +// Page enter animation +@keyframes pageEnter { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +// Fade in +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +// Slide in from left +@keyframes slideInLeft { + from { + opacity: 0; + transform: translateX(-16px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +// Slide in from right +@keyframes slideInRight { + from { + opacity: 0; + transform: translateX(16px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +// Scale in +@keyframes scaleIn { + from { + opacity: 0; + transform: scale(0.96); + } + to { + opacity: 1; + transform: scale(1); + } +} + +// Stagger fade +@keyframes staggerFade { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +// Pop in (for buttons, badges) +@keyframes popIn { + 0% { + opacity: 0; + transform: scale(0.8); + } + 70% { + transform: scale(1.05); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +// Pulse (for notifications) +@keyframes pulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.05); } +} + +// Spin +@keyframes spin { + to { transform: rotate(360deg); } +} + +// Shimmer (loading skeleton) +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} + +// Legacy animation names for backwards compatibility +@keyframes adminFadeIn { + from { + opacity: 0; + transform: translateY(15px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes adminSlideIn { + from { + opacity: 0; + transform: translateX(-20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes adminScaleIn { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes adminStaggerIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes admin-spin { + to { transform: rotate(360deg); } +} + +// ============================================ +// 📄 PAGE CONTAINER +// ============================================ + +.admin-page { + min-height: calc(100vh - 72px); + padding: $admin-space-lg; + background: $gradient-bg; + animation: pageEnter $transition-slow ease-out; + + @media (max-width: $admin-container-md) { + padding: $admin-space-md; + } +} + +.admin-container { + max-width: $admin-container-2xl; + margin: 0 auto; +} + +// ============================================ +// 🎯 PAGE HEADER (Compact) +// ============================================ + +.admin-page-header { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: $admin-space-md; + padding: $admin-space-lg; + background: $glass-bg; + backdrop-filter: blur(20px); + border: 1px solid $glass-border; + border-radius: $radius-lg; + box-shadow: $shadow-glass; + margin-bottom: $admin-space-lg; + animation: slideInLeft $transition-slow ease-out; + + @media (max-width: $admin-container-sm) { + flex-direction: column; + align-items: stretch; + padding: $admin-space-md; + gap: $admin-space-sm; + } +} + +.admin-page-title { + margin: 0; + font-size: 1.375rem; + font-weight: 800; + background: $gradient-primary; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + line-height: 1.2; + + @media (max-width: $admin-container-sm) { + font-size: 1.125rem; + } +} + +.admin-page-subtitle { + margin: $admin-space-xs 0 0; + font-size: 0.8125rem; + color: $color-text-secondary; + font-weight: 500; +} + +.admin-header-actions { + display: flex; + gap: $admin-space-sm; + flex-wrap: wrap; + align-items: center; + + @media (max-width: $admin-container-sm) { + justify-content: stretch; + + .admin-btn { + flex: 1; + justify-content: center; + } + } +} + +// ============================================ +// 📊 STATS ROW (Horizontal Chips) +// ============================================ + +.admin-stats-row { + display: flex; + gap: $admin-space-sm; + flex-wrap: wrap; + margin-bottom: $admin-space-lg; + + @media (max-width: $admin-container-sm) { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: $admin-space-sm; + } +} + +.admin-stat-chip { + display: flex; + align-items: center; + gap: $admin-space-sm; + padding: $admin-space-sm $admin-space-md; + background: $glass-bg; + backdrop-filter: blur(16px); + border: 1px solid $glass-border; + border-radius: $radius-md; + box-shadow: $shadow-sm; + transition: all $transition-fast; + animation: staggerFade $transition-normal ease-out both; + + // Stagger animation + @for $i from 1 through 8 { + &:nth-child(#{$i}) { + animation-delay: #{$i * 0.04}s; + } + } + + &:hover { + transform: translateY(-2px); + box-shadow: $shadow-md; + border-color: rgba($color-brand-primary, 0.2); + } + + .stat-icon, + .material-symbols-outlined { + font-size: 1.125rem; + color: $color-brand-primary; + } + + .stat-value { + font-size: 1.125rem; + font-weight: 800; + color: $color-text-primary; + line-height: 1; + } + + .stat-label { + font-size: 0.6875rem; + font-weight: 600; + color: $color-text-tertiary; + text-transform: uppercase; + letter-spacing: 0.3px; + } + + // Variants + &--success { + .stat-icon, .stat-value, .material-symbols-outlined { color: $color-success; } + border-color: rgba($color-success, 0.2); + } + + &--warning { + .stat-icon, .stat-value, .material-symbols-outlined { color: $color-warning; } + border-color: rgba($color-warning, 0.2); + } + + &--error { + .stat-icon, .stat-value, .material-symbols-outlined { color: $color-error; } + border-color: rgba($color-error, 0.2); + } + + &--primary { + .stat-icon, .stat-value, .material-symbols-outlined { color: $color-brand-primary; } + border-color: rgba($color-brand-primary, 0.2); + } + + &--muted { + .stat-icon, .stat-value, .material-symbols-outlined { color: $color-gray-400; } + } + + &--highlight { + background: linear-gradient(135deg, rgba($color-brand-primary, 0.08), rgba($color-brand-secondary, 0.1)); + border-color: rgba($color-brand-primary, 0.25); + } + + &--info { + .stat-icon, .stat-value, .material-symbols-outlined { color: $color-brand-primary; } + } +} + +// ============================================ +// 🔘 BUTTONS +// ============================================ + +.admin-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: $admin-space-sm; + padding: $admin-space-sm $admin-space-md; + border-radius: $radius-md; + font-weight: 600; + font-size: 0.8125rem; + border: 1px solid transparent; + cursor: pointer; + transition: all $transition-fast; + white-space: nowrap; + text-decoration: none; + min-height: 36px; + + .material-symbols-outlined, + app-icon { + font-size: 1rem; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none !important; + } + + &:focus-visible { + outline: none; + box-shadow: 0 0 0 3px rgba($color-brand-primary, 0.2); + } + + // Primary + &--primary { + background: $gradient-primary; + color: white; + box-shadow: $shadow-brand; + + &:hover:not(:disabled) { + background: $gradient-primary-hover; + transform: translateY(-1px); + box-shadow: $shadow-brand-hover; + } + + &:active:not(:disabled) { + transform: translateY(0); + } + } + + // Secondary + &--secondary { + background: white; + color: $color-text-secondary; + border-color: $glass-border; + box-shadow: $shadow-sm; + + &:hover:not(:disabled) { + background: $color-gray-50; + border-color: $color-gray-300; + color: $color-text-primary; + } + } + + // Ghost + &--ghost { + background: transparent; + color: $color-brand-primary; + border-color: rgba($color-brand-primary, 0.25); + + &:hover:not(:disabled) { + background: rgba($color-brand-primary, 0.08); + border-color: $color-brand-primary; + } + } + + // Danger + &--danger { + background: rgba($color-error, 0.1); + color: $color-error; + border-color: rgba($color-error, 0.2); + + &:hover:not(:disabled) { + background: rgba($color-error, 0.15); + border-color: $color-error; + } + } + + // Danger outline + &--danger-outline { + background: transparent; + color: $color-error; + border-color: rgba($color-error, 0.3); + + &:hover:not(:disabled) { + background: rgba($color-error, 0.08); + border-color: $color-error; + } + } + + // Icon only + &--icon { + width: 36px; + height: 36px; + padding: 0; + min-height: auto; + + .material-symbols-outlined { + font-size: 1.125rem; + } + } + + // Small + &--sm { + padding: $admin-space-xs $admin-space-sm; + font-size: 0.75rem; + min-height: 28px; + + .material-symbols-outlined { + font-size: 0.875rem; + } + } + + // Spinning state + &.spin { + .material-symbols-outlined, + app-icon { + animation: spin 0.8s linear infinite; + } + } +} + +// ============================================ +// 🔍 FILTER BAR +// ============================================ + +.admin-filter-bar { + display: flex; + gap: $admin-space-sm; + align-items: center; + padding: $admin-space-sm $admin-space-md; + background: $glass-bg; + backdrop-filter: blur(16px); + border: 1px solid $glass-border; + border-radius: $radius-lg; + box-shadow: $shadow-sm; + margin-bottom: $admin-space-lg; + animation: slideInLeft $transition-normal ease-out 0.05s both; + + @media (max-width: $admin-container-md) { + flex-wrap: wrap; + } +} + +.admin-search-box { + flex: 1; + min-width: 160px; + position: relative; + display: flex; + align-items: center; + + .search-icon { + position: absolute; + left: $admin-space-sm; + font-size: 1.125rem; + color: $color-text-tertiary; + pointer-events: none; + } + + .search-input { + width: 100%; + padding: $admin-space-sm $admin-space-2xl $admin-space-sm $admin-space-xl; + border: 1px solid rgba($color-gray-200, 0.6); + border-radius: $radius-md; + font-size: 0.8125rem; + font-weight: 500; + background: white; + transition: all $transition-fast; + + &::placeholder { + color: $color-text-tertiary; + } + + &:focus { + outline: none; + border-color: $color-brand-primary; + box-shadow: 0 0 0 3px rgba($color-brand-primary, 0.1); + } + } + + .clear-btn { + position: absolute; + right: $admin-space-xs; + background: none; + border: none; + padding: $admin-space-xs; + cursor: pointer; + color: $color-text-tertiary; + display: flex; + border-radius: 50%; + transition: all $transition-fast; + + &:hover { + background: rgba(0, 0, 0, 0.05); + color: $color-text-secondary; + } + + .material-symbols-outlined { + font-size: 1rem; + } + } +} + +.admin-filter-chips { + display: flex; + gap: $admin-space-xs; + flex-wrap: wrap; +} + +.admin-chip { + display: inline-flex; + align-items: center; + gap: $admin-space-xs; + padding: $admin-space-xs $admin-space-sm; + border: 1px solid rgba($color-gray-200, 0.8); + border-radius: $radius-full; + background: white; + font-size: 0.75rem; + font-weight: 600; + color: $color-text-secondary; + cursor: pointer; + transition: all $transition-fast; + white-space: nowrap; + + .material-symbols-outlined { + font-size: 0.875rem; + } + + &:hover { + border-color: $color-brand-primary; + color: $color-brand-primary; + } + + &.active { + background: $gradient-primary; + border-color: $color-brand-primary; + color: white; + } +} + +// ============================================ +// 🃏 CARDS +// ============================================ + +.admin-card { + background: $glass-bg; + backdrop-filter: blur(16px); + border: 1px solid $glass-border; + border-radius: $radius-lg; + box-shadow: $shadow-glass; + transition: all $transition-fast; + animation: scaleIn $transition-normal ease-out both; + overflow: hidden; + + &:hover { + box-shadow: $shadow-glass-hover; + transform: translateY(-2px); + border-color: rgba($color-brand-primary, 0.15); + } + + &__header { + display: flex; + justify-content: space-between; + align-items: center; + padding: $admin-space-md $admin-space-lg; + border-bottom: 1px solid rgba($color-gray-200, 0.5); + } + + &__title { + font-size: 0.9375rem; + font-weight: 700; + color: $color-text-primary; + margin: 0; + display: flex; + align-items: center; + gap: $admin-space-sm; + + .material-symbols-outlined { + font-size: 1.125rem; + color: $color-brand-primary; + } + } + + &__body { + padding: $admin-space-lg; + } + + &__footer { + display: flex; + gap: $admin-space-sm; + padding: $admin-space-md $admin-space-lg; + border-top: 1px solid rgba($color-gray-200, 0.5); + background: rgba($color-gray-50, 0.5); + } +} + +// ============================================ +// 📋 TABLE +// ============================================ + +.admin-table-container { + background: $glass-bg; + backdrop-filter: blur(16px); + border: 1px solid $glass-border; + border-radius: $radius-lg; + box-shadow: $shadow-glass; + overflow: hidden; + animation: fadeIn $transition-normal ease-out 0.1s both; +} + +.admin-table-header { + display: grid; + gap: $admin-space-sm; + padding: $admin-space-sm $admin-space-lg; + background: rgba($color-gray-50, 0.8); + font-size: 0.6875rem; + font-weight: 700; + color: $color-text-tertiary; + text-transform: uppercase; + letter-spacing: 0.5px; + border-bottom: 1px solid rgba($color-gray-200, 0.6); +} + +.admin-table-row { + display: grid; + gap: $admin-space-sm; + padding: $admin-space-md $admin-space-lg; + align-items: center; + border-bottom: 1px solid rgba($color-gray-200, 0.4); + transition: all $transition-fast; + animation: staggerFade $transition-fast ease-out both; + + @for $i from 1 through 30 { + &:nth-child(#{$i}) { + animation-delay: #{$i * 0.02}s; + } + } + + &:last-child { + border-bottom: none; + } + + &:hover { + background: rgba($color-brand-primary, 0.02); + } + + &--clickable { + cursor: pointer; + + &:hover { + background: rgba($color-brand-primary, 0.04); + } + } +} + +// ============================================ +// 💫 STATES (Loading, Empty, Error) +// ============================================ + +.admin-loading-state, +.admin-empty-state, +.admin-error-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: $admin-space-2xl $admin-space-lg; + text-align: center; + background: $glass-bg; + backdrop-filter: blur(16px); + border: 1px solid $glass-border; + border-radius: $radius-lg; + box-shadow: $shadow-glass; + animation: scaleIn $transition-normal ease-out; + + .state-icon { + font-size: 3rem; + color: $color-text-tertiary; + margin-bottom: $admin-space-md; + animation: popIn $transition-bounce ease-out 0.1s both; + } + + h3 { + margin: 0 0 $admin-space-sm; + font-size: 1.125rem; + font-weight: 700; + color: $color-text-primary; + } + + p { + margin: 0 0 $admin-space-lg; + color: $color-text-secondary; + max-width: 360px; + font-size: 0.875rem; + } +} + +.admin-spinner { + width: 32px; + height: 32px; + border: 3px solid rgba($color-brand-primary, 0.15); + border-top-color: $color-brand-primary; + border-radius: 50%; + margin-bottom: $admin-space-md; + animation: spin 0.8s linear infinite; +} + +// Skeleton loading +.admin-skeleton { + background: linear-gradient( + 90deg, + rgba($color-gray-200, 0.6) 25%, + rgba($color-gray-100, 0.6) 50%, + rgba($color-gray-200, 0.6) 75% + ); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; + border-radius: $radius-sm; + + &--text { + height: 14px; + width: 60%; + } + + &--title { + height: 20px; + width: 40%; + } + + &--avatar { + width: 40px; + height: 40px; + border-radius: 50%; + } +} + +// ============================================ +// 🏷️ BADGES +// ============================================ + +.admin-badge { + display: inline-flex; + align-items: center; + gap: $admin-space-xs; + padding: $admin-space-xs $admin-space-sm; + border-radius: $radius-full; + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.3px; + background: rgba($color-gray-500, 0.1); + color: $color-text-secondary; + + .material-symbols-outlined { + font-size: 0.75rem; + } + + &--success { + background: rgba($color-success, 0.12); + color: $color-success; + } + + &--warning { + background: rgba($color-warning, 0.12); + color: darken($color-warning, 10%); + } + + &--error { + background: rgba($color-error, 0.12); + color: $color-error; + } + + &--primary { + background: rgba($color-brand-primary, 0.12); + color: $color-brand-primary; + } +} + +// Status dot +.admin-status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: $color-gray-400; + flex-shrink: 0; + + &--active, + &--success { + background: $color-success; + box-shadow: 0 0 8px rgba($color-success, 0.5); + } + + &--warning { + background: $color-warning; + box-shadow: 0 0 8px rgba($color-warning, 0.5); + } + + &--error { + background: $color-error; + box-shadow: 0 0 8px rgba($color-error, 0.5); + } + + &--pending { + background: $color-warning; + animation: pulse 2s ease-in-out infinite; + } +} + +// ============================================ +// 📝 FORM ELEMENTS +// ============================================ + +.admin-form-group { + margin-bottom: $admin-space-md; + + &:last-child { + margin-bottom: 0; + } + + label { + display: block; + margin-bottom: $admin-space-xs; + font-size: 0.8125rem; + font-weight: 600; + color: $color-text-primary; + } +} + +.admin-input, +.admin-textarea, +.admin-select { + width: 100%; + border: 1px solid rgba($color-gray-200, 0.6); + border-radius: $radius-md; + padding: $admin-space-sm $admin-space-md; + font-size: 0.8125rem; + font-weight: 500; + background: white; + color: $color-text-primary; + transition: all $transition-fast; + font-family: inherit; + + &::placeholder { + color: $color-text-tertiary; + } + + &:focus { + outline: none; + border-color: $color-brand-primary; + box-shadow: 0 0 0 3px rgba($color-brand-primary, 0.1); + } + + &:disabled { + background: rgba($color-gray-100, 0.5); + cursor: not-allowed; + opacity: 0.6; + } +} + +.admin-textarea { + resize: vertical; + min-height: 80px; + line-height: 1.5; +} + +.admin-help-text { + display: block; + margin-top: $admin-space-xs; + font-size: 0.6875rem; + color: $color-text-tertiary; +} + +// ============================================ +// 🗂️ TABS +// ============================================ + +.admin-tabs { + display: flex; + gap: $admin-space-xs; + padding: $admin-space-xs; + background: rgba($color-gray-100, 0.5); + border-radius: $radius-md; + margin-bottom: $admin-space-lg; +} + +.admin-tab { + display: flex; + align-items: center; + gap: $admin-space-xs; + padding: $admin-space-sm $admin-space-md; + background: transparent; + border: none; + border-radius: $radius-sm; + cursor: pointer; + color: $color-text-secondary; + font-size: 0.8125rem; + font-weight: 600; + transition: all $transition-fast; + + .material-symbols-outlined { + font-size: 1rem; + } + + &:hover { + color: $color-brand-primary; + background: rgba(white, 0.5); + } + + &.active { + background: white; + color: $color-brand-primary; + box-shadow: $shadow-sm; + } +} + +// ============================================ +// 🔲 MODAL +// ============================================ + +.admin-modal-overlay { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.6); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + padding: $admin-space-lg; + z-index: 2000; + animation: fadeIn $transition-fast ease; +} + +.admin-modal { + width: 100%; + max-width: 560px; + max-height: 90vh; + background: white; + border-radius: $radius-xl; + box-shadow: $shadow-2xl; + display: flex; + flex-direction: column; + animation: scaleIn $transition-normal ease-out; + overflow: hidden; + + &--lg, &--large { + max-width: 800px; + } + + &--sm, &--small { + max-width: 400px; + } + + &--full { + max-width: calc(100vw - 2rem); + max-height: calc(100vh - 2rem); + } +} + +.admin-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: $admin-space-lg; + border-bottom: 1px solid $color-gray-200; + + h2 { + margin: 0; + font-size: 1.125rem; + font-weight: 700; + color: $color-text-primary; + } + + .close-btn { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + border-radius: $radius-md; + cursor: pointer; + color: $color-text-tertiary; + transition: all $transition-fast; + + &:hover { + background: $color-gray-100; + color: $color-text-primary; + } + } +} + +.admin-modal-body { + flex: 1; + overflow-y: auto; + padding: $admin-space-lg; +} + +.admin-modal-footer { + display: flex; + justify-content: flex-end; + gap: $admin-space-sm; + padding: $admin-space-lg; + border-top: 1px solid $color-gray-200; + background: rgba($color-gray-50, 0.5); +} + +// ============================================ +// 📜 LIST +// ============================================ + +.admin-list { + display: flex; + flex-direction: column; + gap: $admin-space-sm; +} + +.admin-list-item { + display: flex; + justify-content: space-between; + align-items: center; + gap: $admin-space-md; + padding: $admin-space-md $admin-space-lg; + background: $glass-bg; + backdrop-filter: blur(16px); + border: 1px solid $glass-border; + border-radius: $radius-lg; + transition: all $transition-fast; + animation: staggerFade $transition-fast ease-out both; + + @for $i from 1 through 20 { + &:nth-child(#{$i}) { + animation-delay: #{$i * 0.03}s; + } + } + + &:hover { + box-shadow: $shadow-md; + border-color: rgba($color-brand-primary, 0.2); + transform: translateY(-1px); + } + + &--clickable { + cursor: pointer; + + &:hover { + background: rgba($color-brand-primary, 0.02); + } + } +} + +// ============================================ +// 📌 SECTION HEADER +// ============================================ + +.admin-section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: $admin-space-md; + animation: slideInLeft $transition-normal ease-out; + + h2 { + font-size: 1rem; + font-weight: 700; + color: $color-text-primary; + margin: 0; + display: flex; + align-items: center; + gap: $admin-space-sm; + + .material-symbols-outlined { + font-size: 1.125rem; + color: $color-brand-primary; + } + } +} + +// ============================================ +// 📱 MOBILE SPECIFIC +// ============================================ + +// Swipeable cards container +.admin-swipe-container { + @media (max-width: $admin-container-md) { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scroll-snap-type: x mandatory; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } + + > * { + scroll-snap-align: start; + } + } +} + +// Bottom sheet style for mobile +.admin-bottom-sheet { + @media (max-width: $admin-container-md) { + position: fixed; + bottom: 0; + left: 0; + right: 0; + max-height: 85vh; + border-radius: $radius-xl $radius-xl 0 0; + animation: slideUp $transition-normal ease-out; + } +} + +@keyframes slideUp { + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } +} + +// ============================================ +// ♿ MOTION PREFERENCES +// ============================================ + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + +// ============================================ +// 🔧 ADDITIONAL UTILITY CLASSES +// ============================================ + +// Button variants +.admin-btn--success { + background: rgba($color-success, 0.1); + color: $color-success; + border-color: rgba($color-success, 0.2); + + &:hover:not(:disabled) { + background: rgba($color-success, 0.15); + border-color: $color-success; + } +} + +.admin-btn--warning { + background: rgba($color-warning, 0.1); + color: darken($color-warning, 10%); + border-color: rgba($color-warning, 0.2); + + &:hover:not(:disabled) { + background: rgba($color-warning, 0.15); + border-color: $color-warning; + } +} + +// Form row utilities +.form-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: $admin-space-md; + margin-bottom: $admin-space-md; + + &.two-col { + grid-template-columns: 1fr 1fr; + } + + &.three-col { + grid-template-columns: 1fr 1fr 1fr; + } + + @media (max-width: $admin-container-sm) { + grid-template-columns: 1fr; + + &.two-col, &.three-col { + grid-template-columns: 1fr; + } + } +} + +// Form section +.form-section { + margin-bottom: $admin-space-xl; + padding-bottom: $admin-space-lg; + border-bottom: 1px solid rgba($color-gray-200, 0.5); + + &:last-child { + margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; + } + + h3 { + font-size: 1rem; + font-weight: 700; + color: $color-text-primary; + margin: 0 0 $admin-space-md; + display: flex; + align-items: center; + gap: $admin-space-sm; + + .material-symbols-outlined, + app-icon { + font-size: 1.125rem; + color: $color-brand-primary; + } + } +} + +// Switch toggle +.switch { + position: relative; + display: inline-block; + width: 48px; + height: 24px; + flex-shrink: 0; + + input { + opacity: 0; + width: 0; + height: 0; + + &:checked + .slider { + background: $gradient-primary; + } + + &:checked + .slider:before { + transform: translateX(24px); + } + + &:focus + .slider { + box-shadow: 0 0 0 3px rgba($color-brand-primary, 0.2); + } + } + + .slider { + position: absolute; + cursor: pointer; + inset: 0; + background: $color-gray-300; + border-radius: $radius-full; + transition: all $transition-fast; + + &:before { + position: absolute; + content: ""; + height: 20px; + width: 20px; + left: 2px; + bottom: 2px; + background: white; + border-radius: 50%; + transition: all $transition-fast; + box-shadow: $shadow-sm; + } + } +} + +.switch-group { + display: flex; + align-items: flex-start; + gap: $admin-space-md; + + .switch-label { + display: flex; + flex-direction: column; + gap: $admin-space-2xs; + + .label-text { + font-weight: 600; + color: $color-text-primary; + } + } +} + +// Checkbox styling +.checkbox-label { + display: flex; + align-items: center; + gap: $admin-space-sm; + cursor: pointer; + font-weight: 500; + + input[type="checkbox"] { + width: 18px; + height: 18px; + accent-color: $color-brand-primary; + } +} + +// Calendar specific styles +.calendar-nav { + display: flex; + align-items: center; + justify-content: space-between; + padding: $admin-space-md; + background: $glass-bg; + backdrop-filter: blur(16px); + border: 1px solid $glass-border; + border-radius: $radius-lg; + margin-bottom: $admin-space-lg; + + .nav-center { + display: flex; + align-items: center; + gap: $admin-space-md; + + h2 { + margin: 0; + font-size: 1.125rem; + font-weight: 700; + } + } +} + +.calendar-container { + background: $glass-bg; + backdrop-filter: blur(16px); + border: 1px solid $glass-border; + border-radius: $radius-lg; + overflow: hidden; + margin-bottom: $admin-space-lg; +} + +.calendar-header { + display: grid; + grid-template-columns: repeat(7, 1fr); + background: rgba($color-gray-100, 0.5); + border-bottom: 1px solid $glass-border; + + .weekday { + padding: $admin-space-sm; + text-align: center; + font-size: 0.75rem; + font-weight: 600; + color: $color-text-tertiary; + text-transform: uppercase; + } +} + +.calendar-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); +} + +.calendar-day { + min-height: 100px; + padding: $admin-space-sm; + border-right: 1px solid rgba($color-gray-200, 0.3); + border-bottom: 1px solid rgba($color-gray-200, 0.3); + position: relative; + transition: all $transition-fast; + + &:nth-child(7n) { + border-right: none; + } + + &.other-month { + background: rgba($color-gray-100, 0.3); + + .day-number { + color: $color-text-tertiary; + } + } + + &.today { + background: rgba($color-brand-primary, 0.05); + + .day-number { + background: $gradient-primary; + color: white; + border-radius: 50%; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + } + } + + &:hover { + background: rgba($color-brand-primary, 0.02); + } + + .day-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: $admin-space-xs; + } + + .day-number { + font-weight: 600; + font-size: 0.875rem; + } + + .slot-count { + font-size: 0.6875rem; + font-weight: 600; + background: rgba($color-brand-primary, 0.1); + color: $color-brand-primary; + padding: 2px 6px; + border-radius: $radius-full; + } + + .day-slots { + display: flex; + flex-direction: column; + gap: 2px; + } + + .slot-preview { + display: flex; + align-items: center; + justify-content: space-between; + padding: 2px 6px; + border-radius: $radius-sm; + font-size: 0.6875rem; + cursor: pointer; + transition: all $transition-fast; + + &.available { + background: rgba($color-success, 0.1); + color: $color-success; + } + + &.booked { + background: rgba($color-warning, 0.1); + color: darken($color-warning, 10%); + } + + &.unavailable { + background: rgba($color-error, 0.1); + color: $color-error; + } + + &:hover { + transform: scale(1.02); + } + } + + .more-slots { + font-size: 0.625rem; + color: $color-brand-primary; + background: none; + border: none; + cursor: pointer; + padding: 2px; + text-align: center; + + &:hover { + text-decoration: underline; + } + } + + .btn-add-slot { + position: absolute; + bottom: $admin-space-xs; + right: $admin-space-xs; + width: 24px; + height: 24px; + border-radius: 50%; + background: transparent; + border: 1px dashed $color-gray-300; + color: $color-gray-400; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: all $transition-fast; + + .material-symbols-outlined, + app-icon { + font-size: 0.875rem; + } + + &:hover { + border-color: $color-brand-primary; + color: $color-brand-primary; + background: rgba($color-brand-primary, 0.05); + } + } + + &:hover .btn-add-slot { + opacity: 1; + } +} + +// Mobile view specific +.mobile-list-view { + display: none; + + @media (max-width: $admin-container-md) { + display: block; + padding: $admin-space-md; + } +} + +.calendar-container, +.calendar-nav { + @media (max-width: $admin-container-md) { + display: none; + } +} + +// Preview box +.preview-box { + display: flex; + align-items: center; + gap: $admin-space-sm; + padding: $admin-space-md; + background: rgba($color-brand-primary, 0.05); + border: 1px solid rgba($color-brand-primary, 0.2); + border-radius: $radius-md; + color: $color-brand-primary; + font-weight: 600; + margin-top: $admin-space-md; +} + +// Pattern grid +.pattern-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); + gap: $admin-space-sm; +} + +// Tags editor +.tags-editor { + display: flex; + flex-direction: column; + gap: $admin-space-sm; + + .tag-input { + display: flex; + gap: $admin-space-sm; + + .admin-input { + flex: 1; + } + } +} + +// List add +.list-add { + display: flex; + gap: $admin-space-sm; + margin-top: $admin-space-sm; + + &--inline { + flex-direction: row; + + .admin-input { + flex: 1; + } + } +} diff --git a/apps/frontend/src/app/components/admin/admin-users/admin-users.component.html b/apps/frontend/src/app/components/admin/admin-users/admin-users.component.html new file mode 100644 index 0000000..24af664 --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-users/admin-users.component.html @@ -0,0 +1,173 @@ + +
+
+ + + + + +
+ + +
+ + + +
+
+ + +
+
+

Lade Benutzer...

+
+ + +
+ +

{{ error }}

+ +
+ + +
+ +

Keine Benutzer gefunden

+ +
+ + +
+
+ +
+
+ {{ user.name.charAt(0).toUpperCase() }} + + + +
+ +
+ + +
+ + + {{ formatDate(user.createdAt) }} + + +
+ + + +
+
+ + + + +
+
+
+ + +
+ + {{ toastMessage }} +
\ No newline at end of file diff --git a/apps/frontend/src/app/components/admin/admin-users/admin-users.component.scss b/apps/frontend/src/app/components/admin/admin-users/admin-users.component.scss new file mode 100644 index 0000000..1cf5711 --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-users/admin-users.component.scss @@ -0,0 +1,530 @@ +@import '../admin-shared.scss'; + +// ===== ADMIN USERS - Compact & Mobile-First ===== + +.admin-users { + min-height: calc(100vh - 72px); + padding: $admin-space-lg; + background: $gradient-bg; + animation: pageEnter 0.4s ease-out; + + .container { + display: flex; + flex-direction: column; + gap: $admin-space-md; + max-width: $admin-container-xl; + margin: 0 auto; + } + + @media (max-width: $admin-container-md) { + padding: $admin-space-md; + } +} + +// ===== PAGE HEADER ===== +.page-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: $admin-space-md; + padding: $admin-space-md $admin-space-lg; + background: $glass-bg; + backdrop-filter: blur(20px); + border: 1px solid $glass-border; + border-radius: $radius-lg; + box-shadow: $shadow-glass; + animation: slideInLeft 0.3s ease-out; + + @media (max-width: $admin-container-sm) { + flex-direction: column; + align-items: stretch; + padding: $admin-space-md; + } +} + +.header-content { + h1 { + font-size: 1.25rem; + font-weight: 800; + color: $color-text-primary; + margin: 0; + line-height: 1.2; + } + + .subtitle { + font-size: 0.75rem; + color: $color-text-secondary; + margin: $admin-space-xs 0 0; + } +} + +.header-stats { + display: flex; + gap: $admin-space-xs; + + @media (max-width: $admin-container-sm) { + justify-content: space-between; + } +} + +.mini-stat { + display: flex; + flex-direction: column; + align-items: center; + padding: $admin-space-xs $admin-space-sm; + background: rgba(255, 255, 255, 0.5); + border-radius: $radius-md; + min-width: 56px; + + &.highlight { + background: rgba($color-brand-primary, 0.1); + + .mini-stat__value { color: $color-brand-primary; } + } + + &__value { + font-size: 1.125rem; + font-weight: 800; + color: $color-text-primary; + line-height: 1; + } + + &__label { + font-size: 0.5625rem; + font-weight: 600; + color: $color-text-tertiary; + text-transform: uppercase; + margin-top: 2px; + } +} + +// ===== FILTERS BAR ===== +.filters-bar { + display: flex; + gap: $admin-space-sm; + align-items: center; + padding: $admin-space-sm $admin-space-md; + background: $glass-bg; + backdrop-filter: blur(20px); + border: 1px solid $glass-border; + border-radius: $radius-lg; + box-shadow: $shadow-sm; + animation: slideInLeft 0.3s ease-out 0.05s both; + + @media (max-width: $admin-container-sm) { + flex-wrap: wrap; + } +} + +.search-box { + flex: 1; + min-width: 140px; + position: relative; + display: flex; + align-items: center; + + .search-icon { + position: absolute; + left: $admin-space-sm; + font-size: 1.125rem; + color: $color-text-tertiary; + pointer-events: none; + } + + .search-input { + width: 100%; + padding: $admin-space-sm 2rem $admin-space-sm 2.25rem; + border: 1px solid rgba(226, 232, 240, 0.6); + border-radius: $radius-md; + font-size: 0.8125rem; + font-weight: 500; + background: #fff; + transition: all 0.2s; + + &::placeholder { color: $color-text-tertiary; } + + &:focus { + outline: none; + border-color: $color-brand-primary; + box-shadow: 0 0 0 3px rgba($color-brand-primary, 0.1); + } + } + + .clear-btn { + position: absolute; + right: $admin-space-xs; + background: none; + border: none; + padding: 2px; + cursor: pointer; + color: $color-text-tertiary; + display: flex; + border-radius: 50%; + transition: all 0.2s; + + &:hover { + background: rgba(0, 0, 0, 0.05); + color: $color-text-secondary; + } + + .material-symbols-rounded { font-size: 1rem; } + } +} + +.filter-chips { + display: flex; + gap: $admin-space-xs; +} + +.chip { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: $admin-space-xs $admin-space-sm; + border: 1px solid rgba(226, 232, 240, 0.8); + border-radius: $radius-full; + background: #fff; + font-size: 0.6875rem; + font-weight: 600; + color: $color-text-secondary; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; + + .material-symbols-rounded { font-size: 0.875rem; } + + &:hover { + border-color: $color-brand-primary; + color: $color-brand-primary; + } + + &.active { + background: $color-brand-primary; + border-color: $color-brand-primary; + color: #fff; + } +} + +// ===== STATE MESSAGES ===== +.state-message { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: $admin-space-sm; + padding: $admin-space-2xl $admin-space-lg; + background: $glass-bg; + backdrop-filter: blur(20px); + border: 1px solid $glass-border; + border-radius: $radius-lg; + text-align: center; + animation: scaleIn 0.3s ease-out; + + .material-symbols-rounded { + font-size: 2.5rem; + color: $color-text-tertiary; + } + + p { + font-size: 0.875rem; + color: $color-text-secondary; + margin: 0; + } + + &.error { + .material-symbols-rounded { color: $color-error; } + p { color: $color-error; } + } +} + +.spinner { + width: 28px; + height: 28px; + border: 3px solid rgba($color-brand-primary, 0.15); + border-top-color: $color-brand-primary; + border-radius: 50%; + animation: spin 0.7s linear infinite; +} + +// ===== USERS LIST ===== +.users-list { + display: flex; + flex-direction: column; + gap: $admin-space-sm; +} + +.user-card { + display: grid; + grid-template-columns: 1fr auto auto; + align-items: center; + gap: $admin-space-md; + padding: $admin-space-md; + background: $glass-bg; + backdrop-filter: blur(20px); + border: 1px solid $glass-border; + border-radius: $radius-lg; + transition: all 0.2s; + animation: staggerFade 0.25s ease-out both; + + @for $i from 1 through 20 { + &:nth-child(#{$i}) { + animation-delay: #{$i * 0.02}s; + } + } + + &:hover { + box-shadow: $shadow-glass-hover; + transform: translateY(-1px); + } + + &.is-admin { + border-left: 3px solid $color-brand-primary; + } + + @media (max-width: $admin-container-md) { + grid-template-columns: 1fr; + gap: $admin-space-sm; + } +} + +.user-main { + display: flex; + align-items: center; + gap: $admin-space-sm; + min-width: 0; +} + +.user-avatar { + position: relative; + width: 40px; + height: 40px; + border-radius: $radius-md; + background: linear-gradient(135deg, #64748b, #475569); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 0.9375rem; + flex-shrink: 0; + + &.admin { + background: $gradient-primary; + box-shadow: $shadow-brand; + } + + .admin-badge { + position: absolute; + bottom: -3px; + right: -3px; + width: 16px; + height: 16px; + background: #fff; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); + + .material-symbols-rounded { + font-size: 0.625rem; + color: $color-brand-primary; + } + } +} + +.user-info { + min-width: 0; + flex: 1; +} + +.user-name { + font-weight: 600; + font-size: 0.875rem; + color: $color-text-primary; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.user-email { + display: inline-flex; + align-items: center; + gap: 0.25rem; + font-size: 0.75rem; + color: $color-text-secondary; + cursor: pointer; + transition: all 0.2s; + padding: 1px 0; + + .copy-icon { + font-size: 0.75rem; + opacity: 0; + transition: opacity 0.2s; + } + + &:hover { + color: $color-brand-primary; + .copy-icon { opacity: 1; } + } +} + +.user-meta { + display: flex; + align-items: center; + gap: $admin-space-sm; + + @media (max-width: $admin-container-md) { + padding-left: 48px; + } +} + +.meta-item { + display: flex; + align-items: center; + gap: 0.25rem; + font-size: 0.6875rem; + color: $color-text-tertiary; + + .material-symbols-rounded { font-size: 0.875rem; } + + &.newsletter { color: $color-brand-primary; } +} + +.user-actions { + display: flex; + gap: $admin-space-xs; + + @media (max-width: $admin-container-md) { + padding-left: 48px; + + .action-btn { + flex: 1; + height: 36px; + width: auto; + padding: 0 $admin-space-sm; + gap: $admin-space-xs; + font-size: 0.6875rem; + font-weight: 600; + + &::after { content: attr(title); } + } + } +} + +.action-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: none; + border-radius: $radius-md; + cursor: pointer; + transition: all 0.2s; + background: rgba(0, 0, 0, 0.03); + color: $color-text-secondary; + + .material-symbols-rounded { font-size: 1.125rem; } + + &:hover:not(:disabled) { transform: translateY(-1px); } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + &.promote:hover:not(:disabled) { + background: rgba($color-brand-primary, 0.1); + color: $color-brand-primary; + } + + &.demote:hover:not(:disabled) { + background: rgba(245, 158, 11, 0.1); + color: #d97706; + } + + &.delete:hover:not(:disabled) { + background: rgba($color-error, 0.1); + color: $color-error; + } +} + +// ===== LIST FOOTER ===== +.list-footer { + text-align: center; + padding: $admin-space-sm; + font-size: 0.75rem; + color: $color-text-tertiary; + background: $glass-bg; + backdrop-filter: blur(20px); + border: 1px solid $glass-border; + border-radius: $radius-lg; +} + +// ===== TOAST ===== +.toast { + position: fixed; + bottom: 5rem; + left: 50%; + transform: translateX(-50%) translateY(100px); + display: flex; + align-items: center; + gap: $admin-space-sm; + padding: $admin-space-sm $admin-space-md; + background: #1e293b; + color: #fff; + border-radius: $radius-lg; + font-size: 0.8125rem; + font-weight: 500; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); + opacity: 0; + visibility: hidden; + transition: all 0.3s ease; + z-index: 1000; + + .material-symbols-rounded { + font-size: 1.125rem; + color: #22c55e; + } + + &.visible { + transform: translateX(-50%) translateY(0); + opacity: 1; + visibility: visible; + } + + &.error .material-symbols-rounded { color: #ef4444; } +} + +// ===== BUTTONS ===== +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: $admin-space-xs; + padding: $admin-space-sm $admin-space-md; + border-radius: $radius-md; + font-weight: 600; + font-size: 0.8125rem; + cursor: pointer; + border: none; + transition: all 0.2s; + + &--secondary { + background: rgba($color-brand-primary, 0.1); + color: $color-brand-primary; + + &:hover { background: rgba($color-brand-primary, 0.15); } + } + + &--ghost { + background: transparent; + color: $color-text-secondary; + + &:hover { + background: rgba(0, 0, 0, 0.05); + color: $color-text-primary; + } + } +} diff --git a/apps/frontend/src/app/components/admin/admin-users/admin-users.component.spec.ts b/apps/frontend/src/app/components/admin/admin-users/admin-users.component.spec.ts new file mode 100644 index 0000000..222eb0e --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-users/admin-users.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AdminUsersComponent } from './admin-users.component'; + +describe('AdminUsersComponent', () => { + let component: AdminUsersComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AdminUsersComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AdminUsersComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/frontend/src/app/components/admin/admin-users/admin-users.component.ts b/apps/frontend/src/app/components/admin/admin-users/admin-users.component.ts new file mode 100644 index 0000000..da5e2a1 --- /dev/null +++ b/apps/frontend/src/app/components/admin/admin-users/admin-users.component.ts @@ -0,0 +1,204 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ApiService } from '../../../api/api.service'; +import { User, UserRole } from '../../../services/auth.service'; +import { AdminLayoutComponent } from '../admin-layout/admin-layout.component'; +import { ConfirmationService } from '../../../shared/confirmation/confirmation.service'; +import { IconComponent } from '../../../shared/icon/icon.component'; + +interface UserStats { + totalUsers: number; + newsletterSubscribers: number; + subscriberRate: number; +} + +@Component({ + selector: 'app-admin-users', + standalone: true, + imports: [CommonModule, FormsModule, AdminLayoutComponent, IconComponent], + templateUrl: './admin-users.component.html', + styleUrl: './admin-users.component.scss' +}) +export class AdminUsersComponent implements OnInit { + users: User[] = []; + stats: UserStats | null = null; + loading = true; + error = ''; + searchTerm = ''; + filterRole: 'all' | 'admin' | 'user' = 'all'; + processingUserId: string | null = null; + + // Toast + toastMessage = ''; + toastType: 'success' | 'error' = 'success'; + private toastTimeout: any; + + UserRole = UserRole; + + constructor( + private api: ApiService, + private confirmationService: ConfirmationService + ) { } + + ngOnInit(): void { + this.loadData(); + } + + loadData() { + this.loading = true; + this.error = ''; + + Promise.all([ + this.api.getAllUsers().toPromise(), + this.api.getUserStats().toPromise() + ]) + .then(([users, stats]) => { + this.users = users || []; + this.stats = stats || null; + this.loading = false; + }) + .catch((err) => { + console.error('Fehler beim Laden:', err); + this.error = 'Fehler beim Laden der Daten'; + this.loading = false; + }); + } + + get filteredUsers(): User[] { + return this.users.filter(user => { + const matchesSearch = !this.searchTerm || + user.name.toLowerCase().includes(this.searchTerm.toLowerCase()) || + user.email.toLowerCase().includes(this.searchTerm.toLowerCase()); + + const matchesRole = this.filterRole === 'all' || + (this.filterRole === 'admin' && user.role === UserRole.ADMIN) || + (this.filterRole === 'user' && user.role === UserRole.USER); + + return matchesSearch && matchesRole; + }); + } + + async deleteUser(user: User) { + const confirmed = await this.confirmationService.confirm({ + title: 'User löschen', + message: `Möchtest du "${user.name}" wirklich löschen?`, + confirmText: 'Löschen', + cancelText: 'Abbrechen', + type: 'danger', + icon: 'delete' + }); + + if (!confirmed) return; + + this.processingUserId = user.id; + this.api.deleteUser(user.id).subscribe({ + next: () => { + this.users = this.users.filter(u => u.id !== user.id); + this.showToast(`${user.name} wurde gelöscht`); + this.processingUserId = null; + this.loadData(); // Refresh stats + }, + error: (err) => { + console.error('Fehler beim Löschen:', err); + this.showToast('Fehler beim Löschen', 'error'); + this.processingUserId = null; + } + }); + } + + async makeAdmin(user: User) { + const confirmed = await this.confirmationService.confirm({ + title: 'Admin-Rechte vergeben', + message: `"${user.name}" zum Administrator machen?`, + confirmText: 'Zum Admin machen', + cancelText: 'Abbrechen', + type: 'warning', + icon: 'shield_person' + }); + + if (!confirmed) return; + + this.processingUserId = user.id; + this.api.updateUser(user.id, { role: UserRole.ADMIN }).subscribe({ + next: (updated) => { + const index = this.users.findIndex(u => u.id === user.id); + if (index !== -1) { + this.users[index] = updated; + } + this.showToast(`${user.name} ist jetzt Admin`); + this.processingUserId = null; + }, + error: (err) => { + console.error('Fehler:', err); + this.showToast('Fehler beim Aktualisieren', 'error'); + this.processingUserId = null; + } + }); + } + + async removeAdmin(user: User) { + const confirmed = await this.confirmationService.confirm({ + title: 'Admin-Rechte entfernen', + message: `Admin-Rechte von "${user.name}" entfernen?`, + confirmText: 'Entfernen', + cancelText: 'Abbrechen', + type: 'warning', + icon: 'remove_moderator' + }); + + if (!confirmed) return; + + this.processingUserId = user.id; + this.api.updateUser(user.id, { role: UserRole.USER }).subscribe({ + next: (updated) => { + const index = this.users.findIndex(u => u.id === user.id); + if (index !== -1) { + this.users[index] = updated; + } + this.showToast(`Admin-Rechte von ${user.name} entfernt`); + this.processingUserId = null; + }, + error: (err) => { + console.error('Fehler:', err); + this.showToast('Fehler beim Aktualisieren', 'error'); + this.processingUserId = null; + } + }); + } + + async copyEmail(email: string) { + try { + await navigator.clipboard.writeText(email); + this.showToast('E-Mail kopiert'); + } catch (err) { + this.showToast('Kopieren fehlgeschlagen', 'error'); + } + } + + showToast(message: string, type: 'success' | 'error' = 'success') { + if (this.toastTimeout) { + clearTimeout(this.toastTimeout); + } + this.toastMessage = message; + this.toastType = type; + this.toastTimeout = setTimeout(() => { + this.toastMessage = ''; + }, 3000); + } + + formatDate(dateString?: Date): string { + if (!dateString) return '-'; + const date = new Date(dateString); + return date.toLocaleDateString('de-DE', { + day: '2-digit', + month: '2-digit', + year: '2-digit' + }); + } + + clearFilters() { + this.searchTerm = ''; + this.filterRole = 'all'; + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/components/booking/booking.component.html b/apps/frontend/src/app/components/booking/booking.component.html new file mode 100644 index 0000000..b465573 --- /dev/null +++ b/apps/frontend/src/app/components/booking/booking.component.html @@ -0,0 +1,257 @@ +
+
+ + +
+
+
+ check_circle +
+

Termin erfolgreich gebucht! 🎉

+

Vielen Dank für deine Buchung!

+ +
+
+ event +
+ Dein Termin: +

{{ bookedSlotDate }}

+
+
+
+ schedule +
+ Uhrzeit: +

{{ bookedSlotTime }}

+
+
+
+ +
+

Was passiert als Nächstes?

+
    +
  • + email + Du bekommst gleich eine Bestätigungs-E-Mail mit dem Video-Call Link +
  • +
  • + videocam + Der Termin findet online per Video-Call statt +
  • +
  • + notifications + 24h vorher erhältst du eine Erinnerung +
  • +
+
+ +
+ + +
+ +

+ help + Falls du keine E-Mail erhalten solltest, check bitte deinen Spam-Ordner oder kontaktiere uns direkt. +

+
+
+ + +
+ +
+
+ event +
+

Lass uns reden

+
+
+ schedule + 30 Minuten +
+
+ videocam + Online per Video +
+
+ check_circle + Kostenlos +
+
+
+ + +
+
+

Lade verfügbare Termine...

+
+ + +
+ error +

{{ errorMessage }}

+
+ + +
+ check_circle +

{{ successMessage }}

+
+ + +
+ event_busy +

Aktuell keine Termine verfügbar

+

Bitte schau später noch einmal vorbei oder kontaktiere uns direkt.

+ +
+ + +
+ + +
+
+

Wähle einen Tag

+
+ + {{ weekNavigationLabel }} + +
+
+ +
+ +
+ + +
+

Wähle eine Uhrzeit

+
+ +
+
+
+ + +
+
+
+ person +

Deine Infos

+
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+ event_available + Dein Termin +
+
+

📅 {{ getFormattedDate() }}

+

🕐 {{ getFormattedTime() }} Uhr

+

⏱️ 30 Minuten

+

💻 Online per Video-Call

+
+
+ + + +

+ info + Du bekommst direkt eine Bestätigung per E-Mail mit dem Video-Call Link. +

+
+
+ + +
+

Was wir besprechen

+
    +
  • + chat + Was du brauchst & deine Ziele +
  • +
  • + calculate + Ehrliche Kosten-Einschätzung +
  • +
  • + calendar_month + Wie lange es dauert +
  • +
  • + route + Wie wir zusammen arbeiten +
  • +
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/apps/frontend/src/app/components/booking/booking.component.scss b/apps/frontend/src/app/components/booking/booking.component.scss new file mode 100644 index 0000000..e62f34d --- /dev/null +++ b/apps/frontend/src/app/components/booking/booking.component.scss @@ -0,0 +1,1508 @@ +@import '../../utils/shared-styles.scss'; + +/* ===== MOBILE FIRST BASE ===== */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; + + @media (min-width: 768px) { + padding: 0 1.5rem; + } +} + +.booking-page { + padding: 1.5rem 0 3rem; + min-height: 100vh; + + @media (min-width: 768px) { + padding: 3rem 0; + } +} + +/* ===== HERO ===== */ +.booking-hero { + text-align: center; + margin-bottom: 2rem; + display: grid; + gap: 0.75rem; + + @media (min-width: 768px) { + margin-bottom: 3rem; + gap: 1rem; + } + + .hero-icon { + width: 64px; + height: 64px; + margin: 0 auto; + background: linear-gradient(135deg, #2563eb, #3b82f6); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 8px 24px rgba(37, 99, 235, 0.3); + + @media (min-width: 768px) { + width: 80px; + height: 80px; + } + + .material-symbols-outlined { + font-size: 36px; + color: white; + + @media (min-width: 768px) { + font-size: 48px; + } + } + } + + h1 { + font-size: 1.75rem; + font-weight: 900; + color: $color-text-primary; + margin: 0; + + @media (min-width: 768px) { + font-size: clamp(2rem, 4vw, 3rem); + } + } + + .hero-facts { + display: flex; + gap: 1rem; + justify-content: center; + flex-wrap: wrap; + margin-top: 0.25rem; + + @media (min-width: 768px) { + gap: 2rem; + margin-top: 0.5rem; + } + + .fact-item { + display: flex; + align-items: center; + gap: 0.375rem; + font-size: 0.8125rem; + font-weight: 600; + color: $color-text-primary; + + @media (min-width: 768px) { + gap: 0.5rem; + font-size: 0.9375rem; + } + + .material-symbols-outlined { + color: #10b981; + font-size: 18px; + + @media (min-width: 768px) { + font-size: 20px; + } + } + } + } +} + +/* ===== STEP INDICATOR (Mobile) ===== */ +.step-indicator { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 1.5rem; + padding: 1rem; + background: white; + border: 2px solid #e2e8f0; + border-radius: 16px; + + @media (min-width: 1024px) { + display: none; + } + + .step { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.25rem; + flex: 1; + + .step-number { + width: 32px; + height: 32px; + border-radius: 50%; + background: #f1f5f9; + color: #94a3b8; + display: flex; + align-items: center; + justify-content: center; + font-weight: 800; + font-size: 0.875rem; + transition: all 0.2s ease; + } + + .step-label { + font-size: 0.6875rem; + font-weight: 600; + color: #94a3b8; + transition: all 0.2s ease; + text-align: center; + } + + &.active { + .step-number { + background: linear-gradient(135deg, #2563eb, #3b82f6); + color: white; + } + + .step-label { + color: $color-text-primary; + } + } + + &.completed { + .step-number { + background: #10b981; + color: white; + } + + .step-label { + color: #10b981; + } + } + } + + .step-divider { + flex: 1; + height: 2px; + background: #e2e8f0; + max-width: 40px; + } +} + +/* ===== STEP BADGE ===== */ +.step-badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + background: linear-gradient(135deg, #2563eb, #3b82f6); + color: white; + border-radius: 50%; + font-size: 0.875rem; + font-weight: 800; + margin-right: 0.5rem; + + @media (min-width: 1024px) { + display: none; + } +} + +/* ===== BOOKING GRID ===== */ +.booking-grid { + display: grid; + gap: 1.25rem; + grid-template-columns: 1fr; + + @media (min-width: 768px) { + gap: 1.5rem; + } + + @media (min-width: 1024px) { + grid-template-columns: 1.2fr 0.8fr; + gap: 2rem; + + .step-indicator { + display: none; + } + } +} + +/* ===== CALENDAR ===== */ +.calendar-section { + background: white; + border: 2px solid #e2e8f0; + border-radius: 16px; + padding: 1.25rem; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.04); + + @media (min-width: 768px) { + border-radius: 24px; + padding: 2rem; + } + + .section-header { + margin-bottom: 1rem; + + @media (min-width: 768px) { + margin-bottom: 1.5rem; + } + + h2 { + font-size: 1.25rem; + font-weight: 800; + color: $color-text-primary; + margin: 0; + display: flex; + align-items: center; + + @media (min-width: 768px) { + font-size: 1.5rem; + } + } + } + + .month-nav { + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; + margin-bottom: 1rem; + + @media (min-width: 768px) { + gap: 1rem; + margin-bottom: 1.5rem; + } + + .btn-nav { + width: 40px; + height: 40px; + background: linear-gradient(135deg, rgba(37, 99, 235, 0.1), rgba(59, 130, 246, 0.1)); + border: 1px solid rgba(37, 99, 235, 0.2); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + touch-action: manipulation; + + @media (min-width: 768px) { + width: 36px; + height: 36px; + } + + &:active:not(:disabled) { + transform: scale(0.95); + } + + &:hover:not(:disabled) { + background: linear-gradient(135deg, #2563eb, #3b82f6); + + .material-symbols-outlined { + color: white; + } + } + + &:disabled { + opacity: 0.3; + cursor: not-allowed; + } + + .material-symbols-outlined { + font-size: 24px; + color: #2563eb; + + @media (min-width: 768px) { + font-size: 20px; + } + } + } + + .month-label { + font-size: 1rem; + font-weight: 700; + color: $color-text-primary; + min-width: 140px; + text-align: center; + + @media (min-width: 768px) { + font-size: 0.9375rem; + min-width: 120px; + } + } + } + + .days-grid { + display: grid; + gap: 0.5rem; + grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); + margin-bottom: 0; + + @media (min-width: 480px) { + gap: 0.625rem; + grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); + } + + @media (min-width: 768px) { + gap: 0.75rem; + grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); + } + } + + .day-card { + background: #f8fafc; + border: 2px solid #e2e8f0; + border-radius: 12px; + padding: 0.875rem 0.5rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.125rem; + cursor: pointer; + transition: all 0.2s ease; + touch-action: manipulation; + min-height: 90px; + + @media (min-width: 768px) { + padding: 1rem; + gap: 0.25rem; + min-height: 100px; + } + + &:active:not(:disabled) { + transform: scale(0.97); + } + + &:hover:not(:disabled) { + border-color: #3b82f6; + transform: translateY(-2px); + } + + &.selected { + background: linear-gradient(135deg, #2563eb, #3b82f6); + border-color: #2563eb; + + .day-name, + .day-number, + .day-status { + color: white; + } + } + + &.disabled { + opacity: 0.4; + cursor: not-allowed; + } + + .day-name { + font-size: 0.6875rem; + font-weight: 600; + color: $color-text-secondary; + text-transform: uppercase; + + @media (min-width: 768px) { + font-size: 0.8125rem; + } + } + + .day-number { + font-size: 1.5rem; + font-weight: 800; + color: $color-text-primary; + line-height: 1; + + @media (min-width: 768px) { + font-size: 1.5rem; + } + } + + .day-status { + font-size: 0.6875rem; + font-weight: 600; + color: #10b981; + + @media (min-width: 768px) { + font-size: 0.75rem; + } + + &--full { + color: #ef4444; + } + } + } + + .slots-section { + padding-top: 1.25rem; + margin-top: 1.25rem; + border-top: 2px solid #e2e8f0; + + @media (min-width: 768px) { + padding-top: 2rem; + margin-top: 1.5rem; + } + + h3 { + font-size: 1.125rem; + font-weight: 800; + color: $color-text-primary; + margin: 0 0 1rem; + display: flex; + align-items: center; + + @media (min-width: 768px) { + font-size: 1.25rem; + } + } + + .slots-grid { + display: grid; + gap: 0.625rem; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + + @media (min-width: 480px) { + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + } + + @media (min-width: 768px) { + gap: 0.75rem; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + } + } + + .slot-card { + background: #f8fafc; + border: 2px solid #e2e8f0; + border-radius: 12px; + padding: 0.875rem; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + cursor: pointer; + transition: all 0.2s ease; + font-weight: 700; + font-size: 0.9375rem; + color: $color-text-primary; + white-space: nowrap; + touch-action: manipulation; + + @media (min-width: 768px) { + padding: 0.75rem; + } + + &:active { + transform: scale(0.97); + } + + &:hover { + border-color: #3b82f6; + background: white; + transform: translateY(-2px); + } + + &.selected { + background: linear-gradient(135deg, #2563eb, #3b82f6); + border-color: #2563eb; + color: white; + + .material-symbols-outlined { + color: white; + } + } + + .material-symbols-outlined { + font-size: 18px; + color: #2563eb; + } + } + } +} + +/* ===== FORM ===== */ +.form-section { + display: grid; + gap: 1.25rem; + align-content: flex-start; + + @media (min-width: 768px) { + gap: 1.5rem; + } + + .form-card { + background: white; + border: 2px solid #e2e8f0; + border-radius: 16px; + padding: 1.25rem; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.04); + + @media (min-width: 768px) { + border-radius: 24px; + padding: 2rem; + } + + .form-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 1.25rem; + + @media (min-width: 768px) { + gap: 0.75rem; + margin-bottom: 1.5rem; + } + + .material-symbols-outlined { + font-size: 28px; + color: #2563eb; + + @media (min-width: 768px) { + font-size: 32px; + } + } + + h3 { + font-size: 1.25rem; + font-weight: 800; + color: $color-text-primary; + margin: 0; + + @media (min-width: 768px) { + font-size: 1.5rem; + } + } + } + } + + .booking-form { + display: grid; + gap: 1rem; + + @media (min-width: 768px) { + gap: 1.25rem; + } + + .form-group { + display: grid; + gap: 0.5rem; + + label { + font-size: 0.875rem; + font-weight: 700; + color: $color-text-primary; + + @media (min-width: 768px) { + font-size: 0.9375rem; + } + } + + input, + textarea { + padding: 0.875rem 1rem; + border: 2px solid #e2e8f0; + border-radius: 12px; + font-size: 1rem; + font-family: inherit; + transition: all 0.2s ease; + -webkit-appearance: none; + appearance: none; + + @media (min-width: 768px) { + padding: 0.875rem 1rem; + } + + &:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.1); + } + + &::placeholder { + color: #94a3b8; + } + } + + textarea { + resize: vertical; + min-height: 80px; + + @media (min-width: 768px) { + min-height: 100px; + } + } + } + + .booking-summary { + background: linear-gradient(135deg, rgba(37, 99, 235, 0.05), rgba(59, 130, 246, 0.05)); + border: 2px solid rgba(37, 99, 235, 0.15); + border-radius: 12px; + padding: 1.25rem; + margin: 0.5rem 0; + + @media (min-width: 768px) { + border-radius: 16px; + padding: 1.5rem; + } + + .summary-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 1rem; + + .material-symbols-outlined { + font-size: 24px; + color: #2563eb; + } + + strong { + font-size: 1rem; + color: $color-text-primary; + + @media (min-width: 768px) { + font-size: 1.0625rem; + } + } + } + + .summary-content { + display: grid; + gap: 0.625rem; + + .summary-item { + display: flex; + align-items: center; + gap: 0.625rem; + font-size: 0.9375rem; + font-weight: 600; + color: $color-text-secondary; + + span:first-child { + font-size: 1.125rem; + flex-shrink: 0; + } + } + } + } + + .form-note { + display: flex; + align-items: flex-start; + gap: 0.5rem; + font-size: 0.8125rem; + color: $color-text-secondary; + margin: 0; + line-height: 1.5; + + @media (min-width: 768px) { + font-size: 0.875rem; + } + + .material-symbols-outlined { + font-size: 18px; + flex-shrink: 0; + margin-top: 0.125rem; + } + } + } + + .info-card { + background: #f8fafc; + border: 2px solid #e2e8f0; + border-radius: 12px; + padding: 1.25rem; + + @media (min-width: 768px) { + border-radius: 16px; + padding: 1.5rem; + } + + h4 { + font-size: 1rem; + font-weight: 800; + color: $color-text-primary; + margin: 0 0 1rem; + + @media (min-width: 768px) { + font-size: 1.125rem; + } + } + + .info-list { + list-style: none; + padding: 0; + margin: 0; + display: grid; + gap: 0.75rem; + + li { + display: flex; + align-items: flex-start; + gap: 0.625rem; + font-size: 0.875rem; + color: $color-text-secondary; + + @media (min-width: 768px) { + gap: 0.75rem; + font-size: 0.9375rem; + } + + .material-symbols-outlined { + font-size: 20px; + color: #2563eb; + flex-shrink: 0; + margin-top: 0.125rem; + } + } + } + } +} + +/* ===== STATES ===== */ + +// Loading State +.loading-state { + text-align: center; + padding: 3rem 1.5rem; + + @media (min-width: 768px) { + padding: 4rem 2rem; + } + + .spinner { + width: 40px; + height: 40px; + border: 3px solid #e2e8f0; + border-top-color: #2563eb; + border-radius: 50%; + animation: spin 0.8s linear infinite; + margin: 0 auto 1rem; + + @media (min-width: 768px) { + width: 48px; + height: 48px; + border-width: 4px; + } + } + + p { + color: $color-text-secondary; + font-weight: 600; + font-size: 0.9375rem; + + @media (min-width: 768px) { + font-size: 1rem; + } + } +} + +// Error Banner +.error-banner { + background: linear-gradient(135deg, rgba(239, 68, 68, 0.1), rgba(220, 38, 38, 0.1)); + border: 2px solid rgba(239, 68, 68, 0.3); + border-radius: 12px; + padding: 1.25rem; + display: flex; + align-items: center; + gap: 0.875rem; + margin-bottom: 1.5rem; + + @media (min-width: 768px) { + border-radius: 16px; + padding: 1.5rem; + gap: 1rem; + margin-bottom: 2rem; + } + + .material-symbols-outlined { + font-size: 28px; + color: #ef4444; + flex-shrink: 0; + + @media (min-width: 768px) { + font-size: 32px; + } + } + + p { + margin: 0; + color: #991b1b; + font-weight: 600; + font-size: 0.875rem; + line-height: 1.5; + + @media (min-width: 768px) { + font-size: 1rem; + } + } +} + +// Success Banner +.success-banner { + background: linear-gradient(135deg, rgba(16, 185, 129, 0.1), rgba(5, 150, 105, 0.1)); + border: 2px solid rgba(16, 185, 129, 0.3); + border-radius: 12px; + padding: 1.25rem; + display: flex; + align-items: center; + gap: 0.875rem; + margin-bottom: 1.5rem; + animation: slideDown 0.3s ease; + + @media (min-width: 768px) { + border-radius: 16px; + padding: 1.5rem; + gap: 1rem; + margin-bottom: 2rem; + } + + .material-symbols-outlined { + font-size: 28px; + color: #10b981; + flex-shrink: 0; + + @media (min-width: 768px) { + font-size: 32px; + } + } + + p { + margin: 0; + color: #065f46; + font-weight: 600; + font-size: 0.875rem; + line-height: 1.5; + + @media (min-width: 768px) { + font-size: 1rem; + } + } +} + +// Empty State +.empty-state { + text-align: center; + padding: 3rem 1.5rem; + + @media (min-width: 768px) { + padding: 4rem 2rem; + } + + .material-symbols-outlined { + font-size: 64px; + color: #cbd5e1; + margin-bottom: 1rem; + + @media (min-width: 768px) { + font-size: 80px; + } + } + + h2 { + font-size: 1.25rem; + font-weight: 800; + color: $color-text-primary; + margin: 0 0 0.5rem; + + @media (min-width: 768px) { + font-size: 1.5rem; + } + } + + p { + color: $color-text-secondary; + margin: 0 0 1.5rem; + font-size: 0.9375rem; + + @media (min-width: 768px) { + margin: 0 0 2rem; + font-size: 1rem; + } + } + + .btn { + display: inline-flex; + align-items: center; + gap: 0.5rem; + } +} + +// Small Spinner for Button +.spinner-small { + width: 18px; + height: 18px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-top-color: white; + border-radius: 50%; + animation: spin 0.8s linear infinite; + display: inline-block; + + @media (min-width: 768px) { + width: 20px; + height: 20px; + } +} + +/* ===== ANIMATIONS ===== */ +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-20px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +/* ===== BUTTON STATES ===== */ +.btn { + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } + + &:active:not(:disabled) { + transform: scale(0.98); + } +} + +/* ===== ACCESSIBILITY & UX ===== */ +@media (prefers-reduced-motion: reduce) { + * { + animation: none !important; + transition: none !important; + } +} + +// Improve touch targets on mobile +@media (max-width: 767px) { + + button, + input, + select, + textarea { + min-height: 44px; + } +} + +// Prevent zoom on input focus (iOS) +@media screen and (max-width: 767px) { + + input[type="text"], + input[type="email"], + input[type="tel"], + textarea { + font-size: 16px; + } +} + +/* ===== SUCCESS SCREEN ===== */ +.success-screen { + min-height: 80vh; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem 1rem; + animation: fadeIn 0.5s ease; + + @media (min-width: 768px) { + padding: 3rem 1.5rem; + } + + .success-content { + background: white; + border: 2px solid #e2e8f0; + border-radius: 24px; + padding: 2rem 1.5rem; + max-width: 600px; + width: 100%; + text-align: center; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06); + + @media (min-width: 768px) { + padding: 3rem 2.5rem; + border-radius: 32px; + } + + .success-icon { + width: 80px; + height: 80px; + margin: 0 auto 1.5rem; + background: linear-gradient(135deg, #10b981, #34d399); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 8px 24px rgba(16, 185, 129, 0.3); + animation: scaleIn 0.5s ease 0.2s both; + + @media (min-width: 768px) { + width: 96px; + height: 96px; + } + + .material-symbols-outlined { + font-size: 48px; + color: white; + + @media (min-width: 768px) { + font-size: 56px; + } + } + } + + h2 { + font-size: 1.5rem; + font-weight: 900; + color: $color-text-primary; + margin: 0 0 0.5rem; + animation: slideUp 0.5s ease 0.3s both; + + @media (min-width: 768px) { + font-size: 2rem; + } + } + + .success-subtitle { + font-size: 1rem; + color: $color-text-secondary; + margin: 0 0 2rem; + animation: slideUp 0.5s ease 0.4s both; + + @media (min-width: 768px) { + font-size: 1.125rem; + } + } + + .success-details { + display: grid; + gap: 1rem; + margin-bottom: 2rem; + animation: slideUp 0.5s ease 0.5s both; + + @media (min-width: 480px) { + grid-template-columns: 1fr 1fr; + } + + .detail-card { + background: linear-gradient(135deg, rgba(37, 99, 235, 0.05), rgba(59, 130, 246, 0.05)); + border: 2px solid rgba(37, 99, 235, 0.15); + border-radius: 16px; + padding: 1.25rem; + display: flex; + align-items: center; + gap: 1rem; + text-align: left; + + .material-symbols-outlined { + font-size: 32px; + color: #2563eb; + flex-shrink: 0; + } + + strong { + display: block; + font-size: 0.875rem; + color: $color-text-secondary; + margin-bottom: 0.25rem; + } + + p { + margin: 0; + font-size: 1rem; + font-weight: 700; + color: $color-text-primary; + } + } + } + + .success-info { + background: #f8fafc; + border-radius: 16px; + padding: 1.5rem; + margin-bottom: 2rem; + text-align: left; + animation: slideUp 0.5s ease 0.6s both; + + h3 { + font-size: 1rem; + font-weight: 800; + color: $color-text-primary; + margin: 0 0 1rem; + text-align: center; + + @media (min-width: 768px) { + font-size: 1.125rem; + } + } + + ul { + list-style: none; + padding: 0; + margin: 0; + display: grid; + gap: 1rem; + + li { + display: flex; + align-items: flex-start; + gap: 0.75rem; + font-size: 0.875rem; + color: $color-text-secondary; + line-height: 1.5; + + @media (min-width: 768px) { + font-size: 0.9375rem; + } + + .material-symbols-outlined { + font-size: 20px; + color: #10b981; + flex-shrink: 0; + margin-top: 0.125rem; + } + } + } + } + + .success-actions { + display: grid; + gap: 1rem; + margin-bottom: 1.5rem; + animation: slideUp 0.5s ease 0.7s both; + + @media (min-width: 480px) { + grid-template-columns: 1fr 1fr; + } + + .btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + width: 100%; + + &--secondary { + background: white; + color: $color-text-primary; + border: 2px solid #e2e8f0; + + &:hover { + background: #f8fafc; + border-color: #cbd5e1; + } + } + } + } + + .success-note { + display: flex; + align-items: flex-start; + gap: 0.5rem; + font-size: 0.75rem; + color: $color-text-secondary; + margin: 0; + line-height: 1.5; + text-align: left; + animation: slideUp 0.5s ease 0.8s both; + + @media (min-width: 768px) { + font-size: 0.8125rem; + } + + .material-symbols-outlined { + font-size: 16px; + flex-shrink: 0; + margin-top: 0.125rem; + } + } + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes scaleIn { + from { + transform: scale(0); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } +} + +@keyframes slideUp { + from { + transform: translateY(20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +/* ===== STATES ===== */ +/* ===== SUCCESS SCREEN ===== */ +.success-screen { + min-height: 80vh; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem 1rem; + animation: fadeIn 0.5s ease; + + @media (min-width: 768px) { + padding: 3rem 1.5rem; + } + + .success-content { + background: white; + border: 2px solid #e2e8f0; + border-radius: 24px; + padding: 2rem 1.5rem; + max-width: 600px; + width: 100%; + text-align: center; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06); + + @media (min-width: 768px) { + padding: 3rem 2.5rem; + border-radius: 32px; + } + + .success-icon { + width: 80px; + height: 80px; + margin: 0 auto 1.5rem; + background: linear-gradient(135deg, #10b981, #34d399); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 8px 24px rgba(16, 185, 129, 0.3); + animation: scaleIn 0.5s ease 0.2s both; + + @media (min-width: 768px) { + width: 96px; + height: 96px; + } + + .material-symbols-outlined { + font-size: 48px; + color: white; + + @media (min-width: 768px) { + font-size: 56px; + } + } + } + + h2 { + font-size: 1.5rem; + font-weight: 900; + color: $color-text-primary; + margin: 0 0 0.5rem; + animation: slideUp 0.5s ease 0.3s both; + + @media (min-width: 768px) { + font-size: 2rem; + } + } + + .success-subtitle { + font-size: 1rem; + color: $color-text-secondary; + margin: 0 0 2rem; + animation: slideUp 0.5s ease 0.4s both; + + @media (min-width: 768px) { + font-size: 1.125rem; + } + } + + .success-details { + display: grid; + gap: 1rem; + margin-bottom: 2rem; + animation: slideUp 0.5s ease 0.5s both; + + @media (min-width: 480px) { + grid-template-columns: 1fr 1fr; + } + + .detail-card { + background: linear-gradient(135deg, rgba(37, 99, 235, 0.05), rgba(59, 130, 246, 0.05)); + border: 2px solid rgba(37, 99, 235, 0.15); + border-radius: 16px; + padding: 1.25rem; + display: flex; + align-items: center; + gap: 1rem; + text-align: left; + + .material-symbols-outlined { + font-size: 32px; + color: #2563eb; + flex-shrink: 0; + } + + strong { + display: block; + font-size: 0.875rem; + color: $color-text-secondary; + margin-bottom: 0.25rem; + } + + p { + margin: 0; + font-size: 1rem; + font-weight: 700; + color: $color-text-primary; + } + } + } + + .success-info { + background: #f8fafc; + border-radius: 16px; + padding: 1.5rem; + margin-bottom: 2rem; + text-align: left; + animation: slideUp 0.5s ease 0.6s both; + + h3 { + font-size: 1rem; + font-weight: 800; + color: $color-text-primary; + margin: 0 0 1rem; + text-align: center; + + @media (min-width: 768px) { + font-size: 1.125rem; + } + } + + ul { + list-style: none; + padding: 0; + margin: 0; + display: grid; + gap: 1rem; + + li { + display: flex; + align-items: flex-start; + gap: 0.75rem; + font-size: 0.875rem; + color: $color-text-secondary; + line-height: 1.5; + + @media (min-width: 768px) { + font-size: 0.9375rem; + } + + .material-symbols-outlined { + font-size: 20px; + color: #10b981; + flex-shrink: 0; + margin-top: 0.125rem; + } + } + } + } + + .success-actions { + display: grid; + gap: 1rem; + margin-bottom: 1.5rem; + animation: slideUp 0.5s ease 0.7s both; + + @media (min-width: 480px) { + grid-template-columns: 1fr 1fr; + } + + .btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + width: 100%; + + &--secondary { + background: white; + color: $color-text-primary; + border: 2px solid #e2e8f0; + + &:hover { + background: #f8fafc; + border-color: #cbd5e1; + } + } + } + } + + .success-note { + display: flex; + align-items: flex-start; + gap: 0.5rem; + font-size: 0.75rem; + color: $color-text-secondary; + margin: 0; + line-height: 1.5; + text-align: left; + animation: slideUp 0.5s ease 0.8s both; + + @media (min-width: 768px) { + font-size: 0.8125rem; + } + + .material-symbols-outlined { + font-size: 16px; + flex-shrink: 0; + margin-top: 0.125rem; + } + } + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes scaleIn { + from { + transform: scale(0); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } +} + +@keyframes slideUp { + from { + transform: translateY(20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +/* ===== STATES ===== */ \ No newline at end of file diff --git a/apps/frontend/src/app/components/booking/booking.component.spec.ts b/apps/frontend/src/app/components/booking/booking.component.spec.ts new file mode 100644 index 0000000..8b49c60 --- /dev/null +++ b/apps/frontend/src/app/components/booking/booking.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BookingComponent } from './booking.component'; + +describe('BookingComponent', () => { + let component: BookingComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [BookingComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(BookingComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/frontend/src/app/components/booking/booking.component.ts b/apps/frontend/src/app/components/booking/booking.component.ts new file mode 100644 index 0000000..3ae104b --- /dev/null +++ b/apps/frontend/src/app/components/booking/booking.component.ts @@ -0,0 +1,463 @@ +import { CommonModule } from '@angular/common'; +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { Router } from '@angular/router'; +import { Subject, takeUntil } from 'rxjs'; +import { BookingSlot, DayWithSlots, ApiService, CreateBookingDto } from '../../api/api.service'; + +@Component({ + selector: 'app-booking', + standalone: true, + imports: [CommonModule, FormsModule], + templateUrl: './booking.component.html', + styleUrl: './booking.component.scss' +}) +export class BookingComponent implements OnInit, OnDestroy { + private destroy$ = new Subject(); + + // ==================== KONFIGURATION ==================== + + /** + * Mindestvorlaufzeit in Stunden + * Slots müssen mindestens X Stunden in der Zukunft liegen + */ + private readonly MIN_HOURS_ADVANCE = 2; + + private readonly MAX_WEEKS = 8; // Maximal 8 Wochen in die Zukunft + private readonly DAYS_PER_WEEK = 7; // Immer 7 Tage pro Woche anzeigen + + // ==================== STATE ==================== + + selectedDate: string = ''; + selectedSlot: BookingSlot | null = null; + currentWeekIndex: number = 0; + currentMonthLabel: string = ''; + totalWeeksInMonth: number = 0; + currentWeekOfMonth: number = 1; + isLoading: boolean = false; + isSubmitting: boolean = false; + errorMessage: string = ''; + successMessage: string = ''; + + // Success State + bookingSuccessful: boolean = false; + bookedSlotDate: string = ''; + bookedSlotTime: string = ''; + + availableSlots: BookingSlot[] = []; + weeks: DayWithSlots[][] = []; + allSlots: BookingSlot[] = []; + + // Form Data + bookingData = { + name: '', + email: '', + phone: '', + message: '' + }; + + constructor( + private apiService: ApiService, + public router: Router + ) { } + + ngOnInit(): void { + this.loadAvailableSlots(); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + formatDate(dateStr: string): string { + const date = this.parseLocalDate(dateStr); // statt new Date(dateStr) + return `${String(date.getDate()).padStart(2, '0')}.${String(date.getMonth() + 1).padStart(2, '0')}.${date.getFullYear()}`; + } + + formatTime(time: string): string { + const [hours, minutes] = time.split(':').map(Number); + const date = new Date(); + date.setHours(hours, minutes); + return date.toLocaleString('de-DE', { hour: 'numeric', minute: 'numeric', hour12: false }) || time; + } + + // ==================== DATA LOADING ==================== + + loadAvailableSlots(): void { + this.isLoading = true; + this.errorMessage = ''; + + const today = this.toLocalYMD(new Date()); + + this.apiService.getAvailableBookingSlots(today) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (slots) => { + // Slots filtern: nur zukünftige mit Mindestvorlauf + const filteredSlots = this.filterValidSlots(slots); + this.allSlots = filteredSlots; + + // Wochen erzeugen + this.generateWeeksFromSlots(filteredSlots); + + // Neu: nur zur ersten verfügbaren Woche springen (ohne Auswahl) + this.jumpToFirstAvailableWeek(); + + this.isLoading = false; + }, + error: (error) => { + console.error('Error loading slots:', error); + this.errorMessage = 'Slots konnten nicht geladen werden. Bitte versuche es später erneut.'; + this.isLoading = false; + } + }); + } + + /** + * Filtert Slots nach folgenden Kriterien: + * - Slot liegt in der Zukunft + * - Slot liegt mindestens MIN_HOURS_ADVANCE Stunden in der Zukunft + */ + private filterValidSlots(slots: BookingSlot[]): BookingSlot[] { + const now = new Date(); + const minDateTime = new Date(now.getTime() + this.MIN_HOURS_ADVANCE * 60 * 60 * 1000); + + return slots.filter(slot => { + const slotDateTime = this.getSlotDateTime(slot.date, slot.timeFrom); + return slotDateTime >= minDateTime; + }); + } + + /** Erstellt ein Date-Objekt aus Datum und Uhrzeit */ + private getSlotDateTime(date: string, time: string): Date { + const [hours, minutes] = time.split(':').map(Number); + const base = this.parseLocalDate(date); + base.setHours(hours, minutes, 0, 0); + return base; + } + + generateWeeksFromSlots(slots: BookingSlot[]): void { + // Slots nach Datum gruppieren + const slotsByDate = new Map(); + + slots.forEach(slot => { + if (!slotsByDate.has(slot.date)) { + slotsByDate.set(slot.date, []); + } + slotsByDate.get(slot.date)!.push(slot); + }); + + // Startdatum: Heute + const startDate = new Date(); + startDate.setHours(0, 0, 0, 0); + + // Immer am Anfang der Woche starten (Montag) + const dow = (startDate.getDay() + 6) % 7; + startDate.setDate(startDate.getDate() - dow); + startDate.setHours(0, 0, 0, 0); + + this.weeks = []; + + // Wochen generieren + for (let weekIndex = 0; weekIndex < this.MAX_WEEKS; weekIndex++) { + const week: DayWithSlots[] = []; + + for (let dayIndex = 0; dayIndex < this.DAYS_PER_WEEK; dayIndex++) { + const currentDate = new Date(startDate); + currentDate.setDate(startDate.getDate() + (weekIndex * 7) + dayIndex); + + const dateStr = this.toLocalYMD(currentDate); + const daySlots = slotsByDate.get(dateStr) || []; + + // Nur validierte Slots für diesen Tag + const validDaySlots = this.filterValidSlots(daySlots); + + week.push({ + date: dateStr, + dayName: this.getDayName(currentDate.getDay()), + dayNumber: currentDate.getDate(), + available: validDaySlots.length > 0, + slots: validDaySlots, + isPast: currentDate < new Date() + }); + } + + this.weeks.push(week); + } + } + + getDayName(dayNumber: number): string { + const days = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa']; + return days[dayNumber]; + } + + get currentWeek(): DayWithSlots[] { + return this.weeks[this.currentWeekIndex] || []; + } + + updateWeekLabels(): void { + if (this.currentWeek.length === 0) return; + + const firstDay = new Date(this.currentWeek[0].date + 'T12:00:00'); + const months = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', + 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember']; + + // Aktuellen Monat bestimmen + this.currentMonthLabel = months[firstDay.getMonth()]; + + // Welche Woche des Monats? + const firstDayOfMonth = new Date(firstDay.getFullYear(), firstDay.getMonth(), 1); + const weekOfMonth = Math.ceil((firstDay.getDate() + firstDayOfMonth.getDay()) / 7); + this.currentWeekOfMonth = weekOfMonth; + + // Wie viele Wochen hat dieser Monat insgesamt? + const lastDayOfMonth = new Date(firstDay.getFullYear(), firstDay.getMonth() + 1, 0); + const weeksInMonth = Math.ceil((lastDayOfMonth.getDate() + firstDayOfMonth.getDay()) / 7); + this.totalWeeksInMonth = weeksInMonth; + } + + get weekNavigationLabel(): string { + if (!this.currentMonthLabel) return ''; + return `${this.currentMonthLabel} Woche ${this.currentWeekOfMonth}/${this.totalWeeksInMonth}`; + } + + // ==================== NAVIGATION ==================== + + previousWeek(): void { + if (this.currentWeekIndex > 0) { + this.currentWeekIndex--; + this.updateWeekLabels(); + this.clearSelection(); + } + } + + nextWeek(): void { + if (this.currentWeekIndex < this.weeks.length - 1) { + this.currentWeekIndex++; + this.updateWeekLabels(); + this.clearSelection(); + } + } + + clearSelection(): void { + this.selectedDate = ''; + this.selectedSlot = null; + this.availableSlots = []; + } + + // ==================== SELECTION ==================== + + selectDate(date: string): void { + this.selectedDate = date; + this.selectedSlot = null; + + // Slots für diesen Tag laden und nochmal validieren + const day = this.currentWeek.find(d => d.date === date); + const daySlots = day?.slots || []; + + // Slots nochmal filtern (falls Zeit mittlerweile abgelaufen) + this.availableSlots = this.filterValidSlots(daySlots); + } + + selectSlot(slot: BookingSlot): void { + // Vor dem Auswählen nochmal prüfen ob Slot noch gültig ist + const slotDateTime = this.getSlotDateTime(slot.date, slot.timeFrom); + const now = new Date(); + const minDateTime = new Date(now.getTime() + this.MIN_HOURS_ADVANCE * 60 * 60 * 1000); + + if (slotDateTime < minDateTime) { + this.errorMessage = 'Dieser Slot ist leider nicht mehr verfügbar. Bitte lade die Seite neu.'; + this.loadAvailableSlots(); + return; + } + + this.selectedSlot = slot; + } + + // ==================== NEU: Nur auf erste verfügbare Woche springen (keine Auto-Selektion) ==================== + + /** Setzt den Week-Index auf die erste Woche mit mindestens einem verfügbaren Tag. */ + private jumpToFirstAvailableWeek(): void { + if (!this.weeks || this.weeks.length === 0) { + this.currentWeekIndex = 0; + this.clearSelection(); + this.updateWeekLabels(); + return; + } + + const weekIndex = this.weeks.findIndex(week => week.some(d => d.available)); + if (weekIndex === -1) { + // Gar nichts verfügbar + this.currentWeekIndex = 0; + this.clearSelection(); + this.updateWeekLabels(); + return; + } + + // Auf die gefundene Woche springen + this.currentWeekIndex = weekIndex; + this.clearSelection(); // sicherstellen, dass nichts vorselektiert ist + this.updateWeekLabels(); + } + + // ==================== PARSE/FORMAT HELPERS ==================== + + /** "2025-10-16" -> Date (lokal, 00:00) */ + private parseLocalDate(dateStr: string): Date { + const [y, m, d] = dateStr.split('-').map(Number); + return new Date(y, (m - 1), d, 0, 0, 0, 0); + } + + /** Date -> "YYYY-MM-DD" (lokal) */ + private toLocalYMD(date: Date): string { + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, '0'); + const d = String(date.getDate()).padStart(2, '0'); + return `${y}-${m}-${d}`; + } + + getFormattedDate(): string { + const dateStr = this.selectedSlot?.date || this.selectedDate; + if (!dateStr) return ''; + const date = this.parseLocalDate(dateStr); + const days = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa']; + return `${days[date.getDay()]}, ${date.getDate()}.${date.getMonth() + 1}.${date.getFullYear()}`; + } + + getFormattedTime(): string { + if (!this.selectedSlot) return ''; + return `${this.formatTime(this.selectedSlot.timeFrom)} - ${this.formatTime(this.selectedSlot.timeTo)}`; + } + + // ==================== BOOKING SUBMIT ==================== + + bookAnotherSlot(): void { + this.bookingSuccessful = false; + this.bookedSlotDate = ''; + this.bookedSlotTime = ''; + this.loadAvailableSlots(); + } + + backToHome(): void { + this.router.navigate(['/']); + } + + scrollToTop() { + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + + submitBooking(): void { + if (!this.selectedSlot || !this.bookingData.name || !this.bookingData.email) { + return; + } + + if (!this.selectedSlot || !this.bookingData.name || !this.bookingData.email) return; + + // Sanity: Datum & Zeiten müssen aus selectedSlot kommen + const dateStr = this.selectedSlot.date; + const fromStr = this.selectedSlot.timeFrom; + const toStr = this.selectedSlot.timeTo; + + // Wenn etwas leer/komisch ist -> abbrechen + if (!dateStr || !fromStr || !toStr) { + this.errorMessage = 'Unerwarteter Fehler: Slot-Daten unvollständig. Bitte Seite neu laden.'; + return; + } + + // Optional streng: prüfen, ob UI-Strings zum Slot passen + const uiDate = this.selectedDate; // (nur zu Diagnose) + if (uiDate && uiDate !== dateStr) { + this.errorMessage = 'Interner Abgleich fehlgeschlagen (Datum). Bitte Tag neu auswählen.'; + return; + } + + // Finale Validierung vor dem Absenden + const slotDateTime = this.getSlotDateTime(this.selectedSlot.date, this.selectedSlot.timeFrom); + const now = new Date(); + const minDateTime = new Date(now.getTime() + this.MIN_HOURS_ADVANCE * 60 * 60 * 1000); + + if (slotDateTime < minDateTime) { + this.errorMessage = `Dieser Slot liegt weniger als ${this.MIN_HOURS_ADVANCE} Stunden in der Zukunft. Bitte wähle einen anderen Termin.`; + this.loadAvailableSlots(); + return; + } + + this.isSubmitting = true; + this.errorMessage = ''; + this.successMessage = ''; + + const bookingDto: CreateBookingDto = { + name: this.bookingData.name.trim(), + email: this.bookingData.email.trim(), + phone: this.bookingData.phone?.trim() || undefined, + message: this.bookingData.message?.trim() || undefined, + slotId: this.selectedSlot.id + }; + + this.apiService.createBooking(bookingDto) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (booking) => { + // Success Screen anzeigen + this.bookingSuccessful = true; + this.bookedSlotDate = this.getFormattedDate(); + this.bookedSlotTime = this.getFormattedTime(); + this.isSubmitting = false; + this.scrollToTop(); + + // Form zurücksetzen (aber Success Screen bleibt) + this.resetForm(); + }, + error: (error) => { + console.error('Booking error:', error); + this.isSubmitting = false; + + if (error.status === 409) { + this.errorMessage = 'Dieser Slot ist leider nicht mehr verfügbar. Bitte wähle einen anderen Termin.'; + } else if (error.status === 404) { + this.errorMessage = 'Dieser Slot wurde nicht gefunden. Bitte lade die Seite neu.'; + } else if (error.status === 400) { + this.errorMessage = 'Ungültige Buchungsdaten. Bitte überprüfe deine Eingaben.'; + } else { + this.errorMessage = 'Buchung fehlgeschlagen. Bitte versuche es erneut oder kontaktiere uns direkt.'; + } + + // Slots neu laden um aktuelle Verfügbarkeit zu zeigen + this.loadAvailableSlots(); + } + }); + } + + resetForm(): void { + this.selectedDate = ''; + this.selectedSlot = null; + this.availableSlots = []; + this.bookingData = { + name: '', + email: '', + phone: '', + message: '' + }; + } + + // ==================== HELFER ==================== + + getSlotsCountForDay(day: DayWithSlots): number { + return day.slots.length; + } + + isFormValid(): boolean { + return !!( + this.selectedSlot && + this.bookingData.name?.trim() && + this.bookingData.email?.trim() && + this.isValidEmail(this.bookingData.email) + ); + } + + isValidEmail(email: string): boolean { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + } +} diff --git a/apps/frontend/src/app/components/contact/contact.component.html b/apps/frontend/src/app/components/contact/contact.component.html new file mode 100644 index 0000000..4fa9fa3 --- /dev/null +++ b/apps/frontend/src/app/components/contact/contact.component.html @@ -0,0 +1,246 @@ + +
+
+ +
+ + +
+ {{ selectedService.icon }} + {{ selectedService.title }} + +
+ + + + + +
+ + +
+
+ celebration +
+

Danke! Nachricht angekommen. 🎉

+

Wir melden uns innerhalb von 24 Stunden bei dir.

+
+ + +
+
+ + +
+ +
+

Schnelle Nachricht

+

+ Anfrage zu: {{ selectedService.title }} +

+

+ Wir freuen uns auf deine Nachricht! +

+
+ + +
+ +
+ person + + check_circle +
+ + error + Bitte Namen angeben + +
+ + +
+ +
+ mail + + check_circle +
+ + error + Bitte E-Mail angeben + +
+ + +
+ +
+ +
+ + + error + Bitte beschreib kurz dein Anliegen + +
+ + +
+ +
+ + +
+ +
+ phone + +
+
+ + +
+ +

+ bolt + Antwort in der Regel innerhalb von 24h +

+
+ + +

+ shield + Mit dem Absenden stimmst du der Verarbeitung deiner Daten zur Bearbeitung deiner Anfrage zu. + Datenschutz +

+ + + + +
+ +
+ +
+
+
\ No newline at end of file diff --git a/apps/frontend/src/app/components/contact/contact.component.scss b/apps/frontend/src/app/components/contact/contact.component.scss new file mode 100644 index 0000000..4c73959 --- /dev/null +++ b/apps/frontend/src/app/components/contact/contact.component.scss @@ -0,0 +1,1074 @@ +@import '../../utils/shared-styles.scss'; + +/* ===== CONTAINER ===== */ + +.container { + width: 100%; + max-width: 1200px; + margin-inline: auto; + padding-inline: clamp(1rem, 4vw, 2rem); +} + +/* ===== CONTACT SECTION ===== */ + +.contact { + padding-bottom: 1rem; + + @media (min-width: 960px) { + padding-bottom: 0; + } +} + +/* ===== MOBILE SERVICE BADGE ===== */ + +.mobile-service-badge { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.875rem 1rem; + background: linear-gradient(135deg, rgba($color-brand-primary, 0.1), rgba($color-brand-primary, 0.05)); + border: 2px solid rgba($color-brand-primary, 0.2); + border-radius: $radius-lg; + animation: slideDown 0.3s ease; + + @media (min-width: 960px) { + display: none; + } + + &__emoji { + font-size: 1.5rem; + flex-shrink: 0; + } + + &__title { + flex: 1; + font-weight: 700; + font-size: 0.9375rem; + color: $color-text-primary; + } + + &__remove { + background: rgba(0, 0, 0, 0.08); + border: none; + border-radius: 50%; + width: 36px; + height: 36px; + min-width: 36px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + transition: all 0.2s ease; + + .material-symbols-outlined { + font-size: 20px; + color: $color-text-secondary; + } + + &:active { + transform: scale(0.95); + background: rgba(0, 0, 0, 0.15); + } + } +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +/* ===== GRID LAYOUT ===== */ + +.contact__grid { + display: grid; + grid-template-columns: 1fr; + gap: 1.5rem; + align-items: start; + + @media (min-width: 960px) { + grid-template-columns: 380px 1fr; + gap: 2.5rem; + } +} + +/* ===== SIDEBAR ===== */ + +.contact__sidebar { + display: flex; + flex-direction: column; + gap: 1.25rem; + + @media (max-width: 959px) { + order: 2; + } +} + +/* ===== SELECTED SERVICE CARD (Desktop) ===== */ + +.selected-service { + background: linear-gradient(135deg, rgba($color-brand-primary, 0.08), rgba($color-brand-primary, 0.02)); + border: 2px solid rgba($color-brand-primary, 0.15); + border-radius: $radius-lg; + padding: 1.25rem; + animation: fadeIn 0.3s ease; + + // Hide on mobile - show mobile-service-badge instead + @media (max-width: 959px) { + display: none; + } + + &__header { + display: flex; + align-items: flex-start; + gap: 0.75rem; + margin-bottom: 0.75rem; + } + + &__emoji { + font-size: 1.75rem; + line-height: 1; + flex-shrink: 0; + } + + &__info { + flex: 1; + min-width: 0; + } + + &__label { + display: block; + font-size: 0.6875rem; + font-weight: 700; + color: $color-brand-primary; + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 0.125rem; + } + + &__title { + font-size: 1.0625rem; + font-weight: 700; + color: $color-text-primary; + margin: 0; + line-height: 1.3; + } + + &__remove { + background: rgba(0, 0, 0, 0.05); + border: none; + border-radius: 50%; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + flex-shrink: 0; + transition: all 0.2s ease; + + .material-symbols-outlined { + font-size: 18px; + color: $color-text-secondary; + } + + &:hover { + background: rgba(0, 0, 0, 0.1); + } + } + + &__desc { + font-size: 0.875rem; + color: $color-text-secondary; + line-height: 1.6; + margin: 0; + padding-left: 2.5rem; + } +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-8px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Quick Contact */ +.quick-contact { + h3 { + font-size: 1.125rem; + font-weight: 700; + color: $color-text-primary; + margin-bottom: 1rem; + } + + .quick__list { + display: flex; + flex-direction: column; + gap: 0.75rem; + } + + .quick__card { + display: flex; + gap: 1rem; + padding: 1.25rem; + background: $glass-bg; + backdrop-filter: blur(10px); + border: 1px solid $glass-border; + border-radius: $radius-lg; + text-decoration: none; + transition: all 0.3s ease; + box-shadow: $shadow-md; + + &:hover { + transform: translateY(-2px); + box-shadow: $shadow-glass-hover; + border-color: $color-brand-primary; + } + + .quick__icon { + width: 48px; + height: 48px; + flex-shrink: 0; + background: $gradient-primary; + border-radius: $radius-md; + display: flex; + align-items: center; + justify-content: center; + color: white; + box-shadow: $shadow-brand; + + .material-symbols-outlined { + font-size: 24px; + } + } + + .quick__content { + display: flex; + flex-direction: column; + gap: 0.25rem; + flex: 1; + + .quick__title { + font-weight: 700; + font-size: 1rem; + color: $color-text-primary; + } + + .quick__value { + font-weight: 600; + font-size: 0.9375rem; + color: $color-brand-primary; + } + + .quick__meta { + font-size: 0.875rem; + color: $color-text-secondary; + } + } + } +} + +/* Info Card */ +.info-card { + background: $gradient-subtle; + border: 1px solid $color-gray-200; + border-radius: $radius-lg; + padding: 1.5rem; + + .info-card__icon { + width: 40px; + height: 40px; + background: $color-brand-primary; + border-radius: $radius-md; + display: flex; + align-items: center; + justify-content: center; + color: white; + margin-bottom: 1rem; + + .material-symbols-outlined { + font-size: 24px; + } + } + + h3 { + font-size: 1.125rem; + font-weight: 700; + color: $color-text-primary; + margin-bottom: 1rem; + } + + .info-list { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 0.75rem; + + li { + display: flex; + align-items: flex-start; + gap: 0.5rem; + font-size: 0.9375rem; + color: $color-text-secondary; + line-height: 1.5; + + .material-symbols-outlined { + color: $color-success; + font-size: 20px; + flex-shrink: 0; + margin-top: 0.125rem; + } + } + } +} + +/* Trust Badge */ +.trust-badge { + display: flex; + gap: 0.75rem; + padding: 1rem; + background: $color-white; + border: 2px solid $color-success; + border-radius: $radius-md; + align-items: center; + + .material-symbols-outlined { + font-size: 32px; + color: $color-success; + flex-shrink: 0; + } + + strong { + display: block; + font-weight: 700; + color: $color-text-primary; + margin-bottom: 0.125rem; + } + + p { + margin: 0; + font-size: 0.875rem; + color: $color-text-secondary; + line-height: 1.4; + } +} + +/* ===== MAIN CONTENT ===== */ + +.contact__main { + @media (max-width: 959px) { + order: 1; + } +} + +/* ===== SUCCESS CARD ===== */ + +.success-card { + background: $glass-bg; + backdrop-filter: blur(20px); + border: 1px solid $glass-border; + border-radius: $radius-xl; + padding: 2rem; + text-align: center; + box-shadow: $shadow-glass; + animation: scaleIn 0.4s ease; + + @media (min-width: 768px) { + padding: 2.5rem; + } + + .success-icon { + width: 72px; + height: 72px; + background: $gradient-primary; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 1.25rem; + box-shadow: $shadow-brand; + animation: bounce 0.6s ease 0.2s; + + @media (min-width: 768px) { + width: 80px; + height: 80px; + margin-bottom: 1.5rem; + } + + .material-symbols-outlined { + font-size: 40px; + color: white; + + @media (min-width: 768px) { + font-size: 48px; + } + } + } + + h2 { + font-size: 1.5rem; + font-weight: 800; + color: $color-text-primary; + margin-bottom: 0.5rem; + + @media (min-width: 768px) { + font-size: 1.75rem; + margin-bottom: 0.75rem; + } + } + + p { + font-size: 1rem; + color: $color-text-secondary; + margin-bottom: 1.5rem; + + @media (min-width: 768px) { + font-size: 1.0625rem; + } + } + + .success-actions { + display: flex; + flex-direction: column; + gap: 0.75rem; + justify-content: center; + + @media (min-width: 480px) { + flex-direction: row; + } + + .btn { + width: 100%; + + @media (min-width: 480px) { + width: auto; + } + } + } +} + +@keyframes scaleIn { + from { + opacity: 0; + transform: scale(0.95); + } + + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes bounce { + + 0%, + 100% { + transform: scale(1); + } + + 50% { + transform: scale(1.1); + } +} + +/* ===== FORM ===== */ + +.contact__form { + background: $glass-bg; + backdrop-filter: blur(20px); + border: 1px solid $glass-border; + border-radius: $radius-xl; + padding: 1.5rem; + box-shadow: $shadow-glass; + transition: opacity 0.3s ease; + + @media (min-width: 768px) { + padding: 2.5rem; + } + + &.is-loading { + pointer-events: none; + opacity: 0.7; + } + + .form-header { + margin-bottom: 1.25rem; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + + @media (min-width: 768px) { + margin-bottom: 1.75rem; + } + + h2 { + font-size: 1.5rem; + font-weight: 800; + color: $color-text-primary; + margin-bottom: 0.375rem; + + @media (min-width: 768px) { + font-size: 1.75rem; + margin-bottom: 0.5rem; + } + } + + .form-subtitle { + font-size: 0.9375rem; + color: $color-text-secondary; + margin: 0; + + @media (min-width: 768px) { + font-size: 1rem; + } + + strong { + color: $color-brand-primary; + } + } + } +} + +/* ===== FORM FIELDS ===== */ + +.field { + margin-bottom: 1rem; + + &:last-of-type { + margin-bottom: 0; + } + + label { + display: flex; + align-items: center; + gap: 0.25rem; + font-size: 0.9375rem; + font-weight: 600; + color: $color-text-primary; + margin-bottom: 0.5rem; + + .required { + color: $color-error; + margin-left: 0.125rem; + } + + .label-hint { + font-weight: 400; + color: $color-text-tertiary; + font-size: 0.8125rem; + } + } + + .input-wrapper { + position: relative; + + .input-icon { + position: absolute; + left: 1rem; + top: 50%; + transform: translateY(-50%); + color: $color-text-secondary; + font-size: 20px; + pointer-events: none; + transition: color 0.2s ease; + } + + .input-check { + position: absolute; + right: 1rem; + top: 50%; + transform: translateY(-50%); + color: $color-success; + font-size: 20px; + pointer-events: none; + animation: popIn 0.3s ease; + } + + input, + select, + textarea { + width: 100%; + padding: 1rem; + padding-left: 3rem; + border: 2px solid $color-gray-200; + border-radius: $radius-md; + font-size: 1rem; // 16px prevents zoom on iOS + font-family: inherit; + color: $color-text-primary; + background: $color-white; + transition: all 0.3s ease; + -webkit-appearance: none; + + &::placeholder { + color: $color-gray-400; + } + + &:hover:not(:disabled) { + border-color: $color-gray-300; + } + + &:focus { + outline: none; + border-color: $color-brand-primary; + box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.1); + + &+.input-icon, + &~.input-icon { + color: $color-brand-primary; + } + } + + &:disabled { + background: $color-gray-100; + color: $color-text-tertiary; + cursor: not-allowed; + } + } + + textarea { + padding: 1rem; + min-height: 100px; + resize: vertical; + line-height: 1.6; + + @media (min-width: 768px) { + min-height: 120px; + } + } + + select { + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236B7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E"); + background-position: right 1rem center; + background-repeat: no-repeat; + background-size: 1.25rem; + padding-right: 3rem; + } + } + + &.has-error { + .input-wrapper { + + input, + select, + textarea { + border-color: $color-error; + animation: shake 0.4s ease; + } + } + } + + &.has-value { + .input-wrapper { + + input:not(:focus), + textarea:not(:focus) { + border-color: $color-success; + } + + .input-icon { + color: $color-success; + } + } + } + + .field-footer { + display: flex; + justify-content: flex-end; + margin-top: 0.375rem; + } + + .char-counter { + font-size: 0.75rem; + color: $color-text-tertiary; + transition: color 0.2s ease; + + &.near-limit { + color: $color-warning; + font-weight: 600; + } + } + + .error-message { + display: flex; + align-items: center; + gap: 0.375rem; + margin-top: 0.5rem; + font-size: 0.875rem; + color: $color-error; + font-weight: 500; + animation: slideDown 0.2s ease; + + .material-symbols-outlined { + font-size: 16px; + } + } +} + +@keyframes popIn { + from { + opacity: 0; + transform: translateY(-50%) scale(0.5); + } + + to { + opacity: 1; + transform: translateY(-50%) scale(1); + } +} + +@keyframes shake { + + 0%, + 100% { + transform: translateX(0); + } + + 20%, + 60% { + transform: translateX(-6px); + } + + 40%, + 80% { + transform: translateX(6px); + } +} + +/* Field Phone Animation */ +.field--phone { + animation: slideDown 0.3s ease; +} + +/* Checkbox Field */ +.field--checkbox { + margin-top: 0.5rem; + + .checkbox-label { + display: flex; + align-items: center; + gap: 0.75rem; + cursor: pointer; + padding: 1rem; + background: $color-gray-50; + border: 2px solid $color-gray-200; + border-radius: $radius-md; + transition: all 0.3s ease; + -webkit-tap-highlight-color: transparent; + + &:hover { + border-color: $color-brand-primary; + background: rgba(37, 99, 235, 0.04); + } + + &:active { + transform: scale(0.99); + } + + input[type="checkbox"] { + position: absolute; + opacity: 0; + pointer-events: none; + + &:checked+.checkbox-custom { + background: $gradient-primary; + border-color: $color-brand-primary; + + &::after { + opacity: 1; + transform: scale(1); + } + } + + &:focus-visible+.checkbox-custom { + box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.2); + } + } + + .checkbox-custom { + width: 26px; + height: 26px; + min-width: 26px; + border: 2px solid $color-gray-300; + border-radius: 6px; + background: $color-white; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; + position: relative; + + &::after { + content: '✓'; + color: white; + font-weight: 700; + font-size: 14px; + opacity: 0; + transform: scale(0.5); + transition: all 0.2s ease; + } + } + + .checkbox-text { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.9375rem; + font-weight: 500; + color: $color-text-primary; + + .material-symbols-outlined { + font-size: 20px; + color: $color-text-secondary; + } + } + } +} + +/* ===== FORM ACTIONS ===== */ + +.form-actions { + margin-top: 1.5rem; + margin-bottom: 1rem; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + + @media (min-width: 768px) { + margin-top: 2rem; + } + + .btn--submit { + width: 100%; + min-height: 52px; + + @media (min-width: 640px) { + width: auto; + min-width: 220px; + } + } + + .btn-content { + display: flex; + align-items: center; + justify-content: center; + gap: 0.625rem; + } + + .form-meta { + display: flex; + align-items: center; + justify-content: center; + gap: 0.375rem; + margin-top: 1rem; + font-size: 0.875rem; + color: $color-text-secondary; + + @media (min-width: 640px) { + justify-content: flex-start; + } + + .material-symbols-outlined { + font-size: 18px; + color: $color-brand-primary; + } + } +} + +/* Loading State */ +.loading-content { + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; +} + +.spinner { + width: 18px; + height: 18px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-top-color: white; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* ===== PRIVACY NOTICE ===== */ + +.privacy-notice { + display: flex; + align-items: flex-start; + gap: 0.5rem; + padding: 1rem; + background: $color-gray-50; + border-radius: $radius-md; + font-size: 0.875rem; + line-height: 1.6; + color: $color-text-secondary; + margin-top: 1.5rem; + + .material-symbols-outlined { + font-size: 18px; + color: $color-text-secondary; + flex-shrink: 0; + margin-top: 0.125rem; + } + + a { + color: $color-brand-primary; + text-decoration: none; + font-weight: 600; + cursor: pointer; + + &:hover { + text-decoration: underline; + } + } +} + +/* ===== ALERT ===== */ + +.alert { + display: flex; + gap: 0.75rem; + padding: 1rem 1.25rem; + border-radius: $radius-md; + margin-top: 1.5rem; + + .material-symbols-outlined { + font-size: 24px; + flex-shrink: 0; + margin-top: 0.125rem; + } + + strong { + display: block; + margin-bottom: 0.25rem; + } + + p { + margin: 0; + font-size: 0.9375rem; + line-height: 1.5; + } + + &--error { + background: $color-error-light; + border: 1px solid darken($color-error-light, 10%); + color: darken($color-error, 10%); + + .material-symbols-outlined { + color: $color-error; + } + } +} + +/* ===== MOTION PREFERENCES ===== */ + +@media (prefers-reduced-motion: reduce) { + + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + +/* ===== MOBILE STICKY SUBMIT ===== */ + +.mobile-sticky-submit { + position: fixed; + bottom: 0; + left: 0; + right: 0; + padding: 1rem; + padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px)); + background: rgba(255, 255, 255, 0.98); + backdrop-filter: blur(20px); + border-top: 1px solid $color-gray-200; + box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.08); + z-index: 1000; + animation: slideUp 0.3s ease; + + @media (min-width: 768px) { + display: none; + } + + .btn--full { + width: 100%; + min-height: 52px; + font-size: 1rem; + font-weight: 700; + } + + .btn-content, + .loading-content { + display: flex; + align-items: center; + justify-content: center; + gap: 0.625rem; + } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(100%); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +/* ===== GHOST BUTTON ===== */ + +.btn--ghost { + background: transparent; + border: 2px solid $color-gray-200; + color: $color-text-primary; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.75rem 1.25rem; + border-radius: $radius-md; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + border-color: $color-gray-300; + background: $color-gray-50; + } + + .material-symbols-outlined { + font-size: 20px; + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/components/contact/contact.component.spec.ts b/apps/frontend/src/app/components/contact/contact.component.spec.ts new file mode 100644 index 0000000..dae8ee6 --- /dev/null +++ b/apps/frontend/src/app/components/contact/contact.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ContactComponent } from './contact.component'; + +describe('ContactComponent', () => { + let component: ContactComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ContactComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ContactComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/frontend/src/app/components/contact/contact.component.ts b/apps/frontend/src/app/components/contact/contact.component.ts new file mode 100644 index 0000000..62f1f49 --- /dev/null +++ b/apps/frontend/src/app/components/contact/contact.component.ts @@ -0,0 +1,213 @@ +import { Component, OnInit, ViewChild, ElementRef, AfterViewInit, OnDestroy } from '@angular/core'; +import { PageTitleComponent } from "../../shared/page-title/page-title.component"; +import { Router, ActivatedRoute } from '@angular/router'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ServiceDataService, Service } from '../../shared/service-data.service'; +import { ApiService, CreateContactRequestDto } from '../../api/api.service'; +import { finalize } from 'rxjs'; +import { ToastService } from '../../shared/toasts/toast.service'; +import { trigger, transition, style, animate } from '@angular/animations'; +import { AnalyticsService } from '../../services/analytics.service'; + +type State = 'idle' | 'loading' | 'success' | 'error'; + +@Component({ + selector: 'app-contact', + standalone: true, + imports: [PageTitleComponent, CommonModule, FormsModule], + templateUrl: './contact.component.html', + styleUrl: './contact.component.scss', + animations: [ + trigger('slideDown', [ + transition(':enter', [ + style({ opacity: 0, height: 0, overflow: 'hidden' }), + animate('300ms ease-out', style({ opacity: 1, height: '*' })) + ]), + transition(':leave', [ + style({ opacity: 1, height: '*', overflow: 'hidden' }), + animate('200ms ease-in', style({ opacity: 0, height: 0 })) + ]) + ]) + ] +}) +export class ContactComponent implements OnInit, AfterViewInit, OnDestroy { + @ViewChild('nameInput') nameInput!: ElementRef; + @ViewChild('formContainer') formContainer!: ElementRef; + @ViewChild('successCard') successCard!: ElementRef; + @ViewChild('submitButton') submitButton!: ElementRef; + + constructor( + public router: Router, + private route: ActivatedRoute, + private serviceData: ServiceDataService, + private api: ApiService, + private toasts: ToastService, + private analytics: AnalyticsService + ) { } + + state: State = 'idle'; + busy = false; + showStickyButton = false; + private intersectionObserver?: IntersectionObserver; + + // Selected service from query params + selectedService: Service | null = null; + + // Character limit for message + readonly messageMaxLength = 1000; + + model = { + name: '', + email: '', + message: '', + callback: false, + phone: '', + service: '' + }; + + // Computed property for character count + get messageLength(): number { + return this.model.message?.length || 0; + } + + get isMessageNearLimit(): boolean { + return this.messageLength > this.messageMaxLength * 0.8; + } + + ngOnInit(): void { + // Read service ID from query params + this.route.queryParams.subscribe(params => { + const serviceId = params['service']; + if (serviceId) { + const service = this.serviceData.getServiceById(serviceId); + if (service) { + this.selectedService = service; + this.model.service = service.title; + } + } + }); + } + + ngAfterViewInit(): void { + // Auto-focus name input on desktop (nicht auf Mobile wegen Keyboard) + if (window.innerWidth > 768) { + setTimeout(() => this.nameInput?.nativeElement?.focus(), 300); + } + + // Intersection Observer für Sticky Button (nur Mobile) + if (window.innerWidth <= 768 && this.submitButton) { + this.intersectionObserver = new IntersectionObserver( + (entries) => { + entries.forEach(entry => { + // Sticky Button zeigen wenn Submit-Button nicht sichtbar ist + this.showStickyButton = !entry.isIntersecting; + }); + }, + { threshold: 0.1 } + ); + + this.intersectionObserver.observe(this.submitButton.nativeElement); + } + } + + clearSelectedService(): void { + this.selectedService = null; + this.model.service = ''; + this.model.message = ''; + this.router.navigate([], { + relativeTo: this.route, + queryParams: {}, + replaceUrl: true + }); + } + + submit() { + if (this.busy || this.state === 'loading' || !this.model.name || !this.model.email || !this.model.message) { + return; + } + + this.busy = true; + this.state = 'loading'; + + // Service-Slug aus dem ausgewählten Service ermitteln + const serviceSlug = this.selectedService + ? this.serviceData.getServiceSlug(this.selectedService.id) + : 'allgemeine-anfrage'; + + const contactRequest: CreateContactRequestDto = { + name: this.model.name, + email: this.model.email, + message: this.model.message || 'Keine Nachricht angegeben', + serviceType: serviceSlug, + prefersCallback: this.model.callback, + phoneNumber: this.model.callback ? this.model.phone : undefined + }; + + this.api.createContactRequest(contactRequest) + .pipe(finalize(() => { this.busy = false; })) + .subscribe({ + next: () => { + this.state = 'success'; + this.toasts.success('Kontaktanfrage erfolgreich gesendet!', { duration: 5000 }); + + // Track conversion for analytics + this.analytics.trackConversion('contact_form', { + service: serviceSlug, + prefersCallback: this.model.callback + }); + + // Scroll to success message on mobile + setTimeout(() => this.scrollToTop(), 100); + }, + error: (error) => { + console.error('Fehler beim Senden der Kontaktanfrage:', error); + this.state = 'error'; + this.toasts.error('Fehler beim Senden der Kontaktanfrage.', { duration: 5000 }); + setTimeout(() => { this.state = 'idle'; }, 5000); + } + }); + } + + newMessage() { + this.resetForm(); + } + + private resetForm() { + this.model = { + name: '', + email: '', + message: '', + callback: false, + phone: '', + service: '' + }; + this.selectedService = null; + this.state = 'idle'; + + // Focus auf erstes Feld nach Reset + setTimeout(() => { + this.nameInput?.nativeElement?.focus(); + this.scrollToTop(); + }, 100); + } + + private scrollToTop(): void { + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + + ngOnDestroy(): void { + // Cleanup Intersection Observer + if (this.intersectionObserver) { + this.intersectionObserver.disconnect(); + } + } + + // Keyboard submit mit Enter (nur wenn nicht im Textarea) + onKeydown(event: KeyboardEvent): void { + if (event.key === 'Enter' && !event.shiftKey && !(event.target instanceof HTMLTextAreaElement)) { + event.preventDefault(); + this.submit(); + } + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/components/faq/faq.component.html b/apps/frontend/src/app/components/faq/faq.component.html new file mode 100644 index 0000000..b7e076f --- /dev/null +++ b/apps/frontend/src/app/components/faq/faq.component.html @@ -0,0 +1,116 @@ + + +
+
+ + +
+
+

FAQs werden geladen...

+
+ + +
+
+ error +
+

Fehler beim Laden

+

Die FAQs konnten leider nicht geladen werden. Bitte versuche es später erneut.

+ +
+ + +
+
+ construction +
+

In Arbeit

+

Die FAQs werden gerade erstellt und sind bald verfügbar.

+

Hast du eine dringende Frage? Kontaktiere uns direkt!

+ +
+ + + + + +
+ +
+
+ search + +
+
+ + +
+
+
+ + +
+
+ + help + {{ f.q }} + expand_more + +
+

{{ p }}

+
    +
  • {{ li }}
  • +
+
+
+
+ + + +
+
+ search_off +
+

+ Keine Treffer gefunden. + Stelle uns deine Frage direkt +

+
+
+ + +
+
+
+ contact_support +
+

Nicht fündig geworden?

+

Kurzes Erstgespräch, klare Einschätzung, kein Sales-Druck.

+ +
+
+ +
+ +
+
diff --git a/apps/frontend/src/app/components/faq/faq.component.scss b/apps/frontend/src/app/components/faq/faq.component.scss new file mode 100644 index 0000000..fe7da3f --- /dev/null +++ b/apps/frontend/src/app/components/faq/faq.component.scss @@ -0,0 +1,411 @@ +@import '../../utils/shared-styles.scss'; + +.container { + width: 100%; + max-width: 1000px; + margin-inline: auto; + padding-inline: clamp(1rem, 4vw, 2rem); +} + +.faq { + padding-block: 2rem 3rem; +} + +/* ===== INTRO ===== */ + +.faq-intro { + margin-bottom: 3rem; + + h2 { + font-size: clamp(1.75rem, 3vw + 1rem, 2.25rem); + font-weight: 800; + color: $color-text-primary; + margin: 0 0 0.5rem; + } + + p { + font-size: 1.0625rem; + color: $color-text-secondary; + margin: 0 0 2rem; + } +} + +/* ===== TOOLS ===== */ + +.faq-tools { + display: flex; + gap: 1rem; + flex-wrap: wrap; + align-items: center; + + @media (max-width: 640px) { + flex-direction: column; + align-items: stretch; + } +} + +.search-wrapper { + position: relative; + flex: 1; + max-width: 520px; + + @media (max-width: 640px) { + max-width: 100%; + } + + .search-icon { + position: absolute; + left: 1rem; + top: 50%; + transform: translateY(-50%); + color: $color-text-secondary; + font-size: 20px; + pointer-events: none; + } + + input { + width: 100%; + height: 48px; + padding: 0 1rem 0 3rem; + border: 2px solid $color-gray-200; + border-radius: $radius-md; + background: $color-white; + color: $color-text-primary; + font-size: 0.9375rem; + transition: all 0.3s ease; + + &::placeholder { + color: $color-gray-400; + } + + &:focus { + outline: none; + border-color: $color-brand-primary; + box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.1); + } + } +} + +.tool-buttons { + display: flex; + gap: 0.5rem; + + @media (max-width: 640px) { + width: 100%; + + .btn { + flex: 1; + } + } +} + +/* ===== FAQ LIST ===== */ + +.faq-list { + display: grid; + gap: 1rem; + margin-bottom: 3rem; +} + +.faq-item { + background: $glass-bg; + backdrop-filter: blur(10px); + border: 1px solid $glass-border; + border-radius: $radius-lg; + padding: 1.25rem; + box-shadow: $shadow-md; + transition: all 0.3s ease; + + &:hover { + box-shadow: $shadow-lg; + border-color: rgba(37, 99, 235, 0.2); + } + + summary { + list-style: none; + display: flex; + align-items: center; + gap: 0.75rem; + font-weight: 700; + color: $color-text-primary; + cursor: pointer; + padding: 0.5rem; + border-radius: $radius-sm; + transition: background 0.2s ease; + + &::-webkit-details-marker { + display: none; + } + + .faq-icon { + color: $color-brand-primary; + font-size: 24px; + flex-shrink: 0; + } + + .faq-question { + flex: 1; + font-size: 1.0625rem; + } + + .faq-arrow { + color: $color-text-secondary; + font-size: 24px; + flex-shrink: 0; + transition: transform 0.3s ease; + } + + &:hover { + background: rgba(37, 99, 235, 0.04); + } + + &:focus-visible { + outline: none; + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2); + } + } + + &[open] summary .faq-arrow { + transform: rotate(180deg); + } + + .faq-answer { + padding: 1rem 0 0 3rem; + color: $color-text-secondary; + line-height: 1.6; + + @media (max-width: 640px) { + padding-left: 0; + } + + p { + margin: 0.75rem 0; + font-size: 0.9375rem; + + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } + } + + ul { + margin: 0.75rem 0; + padding-left: 1.5rem; + + li { + margin-bottom: 0.5rem; + font-size: 0.9375rem; + } + } + } +} + +/* ===== NO HITS ===== */ + +.faq-nohits { + text-align: center; + padding: 3rem 2rem; + + .nohits-icon { + width: 72px; + height: 72px; + margin: 0 auto 1.5rem; + background: $color-gray-100; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + + .material-symbols-outlined { + font-size: 36px; + color: $color-text-secondary; + } + } + + p { + font-size: 1rem; + color: $color-text-secondary; + margin: 0; + + a { + color: $color-brand-primary; + text-decoration: none; + font-weight: 600; + cursor: pointer; + + &:hover { + text-decoration: underline; + } + } + } +} + +/* ===== CTA ===== */ + +.faq-cta { + .cta-card { + background: $gradient-subtle; + border: 1px solid rgba(37, 99, 235, 0.15); + border-radius: $radius-2xl; + padding: 3rem 2rem; + text-align: center; + position: relative; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + inset: 0; + background: + radial-gradient(circle at 20% 50%, rgba(37, 99, 235, 0.06), transparent 50%), + radial-gradient(circle at 80% 50%, rgba(59, 130, 246, 0.04), transparent 50%); + pointer-events: none; + } + + > * { + position: relative; + z-index: 1; + } + + .cta-icon { + width: 72px; + height: 72px; + margin: 0 auto 1.5rem; + background: $gradient-primary; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: $shadow-brand; + + .material-symbols-outlined { + font-size: 36px; + color: white; + } + } + + h3 { + font-size: clamp(1.5rem, 3vw + 1rem, 1.75rem); + font-weight: 800; + color: $color-text-primary; + margin: 0 0 0.75rem; + } + + p { + font-size: 1.0625rem; + color: $color-text-secondary; + margin: 0 0 2rem; + max-width: 500px; + margin-inline: auto; + } + + .btn { + @media (max-width: 640px) { + width: 100%; + } + } + } +} + +/* ===== LOADING STATE ===== */ + +.faq-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4rem 2rem; + gap: 1rem; + + .loading-spinner { + width: 48px; + height: 48px; + border: 4px solid rgba($color-brand-primary, 0.2); + border-top-color: $color-brand-primary; + border-radius: 50%; + animation: spin 1s linear infinite; + } + + p { + color: $color-text-secondary; + font-size: 1rem; + margin: 0; + } +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* ===== MESSAGE STATES (Error / Empty) ===== */ + +.faq-message { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: 4rem 2rem; + background: $color-background-elevated; + border-radius: 1.5rem; + border: 1px solid $color-gray-200; + + .message-icon { + width: 72px; + height: 72px; + display: flex; + align-items: center; + justify-content: center; + background: rgba($color-brand-primary, 0.1); + border-radius: 50%; + margin-bottom: 1.5rem; + + .material-symbols-outlined { + font-size: 36px; + color: $color-brand-primary; + } + + &.message-icon--construction { + background: rgba(#f59e0b, 0.15); + + .material-symbols-outlined { + color: #f59e0b; + } + } + } + + h3 { + font-size: 1.5rem; + font-weight: 700; + color: $color-text-primary; + margin: 0 0 0.75rem; + } + + p { + font-size: 1.0625rem; + color: $color-text-secondary; + margin: 0 0 1rem; + max-width: 400px; + } + + .message-sub { + font-size: 0.9375rem; + margin-bottom: 1.5rem; + } + + .btn { + margin-top: 0.5rem; + } +} + +/* ===== MOTION ===== */ + +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/components/faq/faq.component.spec.ts b/apps/frontend/src/app/components/faq/faq.component.spec.ts new file mode 100644 index 0000000..f033478 --- /dev/null +++ b/apps/frontend/src/app/components/faq/faq.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FaqComponent } from './faq.component'; + +describe('FaqComponent', () => { + let component: FaqComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FaqComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(FaqComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/frontend/src/app/components/faq/faq.component.ts b/apps/frontend/src/app/components/faq/faq.component.ts new file mode 100644 index 0000000..82e3199 --- /dev/null +++ b/apps/frontend/src/app/components/faq/faq.component.ts @@ -0,0 +1,75 @@ +import { CommonModule } from '@angular/common'; +import { Component, OnInit } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { Router } from '@angular/router'; +import { PageTitleComponent } from '../../shared/page-title/page-title.component'; +import { ApiService, Faq } from '../../api/api.service'; + +type FaqItem = { id: string; q: string; a: string[]; list?: string[] }; + +@Component({ + selector: 'app-faq', + standalone: true, + imports: [CommonModule, FormsModule, PageTitleComponent], + templateUrl: './faq.component.html', + styleUrl: './faq.component.scss' +}) +export class FaqComponent implements OnInit { + constructor( + public router: Router, + private api: ApiService + ) { } + + q = ''; + expandAll = false; + loading = true; + error = false; + isEmpty = false; + + // FAQs aus der Datenbank + faqs: FaqItem[] = []; + + ngOnInit(): void { + this.loadFaqs(); + } + + private loadFaqs(): void { + this.loading = true; + this.error = false; + this.isEmpty = false; + + this.api.getPublishedFaqs().subscribe({ + next: (data) => { + // API-Daten zu FaqItem-Format mappen + this.faqs = data.map(faq => ({ + id: faq.slug, + q: faq.question, + a: faq.answers, + list: faq.listItems ?? undefined + })); + + this.isEmpty = this.faqs.length === 0; + this.loading = false; + }, + error: (err) => { + console.error('Fehler beim Laden der FAQs:', err); + this.loading = false; + this.error = true; + } + }); + } + + get filtered(): FaqItem[] { + const q = this.q.trim().toLowerCase(); + if (!q) return this.faqs; + return this.faqs.filter(f => + f.q.toLowerCase().includes(q) || + f.a.join(' ').toLowerCase().includes(q) || + (f.list?.join(' ').toLowerCase().includes(q) ?? false) + ); + } + + setAll(open: boolean) { this.expandAll = open; } + + trackById(_: number, f: FaqItem) { return f.id; } +} diff --git a/apps/frontend/src/app/components/home/home.component.html b/apps/frontend/src/app/components/home/home.component.html new file mode 100644 index 0000000..b337b41 --- /dev/null +++ b/apps/frontend/src/app/components/home/home.component.html @@ -0,0 +1,466 @@ + + + +
+
+
+
+
+
+
+
+ +
+
+ +
+ + Westerwald & Umgebung +
+ + +

+ IT-Stress? + Wir regeln das. +

+ + +

+ PC kaputt? Website gebraucht? Software-Chaos?
+ Wir helfen. Vor Ort oder remote. Festpreis, kein Bullshit. +

+ + +
+ + +
+ + +
+
+ location_on + Westerwald & Umgebung +
+
+ euro + Faire Festpreise +
+
+ bolt + Schnelle Reaktion +
+
+
+ + +
+
+
+
+ L&B IT-Service +
+
+
+ build +
+ Reparatur & Wartung + PC, Laptop, Smartphone +
+
+
+ code +
+ Webentwicklung + Websites & Web-Apps +
+
+
+ support_agent +
+ IT-Support + Remote & Vor-Ort +
+
+
+
+ + +
computer
+
code
+
build
+
+
+ + +
+ expand_more +
+
+ + + + +
+
+
+ Kennst du das? +

Diese IT-Probleme nerven.

+
+ +
+
+ 😤 +

PC zu langsam?

+

Ewig warten beim Hochfahren, Programme hängen – Produktivität im Keller.

+ + Wir machen schnell. +
+ +
+ 🌐 +

Keine Website?

+

Kunden suchen online – und finden dich nicht. Umsatz geht verloren.

+ + Wir bauen sie. +
+ +
+ 🔧 +

Hardware kaputt?

+

Laptop defekt, Daten weg? Jede Stunde Ausfall kostet Zeit & Geld.

+ + Wir reparieren. +
+ +
+ 🤯 +

IT-Chaos?

+

Keine Ahnung von Technik? Berater reden nur Fachchinesisch?

+ + Wir erklären. +
+
+ +
+

Das muss nicht sein.

+ +
+
+
+ + + + +
+
+
+ Unsere Leistungen +

Alles aus einer Hand.

+

Ein Ansprechpartner für alle IT-Themen.

+
+ +
+
+
+ build +
+
+

Reparatur & Wartung

+

PC, Laptop, Smartphone – wir machen's wieder heile.

+
    +
  • Hardware-Reparatur
  • +
  • Datenrettung
  • +
  • Virenentfernung
  • +
+
+ + Mehr erfahren arrow_forward + +
+ +
+
+ code +
+
+

Webentwicklung

+

Professionelle Websites & Web-Apps nach Maß.

+
    +
  • Websites & Landingpages
  • +
  • Online-Shops
  • +
  • Web-Anwendungen
  • +
+
+ + Mehr erfahren arrow_forward + +
+ +
+
+ devices +
+
+

Beratung & Einrichtung

+

Die richtige Technik finden und einrichten.

+
    +
  • Kaufberatung
  • +
  • PC-Zusammenstellung
  • +
  • Komplettes Setup
  • +
+
+ + Mehr erfahren arrow_forward + +
+ +
+
+ support_agent +
+
+

IT-Support

+

Laufende Betreuung – damit alles läuft.

+
    +
  • Remote-Hilfe
  • +
  • Vor-Ort-Service
  • +
  • Wartungsverträge
  • +
+
+ + Mehr erfahren arrow_forward + +
+
+ +
+ +
+
+
+ + + + +
+
+
+ Für wen wir da sind +

Dein Problem ist unser Job.

+
+ +
+
+ 💡 +

Startups & Gründer

+

MVP schnell umsetzen. Von der Idee zum Prototyp – ohne unnötige Features.

+
+ +
+ 🏢 +

Kleine Unternehmen

+

Prozesse digitalisieren. Interne Tools, Verwaltung oder Kundenschnittstellen.

+
+ +
+ 🏪 +

Lokale Dienstleister

+

Online-Präsenz aufbauen. Buchungssysteme, Produktkataloge, Kundenverwaltung.

+
+
+
+
+ + + + +
+
+
+
+ Warum L&B? +

Wir liefern. Punkt.

+

+ Keine leeren Versprechen. Keine versteckten Kosten.
+ Klartext und Ergebnisse – das ist unser Ding. +

+ +
+
+ check_circle +
+ Schnelle Umsetzung + Von der Idee zum Ergebnis in Wochen, nicht Monaten. +
+
+
+ check_circle +
+ Faire Preise + Transparente Kalkulation ohne Agentur-Overhead. +
+
+
+ check_circle +
+ Direkte Kommunikation + Du sprichst direkt mit dem, der dein Projekt umsetzt. +
+
+
+ check_circle +
+ Persönlicher Service + Vor Ort im Westerwald oder remote – wie du willst. +
+
+
+
+ +
+
+
+ format_quote +
+

+ „Endlich jemand, der versteht was ich brauche und mir nichts aufschwatzt. + Schnell, fair, kompetent." +

+
+
MK
+
+ Michael K. + Handwerksmeister +
+
+
+
+
+
+
+ + + + +
+
+
+ So einfach geht's +

In 4 Schritten zum Ziel.

+

Kein Papierkram, kein Stress.

+
+ +
+
+
1
+
+ waving_hand +
+

Erstgespräch

+

Wir klären deine Anforderungen und Ziele. Kostenlos & unverbindlich.

+
+
+ +
+ +
+
2
+
+ description +
+

Angebot

+

Du bekommst ein transparentes Festpreis-Angebot. Keine Überraschungen.

+
+
+ +
+ +
+
3
+
+ engineering +
+

Umsetzung

+

Wir legen los. Du bekommst regelmäßige Updates zum Fortschritt.

+
+
+ +
+ +
+
4
+
+ celebration +
+

Fertig!

+

Abnahme, Go-Live und optional laufender Support danach.

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

Bereit loszulegen?

+

+ Lass uns in einem kostenlosen 30-Minuten-Gespräch klären,
+ wie wir dir helfen können. Unverbindlich und ohne Verkaufsdruck. +

+ +
+
+ schedule + 30 Min. kostenlos +
+
+ videocam + Per Video oder Telefon +
+
+ block + Keine Verpflichtung +
+
+ + + +

+ info + Einfach Wunschtermin wählen – ohne Registrierung. +

+ +
+ Oder direkt schreiben: + +
+
+
+
+
\ No newline at end of file diff --git a/apps/frontend/src/app/components/home/home.component.scss b/apps/frontend/src/app/components/home/home.component.scss new file mode 100644 index 0000000..bf14d0b --- /dev/null +++ b/apps/frontend/src/app/components/home/home.component.scss @@ -0,0 +1,1693 @@ +// ============================================ +// HOME COMPONENT - Premium Landing Page +// Psychologisch optimiert für Conversion +// ============================================ + +// ===== RESET & HOST ===== +*, *::before, *::after { + box-sizing: border-box; +} + +:host { + display: block; + width: 100%; + overflow-x: hidden; +} + +// ===== DESIGN TOKENS ===== +$color-bg: #fafbfc; +$color-bg-white: #ffffff; +$color-bg-dark: #0f1419; +$color-bg-gradient: linear-gradient(135deg, #f0f4ff 0%, #e8eeff 50%, #f5f7ff 100%); + +$color-text: #0f172a; +$color-text-secondary: #475569; +$color-text-muted: #94a3b8; + +$color-primary: #2563eb; +$color-primary-light: #3b82f6; +$color-primary-dark: #1d4ed8; +$color-primary-glow: rgba(37, 99, 235, 0.4); + +$color-success: #10b981; +$color-warning: #f59e0b; +$color-accent: #8b5cf6; + +$color-border: rgba(0, 0, 0, 0.06); +$color-border-hover: rgba(37, 99, 235, 0.25); + +$radius-sm: 8px; +$radius-md: 12px; +$radius-lg: 16px; +$radius-xl: 20px; +$radius-2xl: 24px; +$radius-full: 9999px; + +$shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.05); +$shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); +$shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.1); +$shadow-xl: 0 20px 48px rgba(0, 0, 0, 0.12); +$shadow-glow: 0 0 40px rgba(37, 99, 235, 0.2); + +$transition-fast: 0.15s ease; +$transition-base: 0.25s cubic-bezier(0.4, 0, 0.2, 1); +$transition-smooth: 0.4s cubic-bezier(0.16, 1, 0.3, 1); +$transition-bounce: 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); + +// ===== CONTAINER ===== +.container { + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 0 1.25rem; + + @media (min-width: 768px) { + padding: 0 2rem; + } +} + +// ===== UTILITY CLASSES ===== +.hide-mobile { + @media (max-width: 767px) { + display: none; + } +} + +// ===== CHIPS / BADGES ===== +.chip { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + background: linear-gradient(135deg, rgba(37, 99, 235, 0.08), rgba(59, 130, 246, 0.12)); + border: 1px solid rgba(37, 99, 235, 0.15); + border-radius: $radius-full; + font-size: 0.8125rem; + font-weight: 600; + color: $color-primary; + letter-spacing: 0.01em; + + &--blue { + background: linear-gradient(135deg, rgba(37, 99, 235, 0.1), rgba(99, 102, 241, 0.1)); + border-color: rgba(37, 99, 235, 0.2); + } + + &--dark { + background: rgba(15, 23, 42, 0.05); + border-color: rgba(15, 23, 42, 0.1); + color: $color-text-secondary; + } + + &--green { + background: rgba(16, 185, 129, 0.1); + border-color: rgba(16, 185, 129, 0.2); + color: $color-success; + } +} + +// ===== BUTTONS ===== +.btn-primary { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.625rem; + padding: 1rem 1.75rem; + background: linear-gradient(135deg, $color-primary 0%, $color-primary-light 100%); + color: white; + font-size: 0.9375rem; + font-weight: 700; + border: none; + border-radius: $radius-md; + cursor: pointer; + transition: all $transition-base; + box-shadow: 0 4px 16px $color-primary-glow; + position: relative; + overflow: hidden; + + .material-symbols-outlined { + font-size: 20px; + } + + &::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); + transform: translateX(-100%); + transition: transform 0.6s ease; + } + + &:hover { + transform: translateY(-3px); + box-shadow: 0 8px 28px rgba(37, 99, 235, 0.45); + + &::before { + transform: translateX(100%); + } + } + + &:active { + transform: translateY(-1px); + } +} + +.btn-ghost { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 1rem 1.5rem; + background: transparent; + color: $color-text-secondary; + font-size: 0.9375rem; + font-weight: 600; + border: 2px solid $color-border; + border-radius: $radius-md; + cursor: pointer; + transition: all $transition-base; + + .material-symbols-outlined { + font-size: 18px; + transition: transform $transition-base; + } + + &:hover { + border-color: $color-primary; + color: $color-primary; + background: rgba(37, 99, 235, 0.03); + + .material-symbols-outlined { + transform: translateX(4px); + } + } +} + +.btn-secondary { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.875rem 1.5rem; + background: white; + color: $color-primary; + font-size: 0.9375rem; + font-weight: 700; + border: 2px solid $color-primary; + border-radius: $radius-md; + cursor: pointer; + transition: all $transition-base; + + .material-symbols-outlined { + font-size: 20px; + } + + &:hover { + background: rgba(37, 99, 235, 0.05); + transform: translateY(-2px); + } +} + +.btn-cta { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.75rem; + padding: 1.125rem 2.25rem; + background: linear-gradient(135deg, $color-primary 0%, $color-primary-light 100%); + color: white; + font-size: 1.0625rem; + font-weight: 700; + border: none; + border-radius: $radius-lg; + cursor: pointer; + transition: all $transition-base; + box-shadow: + 0 4px 20px $color-primary-glow, + inset 0 1px 0 rgba(255, 255, 255, 0.15); + position: relative; + overflow: hidden; + + .material-symbols-outlined { + font-size: 22px; + } + + &__shine { + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.25), transparent); + animation: shine 3s ease-in-out infinite; + } + + &:hover { + transform: translateY(-3px) scale(1.02); + box-shadow: 0 8px 32px rgba(37, 99, 235, 0.5); + } +} + +.btn-text { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.5rem 0.75rem; + background: transparent; + color: $color-primary; + font-size: 0.9375rem; + font-weight: 600; + border: none; + border-radius: $radius-sm; + cursor: pointer; + transition: all $transition-base; + + .material-symbols-outlined { + font-size: 18px; + } + + &:hover { + background: rgba(37, 99, 235, 0.08); + } +} + +@keyframes shine { + 0%, 100% { left: -100%; } + 50% { left: 100%; } +} + +// ============================================ +// HERO SECTION +// ============================================ +.hero { + position: relative; + min-height: 100vh; + min-height: 100dvh; + display: flex; + align-items: center; + padding: 6rem 0 4rem; + overflow: hidden; + + @media (min-width: 768px) { + padding: 0; + } + + // Background + &__bg { + position: absolute; + inset: 0; + z-index: 0; + } + + &__gradient { + position: absolute; + inset: 0; + background: $color-bg-gradient; + } + + &__grid { + position: absolute; + inset: 0; + background-image: + linear-gradient(rgba(37, 99, 235, 0.03) 1px, transparent 1px), + linear-gradient(90deg, rgba(37, 99, 235, 0.03) 1px, transparent 1px); + background-size: 60px 60px; + mask-image: radial-gradient(ellipse 80% 60% at 50% 40%, black 20%, transparent 100%); + } + + &__orb { + position: absolute; + border-radius: 50%; + filter: blur(80px); + opacity: 0.6; + animation: orbFloat 20s ease-in-out infinite; + + &--1 { + width: 500px; + height: 500px; + background: radial-gradient(circle, rgba(37, 99, 235, 0.25) 0%, transparent 70%); + top: -100px; + left: -100px; + } + + &--2 { + width: 400px; + height: 400px; + background: radial-gradient(circle, rgba(139, 92, 246, 0.2) 0%, transparent 70%); + bottom: -50px; + right: -50px; + animation-delay: -7s; + } + + &--3 { + width: 300px; + height: 300px; + background: radial-gradient(circle, rgba(16, 185, 129, 0.15) 0%, transparent 70%); + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + animation-delay: -14s; + } + } + + > .container { + position: relative; + z-index: 10; + display: grid; + grid-template-columns: 1fr; + gap: 3rem; + align-items: center; + + @media (min-width: 1024px) { + grid-template-columns: 1.1fr 0.9fr; + gap: 4rem; + } + } + + // Content + &__content { + display: flex; + flex-direction: column; + gap: 1.5rem; + + @media (min-width: 768px) { + gap: 1.75rem; + } + } + + &__badge { + display: inline-flex; + align-items: center; + gap: 0.625rem; + align-self: flex-start; + padding: 0.5rem 1rem; + background: rgba(255, 255, 255, 0.9); + backdrop-filter: blur(10px); + border: 1px solid rgba(16, 185, 129, 0.3); + border-radius: $radius-full; + font-size: 0.8125rem; + font-weight: 600; + color: $color-text-secondary; + box-shadow: $shadow-sm; + + .badge__dot { + width: 8px; + height: 8px; + background: $color-success; + border-radius: 50%; + animation: pulse 2s ease-in-out infinite; + position: relative; + + &::after { + content: ''; + position: absolute; + inset: -3px; + border: 2px solid rgba(16, 185, 129, 0.4); + border-radius: 50%; + animation: pulseRing 2s ease-in-out infinite; + } + } + } + + &__headline { + font-size: clamp(2.25rem, 6vw, 4rem); + font-weight: 900; + line-height: 1.1; + letter-spacing: -0.03em; + margin: 0; + overflow: visible; + } + + &__line-1 { + display: block; + color: $color-text; + } + + &__line-2 { + display: block; + padding-bottom: 0.1em; + background: linear-gradient(135deg, $color-primary 0%, $color-primary-light 40%, $color-accent 100%); + background-size: 200% 200%; + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + animation: gradientFlow 4s ease-in-out infinite; + } + + &__sub { + font-size: clamp(1rem, 2vw, 1.1875rem); + line-height: 1.7; + color: $color-text-secondary; + margin: 0; + max-width: 500px; + + strong { + color: $color-text; + font-weight: 700; + } + } + + &__actions { + display: flex; + flex-wrap: wrap; + gap: 1rem; + margin-top: 0.5rem; + + @media (max-width: 599px) { + flex-direction: column; + + .btn-primary, .btn-ghost { + width: 100%; + justify-content: center; + } + } + } + + &__trust { + display: flex; + flex-wrap: wrap; + gap: 1rem; + margin-top: 0.5rem; + + @media (max-width: 599px) { + gap: 0.75rem; + } + } + + // Visual + &__visual { + display: none; + position: relative; + + @media (min-width: 1024px) { + display: block; + } + } + + // Scroll Indicator + &__scroll { + position: absolute; + bottom: 2rem; + left: 50%; + transform: translateX(-50%); + z-index: 10; + + .material-symbols-outlined { + font-size: 32px; + color: $color-text-muted; + animation: scrollBounce 2s ease-in-out infinite; + } + + @media (max-width: 767px) { + display: none; + } + } +} + +.trust-item { + display: flex; + align-items: center; + gap: 0.375rem; + font-size: 0.8125rem; + color: $color-text-muted; + + .material-symbols-outlined { + font-size: 16px; + color: $color-text-secondary; + } +} + +// Visual Card +.visual-card { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.8); + border-radius: $radius-xl; + overflow: hidden; + box-shadow: $shadow-xl, $shadow-glow; + animation: floatCard 6s ease-in-out infinite; + + &__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.25rem; + background: linear-gradient(135deg, #f8fafc, #f1f5f9); + border-bottom: 1px solid $color-border; + + .dots { + display: flex; + gap: 6px; + + span { + width: 10px; + height: 10px; + border-radius: 50%; + + &:nth-child(1) { background: #ef4444; } + &:nth-child(2) { background: #f59e0b; } + &:nth-child(3) { background: #10b981; } + } + } + } + + &__title { + font-size: 0.75rem; + font-weight: 700; + color: $color-text-muted; + letter-spacing: 0.02em; + } + + &__body { + padding: 1.25rem; + display: flex; + flex-direction: column; + gap: 0.875rem; + } +} + +.stat-row { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.875rem 1rem; + background: rgba(248, 250, 252, 0.8); + border: 1px solid rgba(0, 0, 0, 0.04); + border-radius: $radius-md; + transition: all $transition-base; + + &:hover { + background: rgba(37, 99, 235, 0.04); + border-color: rgba(37, 99, 235, 0.12); + transform: translateX(4px); + } + + .material-symbols-outlined { + font-size: 24px; + color: $color-primary; + } + + div { + display: flex; + flex-direction: column; + gap: 0.125rem; + + strong { + font-size: 0.9375rem; + font-weight: 700; + color: $color-text; + } + + small { + font-size: 0.75rem; + color: $color-text-muted; + } + } +} + +// Floating Elements +.floating { + position: absolute; + width: 52px; + height: 52px; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.8); + border-radius: $radius-md; + display: flex; + align-items: center; + justify-content: center; + box-shadow: $shadow-lg; + + .material-symbols-outlined { + font-size: 26px; + color: $color-primary; + } + + &--1 { + top: 60px; + right: -25px; + animation: floatIcon 4s ease-in-out infinite; + } + + &--2 { + bottom: 40%; + left: -30px; + animation: floatIcon 5s ease-in-out infinite 0.5s; + } + + &--3 { + bottom: -15px; + right: 30%; + animation: floatIcon 4.5s ease-in-out infinite 1s; + } +} + +// ============================================ +// PROBLEMS SECTION +// ============================================ +.problems { + padding: 5rem 0; + background: $color-bg-white; + + @media (min-width: 768px) { + padding: 6rem 0; + } + + &__header { + text-align: center; + margin-bottom: 3rem; + + h2 { + font-size: clamp(1.5rem, 3vw, 2.25rem); + font-weight: 800; + color: $color-text; + margin: 0.75rem 0 0; + letter-spacing: -0.02em; + } + } + + &__grid { + display: grid; + gap: 1.25rem; + grid-template-columns: 1fr; + + @media (min-width: 640px) { + grid-template-columns: repeat(2, 1fr); + } + + @media (min-width: 1024px) { + grid-template-columns: repeat(4, 1fr); + } + } + + &__cta { + text-align: center; + margin-top: 3rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + + p { + font-size: 1rem; + color: $color-text-secondary; + margin: 0; + font-weight: 600; + } + } +} + +.problem-card { + background: $color-bg-white; + border: 1px solid $color-border; + border-radius: $radius-xl; + padding: 1.75rem; + text-align: center; + transition: all $transition-smooth; + position: relative; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(135deg, rgba(245, 158, 11, 0.03), rgba(239, 68, 68, 0.05)); + opacity: 0; + transition: opacity $transition-base; + } + + &:hover { + transform: translateY(-8px); + border-color: rgba(245, 158, 11, 0.3); + box-shadow: $shadow-lg; + + &::before { + opacity: 1; + } + + .problem-card__emoji { + transform: scale(1.1); + } + + .problem-card__arrow, + .problem-card__solution { + opacity: 1; + transform: translateY(0); + } + } + + &__emoji { + font-size: 2.5rem; + display: block; + margin-bottom: 1rem; + transition: transform $transition-bounce; + } + + h3 { + font-size: 1.0625rem; + font-weight: 700; + color: $color-text; + margin: 0 0 0.5rem; + } + + p { + font-size: 0.875rem; + line-height: 1.6; + color: $color-text-secondary; + margin: 0; + } + + &__arrow { + display: block; + font-size: 1.5rem; + color: $color-success; + margin: 1rem 0 0.25rem; + opacity: 0; + transform: translateY(10px); + transition: all $transition-base; + } + + &__solution { + display: block; + font-size: 0.8125rem; + font-weight: 700; + color: $color-success; + opacity: 0; + transform: translateY(10px); + transition: all $transition-base 0.05s; + } +} + +// ============================================ +// SERVICES SECTION +// ============================================ +.services { + padding: 5rem 0; + background: $color-bg-gradient; + position: relative; + + @media (min-width: 768px) { + padding: 6rem 0; + } + + &__header { + text-align: center; + margin-bottom: 3rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.75rem; + + h2 { + font-size: clamp(1.5rem, 3vw, 2.25rem); + font-weight: 800; + color: $color-text; + margin: 0; + letter-spacing: -0.02em; + } + + p { + font-size: 1rem; + color: $color-text-secondary; + margin: 0; + } + } + + &__grid { + display: grid; + gap: 1.5rem; + grid-template-columns: 1fr; + + @media (min-width: 640px) { + grid-template-columns: repeat(2, 1fr); + } + + @media (min-width: 1024px) { + grid-template-columns: repeat(4, 1fr); + } + } + + &__cta { + text-align: center; + margin-top: 3rem; + } +} + +.service-card { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.8); + border-radius: $radius-xl; + padding: 1.75rem; + display: flex; + flex-direction: column; + transition: all $transition-smooth; + cursor: pointer; + position: relative; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, $color-primary, $color-primary-light); + transform: scaleX(0); + transform-origin: left; + transition: transform $transition-base; + } + + &:hover { + transform: translateY(-8px); + box-shadow: $shadow-xl; + + &::before { + transform: scaleX(1); + } + + .service-card__icon { + transform: scale(1.1) rotate(5deg); + background: linear-gradient(135deg, rgba(37, 99, 235, 0.15), rgba(99, 102, 241, 0.15)); + } + + .service-card__link { + color: $color-primary; + + .material-symbols-outlined { + transform: translateX(4px); + } + } + } + + &__icon { + width: 56px; + height: 56px; + background: linear-gradient(135deg, rgba(37, 99, 235, 0.08), rgba(99, 102, 241, 0.08)); + border-radius: $radius-lg; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 1.25rem; + transition: all $transition-bounce; + + .material-symbols-outlined { + font-size: 28px; + color: $color-primary; + } + } + + &__content { + flex: 1; + + h3 { + font-size: 1.125rem; + font-weight: 700; + color: $color-text; + margin: 0 0 0.5rem; + } + + p { + font-size: 0.875rem; + line-height: 1.6; + color: $color-text-secondary; + margin: 0 0 1rem; + } + + ul { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 0.375rem; + + li { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.8125rem; + color: $color-text-muted; + + &::before { + content: ''; + width: 5px; + height: 5px; + background: $color-primary; + border-radius: 50%; + } + } + } + } + + &__link { + display: flex; + align-items: center; + gap: 0.375rem; + font-size: 0.8125rem; + font-weight: 600; + color: $color-text-muted; + margin-top: auto; + padding-top: 1rem; + transition: color $transition-base; + + .material-symbols-outlined { + font-size: 16px; + transition: transform $transition-base; + } + } +} + +// ============================================ +// AUDIENCE SECTION +// ============================================ +.audience { + padding: 5rem 0; + background: $color-bg-white; + + @media (min-width: 768px) { + padding: 6rem 0; + } + + &__header { + text-align: center; + margin-bottom: 3rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.75rem; + + h2 { + font-size: clamp(1.5rem, 3vw, 2.25rem); + font-weight: 800; + color: $color-text; + margin: 0; + letter-spacing: -0.02em; + } + } + + &__grid { + display: grid; + gap: 1.5rem; + grid-template-columns: 1fr; + + @media (min-width: 768px) { + grid-template-columns: repeat(3, 1fr); + } + } +} + +.audience-card { + background: $color-bg-white; + border: 1px solid $color-border; + border-radius: $radius-xl; + padding: 2rem; + text-align: center; + transition: all $transition-smooth; + position: relative; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(135deg, rgba(37, 99, 235, 0.02), rgba(139, 92, 246, 0.04)); + opacity: 0; + transition: opacity $transition-base; + } + + &:hover { + transform: translateY(-8px); + border-color: $color-border-hover; + box-shadow: $shadow-lg, $shadow-glow; + + &::before { + opacity: 1; + } + + .audience-card__emoji { + transform: scale(1.15) rotate(5deg); + } + } + + &__emoji { + font-size: 3rem; + display: block; + margin-bottom: 1.25rem; + transition: transform $transition-bounce; + position: relative; + z-index: 1; + } + + h3 { + font-size: 1.25rem; + font-weight: 800; + color: $color-text; + margin: 0 0 0.75rem; + position: relative; + z-index: 1; + } + + p { + font-size: 0.9375rem; + line-height: 1.7; + color: $color-text-secondary; + margin: 0; + position: relative; + z-index: 1; + } +} + +// ============================================ +// PROCESS SECTION +// ============================================ +.process { + padding: 5rem 0; + background: $color-bg-gradient; + + @media (min-width: 768px) { + padding: 6rem 0; + } + + &__header { + text-align: center; + margin-bottom: 3rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.75rem; + + h2 { + font-size: clamp(1.5rem, 3vw, 2.25rem); + font-weight: 800; + color: $color-text; + margin: 0; + letter-spacing: -0.02em; + } + + p { + font-size: 1rem; + color: $color-text-secondary; + margin: 0; + } + } + + &__steps { + display: flex; + flex-direction: column; + gap: 1.5rem; + max-width: 1000px; + margin: 0 auto; + + @media (min-width: 768px) { + flex-direction: row; + align-items: flex-start; + gap: 1rem; + } + } +} + +.step { + flex: 1; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.8); + border-radius: $radius-xl; + padding: 2rem 1.5rem; + text-align: center; + position: relative; + transition: all $transition-smooth; + + &:hover { + transform: translateY(-8px); + box-shadow: $shadow-xl; + + .step__number { + transform: translateX(-50%) scale(1.1); + } + + .step__icon { + background: linear-gradient(135deg, rgba(37, 99, 235, 0.12), rgba(99, 102, 241, 0.12)); + } + } + + &__number { + position: absolute; + top: -16px; + left: 50%; + transform: translateX(-50%); + width: 40px; + height: 40px; + background: linear-gradient(135deg, $color-primary, $color-primary-light); + border-radius: $radius-md; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.125rem; + font-weight: 800; + color: white; + box-shadow: 0 4px 16px $color-primary-glow; + transition: transform $transition-bounce; + } + + &__icon { + width: 56px; + height: 56px; + background: linear-gradient(135deg, rgba(37, 99, 235, 0.08), rgba(99, 102, 241, 0.08)); + border-radius: $radius-lg; + display: flex; + align-items: center; + justify-content: center; + margin: 1rem auto; + transition: background $transition-base; + + .material-symbols-outlined { + font-size: 28px; + color: $color-primary; + } + } + + h4 { + font-size: 1rem; + font-weight: 700; + color: $color-text; + margin: 0 0 0.5rem; + + @media (min-width: 768px) { + font-size: 1.0625rem; + } + } + + p { + font-size: 0.8125rem; + line-height: 1.6; + color: $color-text-secondary; + margin: 0; + + @media (min-width: 768px) { + font-size: 0.875rem; + } + } + + &__connector { + display: none; + + @media (min-width: 768px) { + display: flex; + align-items: center; + justify-content: center; + padding-top: 4rem; + + svg { + width: 24px; + height: 24px; + fill: $color-text-muted; + } + } + } +} + +// ============================================ +// WHY US SECTION +// ============================================ +.why-us { + padding: 5rem 0; + background: $color-bg-white; + + @media (min-width: 768px) { + padding: 6rem 0; + } + + &__grid { + display: grid; + gap: 3rem; + grid-template-columns: 1fr; + align-items: center; + + @media (min-width: 1024px) { + grid-template-columns: 1.2fr 0.8fr; + gap: 4rem; + } + } + + &__content { + display: flex; + flex-direction: column; + gap: 1.25rem; + + h2 { + font-size: clamp(1.5rem, 3vw, 2.25rem); + font-weight: 800; + color: $color-text; + margin: 0; + letter-spacing: -0.02em; + } + } + + &__text { + font-size: 1rem; + line-height: 1.7; + color: $color-text-secondary; + margin: 0; + + strong { + color: $color-text; + } + + @media (min-width: 768px) { + font-size: 1.0625rem; + } + } + + &__points { + display: flex; + flex-direction: column; + gap: 1rem; + margin-top: 0.5rem; + } + + &__visual { + display: none; + + @media (min-width: 1024px) { + display: block; + } + } +} + +.usp-item { + display: flex; + gap: 0.875rem; + align-items: flex-start; + + .material-symbols-outlined { + font-size: 22px; + color: $color-success; + flex-shrink: 0; + margin-top: 2px; + } + + div { + display: flex; + flex-direction: column; + gap: 0.25rem; + + strong { + font-size: 0.9375rem; + font-weight: 700; + color: $color-text; + } + + span { + font-size: 0.875rem; + color: $color-text-secondary; + line-height: 1.5; + } + } +} + +.testimonial-card { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.8); + border-radius: $radius-xl; + padding: 2rem; + position: relative; + box-shadow: $shadow-lg, $shadow-glow; + + &__quote { + position: absolute; + top: -14px; + left: 24px; + width: 44px; + height: 44px; + background: linear-gradient(135deg, $color-primary, $color-primary-light); + border-radius: $radius-md; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 16px $color-primary-glow; + + .material-symbols-outlined { + font-size: 26px; + color: white; + } + } + + &__text { + font-size: 1rem; + line-height: 1.75; + color: $color-text; + font-style: italic; + margin: 1.25rem 0 1.5rem; + + strong { + font-style: normal; + } + + @media (min-width: 768px) { + font-size: 1.0625rem; + } + } + + &__author { + display: flex; + align-items: center; + gap: 0.875rem; + + .author-avatar { + width: 48px; + height: 48px; + background: linear-gradient(135deg, $color-primary, $color-primary-light); + border-radius: $radius-md; + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 0.9375rem; + color: white; + } + + div { + display: flex; + flex-direction: column; + gap: 0.125rem; + + strong { + font-size: 0.9375rem; + font-weight: 700; + color: $color-text; + } + + span { + font-size: 0.8125rem; + color: $color-text-muted; + } + } + } +} + +// ============================================ +// FINAL CTA SECTION +// ============================================ +.cta-final { + padding: 5rem 0; + background: $color-bg-gradient; + + @media (min-width: 768px) { + padding: 6rem 0; + } +} + +.cta-box { + position: relative; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.8); + border-radius: $radius-2xl; + padding: 3rem 1.75rem; + text-align: center; + overflow: hidden; + box-shadow: $shadow-xl; + + @media (min-width: 768px) { + padding: 4rem 3rem; + } + + &__glow { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 600px; + height: 600px; + background: radial-gradient(circle, rgba(37, 99, 235, 0.08) 0%, transparent 60%); + pointer-events: none; + } + + &__content { + position: relative; + z-index: 10; + display: flex; + flex-direction: column; + align-items: center; + gap: 1.25rem; + max-width: 550px; + margin: 0 auto; + + h2 { + font-size: clamp(1.5rem, 3vw, 2rem); + font-weight: 800; + color: $color-text; + margin: 0; + letter-spacing: -0.02em; + } + + > p { + font-size: 1rem; + line-height: 1.7; + color: $color-text-secondary; + margin: 0; + + strong { + color: $color-text; + } + + @media (min-width: 768px) { + font-size: 1.0625rem; + } + } + } + + &__benefits { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.75rem; + margin: 0.5rem 0; + } + + &__note { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.8125rem; + color: $color-text-muted; + margin: 0; + + .material-symbols-outlined { + font-size: 16px; + } + } + + &__alternative { + display: flex; + align-items: center; + gap: 0.5rem; + margin-top: 0.5rem; + padding-top: 1rem; + border-top: 1px solid $color-border; + + span { + font-size: 0.875rem; + color: $color-text-muted; + } + } +} + +.benefit-tag { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + background: rgba(248, 250, 252, 0.8); + border: 1px solid $color-border; + border-radius: $radius-full; + font-size: 0.8125rem; + font-weight: 600; + color: $color-text-secondary; + + .material-symbols-outlined { + font-size: 18px; + color: $color-success; + } +} + +// ============================================ +// ANIMATIONS +// ============================================ + +// Entrance Animations +.anim-fade-up { + opacity: 0; + transform: translateY(30px); + animation: fadeInUp 0.7s cubic-bezier(0.16, 1, 0.3, 1) forwards; +} + +.anim-fade-left { + opacity: 0; + transform: translateX(40px); + animation: fadeInLeft 0.7s cubic-bezier(0.16, 1, 0.3, 1) forwards; +} + +.anim-delay-1 { animation-delay: 0.1s; } +.anim-delay-2 { animation-delay: 0.2s; } +.anim-delay-3 { animation-delay: 0.35s; } +.anim-delay-4 { animation-delay: 0.5s; } +.anim-delay-5 { animation-delay: 0.65s; } +.anim-delay-6 { animation-delay: 0.8s; } + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeInLeft { + from { + opacity: 0; + transform: translateX(40px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +// Scroll Animations +[data-animate] { + .chip, h2, p, .anim-item { + opacity: 0; + transform: translateY(30px); + transition: all 0.6s cubic-bezier(0.16, 1, 0.3, 1); + } + + &.is-visible { + .chip, h2, p, .anim-item { + opacity: 1; + transform: translateY(0); + } + + h2 { transition-delay: 0.1s; } + p { transition-delay: 0.2s; } + } +} + +[data-animate-card] { + opacity: 0; + transform: translateY(40px); + transition: all 0.6s cubic-bezier(0.16, 1, 0.3, 1); + + .is-visible & { + opacity: 1; + transform: translateY(0); + + @for $i from 1 through 6 { + &:nth-child(#{$i}) { + transition-delay: #{0.1 + $i * 0.08}s; + } + } + } +} + +// Background Animations +@keyframes orbFloat { + 0%, 100% { + transform: translate(0, 0) scale(1); + opacity: 0.6; + } + 33% { + transform: translate(30px, -30px) scale(1.05); + opacity: 0.8; + } + 66% { + transform: translate(-20px, 20px) scale(0.95); + opacity: 0.5; + } +} + +@keyframes gradientFlow { + 0%, 100% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +@keyframes pulseRing { + 0% { + transform: scale(1); + opacity: 1; + } + 100% { + transform: scale(2.5); + opacity: 0; + } +} + +@keyframes floatCard { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-12px); + } +} + +@keyframes floatIcon { + 0%, 100% { + transform: translateY(0) rotate(0deg); + } + 50% { + transform: translateY(-10px) rotate(5deg); + } +} + +@keyframes scrollBounce { + 0%, 100% { + transform: translateY(0); + opacity: 0.5; + } + 50% { + transform: translateY(8px); + opacity: 1; + } +} + +// ============================================ +// REDUCED MOTION +// ============================================ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + +// ============================================ +// MATERIAL SYMBOLS +// ============================================ +.material-symbols-outlined { + font-variation-settings: + "FILL" 0, + "wght" 500, + "GRAD" 0, + "opsz" 24; + line-height: 1; +} diff --git a/apps/frontend/src/app/components/home/home.component.spec.ts b/apps/frontend/src/app/components/home/home.component.spec.ts new file mode 100644 index 0000000..1191557 --- /dev/null +++ b/apps/frontend/src/app/components/home/home.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HomeComponent } from './home.component'; + +describe('HomeComponent', () => { + let component: HomeComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [HomeComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(HomeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/frontend/src/app/components/home/home.component.ts b/apps/frontend/src/app/components/home/home.component.ts new file mode 100644 index 0000000..4b39400 --- /dev/null +++ b/apps/frontend/src/app/components/home/home.component.ts @@ -0,0 +1,106 @@ +import { Component, OnDestroy, OnInit, AfterViewInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { IconComponent } from '../../shared/icon/icon.component'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +@Component({ + selector: 'app-home', + templateUrl: './home.component.html', + styleUrls: ['./home.component.scss'], + standalone: true, + imports: [CommonModule, FormsModule] +}) +export class HomeComponent implements OnInit, OnDestroy, AfterViewInit { + + constructor(public router: Router) { } + + private mouseX = 0; + private mouseY = 0; + private currentX = 0; + private currentY = 0; + private animationFrame: number | null = null; + private intersectionObserver: IntersectionObserver | null = null; + + ngOnInit(): void { + // Maus-Tracking starten + this.initMouseTracking(); + } + + ngAfterViewInit(): void { + // Scroll-Animationen initialisieren + this.initScrollAnimations(); + } + + ngOnDestroy(): void { + // Cleanup + if (this.animationFrame) { + cancelAnimationFrame(this.animationFrame); + } + document.removeEventListener('mousemove', this.handleMouseMove); + + if (this.intersectionObserver) { + this.intersectionObserver.disconnect(); + } + } + + private initScrollAnimations(): void { + const animatedSections = document.querySelectorAll('[data-animate]'); + + this.intersectionObserver = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + entry.target.classList.add('is-visible'); + } + }); + }, + { + threshold: 0.15, + rootMargin: '0px 0px -50px 0px' + } + ); + + animatedSections.forEach((section) => { + this.intersectionObserver?.observe(section); + }); + } + + private initMouseTracking(): void { + const heroSection = document.querySelector('.hero') as HTMLElement; + if (!heroSection) return; + + // Maus-Position tracken + document.addEventListener('mousemove', this.handleMouseMove.bind(this)); + + // Smooth animation loop + this.animate(); + } + + private handleMouseMove = (e: MouseEvent): void => { + this.mouseX = e.clientX; + this.mouseY = e.clientY; + }; + + private animate = (): void => { + // Smooth lerp für flüssige Bewegung + const ease = 0.1; + this.currentX += (this.mouseX - this.currentX) * ease; + this.currentY += (this.mouseY - this.currentY) * ease; + + // Lava-Blob positionieren + const heroSection = document.querySelector('.hero') as HTMLElement; + if (heroSection) { + heroSection.style.setProperty('--mouse-x', `${this.currentX}px`); + heroSection.style.setProperty('--mouse-y', `${this.currentY}px`); + } + + this.animationFrame = requestAnimationFrame(this.animate); + }; + + + routeTo(path: string) { + this.router.navigate([path]); + } + +} \ No newline at end of file diff --git a/apps/frontend/src/app/components/imprint/imprint.component.html b/apps/frontend/src/app/components/imprint/imprint.component.html new file mode 100644 index 0000000..58fc1d5 --- /dev/null +++ b/apps/frontend/src/app/components/imprint/imprint.component.html @@ -0,0 +1,108 @@ + + + \ No newline at end of file diff --git a/apps/frontend/src/app/components/imprint/imprint.component.scss b/apps/frontend/src/app/components/imprint/imprint.component.scss new file mode 100644 index 0000000..8f34399 --- /dev/null +++ b/apps/frontend/src/app/components/imprint/imprint.component.scss @@ -0,0 +1,265 @@ +@import '../../utils/shared-styles.scss'; + +.container { + width: 100%; + max-width: 1200px; + margin-inline: auto; + padding-inline: clamp(1rem, 4vw, 2rem); + margin-bottom: 4rem; +} + +section { + display: flex; + flex-direction: column; + align-items: center; +} + +/* ===== LEGAL/IMPRINT SECTION ===== */ + +.legal--imprint { + background: $color-background-secondary; + padding-block: 2rem; + + @media (min-width: 768px) { + padding-block: 3rem; + } + + .legal__content { + background: $glass-bg; + backdrop-filter: blur(10px); + border: 1px solid $glass-border; + border-radius: $radius-xl; + padding: 2rem; + box-shadow: $shadow-glass; + max-width: 800px; + margin-inline: auto; + + @media (min-width: 768px) { + padding: 3rem; + } + + @media (min-width: 1024px) { + padding: 3.5rem 4rem; + } + + h2 { + font-size: clamp(1.25rem, 2vw + 0.5rem, 1.5rem); + font-weight: 700; + color: $color-text-primary; + margin: 0 0 0.75rem; + letter-spacing: -0.01em; + scroll-margin-top: 120px; + line-height: 1.3; + + &:not(:first-child) { + margin-top: 2.5rem; + padding-top: 2rem; + border-top: 1px solid $color-gray-200; + } + + &:first-child { + margin-top: 0; + } + } + + p { + margin: 0; + color: $color-text-secondary; + line-height: 1.7; + font-size: 1rem; + + &+p { + margin-top: 1rem; + } + + strong { + color: $color-text-primary; + font-weight: 600; + } + + br { + line-height: 1.4; + } + } + + a { + color: $color-brand-primary; + text-decoration: none; + font-weight: 600; + transition: all 0.2s ease; + + &:hover { + text-decoration: underline; + } + + &:focus-visible { + outline: 2px solid $color-brand-primary; + outline-offset: 2px; + border-radius: 2px; + } + } + } +} + +/* ===== INFO CARD ===== */ + +.info-card { + background: $glass-bg; + border: 1px solid $glass-border; + border-radius: $radius-lg; + padding: 1.5rem; + margin-bottom: 1rem; + box-shadow: $shadow-md; + display: grid; + grid-template-columns: auto 1fr; + gap: 1rem; + align-items: start; + transition: all 0.3s ease; + + &:hover { + transform: translateY(-2px); + box-shadow: $shadow-lg; + } + + .info-icon { + width: 48px; + height: 48px; + background: $gradient-primary; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: $shadow-brand; + flex-shrink: 0; + + .material-symbols-outlined { + font-size: 24px; + color: white; + } + } + + .info-content { + h3 { + font-size: 1.0625rem; + font-weight: 700; + color: $color-text-primary; + } + + p { + font-size: 0.9375rem; + color: $color-text-secondary; + line-height: 1.6; + margin: 0; + + &+p { + margin-top: 0.75rem; + } + } + } +} + +/* ===== HIGHLIGHT BOX ===== */ + +.highlight-box { + background: $gradient-subtle; + border: 1px solid rgba(37, 99, 235, 0.15); + border-radius: $radius-lg; + padding: 1.5rem; + margin: 0 0 1rem; + + p { + margin: 0; + + &+p { + margin-top: 0.75rem; + } + } + + strong { + color: $color-text-primary; + } +} + +/* ===== CONTACT BOX ===== */ + +.contact-box { + background: $gradient-subtle; + border: 1px solid rgba(37, 99, 235, 0.15); + border-radius: $radius-xl; + padding: 2rem; + text-align: center; + margin-top: 2rem; + position: relative; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + inset: 0; + background: + radial-gradient(circle at 20% 50%, rgba(37, 99, 235, 0.06), transparent 50%), + radial-gradient(circle at 80% 50%, rgba(59, 130, 246, 0.04), transparent 50%); + pointer-events: none; + } + + >* { + position: relative; + z-index: 1; + } + + .contact-icon { + width: 56px; + height: 56px; + margin: 0 auto 1rem; + background: $gradient-primary; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: $shadow-brand; + + .material-symbols-outlined { + font-size: 28px; + color: white; + } + } + + h3 { + font-size: 1.25rem; + font-weight: 700; + color: $color-text-primary; + margin: 0 0 0.5rem; + } + + p { + color: $color-text-secondary; + margin: 0 0 1rem; + line-height: 1.6; + } + + .contact-details { + display: inline-flex; + flex-direction: column; + gap: 0.5rem; + text-align: left; + + a { + color: $color-brand-primary; + text-decoration: none; + font-weight: 600; + + &:hover { + text-decoration: underline; + } + } + } +} + +/* ===== MOTION PREFERENCES ===== */ + +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/components/imprint/imprint.component.spec.ts b/apps/frontend/src/app/components/imprint/imprint.component.spec.ts new file mode 100644 index 0000000..bcf41aa --- /dev/null +++ b/apps/frontend/src/app/components/imprint/imprint.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ImprintComponent } from './imprint.component'; + +describe('ImprintComponent', () => { + let component: ImprintComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ImprintComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ImprintComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/frontend/src/app/components/imprint/imprint.component.ts b/apps/frontend/src/app/components/imprint/imprint.component.ts new file mode 100644 index 0000000..dfa1d78 --- /dev/null +++ b/apps/frontend/src/app/components/imprint/imprint.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { PageTitleComponent } from "../../shared/page-title/page-title.component"; + +@Component({ + selector: 'app-imprint', + standalone: true, + imports: [PageTitleComponent], + templateUrl: './imprint.component.html', + styleUrl: './imprint.component.scss' +}) +export class ImprintComponent { + +} diff --git a/apps/frontend/src/app/components/it-services/it-services.component.html b/apps/frontend/src/app/components/it-services/it-services.component.html new file mode 100644 index 0000000..96300cf --- /dev/null +++ b/apps/frontend/src/app/components/it-services/it-services.component.html @@ -0,0 +1,119 @@ +
+
+ + +
+

IT-Dienstleistungen die funktionieren

+

+ Von Support bis Cloud-Management. Wähle deine Lösung. +

+
+ + +
+ +
+ + +
+
+
+ help +
+
+

Welche IT-Lösung brauchst du?

+

Lass uns gemeinsam die passende Lösung für dein Unternehmen finden.

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

+ Lieber persönlich sprechen? + + Kostenloses Beratungsgespräch buchen + +

+
+ + +
+
+ security +
+ Zertifiziert +

ISO 27001 Standards

+
+
+
+ schedule +
+ Reaktionszeit +

Support binnen 4h

+
+
+
+ groups +
+ Erfahren +

50+ zufriedene Kunden

+
+
+
+ +
+
\ No newline at end of file diff --git a/apps/frontend/src/app/components/it-services/it-services.component.scss b/apps/frontend/src/app/components/it-services/it-services.component.scss new file mode 100644 index 0000000..e1a7bc4 --- /dev/null +++ b/apps/frontend/src/app/components/it-services/it-services.component.scss @@ -0,0 +1,1055 @@ +@import '../../utils/shared-styles.scss'; + +/* ===== ENHANCED SERVICES SECTION ===== */ +.services-overview { + position: relative; + padding: 1.5rem 0; + overflow: hidden; + background: linear-gradient(135deg, #ffffff 0%, #f8fafc 50%, #f0f4ff 100%); + min-height: 100vh; + + @media (min-width: 768px) { + padding: clamp(2rem, 4vw, 4rem) 0; + } + + // Animierte Hintergrund-Ebenen + &::before { + content: ''; + position: absolute; + inset: -50%; + background: + radial-gradient(circle at 20% 30%, rgba(37, 99, 235, 0.08) 0%, transparent 50%), + radial-gradient(circle at 80% 70%, rgba(59, 130, 246, 0.06) 0%, transparent 50%), + radial-gradient(circle at 50% 50%, rgba(99, 102, 241, 0.04) 0%, transparent 60%); + animation: servicesGradientRotate 25s ease-in-out infinite; + pointer-events: none; + } + + // Floating Blur Orbs + &::after { + content: ''; + position: absolute; + inset: 0; + background-image: + radial-gradient(circle at 15% 15%, rgba(37, 99, 235, 0.3) 0%, transparent 35%), + radial-gradient(circle at 85% 85%, rgba(147, 51, 234, 0.25) 0%, transparent 35%); + filter: blur(60px); + opacity: 0.5; + animation: orbsPulse 12s ease-in-out infinite; + pointer-events: none; + } + + // Glassmorphism Grid Pattern + .glass-grid { + position: absolute; + inset: 0; + background-image: + linear-gradient(rgba(37, 99, 235, 0.03) 1px, transparent 1px), + linear-gradient(90deg, rgba(37, 99, 235, 0.03) 1px, transparent 1px); + background-size: 50px 50px; + animation: gridMove 20s linear infinite; + pointer-events: none; + } + + // Floating Particles + .service-particles { + position: absolute; + inset: 0; + pointer-events: none; + + .particle { + position: absolute; + width: 3px; + height: 3px; + background: linear-gradient(135deg, #2563eb, #3b82f6); + border-radius: 50%; + opacity: 0; + animation: serviceParticle 15s linear infinite; + + @for $i from 1 through 15 { + &:nth-child(#{$i}) { + left: random(100) * 1%; + top: -10%; + animation-delay: random(15) * 1s; + animation-duration: (10 + random(10)) * 1s; + } + } + } + } +} + +.container { + position: relative; + z-index: 2; + max-width: 1200px; + margin: 0 auto; + + @media (max-width: 640px) { + padding-right: 12px; + padding-left: 12px; + } +} + +/* ===== HERO SECTION ===== */ +.services-hero { + text-align: center; + margin-bottom: 1.5rem; + margin-left: 1rem; + margin-right: 1rem; + animation: heroFadeIn 1s ease-out; + position: relative; + + @media (min-width: 768px) { + margin-bottom: clamp(2rem, 5vw, 3rem); + margin-left: clamp(1rem, 4vw, 2rem); + margin-right: clamp(1rem, 4vw, 2rem); + } + + h1 { + font-size: 1.5rem; + font-weight: 900; + margin: 0 0 .5rem; + background: linear-gradient(135deg, #2563eb 0%, #3b82f6 50%, #6366f1 100%); + background-size: 200% 200%; + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + line-height: 1.2; + animation: gradientText 4s ease-in-out infinite; + position: relative; + + @media (min-width: 768px) { + font-size: clamp(1.75rem, 6vw, 3rem); + } + + // Glow Effect + &::after { + content: attr(data-text); + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + z-index: -1; + filter: blur(20px); + opacity: 0.3; + background: linear-gradient(135deg, #2563eb, #3b82f6); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + } + } + + .services-subtitle { + font-size: .9rem; + color: $color-text-secondary; + margin: 0; + line-height: 1.4; + animation: slideUp 0.8s ease-out 0.2s backwards; + position: relative; + + @media (min-width: 768px) { + font-size: clamp(1rem, 3.2vw, 1.125rem); + } + } +} + +/* ===== SERVICE GRID ===== */ +.services-grid { + display: grid; + grid-template-columns: 1fr; + gap: 1rem; + align-items: stretch; + margin-bottom: 1.5rem; + position: relative; + + @media (min-width: 641px) { + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: clamp(1rem, 3.5vw, 2rem); + margin-bottom: clamp(2rem, 5vw, 4rem); + } +} + +/* ===== SERVICE CARD ===== */ +.service-card { + position: relative; + background: rgba(255, 255, 255, 0.9); + backdrop-filter: blur(20px); + border: 2px solid rgba(153, 153, 153, 0.5); + border-radius: 16px; + padding: 1rem; + display: flex; + flex-direction: column; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.04), + inset 0 0 0 1px rgba(255, 255, 255, 0.5); + overflow: hidden; + animation: cardFadeIn 0.8s ease-out backwards; + + @media (min-width: 641px) { + padding: clamp(1.25rem, 3.5vw, 2rem); + min-height: 400px; + border-radius: 24px; + } + + @for $i from 1 through 3 { + &:nth-child(#{$i}) { + animation-delay: #{$i * 0.1}s; + } + } + + // Glassmorphism overlay + &::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(135deg, + rgba(37, 99, 235, 0.03) 0%, + transparent 40%, + transparent 60%, + rgba(147, 51, 234, 0.03) 100%); + opacity: 0; + transition: opacity 0.4s ease; + pointer-events: none; + } + + // Animated border gradient + &::after { + content: ''; + position: absolute; + inset: -2px; + background: linear-gradient(45deg, + #2563eb, + #3b82f6, + #6366f1, + #8b5cf6, + #2563eb); + border-radius: 24px; + opacity: 0; + z-index: -1; + transition: opacity 0.4s ease; + } + + &:hover { + transform: translateY(-8px) scale(1.02); + box-shadow: + 0 20px 40px rgba(0, 0, 0, 0.12), + 0 0 0 1px rgba(37, 99, 235, 0.2), + inset 0 0 0 1px rgba(255, 255, 255, 0.8); + border-color: transparent; + background: rgba(255, 255, 255, 0.98); + + @media (min-width: 769px) { + transform: translateY(-12px) scale(1.02); + box-shadow: + 0 30px 60px rgba(0, 0, 0, 0.15), + 0 0 0 1px rgba(37, 99, 235, 0.3), + inset 0 0 0 1px rgba(255, 255, 255, 1); + } + + &::before { + opacity: 1; + } + + &::after { + opacity: 0.1; + } + + .card-icon { + transform: scale(1.1) rotate(5deg); + box-shadow: 0 8px 24px rgba(37, 99, 235, 0.3); + + .icon-emoji { + animation: emojiPulse 0.5s ease; + } + } + + .card-badge { + animation: badgeBounce 0.5s ease; + } + } + + // Featured card special styles + &.featured { + border-color: rgba(59, 130, 246, 0.3); + border-width: 3px; + background: linear-gradient(135deg, + rgba(255, 255, 255, 0.95), + rgba(237, 242, 255, 0.95)); + box-shadow: + 0 8px 32px rgba(37, 99, 235, 0.12), + inset 0 0 0 1px rgba(255, 255, 255, 0.8); + position: relative; + + @media (min-width: 641px) { + box-shadow: + 0 16px 48px rgba(37, 99, 235, 0.15), + inset 0 0 0 1px rgba(255, 255, 255, 0.9); + } + + // Glow effect for featured + &::before { + background: radial-gradient(circle at center, + rgba(37, 99, 235, 0.1) 0%, + transparent 70%); + opacity: 1; + animation: featuredGlow 3s ease-in-out infinite; + } + + .card-icon { + background: linear-gradient(135deg, #2563eb, #3b82f6); + box-shadow: + 0 6px 20px rgba(37, 99, 235, 0.25), + inset 0 1px 0 rgba(255, 255, 255, 0.2); + position: relative; + + @media (min-width: 641px) { + box-shadow: + 0 10px 30px rgba(37, 99, 235, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.3); + } + + &::after { + content: ''; + position: absolute; + inset: -4px; + background: linear-gradient(135deg, #2563eb, #3b82f6); + border-radius: inherit; + opacity: 0; + filter: blur(8px); + animation: iconPulse 2s ease-in-out infinite; + } + + .icon-emoji { + filter: drop-shadow(0 2px 8px rgba(255, 255, 255, 0.3)); + } + } + } +} + +.card-badge { + position: static; + align-self: flex-start; + margin-bottom: .5rem; + padding: .3rem .6rem; + background: linear-gradient(135deg, #2563eb, #3b82f6); + color: #fff; + border-radius: 999px; + font-size: .7rem; + font-weight: 800; + box-shadow: + 0 4px 12px rgba(37, 99, 235, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.2); + white-space: nowrap; + z-index: 1; + position: relative; + overflow: hidden; + + @media (min-width: 421px) { + position: absolute; + top: 1rem; + right: 1rem; + font-size: .78rem; + padding: .4rem .8rem; + margin-bottom: 0; + } + + &::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: linear-gradient(45deg, + transparent, + rgba(255, 255, 255, 0.2), + transparent); + animation: badgeShimmer 3s ease-in-out infinite; + } +} + +.card-icon { + width: 52px; + height: 52px; + background: linear-gradient(135deg, rgba(37, 99, 235, 0.1), rgba(59, 130, 246, 0.1)); + border-radius: 14px; + display: grid; + place-items: center; + margin-bottom: .75rem; + transition: all 0.3s ease; + position: relative; + + @media (min-width: 641px) { + width: clamp(64px, 12vw, 80px); + height: clamp(64px, 12vw, 80px); + border-radius: 20px; + margin-bottom: 1.25rem; + } + + .icon-emoji { + font-size: 1.75rem; + position: relative; + z-index: 1; + + @media (min-width: 641px) { + font-size: clamp(2rem, 7vw, 2.5rem); + } + } +} + +.card-content { + display: flex; + flex-direction: column; + gap: .625rem; + flex: 1; + position: relative; + z-index: 1; +} + +.card-header { + display: flex; + flex-direction: column; + gap: .375rem; + margin-bottom: .25rem; + + @media (min-width: 641px) { + flex-direction: row; + justify-content: space-between; + align-items: flex-start; + gap: .75rem; + margin-bottom: .5rem; + } + + h3 { + font-size: 1.15rem; + font-weight: 800; + color: $color-text-primary; + margin: 0; + overflow-wrap: anywhere; + line-height: 1.25; + transition: color 0.3s ease; + + @media (min-width: 641px) { + font-size: clamp(1.25rem, 4.6vw, 1.5rem); + line-height: 1.3; + } + } + + .card-price { + display: inline-flex; + align-items: center; + align-self: flex-start; + background: linear-gradient(135deg, rgba(37, 99, 235, 0.1), rgba(59, 130, 246, 0.1)); + color: #2563eb; + padding: .35rem .6rem; + border-radius: 999px; + font-size: .8rem; + font-weight: 800; + border: 1px solid rgba(37, 99, 235, 0.2); + white-space: nowrap; + transition: all 0.3s ease; + position: relative; + overflow: hidden; + + @media (min-width: 641px) { + align-self: auto; + font-size: .875rem; + padding: .45rem .75rem; + } + + &::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(135deg, #2563eb, #3b82f6); + opacity: 0; + transition: opacity 0.3s ease; + } + + .service-card:hover & { + transform: scale(1.05); + box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2); + } + } +} + +.card-features { + list-style: none; + padding: 0; + margin: 0 0 .75rem; + display: flex; + flex-direction: column; + gap: .5rem; + + @media (min-width: 641px) { + gap: .625rem; + margin: 0 0 1rem; + } + + li { + display: grid; + grid-template-columns: 20px 1fr; + align-items: start; + gap: .5rem; + color: $color-text-secondary; + font-size: .85rem; + line-height: 1.4; + transition: all 0.3s ease; + position: relative; + + @media (min-width: 641px) { + grid-template-columns: 24px 1fr; + gap: .625rem; + font-size: clamp(.9rem, 2.5vw, .9375rem); + line-height: 1.5; + } + + &::before { + content: ''; + position: absolute; + left: -8px; + right: -8px; + top: 50%; + transform: translateY(-50%); + height: 100%; + background: linear-gradient(90deg, transparent, rgba(37, 99, 235, 0.05), transparent); + opacity: 0; + transition: opacity 0.3s ease; + border-radius: 8px; + } + + .service-card:hover & { + transform: translateX(4px); + + &::before { + opacity: 1; + } + } + + .material-symbols-outlined { + color: #10b981; + font-size: 20px; + margin-top: .075rem; + filter: drop-shadow(0 2px 4px rgba(16, 185, 129, 0.2)); + transition: all 0.3s ease; + + @media (min-width: 641px) { + font-size: 22px; + margin-top: .125rem; + } + } + + .service-card:hover & .material-symbols-outlined { + animation: checkPop 0.5s ease; + } + } +} + +/* ===== CTA SECTION ===== */ +.services-cta { + background: rgba(255, 255, 255, 0.8); + backdrop-filter: blur(20px); + border: 2px solid rgba(37, 99, 235, 0.15); + border-radius: 16px; + padding: 1.25rem 1rem; + margin-bottom: 1.5rem; + position: relative; + overflow: hidden; + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.06), + inset 0 0 0 1px rgba(255, 255, 255, 0.5); + animation: ctaFadeIn 1s ease-out 0.5s backwards; + + @media (min-width: 641px) { + padding: clamp(1.5rem, 3.5vw, 2.5rem); + border-radius: 24px; + margin-bottom: clamp(2rem, 5vw, 4rem); + } + + // Animated gradient background + &::before { + content: ''; + position: absolute; + inset: -50%; + background: radial-gradient(circle at 30% 50%, + rgba(37, 99, 235, 0.1) 0%, + transparent 40%); + animation: ctaGradientRotate 15s ease-in-out infinite; + pointer-events: none; + } + + &:hover { + transform: translateY(-2px); + box-shadow: + 0 16px 48px rgba(0, 0, 0, 0.08), + inset 0 0 0 1px rgba(255, 255, 255, 0.8); + border-color: rgba(37, 99, 235, 0.25); + } +} + +.cta-actions { + display: flex; + flex-direction: column; + gap: .75rem; + margin-top: 1rem; +} + +.cta-alternative { + font-size: .875rem; + color: $color-text-secondary; + margin: 0; + margin-top: 20px; + line-height: 1.4; + + @media (min-width: 641px) { + font-size: 1rem; + } + + a { + color: #2563eb; + font-weight: 600; + text-decoration: underline; + transition: color 0.3s ease; + cursor: pointer; + + &:hover { + color: #1e40af; + } + } +} + +.cta-content { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + margin-bottom: 1rem; + gap: .75rem; + position: relative; + z-index: 1; + + @media (min-width: 768px) { + flex-direction: row; + text-align: left; + margin-bottom: clamp(1.25rem, 3vw, 2rem); + gap: 1rem; + } +} + +.cta-icon { + flex-shrink: 0; + position: relative; + + .material-symbols-outlined { + font-size: 36px; + color: #2563eb; + filter: drop-shadow(0 4px 12px rgba(37, 99, 235, 0.2)); + animation: iconFloat 3s ease-in-out infinite; + + @media (min-width: 768px) { + font-size: clamp(40px, 8vw, 56px); + } + } +} + +/* ===== TRUST SECTION ===== */ +.trust-section { + display: grid; + grid-template-columns: 1fr; + gap: .75rem; + animation: trustFadeIn 1s ease-out 0.7s backwards; + position: relative; + + @media (min-width: 641px) { + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 1rem; + } +} + +.trust-item { + display: flex; + gap: .75rem; + padding: .95rem; + background: rgba(255, 255, 255, 0.9); + backdrop-filter: blur(10px); + border: 1px solid rgba(226, 232, 240, 0.5); + border-radius: 12px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; + box-shadow: + 0 2px 8px rgba(0, 0, 0, 0.04), + inset 0 0 0 1px rgba(255, 255, 255, 0.5); + position: relative; + overflow: hidden; + + @media (min-width: 641px) { + padding: 1.25rem; + gap: 1rem; + border-radius: 16px; + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.05), + inset 0 0 0 1px rgba(255, 255, 255, 0.6); + } + + &::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(135deg, + transparent, + rgba(37, 99, 235, 0.05), + transparent); + transform: translateX(-100%); + transition: transform 0.5s ease; + } + + &:hover { + transform: translateY(-4px) scale(1.02); + box-shadow: + 0 12px 32px rgba(0, 0, 0, 0.08), + inset 0 0 0 1px rgba(37, 99, 235, 0.3); + border-color: rgba(37, 99, 235, 0.2); + background: rgba(255, 255, 255, 0.95); + + &::before { + transform: translateX(0); + } + + .material-symbols-outlined { + animation: iconBounce 0.5s ease; + } + } + + .material-symbols-outlined { + font-size: 24px; + color: #2563eb; + flex-shrink: 0; + filter: drop-shadow(0 2px 8px rgba(37, 99, 235, 0.2)); + + @media (min-width: 641px) { + font-size: 32px; + } + } +} + +/* ===== ANIMATIONS ===== */ +@keyframes servicesGradientRotate { + + 0%, + 100% { + transform: rotate(0deg) scale(1); + } + + 50% { + transform: rotate(180deg) scale(1.2); + } +} + +@keyframes orbsPulse { + + 0%, + 100% { + opacity: 0.5; + transform: scale(1); + } + + 50% { + opacity: 0.3; + transform: scale(1.1); + } +} + +@keyframes gridMove { + 0% { + transform: translate(0, 0); + } + + 100% { + transform: translate(50px, 50px); + } +} + +@keyframes serviceParticle { + 0% { + transform: translateY(0) rotate(0deg); + opacity: 0; + } + + 10% { + opacity: 1; + } + + 90% { + opacity: 1; + } + + 100% { + transform: translateY(100vh) rotate(720deg); + opacity: 0; + } +} + +@keyframes heroFadeIn { + from { + opacity: 0; + transform: translateY(-20px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes cardFadeIn { + from { + opacity: 0; + transform: translateY(30px) scale(0.95); + } + + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes gradientText { + + 0%, + 100% { + background-position: 0% 50%; + } + + 50% { + background-position: 100% 50%; + } +} + +@keyframes featuredGlow { + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.6; + } +} + +@keyframes iconPulse { + + 0%, + 100% { + opacity: 0; + } + + 50% { + opacity: 0.5; + } +} + +@keyframes emojiPulse { + + 0%, + 100% { + transform: scale(1) rotate(0deg); + } + + 50% { + transform: scale(1.2) rotate(10deg); + } +} + +@keyframes badgeBounce { + + 0%, + 100% { + transform: translateY(0); + } + + 50% { + transform: translateY(-6px); + } +} + +@keyframes badgeShimmer { + from { + transform: rotate(25deg) translateX(-100%); + } + + to { + transform: rotate(25deg) translateX(100%); + } +} + +@keyframes checkPop { + + 0%, + 100% { + transform: scale(1); + color: #10b981; + } + + 50% { + transform: scale(1.3); + color: #34d399; + } +} + + +@keyframes arrowSlide { + + 0%, + 100% { + transform: translateX(0); + } + + 50% { + transform: translateX(4px); + } +} + +@keyframes ctaFadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes ctaGradientRotate { + + 0%, + 100% { + transform: rotate(0deg) scale(1); + } + + 50% { + transform: rotate(180deg) scale(1.1); + } +} + +@keyframes iconFloat { + + 0%, + 100% { + transform: translateY(0); + } + + 50% { + transform: translateY(-8px); + } +} + +@keyframes trustFadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes iconBounce { + + 0%, + 100% { + transform: translateY(0); + } + + 50% { + transform: translateY(-6px); + } +} + +/* ===== END OF SERVICES COMPONENT STYLES ===== */ + + +/* === PERFORMANCE MODE: Mobile & Reduce Motion === */ +@media (max-width: 640px), +(prefers-reduced-motion: reduce) { + + /* 1) Alle Dauer-Animationen/Transitions aus */ + * { + animation: none !important; + transition: none !important; + } + + /* 2) Teure Hintergrundebenen aus */ + .services-overview::before, + .services-overview::after, + .services-overview .glass-grid, + .services-overview .service-particles { + display: none !important; + } + + /* 3) Backdrop-Filter/Blur reduzieren oder entfernen */ + .service-card, + .services-cta, + .trust-item { + backdrop-filter: none !important; + background: #fff !important; + box-shadow: 0 2px 6px rgba(0, 0, 0, .04) !important; + } + + /* 4) Hover-Styles neutralisieren (auf Mobile eh unnötig) */ + .service-card:hover { + transform: none !important; + box-shadow: 0 2px 6px rgba(0, 0, 0, .04) !important; + } + + /* 5) „Featured“-Glows & Card-Overlays aus */ + .service-card::before, + .service-card::after, + .service-card.featured::before { + display: none !important; + } + + /* 6) Buttons simpler machen */ + .btn::before { + display: none !important; + } + + .btn { + box-shadow: none !important; + } + + /* 7) Weniger Tiefe, kleinere Radien = günstiger */ + .service-card, + .services-cta { + border-radius: 12px !important; + } + + /* 8) Rendering-Hints: nur sichtbare Cards rendern */ + .service-card { + content-visibility: auto; + contain: layout paint style; + } + + + /* 10) Kleine Feinanpassungen */ + .card-icon { + background: #f3f6ff !important; + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/components/it-services/it-services.component.spec.ts b/apps/frontend/src/app/components/it-services/it-services.component.spec.ts new file mode 100644 index 0000000..9339a9f --- /dev/null +++ b/apps/frontend/src/app/components/it-services/it-services.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ItServicesComponent } from './it-services.component'; + +describe('ItServicesComponent', () => { + let component: ItServicesComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ItServicesComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ItServicesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/frontend/src/app/components/it-services/it-services.component.ts b/apps/frontend/src/app/components/it-services/it-services.component.ts new file mode 100644 index 0000000..320efd3 --- /dev/null +++ b/apps/frontend/src/app/components/it-services/it-services.component.ts @@ -0,0 +1,84 @@ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { Router, RouterLink } from '@angular/router'; + +export interface ServiceItem { + title: string; + short: string; + price?: string; + badge?: string; + features: string[]; + cta?: string; + route: string; +} + + +@Component({ + selector: 'app-it-services', + standalone: true, + imports: [RouterLink, CommonModule], + templateUrl: './it-services.component.html', + styleUrl: './it-services.component.scss' +}) +export class ItServicesComponent { + + constructor(public router: Router) {} + + public services: ServiceItem[] = [ + { + title: 'IT-Support & Wartung', + short: 'Zuverlässige Betreuung deiner IT-Infrastruktur mit schneller Reaktionszeit und proaktiver Wartung.', + price: 'ab 89€/Monat', + badge: '🚀 Schnellstart', + features: [ + 'Remote-Support binnen 4 Stunden', + 'Monatliche System-Updates', + 'Backup-Monitoring', + 'Sicherheits-Checks', + 'Helpdesk per E-Mail & Telefon' + ], + cta: 'Support-Paket wählen', + route: '/services/support' + }, + { + title: 'Cloud & Server Management', + short: 'Professionelle Verwaltung deiner Server und Cloud-Infrastruktur für maximale Verfügbarkeit und Performance.', + price: 'ab 299€/Monat', + badge: '⭐ Am beliebtesten', + features: [ + '24/7 Server-Monitoring', + 'Automatische Backups', + 'Performance-Optimierung', + 'Sicherheits-Updates', + 'Cloud-Migration Support', + 'Monatliche Reports' + ], + cta: 'Cloud-Lösung anfragen', + route: '/services/cloud' + }, + { + title: 'IT-Projekt & Beratung', + short: 'Individuelle IT-Projekte und strategische Beratung für deine digitale Transformation.', + price: 'auf Anfrage', + badge: '💎 Individuell', + features: [ + 'Analyse deiner IT-Landschaft', + 'Individuelle Lösungskonzepte', + 'Projekt-Management', + 'Technologie-Beratung', + 'Implementierung & Schulung', + 'Langfristige Partnerschaft' + ], + cta: 'Projekt besprechen', + route: '/services/consulting' + } + ]; + + getServices(): ServiceItem[] { + return this.services; + } + + getServiceByRoute(route: string): ServiceItem | undefined { + return this.services.find(s => s.route === route); + } +} diff --git a/apps/frontend/src/app/components/login/login.component.html b/apps/frontend/src/app/components/login/login.component.html new file mode 100644 index 0000000..b14ca71 --- /dev/null +++ b/apps/frontend/src/app/components/login/login.component.html @@ -0,0 +1,70 @@ + + +
+
+
+ + +
+
+ lock_open +
+

Anmelden

+
+ + +
+ + +
+ +
+ mail + +
+
+ + +
+ +
+ key + + +
+
+ + +
+ error + {{ error }} +
+ + + +
+ + + +
+
+
\ No newline at end of file diff --git a/apps/frontend/src/app/components/login/login.component.scss b/apps/frontend/src/app/components/login/login.component.scss new file mode 100644 index 0000000..4923c3b --- /dev/null +++ b/apps/frontend/src/app/components/login/login.component.scss @@ -0,0 +1,343 @@ +@import '../../utils/shared-styles.scss'; + +.container { + width: 100%; + max-width: 480px; + margin-inline: auto; + padding-inline: clamp(1rem, 4vw, 2rem); +} + +.auth { + min-height: 80vh; + display: flex; + align-items: center; + padding-block: 3rem; +} + +/* ===== AUTH CARD ===== */ + +@keyframes cardExit { + 0% { + opacity: 1; + transform: translateY(0) scale(1); + } + 100% { + opacity: 0; + transform: translateY(-20px) scale(0.98); + } +} + +@keyframes successPulse { + 0% { + box-shadow: $shadow-glass; + } + 50% { + box-shadow: 0 0 0 8px rgba(34, 197, 94, 0.15), $shadow-glass; + } + 100% { + box-shadow: $shadow-glass; + } +} + +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 10%, 30%, 50%, 70%, 90% { transform: translateX(-6px); } + 20%, 40%, 60%, 80% { transform: translateX(6px); } +} + +@keyframes errorPulse { + 0% { + box-shadow: $shadow-glass; + } + 50% { + box-shadow: 0 0 0 8px rgba(239, 68, 68, 0.15), $shadow-glass; + } + 100% { + box-shadow: $shadow-glass; + } +} + +.auth-card { + width: 100%; + background: $glass-bg; + backdrop-filter: blur(20px); + border: 1px solid $glass-border; + border-radius: $radius-2xl; + padding: 2.5rem; + box-shadow: $shadow-glass; + transition: all 0.4s cubic-bezier(0.22, 1, 0.36, 1); + + &.login-success { + animation: successPulse 0.5s ease-out, cardExit 0.4s ease-in 0.3s forwards; + border-color: rgba(34, 197, 94, 0.3); + } + + &.login-error { + animation: shake 0.5s ease-out, errorPulse 0.6s ease-out; + border-color: rgba(239, 68, 68, 0.4); + } + + @media (max-width: 640px) { + padding: 2rem 1.5rem; + } +} + +/* ===== HEADER ===== */ + +.auth-header { + text-align: center; + margin-bottom: 2rem; + + .auth-icon { + width: 72px; + height: 72px; + margin: 0 auto 1.5rem; + background: $gradient-primary; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: $shadow-brand; + + .material-symbols-outlined { + font-size: 36px; + color: white; + } + } + + h1 { + font-size: 1.75rem; + font-weight: 800; + color: $color-text-primary; + margin-bottom: 0.5rem; + } + + p { + color: $color-text-secondary; + font-size: 1rem; + margin: 0; + } +} + +/* ===== FORM ===== */ + +.auth-form { + display: grid; + gap: 1.25rem; +} + +.form-field { + display: grid; + gap: 0.5rem; + + label { + font-weight: 600; + color: $color-text-primary; + font-size: 0.9375rem; + } + + .field-hint { + font-size: 0.875rem; + color: $color-text-secondary; + margin-top: -0.25rem; + } + + &.has-error { + .input-wrapper input { + border-color: $color-error; + box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.1); + } + } +} + +/* Input Wrapper */ +.input-wrapper { + position: relative; + + .input-icon { + position: absolute; + left: 1rem; + top: 50%; + transform: translateY(-50%); + color: $color-text-secondary; + font-size: 20px; + pointer-events: none; + } + + input { + width: 100%; + padding: 0.875rem 1rem; + padding-left: 3rem; + padding-right: 3rem; + border: 2px solid $color-gray-200; + border-radius: $radius-md; + font-size: 0.9375rem; + font-family: inherit; + color: $color-text-primary; + background: $color-white; + transition: all 0.3s ease; + + &::placeholder { + color: $color-gray-400; + } + + &:focus { + outline: none; + border-color: $color-brand-primary; + box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.1); + } + + &:disabled { + background: $color-gray-100; + color: $color-text-tertiary; + cursor: not-allowed; + } + } + + .input-action { + position: absolute; + right: 0.5rem; + top: 50%; + transform: translateY(-50%); + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + border-radius: $radius-sm; + color: $color-text-secondary; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + background: $color-gray-100; + color: $color-text-primary; + } + + &:focus-visible { + outline: none; + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2); + } + + .material-symbols-outlined { + font-size: 20px; + } + } +} + +/* Error Text */ +.error-text { + display: flex; + align-items: center; + gap: 0.375rem; + color: $color-error; + font-size: 0.875rem; + font-weight: 500; + + .material-symbols-outlined { + font-size: 16px; + } +} + +/* Alert */ +.alert { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 1rem 1.25rem; + border-radius: $radius-md; + font-size: 0.9375rem; + + .material-symbols-outlined { + font-size: 20px; + flex-shrink: 0; + } + + &--error { + background: $color-error-light; + border: 1px solid darken($color-error-light, 10%); + color: darken($color-error, 10%); + + .material-symbols-outlined { + color: $color-error; + } + } +} + +/* Button */ +.btn--large { + width: 100%; + margin-top: 0.5rem; + padding: 1rem 1.5rem; + font-size: 1rem; + + &.loading { + pointer-events: none; + opacity: 0.8; + } +} + +.loading-content { + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; +} + +.spinner { + width: 18px; + height: 18px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-top-color: white; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* ===== FOOTER ===== */ + +.auth-footer { + margin-top: 2rem; + padding-top: 1.5rem; + border-top: 1px solid $color-gray-200; + text-align: center; + + p { + color: $color-text-secondary; + font-size: 0.9375rem; + margin: 0; + + a { + color: $color-brand-primary; + text-decoration: none; + font-weight: 600; + transition: color 0.2s ease; + + &:hover { + color: $color-brand-dark; + text-decoration: underline; + } + + &:focus-visible { + outline: none; + text-decoration: underline; + } + } + } +} + +/* ===== MOTION PREFERENCES ===== */ + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/components/login/login.component.spec.ts b/apps/frontend/src/app/components/login/login.component.spec.ts new file mode 100644 index 0000000..18f3685 --- /dev/null +++ b/apps/frontend/src/app/components/login/login.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LoginComponent } from './login.component'; + +describe('LoginComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LoginComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/frontend/src/app/components/login/login.component.ts b/apps/frontend/src/app/components/login/login.component.ts new file mode 100644 index 0000000..5faa9d3 --- /dev/null +++ b/apps/frontend/src/app/components/login/login.component.ts @@ -0,0 +1,160 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { Router, RouterLink, ActivatedRoute } from '@angular/router'; +import { AuthService } from '../../services/auth.service'; +import { PageTitleComponent } from '../../shared/page-title/page-title.component'; +import { ToastService } from '../../shared/toasts/toast.service'; + +@Component({ + selector: 'app-login', + standalone: true, + imports: [CommonModule, FormsModule, RouterLink, PageTitleComponent], + templateUrl: './login.component.html', + styleUrl: './login.component.scss' +}) +export class LoginComponent implements OnInit { + email = ''; + password = ''; + loading = false; + error = ''; + showPassword = false; + returnUrl: string = '/'; + hasReturnUrl: boolean = false; + loginSuccess = false; + loginError = false; + + constructor( + private authService: AuthService, + private router: Router, + private route: ActivatedRoute, + private toasts: ToastService + ) { } + + ngOnInit(): void { + // Lese returnUrl aus Query Parameters + this.route.queryParams.subscribe(params => { + this.returnUrl = params['returnUrl'] || '/'; + this.hasReturnUrl = !!params['returnUrl'] && params['returnUrl'] !== '/'; + // console.log('🔗 Return URL:', this.returnUrl); + // console.log('📍 Has Return URL:', this.hasReturnUrl); + }); + + // Falls bereits eingeloggt, direkt weiterleiten + if (this.authService.isLoggedIn()) { + this.navigateToReturnUrl(); + } + } + + login(): void { + // Validation + if (!this.email || !this.password) { + this.error = 'Bitte fülle alle Felder aus'; + this.toasts.error('Bitte fülle alle Felder aus'); + this.triggerErrorAnimation(); + return; + } + + // Email validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(this.email)) { + this.error = 'Bitte gib eine gültige E-Mail-Adresse ein'; + this.toasts.error('Ungültige E-Mail-Adresse'); + this.triggerErrorAnimation(); + return; + } + + this.loading = true; + this.error = ''; + + this.authService.login(this.email, this.password).subscribe({ + next: (response) => { + // console.log('✅ Login erfolgreich'); + this.toasts.success('Erfolgreich eingeloggt!'); + this.loading = false; + this.loginSuccess = true; + + // Navigate nach Exit-Animation + setTimeout(() => { + this.navigateToReturnUrl(); + }, 700); + }, + error: (err) => { + console.error('❌ Login Fehler:', err); + + // Detaillierte Fehlerbehandlung + let errorMessage = 'Login fehlgeschlagen. Überprüfe deine Zugangsdaten.'; + + if (err.status === 401) { + errorMessage = 'E-Mail oder Passwort ist falsch'; + } else if (err.status === 404) { + errorMessage = 'Kein Account mit dieser E-Mail gefunden'; + } else if (err.status === 429) { + errorMessage = 'Zu viele Login-Versuche. Bitte warte kurz'; + } else if (err.error?.message) { + errorMessage = err.error.message; + } + + this.error = errorMessage; + this.loading = false; + this.triggerErrorAnimation(); + } + }); + } + + private triggerErrorAnimation(): void { + this.loginError = true; + setTimeout(() => { + this.loginError = false; + }, 600); + } + + private navigateToReturnUrl(): void { + // console.log('🚀 Navigiere zu:', this.returnUrl); + + // Decode URL falls encoded + const decodedUrl = decodeURIComponent(this.returnUrl); + + // Parse URL und Query Params separat + const [path, queryString] = decodedUrl.split('?'); + + if (queryString) { + // Parse Query String zu Object + const queryParams: Record = {}; + queryString.split('&').forEach(param => { + const [key, value] = param.split('='); + if (key && value) { + queryParams[key] = decodeURIComponent(value); + } + }); + + // console.log('📍 Path:', path); + // console.log('🔍 Query Params:', queryParams); + + // Navigate mit separaten Query Params + this.router.navigate([path], { queryParams }); + } else { + // Keine Query Params, normale Navigation + this.router.navigate([path]); + } + } + + togglePasswordVisibility(): void { + this.showPassword = !this.showPassword; + } + + navigateToRegister(): void { + // Übergebe returnUrl auch an Register + if (this.hasReturnUrl) { + this.router.navigate(['/register'], { + queryParams: { returnUrl: this.returnUrl } + }); + } else { + this.router.navigate(['/register']); + } + } + + clearError(): void { + this.error = ''; + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/components/maintenance/maintenance.component.html b/apps/frontend/src/app/components/maintenance/maintenance.component.html new file mode 100644 index 0000000..14d1dba --- /dev/null +++ b/apps/frontend/src/app/components/maintenance/maintenance.component.html @@ -0,0 +1,100 @@ + + +
+
+ + +
+ +

Leonards & Brandenburger IT

+
+ + +
+ 🚧 Wartungsmodus aktiv +
+ + +

Wir sind bald wieder da

+ + +

+ {{ message || 'Unsere Website wird gerade gewartet und verbessert. Wir arbeiten daran, dir bald ein noch besseres Erlebnis bieten zu können.' }} +

+ + +
+

Bleib auf dem Laufenden

+

Erfahre als Erster, wenn wir wieder online sind

+ + + +
+ ⚠️ {{ errorMessage }} +
+ +
+ ✅ Danke! Wir melden uns bei dir. +
+
+ + +
+ + +
+

Zugang mit Passwort:

+
+ + +
+
+ ⚠️ {{ errorMessage }} +
+
+
+ + +
+

+ Dringende Anfragen? + + {{ contactEmail }} + +

+
+ +
+
\ No newline at end of file diff --git a/apps/frontend/src/app/components/maintenance/maintenance.component.scss b/apps/frontend/src/app/components/maintenance/maintenance.component.scss new file mode 100644 index 0000000..27610a1 --- /dev/null +++ b/apps/frontend/src/app/components/maintenance/maintenance.component.scss @@ -0,0 +1,577 @@ +/* ===== VARIABLES ===== */ +$gradient-bg: linear-gradient(135deg, #f8fafc 0%, #e0e7ff 50%, #f1f5f9 100%); +$gradient-primary: linear-gradient(135deg, #2563eb 0%, #3b82f6 100%); +$color-brand-primary: #2563eb; +$color-brand-dark: #1e40af; +$color-text-primary: #0f172a; +$color-text-secondary: #475569; +$color-text-tertiary: #94a3b8; +$color-error: #ef4444; +$color-success: #10b981; +$glass-bg: rgba(255, 255, 255, 0.7); +$glass-border: rgba(226, 232, 240, 0.5); +$radius-full: 9999px; +$radius-lg: 1rem; +$radius-md: 0.5rem; +$shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06); +$shadow-glass: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03); +$shadow-glass-hover: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); +$shadow-brand: 0 4px 12px rgba(37, 99, 235, 0.2); +$shadow-brand-hover: 0 8px 20px rgba(37, 99, 235, 0.3); + +/* ===== MAINTENANCE PAGE ===== */ +.page { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: $gradient-bg; + padding: 2rem 1rem; + position: relative; + overflow: hidden; + + // Dezentes Hintergrundmuster + &::before { + content: ''; + position: absolute; + inset: 0; + background: + radial-gradient(circle at 20% 50%, rgba(37, 99, 235, 0.03) 0%, transparent 50%), + radial-gradient(circle at 80% 80%, rgba(37, 99, 235, 0.04) 0%, transparent 50%); + pointer-events: none; + } +} + +.container { + max-width: 800px; + width: 100%; + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; +} + +/* ===== LOGO SECTION ===== */ +.logo-section { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + margin-bottom: 0.5rem; + animation: fadeInDown 0.6s ease-out; + + .logo { + width: 80px; + height: 80px; + object-fit: contain; + filter: drop-shadow(0 4px 12px rgba(37, 99, 235, 0.15)); + transition: transform 0.3s ease; + + &:hover { + transform: scale(1.05) rotate(2deg); + } + } + + .brand-name { + margin: 0; + font-size: 1.75rem; + font-weight: 800; + background: $gradient-primary; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + letter-spacing: -0.02em; + } +} + +/* ===== BADGE ===== */ +.badge { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.625rem 1.25rem; + background: rgba(245, 158, 11, 0.1); + backdrop-filter: blur(10px); + border: 2px solid rgba(245, 158, 11, 0.3); + border-radius: $radius-full; + font-size: 0.875rem; + font-weight: 700; + color: #f59e0b; + margin: 0 auto 2rem; + text-align: center; + box-shadow: 0 4px 12px rgba(245, 158, 11, 0.15); + animation: fadeIn 0.6s ease-out 0.2s both, pulse 2s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { + transform: scale(1); + } + 50% { + transform: scale(1.02); + } +} + +/* ===== MAIN HEADING ===== */ +h1 { + font-size: clamp(2rem, 5vw, 3rem); + font-weight: 900; + text-align: center; + margin: 0 0 1rem; + color: $color-text-primary; + letter-spacing: -0.03em; + line-height: 1.2; + animation: fadeInUp 0.6s ease-out 0.3s both; +} + +/* ===== DESCRIPTION ===== */ +.description { + text-align: center; + font-size: 1.125rem; + line-height: 1.7; + color: $color-text-secondary; + margin: 0 auto 3rem; + max-width: 600px; + font-weight: 500; + animation: fadeInUp 0.6s ease-out 0.4s both; +} + +/* ===== FEATURES GRID ===== */ +.features-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + margin-bottom: 3rem; + animation: fadeInUp 0.6s ease-out 0.5s both; +} + +.feature { + background: $glass-bg; + backdrop-filter: blur(20px); + border: 1px solid $glass-border; + border-radius: $radius-lg; + padding: 1.5rem; + text-align: center; + transition: all 0.3s ease; + box-shadow: $shadow-sm; + + &:hover { + transform: translateY(-4px); + box-shadow: $shadow-glass-hover; + border-color: rgba(37, 99, 235, 0.3); + } + + .icon { + font-size: 2.5rem; + margin-bottom: 0.75rem; + display: inline-block; + animation: bounce 2s ease-in-out infinite; + } + + h3 { + margin: 0 0 0.5rem; + font-size: 1rem; + font-weight: 700; + color: $color-text-primary; + } + + p { + margin: 0; + font-size: 0.875rem; + color: $color-text-secondary; + line-height: 1.5; + } +} + +@keyframes bounce { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-8px); + } +} + +/* ===== NOTIFY BOX ===== */ +.notify-box { + background: $glass-bg; + backdrop-filter: blur(20px); + border: 1px solid $glass-border; + border-radius: $radius-lg; + padding: 2rem; + margin-bottom: 2rem; + box-shadow: $shadow-glass; + animation: fadeInUp 0.6s ease-out 0.6s both; + width: 100%; + + h2 { + margin: 0 0 0.5rem; + font-size: 1.5rem; + font-weight: 800; + text-align: center; + background: $gradient-primary; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + } + + > p { + text-align: center; + color: $color-text-secondary; + margin: 0 0 1.5rem; + font-size: 0.9375rem; + font-weight: 500; + } +} + +/* ===== EMAIL FORM ===== */ +.email-form { + display: flex; + gap: 0.75rem; + margin-bottom: 1rem; + + @media (max-width: 640px) { + flex-direction: column; + } +} + +.email-input { + flex: 1; + padding: 0.875rem 1.25rem; + border: 1px solid rgba(226, 232, 240, 0.6); + border-radius: $radius-md; + font-size: 0.9375rem; + font-weight: 600; + background: rgba(255, 255, 255, 0.9); + color: $color-text-primary; + transition: all 0.2s ease; + + &::placeholder { + color: $color-text-tertiary; + font-weight: 500; + } + + &:focus { + outline: none; + border-color: $color-brand-primary; + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); + background: #fff; + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + background: rgba(241, 245, 249, 0.5); + } +} + +/* ===== BUTTONS ===== */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.875rem 2rem; + border-radius: $radius-md; + font-size: 0.9375rem; + font-weight: 700; + border: none; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; + position: relative; + min-width: 160px; + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } + + &--primary { + background: $gradient-primary; + color: white; + box-shadow: $shadow-brand; + + &:hover:not(:disabled):not(.btn--loading) { + transform: translateY(-2px); + box-shadow: $shadow-brand-hover; + } + + &:active:not(:disabled) { + transform: translateY(0); + } + } + + &--loading { + pointer-events: none; + } + + @media (max-width: 640px) { + width: 100%; + } +} + +.btn-content { + transition: opacity 0.2s ease; + + &--hidden { + opacity: 0; + } +} + +/* ===== SPINNER ===== */ +.spinner { + position: absolute; + width: 20px; + height: 20px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-top-color: white; + border-radius: 50%; + animation: spin 0.6s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* ===== MESSAGES ===== */ +.error-message, +.success-message { + padding: 0.875rem 1.25rem; + border-radius: $radius-md; + font-size: 0.875rem; + font-weight: 600; + text-align: center; + animation: slideIn 0.3s ease-out; +} + +.error-message { + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.3); + color: $color-error; +} + +.success-message { + background: rgba(16, 185, 129, 0.1); + border: 1px solid rgba(16, 185, 129, 0.3); + color: $color-success; +} + +@keyframes slideIn { + from { + transform: translateY(-10px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +/* ===== ACCESS SECTION ===== */ +.access-section { + background: $glass-bg; + backdrop-filter: blur(20px); + border: 1px solid $glass-border; + border-radius: $radius-lg; + padding: 1.5rem; + margin-bottom: 2rem; + box-shadow: $shadow-sm; + animation: fadeInUp 0.6s ease-out 0.7s both; + width: 100%; +} + +.access-toggle { + width: 100%; + padding: 0.875rem 1.25rem; + background: rgba(37, 99, 235, 0.08); + border: 1px solid rgba(37, 99, 235, 0.3); + border-radius: $radius-md; + font-size: 0.9375rem; + font-weight: 700; + color: $color-brand-primary; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + background: rgba(37, 99, 235, 0.12); + border-color: $color-brand-primary; + transform: translateY(-1px); + } + + &:active { + transform: translateY(0); + } +} + +.password-box { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid rgba(226, 232, 240, 0.5); + animation: slideIn 0.3s ease-out; + + .access-label { + margin: 0 0 0.75rem; + font-size: 0.875rem; + font-weight: 600; + color: $color-text-secondary; + text-align: center; + } + + .password-form { + display: flex; + gap: 0.75rem; + + @media (max-width: 640px) { + flex-direction: column; + } + } +} + +/* ===== CONTACT ===== */ +.contact { + text-align: center; + animation: fadeIn 0.6s ease-out 0.8s both; + + .contact-text { + margin: 0; + font-size: 0.9375rem; + color: $color-text-secondary; + font-weight: 500; + } + + .link { + color: $color-brand-primary; + text-decoration: none; + font-weight: 700; + transition: all 0.2s ease; + margin-left: 0.375rem; + + &:hover { + color: $color-brand-dark; + text-decoration: underline; + } + } +} + +/* ===== ANIMATIONS ===== */ +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes fadeInDown { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* ===== RESPONSIVE ===== */ +@media (max-width: 768px) { + .logo-section { + .logo { + width: 64px; + height: 64px; + } + + .brand-name { + font-size: 1.5rem; + } + } + + .badge { + font-size: 0.8125rem; + padding: 0.5rem 1rem; + } + + .description { + font-size: 1rem; + } + + .features-grid { + grid-template-columns: 1fr; + } + + .notify-box { + padding: 1.5rem; + + h2 { + font-size: 1.25rem; + } + } + + .access-section { + padding: 1.25rem; + } +} + +@media (max-width: 480px) { + .page { + padding: 1.5rem 1rem; + } + + h1 { + font-size: 1.75rem; + } + + .feature { + padding: 1.25rem; + + .icon { + font-size: 2rem; + } + } +} + +/* ===== ACCESSIBILITY ===== */ +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } + + .feature:hover, + .btn:hover, + .access-toggle:hover { + transform: none; + } + + .feature .icon { + animation: none; + } + + .badge { + animation: fadeIn 0.6s ease-out 0.2s both; + } +} + +/* ===== FOCUS VISIBLE ===== */ +.btn:focus-visible, +.email-input:focus-visible, +.access-toggle:focus-visible { + outline: 2px solid $color-brand-primary; + outline-offset: 2px; +} \ No newline at end of file diff --git a/apps/frontend/src/app/components/maintenance/maintenance.component.ts b/apps/frontend/src/app/components/maintenance/maintenance.component.ts new file mode 100644 index 0000000..35ef0f1 --- /dev/null +++ b/apps/frontend/src/app/components/maintenance/maintenance.component.ts @@ -0,0 +1,122 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { Router } from '@angular/router'; +import { ApiService } from '../../api/api.service'; + +@Component({ + selector: 'app-maintenance', + standalone: true, + imports: [CommonModule, FormsModule], + templateUrl: './maintenance.component.html', + styleUrl: './maintenance.component.scss' +}) +export class MaintenanceComponent { + @Input() message?: string; + + email: string = ''; + password: string = ''; + submitted: boolean = false; + isLoading: boolean = false; + errorMessage: string = ''; + showPasswordForm: boolean = false; + isCheckingPassword: boolean = false; + + // Verschlüsselte E-Mail-Adresse (Base64) + private encryptedEmail = 'dG9tQGxlb25hcmRzbWVkaWEuZGU='; // tom@Leonards & Brandenburger IT.de + contactEmail: string = ''; + + + constructor( + private apiService: ApiService, + private router: Router + ) { + // E-Mail erst beim Laden der Component entschlüsseln + this.contactEmail = this.decryptEmail(this.encryptedEmail); + } + + private decryptEmail(encrypted: string): string { + try { + return atob(encrypted); + } catch (e) { + return 'tom@leonardsmedia.de'; + } + } + + getMailtoLink(): string { + return `mailto:${this.contactEmail}`; + } + + togglePasswordForm(): void { + this.showPasswordForm = !this.showPasswordForm; + this.password = ''; + this.errorMessage = ''; + } + + onSubmitPassword(): void { + if (!this.password) { + return; + } + + this.isCheckingPassword = true; + this.errorMessage = ''; + + this.apiService.checkMaintenancePassword(this.password).subscribe({ + next: (response) => { + if (response.valid) { + sessionStorage.setItem('maintenanceBypass', 'true'); + // Seite neu laden für vollen Zugriff + window.location.reload(); + } else { + this.errorMessage = 'Ungültiges Passwort'; + this.password = ''; + this.isCheckingPassword = false; + setTimeout(() => this.errorMessage = '', 3000); + } + }, + error: (error) => { + console.error('Fehler bei Passwort-Prüfung:', error); + this.errorMessage = 'Fehler bei der Überprüfung'; + this.isCheckingPassword = false; + setTimeout(() => this.errorMessage = '', 3000); + } + }); + } + + onSubmitNewsletter(): void { + if (!this.email) { + return; + } + + // Einfache E-Mail-Validierung + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(this.email)) { + this.errorMessage = 'Bitte gib eine gültige E-Mail-Adresse ein'; + setTimeout(() => this.errorMessage = '', 3000); + return; + } + + this.isLoading = true; + this.errorMessage = ''; + + // Öffentliche Newsletter-Anmeldung (erstellt Eintrag in newsletter_subscribers Tabelle) + this.apiService.subscribeNewsletter(this.email).subscribe({ + next: (response) => { + this.submitted = true; + this.isLoading = false; + }, + error: (error) => { + console.error('Newsletter subscription failed:', error); + this.isLoading = false; + + if (error.status === 409) { + this.errorMessage = 'Diese E-Mail ist bereits angemeldet'; + } else { + this.errorMessage = 'Etwas ist schiefgelaufen. Bitte versuche es später nochmal.'; + } + + setTimeout(() => this.errorMessage = '', 5000); + } + }); + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/components/policy/policy.component.html b/apps/frontend/src/app/components/policy/policy.component.html new file mode 100644 index 0000000..10f5ad9 --- /dev/null +++ b/apps/frontend/src/app/components/policy/policy.component.html @@ -0,0 +1,330 @@ + + + \ No newline at end of file diff --git a/apps/frontend/src/app/components/policy/policy.component.scss b/apps/frontend/src/app/components/policy/policy.component.scss new file mode 100644 index 0000000..26502c9 --- /dev/null +++ b/apps/frontend/src/app/components/policy/policy.component.scss @@ -0,0 +1,529 @@ +@import '../../utils/shared-styles.scss'; + +.container { + width: 100%; + max-width: 1200px; + margin-inline: auto; + padding-inline: clamp(1rem, 4vw, 2rem); + margin-bottom: 4rem; +} + +section { + display: flex; + flex-direction: column; + align-items: center; +} + +/* ===== LEGAL/POLICY SECTION ===== */ + +.legal--policy { + background: $color-background-secondary; + padding-block: 2rem; + + @media (min-width: 768px) { + padding-block: 3rem; + } + + .legal__content { + background: $glass-bg; + backdrop-filter: blur(10px); + border: 1px solid $glass-border; + border-radius: $radius-xl; + padding: 2rem; + box-shadow: $shadow-glass; + max-width: 900px; + margin-inline: auto; + + @media (min-width: 768px) { + padding: 3rem; + } + + @media (min-width: 1024px) { + padding: 3.5rem 4rem; + } + + h2 { + font-size: clamp(1.25rem, 2vw + 0.5rem, 1.5rem); + font-weight: 700; + color: $color-text-primary; + margin: 0 0 0.75rem; + letter-spacing: -0.01em; + scroll-margin-top: 120px; + line-height: 1.3; + + &:not(:first-child) { + margin-top: 2.5rem; + padding-top: 2rem; + border-top: 1px solid $color-gray-200; + } + + &:first-child { + margin-top: 0; + } + } + + p { + margin: 0; + color: $color-text-secondary; + line-height: 1.7; + font-size: 1rem; + + &+p { + margin-top: 1rem; + } + + strong { + color: $color-text-primary; + font-weight: 600; + } + + em { + color: $color-text-secondary; + font-style: italic; + } + + br { + line-height: 1.4; + } + } + + a { + color: $color-brand-primary; + text-decoration: none; + font-weight: 600; + transition: all 0.2s ease; + + &:hover { + text-decoration: underline; + } + + &:focus-visible { + outline: 2px solid $color-brand-primary; + outline-offset: 2px; + border-radius: 2px; + } + } + + ul, + ol { + margin: 0.75rem 0; + padding-left: 1.5rem; + color: $color-text-secondary; + line-height: 1.7; + + li { + margin-bottom: 0.5rem; + + &:last-child { + margin-bottom: 0; + } + } + } + } +} + +/* ===== HIGHLIGHT BOX ===== */ + +.highlight-box { + background: $gradient-subtle; + border: 1px solid rgba(37, 99, 235, 0.15); + border-radius: $radius-lg; + padding: 1.5rem; + margin: 1.5rem 0; + + p { + margin: 0; + + &+p { + margin-top: 0.75rem; + } + } + + strong { + color: $color-text-primary; + } +} + +/* ===== INFO CARD ===== */ + +.info-card { + background: $glass-bg; + border: 1px solid $glass-border; + border-radius: $radius-lg; + padding: 1.5rem; + margin: 1.5rem 0; + box-shadow: $shadow-md; + display: grid; + grid-template-columns: auto 1fr; + gap: 1rem; + align-items: start; + + .info-icon { + width: 40px; + height: 40px; + background: $gradient-primary; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: $shadow-brand; + flex-shrink: 0; + + .material-symbols-outlined { + font-size: 24px; + color: white; + } + } + + .info-content { + h3 { + font-size: 1.0625rem; + font-weight: 700; + color: $color-text-primary; + margin: 0 0 0.5rem; + } + + p { + font-size: 0.9375rem; + color: $color-text-secondary; + line-height: 1.6; + margin: 0; + + &+p { + margin-top: 0.75rem; + } + } + } +} + +/* ===== RIGHTS LIST ===== */ + +.rights-list { + display: grid; + gap: 1rem; + margin: 1.5rem 0; + + .rights-item { + display: grid; + grid-template-columns: auto 1fr; + gap: 1rem; + padding: 1.25rem; + background: $glass-bg; + border: 1px solid $glass-border; + border-radius: $radius-lg; + box-shadow: $shadow-md; + transition: all 0.3s ease; + + &:hover { + transform: translateY(-2px); + box-shadow: $shadow-lg; + } + + .rights-check { + width: 32px; + height: 32px; + background: rgba(16, 185, 129, 0.1); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + .material-symbols-outlined { + color: $color-success; + font-size: 18px; + } + } + + .rights-content { + h4 { + font-size: 1rem; + font-weight: 700; + color: $color-text-primary; + margin: 0 0 0.25rem; + } + + p { + font-size: 0.9375rem; + color: $color-text-secondary; + line-height: 1.5; + margin: 0; + } + } + } +} + +/* ===== CONTACT BOX ===== */ + +.contact-box { + background: $gradient-subtle; + border: 1px solid rgba(37, 99, 235, 0.15); + border-radius: $radius-xl; + padding: 2rem; + text-align: center; + margin: 2rem 0; + position: relative; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + inset: 0; + background: + radial-gradient(circle at 20% 50%, rgba(37, 99, 235, 0.06), transparent 50%), + radial-gradient(circle at 80% 50%, rgba(59, 130, 246, 0.04), transparent 50%); + pointer-events: none; + } + + >* { + position: relative; + z-index: 1; + } + + .contact-icon { + width: 56px; + height: 56px; + margin: 0 auto 1rem; + background: $gradient-primary; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: $shadow-brand; + + .material-symbols-outlined { + font-size: 28px; + color: white; + } + } + + h3 { + font-size: 1.25rem; + font-weight: 700; + color: $color-text-primary; + margin: 0 0 0.5rem; + } + + p { + color: $color-text-secondary; + margin: 0 0 1rem; + line-height: 1.6; + } + + .contact-details { + display: inline-flex; + flex-direction: column; + gap: 0.5rem; + text-align: left; + + a { + color: $color-brand-primary; + text-decoration: none; + font-weight: 600; + + &:hover { + text-decoration: underline; + } + } + } +} + +/* ===== DATA MANAGEMENT SECTION ===== */ + +.data-btn { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.625rem 1rem; + background: $gradient-primary; + color: white; + border: none; + border-radius: $radius-md; + font-size: 0.875rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + margin-top: 0.5rem; + + &:hover { + transform: translateY(-2px); + box-shadow: $shadow-md; + } + + .material-symbols-outlined { + font-size: 18px; + } +} + +.data-section { + background: rgba($color-brand-primary, 0.03); + border: 1px solid rgba($color-brand-primary, 0.1); + border-radius: $radius-lg; + padding: 1.5rem; + margin-top: 1rem; + animation: slideDown 0.3s ease-out; +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.data-loading { + display: flex; + align-items: center; + gap: 1rem; + color: $color-text-secondary; + + .spinner { + width: 24px; + height: 24px; + border: 3px solid rgba($color-brand-primary, 0.2); + border-top-color: $color-brand-primary; + border-radius: 50%; + animation: spin 1s linear infinite; + } +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.data-error { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 1rem; + background: rgba($color-error, 0.1); + border-radius: $radius-md; + color: $color-error; + + .material-symbols-outlined { + font-size: 20px; + } +} + +.data-success { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 1rem; + background: rgba($color-success, 0.1); + border-radius: $radius-md; + color: $color-success; + + .material-symbols-outlined { + font-size: 20px; + } +} + +.data-summary { + margin-bottom: 1.5rem; + + p { + margin: 0.25rem 0; + font-size: 0.875rem; + } + + .data-info { + color: $color-text-tertiary; + font-size: 0.8125rem; + margin-top: 0.5rem; + } +} + +.data-table { + overflow-x: auto; + margin-bottom: 1.5rem; + + table { + width: 100%; + border-collapse: collapse; + font-size: 0.8125rem; + + th, td { + padding: 0.625rem 0.75rem; + text-align: left; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); + } + + th { + background: rgba($color-brand-primary, 0.05); + font-weight: 600; + color: $color-text-primary; + } + + td { + color: $color-text-secondary; + } + + .page-cell { + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } +} + +.data-empty { + display: flex; + flex-direction: column; + align-items: center; + padding: 2rem; + color: $color-text-tertiary; + + .material-symbols-outlined { + font-size: 48px; + margin-bottom: 0.5rem; + opacity: 0.5; + } + + p { + margin: 0; + } +} + +.data-actions { + display: flex; + justify-content: flex-end; +} + +.delete-btn { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.625rem 1.25rem; + background: transparent; + color: $color-error; + border: 2px solid $color-error; + border-radius: $radius-md; + font-size: 0.875rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: $color-error; + color: white; + } + + .material-symbols-outlined { + font-size: 18px; + } +} + +/* ===== MOTION PREFERENCES ===== */ + +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/components/policy/policy.component.spec.ts b/apps/frontend/src/app/components/policy/policy.component.spec.ts new file mode 100644 index 0000000..72cf9ce --- /dev/null +++ b/apps/frontend/src/app/components/policy/policy.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PolicyComponent } from './policy.component'; + +describe('PolicyComponent', () => { + let component: PolicyComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PolicyComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PolicyComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/frontend/src/app/components/policy/policy.component.ts b/apps/frontend/src/app/components/policy/policy.component.ts new file mode 100644 index 0000000..67a672e --- /dev/null +++ b/apps/frontend/src/app/components/policy/policy.component.ts @@ -0,0 +1,140 @@ +import { Component, Inject, PLATFORM_ID } from '@angular/core'; +import { CommonModule, isPlatformBrowser } from '@angular/common'; +import { HttpClient } from '@angular/common/http'; +import { PageTitleComponent } from "../../shared/page-title/page-title.component"; +import { ConfigService } from '../../services/config.service'; +import { ConfirmationService } from '../../shared/confirmation/confirmation.service'; + +interface MyDataResponse { + sessionId: string; + recordCount: number; + data: Array<{ + type: string; + page: string; + timestamp: Date; + screenSize: string; + userAgent: string; + }>; + info: string; + error?: string; +} + +interface DeleteResponse { + sessionId: string; + deleted: number; + message: string; + error?: string; +} + +@Component({ + selector: 'app-policy', + standalone: true, + imports: [PageTitleComponent, CommonModule], + templateUrl: './policy.component.html', + styleUrl: './policy.component.scss' +}) +export class PolicyComponent { + currentYear = new Date().getFullYear(); + + // DSGVO Data Management + myData: MyDataResponse | null = null; + dataLoading = false; + dataError: string | null = null; + deleteSuccess: string | null = null; + showDataSection = false; + + private isBrowser: boolean; + private get API_URL(): string { + return `${this.configService.apiUrl}/analytics`; + } + + constructor( + private http: HttpClient, + private confirmationService: ConfirmationService, + private configService: ConfigService, + @Inject(PLATFORM_ID) platformId: Object + ) { + this.isBrowser = isPlatformBrowser(platformId); + } + + + getSessionId(): string | null { + if (!this.isBrowser) return null; + return sessionStorage.getItem('lub_session'); + } + + + toggleDataSection(): void { + this.showDataSection = !this.showDataSection; + if (this.showDataSection && !this.myData) { + this.loadMyData(); + } + } + + + loadMyData(): void { + const sessionId = this.getSessionId(); + if (!sessionId) { + this.dataError = 'Keine Session-ID gefunden. Analytics wurden möglicherweise nicht aktiviert.'; + return; + } + + this.dataLoading = true; + this.dataError = null; + this.deleteSuccess = null; + + this.http.get(`${this.API_URL}/my-data?sessionId=${sessionId}`) + .subscribe({ + next: (response) => { + this.myData = response; + this.dataLoading = false; + }, + error: () => { + this.dataError = 'Fehler beim Laden der Daten.'; + this.dataLoading = false; + } + }); + } + + + async deleteMyData(): Promise { + const sessionId = this.getSessionId(); + if (!sessionId) { + this.dataError = 'Keine Session-ID gefunden.'; + return; + } + + const confirmed = await this.confirmationService.confirm({ + title: 'Daten löschen', + message: 'Möchten Sie wirklich alle Ihre Analytics-Daten löschen? Dies kann nicht rückgängig gemacht werden.', + confirmText: 'Ja, löschen', + cancelText: 'Abbrechen', + type: 'danger', + icon: 'delete_forever' + }); + + if (!confirmed) { + return; + } + + this.dataLoading = true; + this.dataError = null; + + this.http.delete(`${this.API_URL}/my-data?sessionId=${sessionId}`) + .subscribe({ + next: (response) => { + this.deleteSuccess = response.message; + this.myData = null; + this.dataLoading = false; + // Session-ID aus Storage entfernen + if (this.isBrowser) { + sessionStorage.removeItem('lub_session'); + } + }, + error: () => { + this.dataError = 'Fehler beim Löschen der Daten.'; + this.dataLoading = false; + } + }); + } +} diff --git a/apps/frontend/src/app/components/profile/profile.component.html b/apps/frontend/src/app/components/profile/profile.component.html new file mode 100644 index 0000000..17ab540 --- /dev/null +++ b/apps/frontend/src/app/components/profile/profile.component.html @@ -0,0 +1,176 @@ +
+
+ + +
+ + +
+
+
+ {{ user.name.charAt(0).toUpperCase() }} +
+
+
+
+

{{ user.name }}

+ +
+ + {{ getRoleIcon() }} + {{ getRoleLabel() }} + + + verified + Verifiziert + +
+
+ +
+ + +
+

+ edit + Profil bearbeiten +

+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ calendar_today +
+
+ {{ formatDate(user.createdAt) }} + Mitglied seit +
+
+
+
+ verified_user +
+
+ {{ user.isVerified ? 'Verifiziert' : 'Ausstehend' }} + Account-Status +
+
+
+
+ {{ user.wantsNewsletter ? 'notifications_active' : 'notifications_off' }} +
+
+ {{ user.wantsNewsletter ? 'Aktiv' : 'Inaktiv' }} + Newsletter +
+
+ + +
+
+
+ + +
+ +
+
+
+ fingerprint +
+ Benutzer-ID + {{ user.id.substring(0, 8) }}...{{ user.id.substring(user.id.length - 4) }} +
+ +
+
+ mail +
+ E-Mail-Adresse + {{ user.email }} +
+ +
+
+ badge +
+ Vollständiger Name + {{ user.name }} +
+
+
+ shield_person +
+ Benutzerrolle + {{ getRoleLabel() }} +
+
+
+
+
+ + + + +
+ + +
+
+

Lade Profil...

+
+ +
+
\ No newline at end of file diff --git a/apps/frontend/src/app/components/profile/profile.component.scss b/apps/frontend/src/app/components/profile/profile.component.scss new file mode 100644 index 0000000..e4b0c0b --- /dev/null +++ b/apps/frontend/src/app/components/profile/profile.component.scss @@ -0,0 +1,652 @@ +@import '../../utils/shared-styles.scss'; + +.container { + width: 100%; + max-width: 800px; + margin-inline: auto; + padding-inline: clamp(1rem, 4vw, 2rem); +} + +.profile-page { + min-height: calc(100vh - 200px); + padding-block: 3rem; + + @media (max-width: 639px) { + padding-block: 2rem; + } +} + +/* ===== PROFILE CARD ===== */ + +.profile-card { + background: $glass-bg; + backdrop-filter: blur(20px); + border: 1px solid $glass-border; + border-radius: $radius-2xl; + padding: 2rem; + box-shadow: $shadow-glass; + display: grid; + gap: 1.5rem; + + @media (max-width: 639px) { + padding: 1.25rem; + gap: 1.25rem; + border-radius: $radius-xl; + } + + &--loading { + text-align: center; + padding: 4rem 2rem; + + p { + color: $color-text-secondary; + font-size: 1rem; + margin: 0; + } + } +} + +/* ===== SPINNER ===== */ + +.spinner { + width: 48px; + height: 48px; + margin: 0 auto 1.5rem; + border: 4px solid $color-gray-200; + border-top-color: $color-brand-primary; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +.spinner-small { + width: 18px; + height: 18px; + border: 2px solid rgba(255,255,255,0.3); + border-top-color: white; + border-radius: 50%; + animation: spin 0.6s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* ===== PROFILE HEADER ===== */ + +.profile-header { + display: flex; + align-items: center; + gap: 1.5rem; + position: relative; + + @media (max-width: 639px) { + flex-direction: column; + gap: 1rem; + text-align: center; + } +} + +.avatar-wrapper { + position: relative; + flex-shrink: 0; +} + +.avatar { + width: 80px; + height: 80px; + background: $gradient-primary; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 2rem; + font-weight: 800; + color: white; + box-shadow: $shadow-brand; + border: 3px solid $color-white; + + @media (max-width: 639px) { + width: 72px; + height: 72px; + font-size: 1.75rem; + } +} + +.avatar-status { + position: absolute; + bottom: 2px; + right: 2px; + width: 18px; + height: 18px; + background: $color-success ; + border: 3px solid $color-white; + border-radius: 50%; + box-shadow: $shadow-sm; + + &.verified { + background: $color-success; + } +} + +.profile-info { + flex: 1; + min-width: 0; + display: grid; + gap: 0.375rem; + + h2 { + font-size: 1.5rem; + font-weight: 800; + color: $color-text-primary; + margin: 0; + line-height: 1.2; + + @media (max-width: 639px) { + font-size: 1.375rem; + } + } + + .email { + font-size: 0.9375rem; + color: $color-text-secondary; + margin: 0; + font-weight: 500; + } +} + +.badges { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 0.25rem; + + @media (max-width: 639px) { + justify-content: center; + } +} + +.role-badge, .verified-badge { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.375rem 0.75rem; + border-radius: $radius-full; + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.role-badge { + &.role-admin { + background: $gradient-primary; + color: white; + box-shadow: $shadow-brand; + } + + &.role-user { + background: $color-gray-100; + color: $color-text-secondary; + border: 1px solid $color-gray-200; + } +} + +.verified-badge { + background: rgba($color-success, 0.1); + color: $color-success; + border: 1px solid rgba($color-success, 0.2); +} + +.edit-btn { + position: absolute; + top: 0; + right: 0; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + background: $color-white; + border: 1px solid $glass-border; + border-radius: $radius-md; + color: $color-text-secondary; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: $gradient-subtle; + color: $color-brand-primary; + border-color: rgba($color-brand-primary, 0.3); + } + + @media (max-width: 639px) { + top: -0.5rem; + right: -0.5rem; + } +} + +/* ===== EDIT SECTION ===== */ + +.edit-section { + background: $gradient-subtle; + border: 1px solid rgba($color-brand-primary, 0.15); + border-radius: $radius-lg; + padding: 1.25rem; + + h3 { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 1rem; + font-weight: 700; + color: $color-brand-primary; + margin: 0 0 1rem; + + .material-symbols-outlined { + font-size: 20px; + } + } +} + +.edit-form { + display: grid; + gap: 1rem; +} + +.form-group { + display: grid; + gap: 0.5rem; + + label { + font-size: 0.8125rem; + font-weight: 600; + color: $color-text-secondary; + } + + input { + padding: 0.75rem 1rem; + border: 2px solid $glass-border; + border-radius: $radius-md; + font-size: 0.9375rem; + font-weight: 500; + color: $color-text-primary; + background: $color-white; + transition: all 0.2s; + + &:focus { + outline: none; + border-color: $color-brand-primary; + box-shadow: 0 0 0 4px rgba($color-brand-primary, 0.1); + } + } +} + +.form-actions { + display: flex; + gap: 0.75rem; + justify-content: flex-end; + + @media (max-width: 639px) { + flex-direction: column-reverse; + } +} + +/* ===== QUICK STATS ===== */ + +.quick-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 0.75rem; + + @media (max-width: 639px) { + grid-template-columns: 1fr; + } +} + +.stat-card { + display: flex; + align-items: center; + gap: 0.875rem; + padding: 1rem; + background: $color-white; + border: 1px solid $glass-border; + border-radius: $radius-lg; + transition: all 0.2s; + + &.clickable { + cursor: pointer; + + &:hover { + border-color: rgba($color-brand-primary, 0.3); + box-shadow: $shadow-sm; + } + } +} + +.stat-icon { + width: 44px; + height: 44px; + display: flex; + align-items: center; + justify-content: center; + background: $gradient-subtle; + border-radius: $radius-md; + color: $color-brand-primary; + flex-shrink: 0; + + &.success { + background: rgba($color-success, 0.1); + color: $color-success; + } + + &.active { + background: $gradient-primary; + color: white; + } + + .material-symbols-outlined { + font-size: 22px; + } +} + +.stat-content { + display: grid; + gap: 0.125rem; + flex: 1; + min-width: 0; + + .stat-value { + font-size: 0.9375rem; + font-weight: 700; + color: $color-text-primary; + } + + .stat-label { + font-size: 0.75rem; + font-weight: 600; + color: $color-text-tertiary; + text-transform: uppercase; + letter-spacing: 0.03em; + } +} + +/* ===== TOGGLE SWITCH ===== */ + +.toggle-switch { + position: relative; + width: 44px; + height: 24px; + flex-shrink: 0; + + input { + opacity: 0; + width: 0; + height: 0; + + &:checked + .toggle-slider { + background: $gradient-primary; + + &::before { + transform: translateX(20px); + } + } + } + + .toggle-slider { + position: absolute; + inset: 0; + background: $color-gray-300; + border-radius: 24px; + cursor: pointer; + transition: all 0.3s; + + &::before { + content: ''; + position: absolute; + left: 2px; + top: 2px; + width: 20px; + height: 20px; + background: white; + border-radius: 50%; + box-shadow: $shadow-sm; + transition: transform 0.3s; + } + } +} + +/* ===== ACCORDION ===== */ + +.details-accordion { + border: 1px solid $glass-border; + border-radius: $radius-lg; + overflow: hidden; +} + +.accordion-header { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.25rem; + background: $color-white; + border: none; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: $color-gray-50; + } + + &.open { + border-bottom: 1px solid $glass-border; + } + + .accordion-title { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.9375rem; + font-weight: 600; + color: $color-text-primary; + + .material-symbols-outlined { + font-size: 20px; + color: $color-brand-primary; + } + } + + .accordion-icon { + color: $color-text-tertiary; + transition: transform 0.2s; + } +} + +.accordion-content { + padding: 1rem 1.25rem; + background: $color-gray-50; +} + +.details-grid { + display: grid; + gap: 0.75rem; +} + +.detail-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + background: $color-white; + border: 1px solid $glass-border; + border-radius: $radius-md; + + > .material-symbols-outlined { + font-size: 20px; + color: $color-text-tertiary; + flex-shrink: 0; + } + + .detail-content { + flex: 1; + min-width: 0; + display: grid; + gap: 0.125rem; + } + + .detail-label { + font-size: 0.6875rem; + font-weight: 600; + color: $color-text-tertiary; + text-transform: uppercase; + letter-spacing: 0.05em; + } + + .detail-value { + font-size: 0.875rem; + font-weight: 600; + color: $color-text-primary; + word-break: break-all; + } +} + +.copy-btn { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: 1px solid transparent; + border-radius: $radius-sm; + color: $color-text-tertiary; + cursor: pointer; + transition: all 0.2s; + flex-shrink: 0; + + .material-symbols-outlined { + font-size: 18px; + } + + &:hover { + background: $gradient-subtle; + color: $color-brand-primary; + border-color: rgba($color-brand-primary, 0.2); + } +} + +/* ===== QUICK ACTIONS ===== */ + +.quick-actions { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.75rem; + + @media (max-width: 639px) { + grid-template-columns: 1fr; + } +} + +.action-card { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + padding: 1.25rem 1rem; + background: $color-white; + border: 1px solid $glass-border; + border-radius: $radius-lg; + text-decoration: none; + cursor: pointer; + transition: all 0.2s; + + .material-symbols-outlined { + font-size: 28px; + color: $color-brand-primary; + } + + .action-label { + font-size: 0.8125rem; + font-weight: 600; + color: $color-text-secondary; + text-align: center; + } + + &:hover { + border-color: rgba($color-brand-primary, 0.3); + box-shadow: $shadow-md; + transform: translateY(-2px); + + .material-symbols-outlined { + transform: scale(1.1); + } + } + + &.danger { + .material-symbols-outlined { + color: $color-error; + } + + &:hover { + background: rgba($color-error, 0.04); + border-color: rgba($color-error, 0.3); + + .action-label { + color: $color-error; + } + } + } +} + +/* ===== BUTTONS ===== */ + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.75rem 1.5rem; + border-radius: $radius-md; + font-weight: 600; + font-size: 0.875rem; + cursor: pointer; + border: none; + transition: all 0.2s; + white-space: nowrap; + + &--primary { + background: $gradient-primary; + color: white; + box-shadow: $shadow-brand; + + &:hover:not(:disabled) { + box-shadow: $shadow-brand-hover; + transform: translateY(-1px); + } + + &:disabled { + opacity: 0.7; + cursor: not-allowed; + } + } + + &--secondary { + background: $color-white; + color: $color-text-secondary; + border: 1px solid $glass-border; + + &:hover { + background: $color-gray-50; + border-color: $color-gray-300; + } + } +} + +/* ===== MOTION PREFERENCES ===== */ + +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/components/profile/profile.component.spec.ts b/apps/frontend/src/app/components/profile/profile.component.spec.ts new file mode 100644 index 0000000..17789ee --- /dev/null +++ b/apps/frontend/src/app/components/profile/profile.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProfileComponent } from './profile.component'; + +describe('ProfileComponent', () => { + let component: ProfileComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ProfileComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ProfileComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/frontend/src/app/components/profile/profile.component.ts b/apps/frontend/src/app/components/profile/profile.component.ts new file mode 100644 index 0000000..1c4474e --- /dev/null +++ b/apps/frontend/src/app/components/profile/profile.component.ts @@ -0,0 +1,168 @@ +// ==================== pages/profile/profile.component.ts ==================== +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { Router, RouterLink } from '@angular/router'; +import { trigger, transition, style, animate } from '@angular/animations'; +import { AuthService, User, UserRole } from '../../services/auth.service'; +import { ApiService } from '../../api/api.service'; +import { ToastService } from '../../shared/toasts/toast.service'; +import { ConfirmationService } from '../../shared/confirmation/confirmation.service'; + +@Component({ + selector: 'app-profile', + standalone: true, + imports: [CommonModule, FormsModule, RouterLink], + templateUrl: './profile.component.html', + styleUrl: './profile.component.scss', + animations: [ + trigger('slideDown', [ + transition(':enter', [ + style({ opacity: 0, height: 0, overflow: 'hidden' }), + animate('300ms ease-out', style({ opacity: 1, height: '*' })) + ]), + transition(':leave', [ + style({ opacity: 1, height: '*', overflow: 'hidden' }), + animate('200ms ease-in', style({ opacity: 0, height: 0 })) + ]) + ]) + ] +}) +export class ProfileComponent implements OnInit { + user: User | null = null; + UserRole = UserRole; + + // Edit mode + editMode = false; + editName = ''; + saving = false; + + // Accordion + showDetails = false; + + constructor( + private authService: AuthService, + private apiService: ApiService, + private router: Router, + private toasts: ToastService, + private confirmationService: ConfirmationService + ) { } + + ngOnInit(): void { + this.authService.currentUser$.subscribe(user => { + this.user = user; + if (user) { + this.editName = user.name; + } + }); + } + + toggleEditMode() { + this.editMode = !this.editMode; + if (this.editMode && this.user) { + this.editName = this.user.name; + } + } + + cancelEdit() { + this.editMode = false; + if (this.user) { + this.editName = this.user.name; + } + } + + saveProfile() { + if (!this.user || !this.editName.trim() || this.editName.length < 2) { + this.toasts.error('Name muss mindestens 2 Zeichen lang sein.'); + return; + } + + this.saving = true; + this.apiService.updateUser(this.user.id, { name: this.editName.trim() }).subscribe({ + next: (updatedUser) => { + // Update local storage and auth service + localStorage.setItem('current_user', JSON.stringify(updatedUser)); + this.user = updatedUser; + this.editMode = false; + this.saving = false; + this.toasts.success('Profil erfolgreich aktualisiert!'); + }, + error: (err) => { + this.saving = false; + this.toasts.error('Fehler beim Speichern: ' + (err.error?.message || 'Unbekannter Fehler')); + } + }); + } + + toggleNewsletter() { + if (!this.user) return; + + const newValue = !this.user.wantsNewsletter; + this.apiService.updateUser(this.user.id, { wantsNewsletter: newValue }).subscribe({ + next: (updatedUser) => { + localStorage.setItem('current_user', JSON.stringify(updatedUser)); + this.user = updatedUser; + this.toasts.success(newValue ? 'Newsletter abonniert!' : 'Newsletter abbestellt.'); + }, + error: (err) => { + this.toasts.error('Fehler: ' + (err.error?.message || 'Konnte Newsletter-Status nicht ändern')); + } + }); + } + + toggleDetails() { + this.showDetails = !this.showDetails; + } + + copyToClipboard(text: string) { + navigator.clipboard.writeText(text).then(() => { + this.toasts.success('In Zwischenablage kopiert!'); + }).catch(() => { + this.toasts.error('Kopieren fehlgeschlagen'); + }); + } + + async logout() { + const confirmed = await this.confirmationService.confirm({ + title: 'Abmelden', + message: 'Möchtest du dich wirklich abmelden?', + confirmText: 'Ja, abmelden', + cancelText: 'Abbrechen', + type: 'danger', + icon: 'logout' + }); + + if (confirmed) { + this.authService.logout(); + this.toasts.success('Erfolgreich abgemeldet.'); + this.router.navigate(['/']); + } + } + + routeTo(route: string) { + this.router.navigate([route]); + } + + getRoleBadgeClass(): string { + return this.user?.role === UserRole.ADMIN ? 'role-admin' : 'role-user'; + } + + getRoleLabel(): string { + return this.user?.role === UserRole.ADMIN ? 'Administrator' : 'Benutzer'; + } + + formatDate(dateString: Date | undefined): string { + if (!dateString) return '-'; + + const date = new Date(dateString); + return date.toLocaleDateString('de-DE', { + day: '2-digit', + month: 'short', + year: 'numeric' + }); + } + + getRoleIcon(): string { + return this.user?.role === UserRole.ADMIN ? 'admin_panel_settings' : 'person'; + } +} diff --git a/apps/frontend/src/app/components/register/register.component.html b/apps/frontend/src/app/components/register/register.component.html new file mode 100644 index 0000000..2de6040 --- /dev/null +++ b/apps/frontend/src/app/components/register/register.component.html @@ -0,0 +1,90 @@ + + +
+
+
+ + +
+
+ person_add +
+

Account erstellen

+
+ + +
+ + +
+
+ +
+ person + +
+
+
+ +
+ person + +
+
+
+ + +
+ +
+ mail + +
+
+ + +
+ +
+ key + + +
+

Mindestens 8 Zeichen

+
+ + +
+ error + {{ error }} +
+ + + +
+ + + +
+
+
\ No newline at end of file diff --git a/apps/frontend/src/app/components/register/register.component.scss b/apps/frontend/src/app/components/register/register.component.scss new file mode 100644 index 0000000..0d9a6a4 --- /dev/null +++ b/apps/frontend/src/app/components/register/register.component.scss @@ -0,0 +1,353 @@ +@import '../../utils/shared-styles.scss'; + +.container { + width: 100%; + max-width: 480px; + margin-inline: auto; + padding-inline: clamp(1rem, 4vw, 2rem); +} + +.auth { + min-height: 80vh; + display: flex; + align-items: center; + padding-block: 3rem; +} + +/* ===== AUTH CARD ===== */ + +@keyframes cardExit { + 0% { + opacity: 1; + transform: translateY(0) scale(1); + } + 100% { + opacity: 0; + transform: translateY(-20px) scale(0.98); + } +} + +@keyframes successPulse { + 0% { + box-shadow: $shadow-glass; + } + 50% { + box-shadow: 0 0 0 8px rgba(34, 197, 94, 0.15), $shadow-glass; + } + 100% { + box-shadow: $shadow-glass; + } +} + +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 10%, 30%, 50%, 70%, 90% { transform: translateX(-6px); } + 20%, 40%, 60%, 80% { transform: translateX(6px); } +} + +@keyframes errorPulse { + 0% { + box-shadow: $shadow-glass; + } + 50% { + box-shadow: 0 0 0 8px rgba(239, 68, 68, 0.15), $shadow-glass; + } + 100% { + box-shadow: $shadow-glass; + } +} + +.auth-card { + width: 100%; + background: $glass-bg; + backdrop-filter: blur(20px); + border: 1px solid $glass-border; + border-radius: $radius-2xl; + padding: 2.5rem; + box-shadow: $shadow-glass; + transition: all 0.4s cubic-bezier(0.22, 1, 0.36, 1); + + &.login-success { + animation: successPulse 0.5s ease-out, cardExit 0.4s ease-in 0.3s forwards; + border-color: rgba(34, 197, 94, 0.3); + } + + &.login-error { + animation: shake 0.5s ease-out, errorPulse 0.6s ease-out; + border-color: rgba(239, 68, 68, 0.4); + } + + @media (max-width: 640px) { + padding: 2rem 1.5rem; + } +} + +/* ===== HEADER ===== */ + +.auth-header { + text-align: center; + margin-bottom: 2rem; + + .auth-icon { + width: 72px; + height: 72px; + margin: 0 auto 1.5rem; + background: $gradient-primary; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: $shadow-brand; + + .material-symbols-outlined { + font-size: 36px; + color: white; + } + } + + h1 { + font-size: 1.75rem; + font-weight: 800; + color: $color-text-primary; + margin-bottom: 0.5rem; + } + + p { + color: $color-text-secondary; + font-size: 1rem; + margin: 0; + } +} + +/* ===== FORM ===== */ + +.auth-form { + display: grid; + gap: 1.25rem; +} + +.form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + + @media (max-width: 480px) { + grid-template-columns: 1fr; + } +} + +.form-field { + display: grid; + gap: 0.5rem; + + label { + font-weight: 600; + color: $color-text-primary; + font-size: 0.9375rem; + } + + .field-hint { + font-size: 0.875rem; + color: $color-text-secondary; + margin-top: -0.25rem; + } + + &.has-error { + .input-wrapper input { + border-color: $color-error; + box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.1); + } + } +} + +/* Input Wrapper */ +.input-wrapper { + position: relative; + + .input-icon { + position: absolute; + left: 1rem; + top: 50%; + transform: translateY(-50%); + color: $color-text-secondary; + font-size: 20px; + pointer-events: none; + } + + input { + width: 100%; + padding: 0.875rem 1rem; + padding-left: 3rem; + padding-right: 3rem; + border: 2px solid $color-gray-200; + border-radius: $radius-md; + font-size: 0.9375rem; + font-family: inherit; + color: $color-text-primary; + background: $color-white; + transition: all 0.3s ease; + + &::placeholder { + color: $color-gray-400; + } + + &:focus { + outline: none; + border-color: $color-brand-primary; + box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.1); + } + + &:disabled { + background: $color-gray-100; + color: $color-text-tertiary; + cursor: not-allowed; + } + } + + .input-action { + position: absolute; + right: 0.5rem; + top: 50%; + transform: translateY(-50%); + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + border-radius: $radius-sm; + color: $color-text-secondary; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + background: $color-gray-100; + color: $color-text-primary; + } + + &:focus-visible { + outline: none; + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2); + } + + .material-symbols-outlined { + font-size: 20px; + } + } +} + +/* Error Text */ +.error-text { + display: flex; + align-items: center; + gap: 0.375rem; + color: $color-error; + font-size: 0.875rem; + font-weight: 500; + + .material-symbols-outlined { + font-size: 16px; + } +} + +/* Alert */ +.alert { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 1rem 1.25rem; + border-radius: $radius-md; + font-size: 0.9375rem; + + .material-symbols-outlined { + font-size: 20px; + flex-shrink: 0; + } + + &--error { + background: $color-error-light; + border: 1px solid darken($color-error-light, 10%); + color: darken($color-error, 10%); + + .material-symbols-outlined { + color: $color-error; + } + } +} + +/* Button */ +.btn--large { + width: 100%; + margin-top: 0.5rem; + padding: 1rem 1.5rem; + font-size: 1rem; + + &.loading { + pointer-events: none; + opacity: 0.8; + } +} + +.loading-content { + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; +} + +.spinner { + width: 18px; + height: 18px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-top-color: white; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* ===== FOOTER ===== */ + +.auth-footer { + margin-top: 2rem; + padding-top: 1.5rem; + border-top: 1px solid $color-gray-200; + text-align: center; + + p { + color: $color-text-secondary; + font-size: 0.9375rem; + margin: 0; + + a { + color: $color-brand-primary; + text-decoration: none; + font-weight: 600; + transition: color 0.2s ease; + + &:hover { + color: $color-brand-dark; + text-decoration: underline; + } + + &:focus-visible { + outline: none; + text-decoration: underline; + } + } + } +} + +/* ===== MOTION PREFERENCES ===== */ + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/components/register/register.component.spec.ts b/apps/frontend/src/app/components/register/register.component.spec.ts new file mode 100644 index 0000000..757b895 --- /dev/null +++ b/apps/frontend/src/app/components/register/register.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RegisterComponent } from './register.component'; + +describe('RegisterComponent', () => { + let component: RegisterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RegisterComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(RegisterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/frontend/src/app/components/register/register.component.ts b/apps/frontend/src/app/components/register/register.component.ts new file mode 100644 index 0000000..3c53c2a --- /dev/null +++ b/apps/frontend/src/app/components/register/register.component.ts @@ -0,0 +1,83 @@ +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { AuthService } from '../../services/auth.service'; +import { PageTitleComponent } from '../../shared/page-title/page-title.component'; +import { ToastService } from '../../shared/toasts/toast.service'; + +@Component({ + selector: 'app-register', + standalone: true, + imports: [CommonModule, FormsModule, RouterLink, PageTitleComponent], + templateUrl: './register.component.html', + styleUrl: './register.component.scss' +}) +export class RegisterComponent { + firstName = ''; + lastName = ''; + email = ''; + password = ''; + loading = false; + error = ''; + showPassword = false; + registerSuccess = false; + registerError = false; + + constructor( + private authService: AuthService, + private router: Router, + private route: ActivatedRoute, + private toasts: ToastService + ) { } + + get fullName(): string { + return `${this.firstName} ${this.lastName}`.trim(); + } + + register() { + if (!this.firstName || !this.lastName || !this.email || !this.password) { + this.error = 'Bitte fülle alle Felder aus'; + this.triggerErrorAnimation(); + return; + } + + if (this.password.length < 8) { + this.error = 'Passwort muss mindestens 8 Zeichen lang sein'; + this.triggerErrorAnimation(); + return; + } + + this.loading = true; + this.error = ''; + + + this.authService.register(this.email, this.fullName, this.password).subscribe({ + next: (response) => { + console.log('✅ Registrierung erfolgreich:', response); + this.toasts.success('Erfolgreich registriert und eingeloggt.'); + this.loading = false; + this.registerSuccess = true; + + // Navigate nach Exit-Animation + setTimeout(() => { + const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/'; + this.router.navigate([returnUrl]); + }, 700); + }, + error: (err) => { + console.error('❌ Registrierung fehlgeschlagen:', err); + this.error = err?.error?.message || 'Registrierung fehlgeschlagen. Email bereits vergeben?'; + this.loading = false; + this.triggerErrorAnimation(); + } + }); + } + + private triggerErrorAnimation(): void { + this.registerError = true; + setTimeout(() => { + this.registerError = false; + }, 600); + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/components/remote-support/remote-support.component.html b/apps/frontend/src/app/components/remote-support/remote-support.component.html new file mode 100644 index 0000000..8e6c067 --- /dev/null +++ b/apps/frontend/src/app/components/remote-support/remote-support.component.html @@ -0,0 +1,156 @@ + + +
+
+ +
+ + +
+ + +
+ + + +
+ +
+

AnyDesk herunterladen

+ Sichere Remote-Desktop Software +
+
+ +
+ +
+ speed + Schnelle Übertragung +
+
+ install_desktop + Keine Installation nötig +
+
+ + + +

+ info + Windows 10/11 · ca. 5 MB · Kostenlos für private Nutzung +

+
+ + + +
+
+ check_circle +
+

Download gestartet! 🎉

+

Die Datei sollte automatisch heruntergeladen werden.

+ +
+
+
+ + +
+ Download startet nicht? + + Direkter Download-Link + open_in_new + +
+
+ + + +
+ +
+
diff --git a/apps/frontend/src/app/components/remote-support/remote-support.component.scss b/apps/frontend/src/app/components/remote-support/remote-support.component.scss new file mode 100644 index 0000000..aa75eb6 --- /dev/null +++ b/apps/frontend/src/app/components/remote-support/remote-support.component.scss @@ -0,0 +1,494 @@ +@import '../../utils/shared-styles'; + +.remote-support { + min-height: calc(100vh - 72px); + padding: 2rem 0 4rem; + background: #EEF4FE; +} + +.container { + max-width: 1100px; + margin: 0 auto; + padding: 0 1.5rem; +} + +// ===== HERO CARD ===== + +.hero-card { + text-align: center; + padding: 2.5rem 2rem; + background: $glass-bg; + backdrop-filter: blur(20px); + border: 1px solid $glass-border; + border-radius: $radius-xl; + box-shadow: $shadow-glass; + margin-bottom: 2rem; + + &__icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 72px; + height: 72px; + background: $gradient-primary; + border-radius: 50%; + margin-bottom: 1.25rem; + + .material-symbols-outlined { + font-size: 36px; + color: white; + } + } + + h1 { + margin: 0 0 0.75rem; + font-size: 2rem; + font-weight: 800; + background: $gradient-primary; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + + @media (max-width: 640px) { + font-size: 1.5rem; + } + } + + p { + margin: 0 auto; + max-width: 600px; + color: $color-text-secondary; + font-size: 1.0625rem; + line-height: 1.6; + + @media (max-width: 640px) { + font-size: 0.9375rem; + } + } +} + +// ===== CONTENT GRID ===== + +.content-grid { + display: grid; + grid-template-columns: 1.2fr 1fr; + gap: 2rem; + align-items: start; + + @media (max-width: 900px) { + grid-template-columns: 1fr; + } +} + +// ===== DOWNLOAD SECTION ===== + +.download-section { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.download-card { + background: $glass-bg; + backdrop-filter: blur(20px); + border: 1px solid $glass-border; + border-radius: $radius-xl; + box-shadow: $shadow-glass; + padding: 2rem; + transition: all 0.3s ease; + + &--success { + border-color: rgba($color-success, 0.3); + background: linear-gradient(135deg, rgba($color-success, 0.05) 0%, $glass-bg 100%); + } + + &__header { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1.5rem; + + h2 { + margin: 0; + font-size: 1.375rem; + font-weight: 700; + color: $color-text-primary; + } + } + + &__logo { + width: 48px; + height: 48px; + border-radius: $radius-md; + box-shadow: $shadow-sm; + } + + &__subtitle { + font-size: 0.875rem; + color: $color-text-secondary; + } + + &__info { + display: flex; + flex-wrap: wrap; + gap: 1rem; + margin-bottom: 1.5rem; + padding: 1rem; + background: rgba($color-brand-primary, 0.05); + border-radius: $radius-md; + + .info-item { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.875rem; + color: $color-text-secondary; + + .material-symbols-outlined { + font-size: 20px; + color: $color-brand-primary; + } + } + } + + &__note { + display: flex; + align-items: center; + gap: 0.5rem; + margin: 1rem 0 0; + font-size: 0.8125rem; + color: $color-text-tertiary; + + .material-symbols-outlined { + font-size: 18px; + } + } +} + +// ===== DOWNLOAD BUTTON ===== + +.download-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; + width: 100%; + padding: 1rem 2rem; + background: $gradient-primary; + color: white; + font-size: 1.0625rem; + font-weight: 700; + border: none; + border-radius: $radius-lg; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: $shadow-brand; + + .material-symbols-outlined { + font-size: 24px; + } + + &:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: $shadow-brand-hover; + } + + &:active:not(:disabled) { + transform: translateY(0); + } + + &:disabled { + opacity: 0.8; + cursor: not-allowed; + } +} + +.spinner { + width: 20px; + height: 20px; + border: 2px solid rgba(white, 0.3); + border-top-color: white; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +// ===== SUCCESS STATE ===== + +.success-state { + text-align: center; + padding: 1rem 0; + + .success-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 72px; + height: 72px; + background: rgba($color-success, 0.1); + border-radius: 50%; + margin-bottom: 1rem; + + .material-symbols-outlined { + font-size: 40px; + color: $color-success; + } + } + + h2 { + margin: 0 0 0.5rem; + font-size: 1.375rem; + color: $color-text-primary; + } + + p { + margin: 0 0 1.5rem; + color: $color-text-secondary; + } +} + +// ===== ALT DOWNLOAD ===== + +.alt-download { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + font-size: 0.875rem; + color: $color-text-tertiary; + + .alt-link { + display: inline-flex; + align-items: center; + gap: 0.25rem; + color: $color-brand-primary; + text-decoration: none; + font-weight: 500; + + .material-symbols-outlined { + font-size: 16px; + } + + &:hover { + text-decoration: underline; + } + } +} + +// ===== INSTRUCTIONS SIDEBAR ===== + +.instructions { + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +.instruction-card { + background: $glass-bg; + backdrop-filter: blur(20px); + border: 1px solid $glass-border; + border-radius: $radius-lg; + padding: 1.5rem; + box-shadow: $shadow-glass; + + h3 { + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0 0 1.25rem; + font-size: 1rem; + font-weight: 700; + color: $color-text-primary; + + .material-symbols-outlined { + color: $color-brand-primary; + } + } +} + +.steps-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 1rem; + + li { + display: flex; + align-items: flex-start; + gap: 0.875rem; + } + + .step-number { + display: flex; + align-items: center; + justify-content: center; + min-width: 28px; + height: 28px; + background: $gradient-primary; + color: white; + font-size: 0.8125rem; + font-weight: 700; + border-radius: 50%; + } + + .step-content { + display: flex; + flex-direction: column; + padding-top: 2px; + + strong { + font-size: 0.9375rem; + color: $color-text-primary; + margin-bottom: 0.125rem; + } + + span { + font-size: 0.8125rem; + color: $color-text-secondary; + } + } +} + +// ===== SECURITY CARD ===== + +.security-card { + background: $glass-bg; + backdrop-filter: blur(20px); + border: 1px solid $glass-border; + border-radius: $radius-lg; + padding: 1.5rem; + box-shadow: $shadow-glass; + + &__header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 1rem; + + .material-symbols-outlined { + font-size: 24px; + color: $color-success; + } + + h3 { + margin: 0; + font-size: 1rem; + font-weight: 700; + color: $color-text-primary; + } + } +} + +.security-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.75rem; + + li { + display: flex; + align-items: center; + gap: 0.625rem; + font-size: 0.875rem; + color: $color-text-secondary; + + .material-symbols-outlined { + font-size: 20px; + color: $color-success; + } + } +} + +// ===== CONTACT CARD ===== + +.contact-card { + background: linear-gradient(135deg, rgba($color-brand-primary, 0.08) 0%, rgba($color-brand-secondary, 0.12) 100%); + border: 1px solid rgba($color-brand-primary, 0.2); + border-radius: $radius-lg; + padding: 1.5rem; + text-align: center; + + h3 { + margin: 0 0 0.5rem; + font-size: 1rem; + font-weight: 700; + color: $color-text-primary; + } + + p { + margin: 0 0 1rem; + font-size: 0.875rem; + color: $color-text-secondary; + line-height: 1.5; + } +} + +// ===== BUTTONS ===== + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.75rem 1.25rem; + font-size: 0.9375rem; + font-weight: 600; + border-radius: $radius-md; + border: none; + cursor: pointer; + text-decoration: none; + transition: all 0.2s ease; + + .material-symbols-outlined { + font-size: 20px; + } + + &--primary { + background: $gradient-primary; + color: white; + box-shadow: $shadow-brand; + + &:hover { + transform: translateY(-1px); + box-shadow: $shadow-brand-hover; + } + } + + &--secondary { + background: rgba($color-brand-primary, 0.1); + color: $color-brand-primary; + border: 1px solid rgba($color-brand-primary, 0.2); + + &:hover { + background: rgba($color-brand-primary, 0.15); + } + } + + &--full { + width: 100%; + } +} + +// ===== PAGE ANIMATION ===== + +.page-animate { + animation: fadeInUp 0.5s ease-out; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/apps/frontend/src/app/components/remote-support/remote-support.component.ts b/apps/frontend/src/app/components/remote-support/remote-support.component.ts new file mode 100644 index 0000000..5006b74 --- /dev/null +++ b/apps/frontend/src/app/components/remote-support/remote-support.component.ts @@ -0,0 +1,41 @@ +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { PageTitleComponent } from '../../shared/page-title/page-title.component'; + +@Component({ + selector: 'app-remote-support', + standalone: true, + imports: [CommonModule, PageTitleComponent], + templateUrl: './remote-support.component.html', + styleUrl: './remote-support.component.scss' +}) +export class RemoteSupportComponent { + downloading = false; + downloadSuccess = false; + + readonly anydeskUrl = 'https://anydesk.com/de/downloads/thank-you?dv=win_exe'; + readonly anydeskDirectUrl = 'https://download.anydesk.com/AnyDesk.exe'; + + downloadAnyDesk(): void { + this.downloading = true; + + // Trigger download + const link = document.createElement('a'); + link.href = this.anydeskDirectUrl; + link.download = 'AnyDesk.exe'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // Show success after a short delay + setTimeout(() => { + this.downloading = false; + this.downloadSuccess = true; + }, 1500); + } + + resetDownload(): void { + this.downloadSuccess = false; + this.downloading = false; + } +} diff --git a/apps/frontend/src/app/components/server-status/server-status.component.html b/apps/frontend/src/app/components/server-status/server-status.component.html new file mode 100644 index 0000000..69445d8 --- /dev/null +++ b/apps/frontend/src/app/components/server-status/server-status.component.html @@ -0,0 +1,67 @@ +
+ +
+ + {{ overallMessage }} + Aktualisiert {{ timeAgo(lastUpdated) }} + +
+ + +
+
+
+

+ {{ s.name }} + · {{ s.region }} +

+ {{ statusLabel(s.status) }} +
+ +
+
+ Uptime 24h + {{ pct(s.uptime24h) }} +
+
+ Uptime 7d + {{ pct(s.uptime7d) }} +
+
+ Uptime 30d + {{ pct(s.uptime30d) }} +
+
+ p95 + {{ fmtMs(s.responseP95Ms) }} +
+
+ Ø Antwort + {{ fmtMs(s.responseAvgMs) }} +
+
+ Letzter Vorfall + {{ s.lastIncidentAt ? timeAgo(s.lastIncidentAt) : '–' }} +
+
+ +
+ + + + Letzte {{ s.latencySeries!.length }} Messungen +
+ +
+ v{{ s.version }} + Geprüft {{ timeAgo(s.lastCheckAt) }} + Nächster Check in {{ until(s.nextCheckAt) }} +
+
+
+
+ \ No newline at end of file diff --git a/apps/frontend/src/app/components/server-status/server-status.component.scss b/apps/frontend/src/app/components/server-status/server-status.component.scss new file mode 100644 index 0000000..7692fbe --- /dev/null +++ b/apps/frontend/src/app/components/server-status/server-status.component.scss @@ -0,0 +1,198 @@ +@import '../../utils/shared-styles.scss'; + +.status { + background: $color-white; + + .status__summary { + display: flex; + align-items: center; + gap: .5rem; + padding: 1rem; + border: 1px solid $color-gray-light; + border-radius: 14px; + margin: 1rem 0; + background: $color-background-main; + + .dot { + width: 10px; + height: 10px; + border-radius: 50%; + display: inline-block; + flex-shrink: 0; + } + + .dot.ok { + background: $color-success; + } + + .dot.warn { + background: $color-warning; + } + + .dot.err { + background: $color-error; + } + + .dot.info { + background: $color-info; + } + + .summary__title { + color: $color-text-primary; + } + + .summary__meta { + margin-left: auto; + color: $color-gray; + font-size: .9rem; + } + + .btn--xs { + height: 34px; + padding: 0 .6rem; + border-radius: 10px; + font-size: .9rem; + } + } + + .grid { + display: grid; + gap: 1rem; + grid-template-columns: 1fr; + + @media (min-width: 700px) { + grid-template-columns: repeat(2, 1fr); + } + + @media (min-width: 1100px) { + grid-template-columns: repeat(3, 1fr); + } + } + + .card { + border: 1px solid $color-gray-light; + border-radius: 14px; + background: $color-white; + display: grid; + gap: .75rem; + padding: 1rem; + + .card__head { + display: flex; + align-items: center; + gap: .5rem; + + .card__title { + margin: 0; + font-size: 1.05rem; + color: $color-text-primary; + } + + .meta { + color: $color-text-secondary; + font-weight: 400; + } + + .chip { + margin-left: auto; + padding: .25rem .6rem; + border-radius: 999px; + font-size: .85rem; + font-weight: 600; + + &.is-up { + background: rgba(22, 163, 74, .12); + color: $color-success; + } + + &.is-degraded { + background: rgba(245, 158, 11, .14); + color: $color-warning; + } + + &.is-down { + background: rgba(220, 38, 38, .14); + color: $color-error; + } + + &.is-maint { + background: rgba(14, 165, 233, .12); + color: $color-info; + } + } + } + + .kpis { + display: grid; + gap: .5rem; + grid-template-columns: repeat(2, minmax(0, 1fr)); + + @media (min-width: 520px) { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .kpi { + display: grid; + gap: .15rem; + padding: .5rem; + border: 1px solid $color-gray-light; + border-radius: 10px; + + .kpi__label { + color: $color-text-secondary; + font-size: .85rem; + } + + .kpi__value { + color: $color-text-primary; + font-weight: 600; + } + } + } + + .spark { + display: flex; + align-items: center; + gap: .5rem; + + .spark__line { + fill: none; + stroke: $color-brand-primary; + stroke-width: 2; + } + + .spark__caption { + color: $color-text-secondary; + font-size: .85rem; + } + } + + .card__foot { + display: flex; + gap: .75rem; + flex-wrap: wrap; + align-items: center; + + .muted { + color: $color-text-secondary; + font-size: .9rem; + } + } + } +} + +/* Ghost-Button (klein) – nutzt deine Farbvariablen */ +.btn.btn--ghost { + background: $color-white; + color: $color-text-primary; + border: 1px solid $color-gray-light; + + &:hover { + background: $color-background-main; + } + + &:focus-visible { + outline: none; + box-shadow: 0 0 0 3px rgba(37, 99, 235, .18); + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/components/server-status/server-status.component.spec.ts b/apps/frontend/src/app/components/server-status/server-status.component.spec.ts new file mode 100644 index 0000000..9e7c19a --- /dev/null +++ b/apps/frontend/src/app/components/server-status/server-status.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ServerStatusComponent } from './server-status.component'; + +describe('ServerStatusComponent', () => { + let component: ServerStatusComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ServerStatusComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ServerStatusComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/frontend/src/app/components/server-status/server-status.component.ts b/apps/frontend/src/app/components/server-status/server-status.component.ts new file mode 100644 index 0000000..f4fb9f2 --- /dev/null +++ b/apps/frontend/src/app/components/server-status/server-status.component.ts @@ -0,0 +1,254 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input } from '@angular/core'; +import { Observable } from 'rxjs'; +import { BehaviorSubject, Subject, timer } from 'rxjs'; +import { map, takeUntil } from 'rxjs/operators'; + +type Status = 'UP' | 'DEGRADED' | 'DOWN' | 'MAINTENANCE'; + +export interface ServiceStatus { + id: string; + name: string; + region?: string; + status: Status; + uptime24h: number; // 0..1 + uptime7d: number; // 0..1 + uptime30d: number; // 0..1 + responseAvgMs?: number; + responseP95Ms?: number; + lastIncidentAt?: string; // ISO + lastCheckAt: string; // ISO + nextCheckAt?: string; // ISO + version?: string; + latencySeries?: number[]; + // interne Mock-Steuerung (wird im Template nicht angezeigt) + mockState?: { degradeTicks?: number; downTicks?: number; baseLatency?: number }; +} + +@Component({ + selector: 'app-server-status', + standalone: true, + imports: [CommonModule], + templateUrl: './server-status.component.html', + styleUrl: './server-status.component.scss' +}) +export class ServerStatusComponent { + /** Aktualisierungsintervall in ms */ + @Input() refreshMs = 3000; + + services$!: Observable; + lastUpdated = new Date().toISOString(); + overall: Status = 'UP'; + overallMessage = 'Alle Systeme betriebsbereit'; + + // Sparkline-Größe (passt zu meinem HTML/SCSS) + sparkWidth = 140; + sparkHeight = 32; + + private store = new BehaviorSubject(createInitialMock()); + private destroy$ = new Subject(); + + ngOnInit(): void { + // Ticker: erzeugt alle refreshMs einen neuen Stand + this.services$ = timer(0, this.refreshMs).pipe( + map(() => { + const next = tickMock(this.store.value, this.refreshMs); + this.store.next(next); + this.lastUpdated = new Date().toISOString(); + this.overall = computeOverall(next); + this.overallMessage = overallToMessage(this.overall); + return next; + }) + ); + + // Option: falls du irgendwo anders aufräumen willst + this.services$.pipe(takeUntil(this.destroy$)).subscribe(); + } + + ngOnDestroy(): void { + this.destroy$.next(); this.destroy$.complete(); + } + + // Template-Helper (gleich wie zuvor) + trackById(_: number, s: ServiceStatus) { return s.id; } + statusLabel(s: Status): string { + return { UP: 'Online', DEGRADED: 'Eingeschränkt', DOWN: 'Offline', MAINTENANCE: 'Wartung' }[s]; + } + statusClass(s: Status): string { + return { UP: 'is-up', DEGRADED: 'is-degraded', DOWN: 'is-down', MAINTENANCE: 'is-maint' }[s]; + } + pct(v?: number) { return v == null ? '–' : (v * 100).toFixed(2) + '%'; } + fmtMs(v?: number) { return v == null || isNaN(v) ? '–' : Math.round(v) + ' ms'; } + timeAgo(iso?: string) { return timeAgo(iso); } + until(iso?: string) { return until(iso); } + toSparklinePath(series: number[], w: number, h: number) { return toSparklinePath(series, w, h); } + manualRefresh() { this.store.next(tickMock(this.store.value, this.refreshMs)); } +} + +/* ---------------- Mock-Helfer ---------------- */ + +function createInitialMock(): ServiceStatus[] { + const now = new Date().toISOString(); + return [ + mk('web', 'Web Frontend', 'eu-central', 120, '2.4.0'), + mk('api', 'Public API', 'eu-central', 90, '1.12.3'), + mk('db', 'Database', 'eu-central', 8, '14.10') + ].map(s => ({ ...s, lastCheckAt: now, nextCheckAt: now })); + + function mk(id: string, name: string, region: string, baseLatency: number, version: string): ServiceStatus { + const series = seedSeries(baseLatency, 24); + return { + id, name, region, status: 'UP', + uptime24h: 0.999, uptime7d: 0.998, uptime30d: 0.9985, + responseAvgMs: avg(series), responseP95Ms: p95(series), + lastCheckAt: new Date().toISOString(), + version, latencySeries: series, + mockState: { baseLatency } + }; + } +} + +function tickMock(prev: ServiceStatus[], refreshMs: number): ServiceStatus[] { + const now = new Date(); + const next = prev.map(s => { + const ms = s.mockState ?? {}; + // Status-Phasen steuern (kurze Degradierungen/Ausfälle) + if (!ms.degradeTicks && !ms.downTicks && s.status === 'UP') { + if (Math.random() < 0.05) ms.degradeTicks = randInt(2, 4); // 10–20s + if (Math.random() < 0.015) ms.downTicks = randInt(1, 2); // 5–10s + } + + let status: Status = s.status; + if (ms.downTicks && ms.downTicks > 0) { status = 'DOWN'; ms.downTicks--; } + else if (ms.degradeTicks && ms.degradeTicks > 0) { status = 'DEGRADED'; ms.degradeTicks--; } + else { status = 'UP'; } + + // Latenz ableiten + const base = (ms.baseLatency ?? 100); + const sample = + status === 'DOWN' ? jitter(base * 10, 0.35) : + status === 'DEGRADED' ? jitter(base * 2.4, 0.25) : + jitter(base * 1.0, 0.18); + + const series = [...(s.latencySeries ?? [])]; + const maxLen = 30; + series.push(Math.max(1, sample)); + if (series.length > maxLen) series.shift(); + + // Metriken + const responseAvgMs = avg(series); + const responseP95Ms = p95(series); + + // Uptime leicht driften lassen (realistisch, aber unaufgeregt) + const u24 = driftUptime(s.uptime24h, status, { up: +0.0025, down: -0.010 }); + const u7 = driftUptime(s.uptime7d, status, { up: +0.0010, down: -0.004 }); + const u30 = driftUptime(s.uptime30d, status, { up: +0.0005, down: -0.002 }); + + const lastIncidentAt = status !== 'UP' ? now.toISOString() : s.lastIncidentAt; + const lastCheckAt = now.toISOString(); + const nextCheckAt = new Date(now.getTime() + refreshMs).toISOString(); + + return { + ...s, + status, + latencySeries: series, + responseAvgMs, responseP95Ms, + uptime24h: clamp01(u24), uptime7d: clamp01(u7), uptime30d: clamp01(u30), + lastIncidentAt, lastCheckAt, nextCheckAt, + mockState: ms + }; + }); + + return next; +} + +function driftUptime(current: number, status: Status, step: { up: number; down: number }): number { + const jitterUp = step.up * (0.8 + Math.random() * 0.4); // ±20% + const jitterDn = step.down * (0.8 + Math.random() * 0.4); + const delta = status === 'UP' ? jitterUp : jitterDn; + // leichte Rückführung in Richtung 0.999..1 + const target = 0.999; + const pull = (target - current) * (status === 'UP' ? 0.05 : 0.01); + return current + delta + pull; +} + +/* --------- Utility --------- */ +function jitter(v: number, pct: number) { + const r = (Math.random() * 2 - 1) * pct; // -pct..+pct + return v * (1 + r); +} +function randInt(a: number, b: number) { return Math.floor(Math.random() * (b - a + 1)) + a; } +function clamp01(n: number) { return Math.max(0, Math.min(1, n)); } +function avg(arr: number[]) { return arr.length ? arr.reduce((s, x) => s + x, 0) / arr.length : 0; } +function p95(arr: number[]) { + if (!arr.length) return 0; + const sorted = [...arr].sort((a, b) => a - b); + const idx = Math.floor(0.95 * (sorted.length - 1)); + return sorted[idx]; +} +function seedSeries(base: number, n: number) { + const out: number[] = []; + let prev = base; + for (let i = 0; i < n; i++) { + prev = prev * (1 + (Math.random() * 0.12 - 0.06)); // ±6% + out.push(Math.max(1, prev)); + } + return out; +} +function computeOverall(services: ServiceStatus[]): Status { + if (!services.length) return 'MAINTENANCE'; + if (services.some(s => s.status === 'DOWN')) return 'DOWN'; + if (services.some(s => s.status === 'DEGRADED')) return 'DEGRADED'; + if (services.every(s => s.status === 'MAINTENANCE')) return 'MAINTENANCE'; + return 'UP'; +} +function overallToMessage(s: Status): string { + switch (s) { + case 'UP': return 'Alle Systeme betriebsbereit'; + case 'DEGRADED': return 'Eingeschränkte Verfügbarkeit'; + case 'DOWN': return 'Störung – wir prüfen das'; + case 'MAINTENANCE': return 'Wartung aktiv'; + } +} +function toSparklinePath(series: number[], w: number, h: number): string { + if (!series.length) return ''; + const min = Math.min(...series); + const max = Math.max(...series); + const range = Math.max(1, max - min); + const pad = 2; + const innerH = h - pad * 2; + const step = (w - pad * 2) / Math.max(1, series.length - 1); + + const pts = series.map((v, i) => { + const x = pad + i * step; + const yNorm = (v - min) / range; + const y = pad + innerH - yNorm * innerH; + return [x, y]; + }); + + let d = `M ${pts[0][0]} ${pts[0][1]}`; + for (let i = 1; i < pts.length; i++) d += ` L ${pts[i][0]} ${pts[i][1]}`; + return d; +} +function timeAgo(iso?: string): string { + if (!iso) return '–'; + const d = new Date(iso).getTime(); + const diff = Date.now() - d; + if (diff < 0) return 'soeben'; + const s = Math.floor(diff / 1000), m = Math.floor(s / 60), h = Math.floor(m / 60), day = Math.floor(h / 24); + if (day > 0) return `vor ${day} Tag${day > 1 ? 'en' : ''}`; + if (h > 0) return `vor ${h} Std`; + if (m > 0) return `vor ${m} Min`; + return 'soeben'; +} +function until(iso?: string): string { + if (!iso) return '–'; + const t = new Date(iso).getTime(); + const diff = t - Date.now(); + if (diff <= 0) return 'gleich'; + const s = Math.ceil(diff / 1000), m = Math.floor(s / 60), h = Math.floor(m / 60), d = Math.floor(h / 24); + if (d > 0) return `${d} Tag${d > 1 ? 'e' : ''}`; + if (h > 0) return `${h} Std`; + if (m > 0) return `${m} Min`; + return `${s} s`; +} \ No newline at end of file diff --git a/apps/frontend/src/app/components/services/services.component.html b/apps/frontend/src/app/components/services/services.component.html new file mode 100644 index 0000000..1ba2576 --- /dev/null +++ b/apps/frontend/src/app/components/services/services.component.html @@ -0,0 +1,182 @@ + +
+
+ + + + +
+
+

Leistungen werden geladen...

+
+ + +
+ error +

Fehler beim Laden

+

Die Leistungen konnten nicht geladen werden.

+ +
+ + +
+ construction +

In Arbeit

+

Unsere Leistungsübersicht wird gerade erstellt und ist bald verfügbar.

+

Hast du eine konkrete Anfrage? Kontaktiere uns direkt!

+ +
+ + + + + + + +

+ {{ filteredCount }} Ergebnisse für "{{ searchQuery }}" +

+ + +
+ +
+ + +
+ + +
+
+
+ {{ cat.materialIcon }} +
+
+

{{ cat.name }}

+

{{ cat.subtitle }}

+
+
+ + +
+ +
+
+ + +
+ search_off +

Nichts gefunden

+

Kein Treffer für "{{ searchQuery }}" – aber wenn es mit IT zu tun hat, können wir helfen!

+ +
+ +
+ + +
+

Nichts gefunden?

+

Kein Problem – frag einfach!

+
+ + +
+
+ +
+ +
+
+ + + \ No newline at end of file diff --git a/apps/frontend/src/app/components/services/services.component.scss b/apps/frontend/src/app/components/services/services.component.scss new file mode 100644 index 0000000..af13bb5 --- /dev/null +++ b/apps/frontend/src/app/components/services/services.component.scss @@ -0,0 +1,1019 @@ +// ============================================ +// SERVICES PAGE - MOBILE-FIRST OPTIMIZED +// ============================================ + +// ===== VARIABLES ===== +$bg: #EDF2FD; +$white: #ffffff; +$text: #1a1a2e; +$text-light: #5a5a72; +$text-muted: #8a8aa3; +$brand: #2563eb; +$brand-light: #3b82f6; +$border: rgba(0, 0, 0, 0.06); +$radius: 16px; +$radius-sm: 10px; + +// Category colors +$c-hardware: #f59e0b; +$c-software: #8b5cf6; +$c-web: #3b82f6; +$c-netzwerk: #06b6d4; +$c-support: #10b981; + +// ===== BASE ===== +:host { + display: block; + width: 100%; + max-width: 100vw; + overflow-x: hidden; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +.services-page { + min-height: 100vh; + background: $bg; + padding: 1rem 0 2rem; + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + overflow-x: hidden; + width: 100%; +} + +// ===== CONTAINER - OVERRIDE GLOBAL STYLES ===== +.container { + width: 100% !important; + max-width: 100% !important; + padding: 0 1rem !important; + margin: 0 auto !important; + box-sizing: border-box !important; + + @media (min-width: 640px) { + padding: 0 1.5rem !important; + } + + @media (min-width: 900px) { + max-width: 1100px !important; + padding: 0 2rem !important; + } +} + +// ===== HEADER - MOBILE OPTIMIZED ===== +.header { + text-align: center; + margin-bottom: 1.25rem; + padding: 0 0.25rem; + + h1 { + font-size: 1.5rem; + font-weight: 800; + color: $text; + margin-bottom: 0.375rem; + letter-spacing: -0.02em; + line-height: 1.2; + } + + > p { + color: $text-light; + font-size: 0.875rem; + margin-bottom: 1rem; + line-height: 1.4; + } +} + +// ===== STATE MESSAGES (Loading, Error, Empty) ===== +.state-message { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: 4rem 2rem; + background: $white; + border-radius: $radius; + margin: 2rem 0; + + .loading-spinner { + width: 48px; + height: 48px; + border: 4px solid rgba($brand, 0.2); + border-top-color: $brand; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 1rem; + } + + .material-symbols-outlined { + font-size: 3rem; + color: $brand; + margin-bottom: 1rem; + } + + h3 { + font-size: 1.25rem; + font-weight: 700; + color: $text; + margin-bottom: 0.5rem; + } + + p { + color: $text-light; + font-size: 1rem; + margin-bottom: 0.5rem; + max-width: 400px; + } + + .sub { + font-size: 0.9rem; + color: $text-muted; + margin-bottom: 1.5rem; + } + + .btn { + margin-top: 1rem; + } + + &--error { + .material-symbols-outlined { + color: #dc2626; + } + } + + &--construction { + .material-symbols-outlined { + color: $c-hardware; + } + } +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +// ===== SEARCH - MOBILE OPTIMIZED ===== +.search-box { + position: relative; + max-width: 100%; + margin: 0 auto; + + > .material-symbols-outlined { + position: absolute; + left: 0.875rem; + top: 50%; + transform: translateY(-50%); + font-size: 20px; + color: $text-muted; + } + + input { + width: 100%; + padding: 0.75rem 2.5rem 0.75rem 2.75rem; + font-size: 0.9375rem; + border: 2px solid $border; + border-radius: 50px; + background: $white; + color: $text; + transition: all 0.2s; + + &::placeholder { + color: $text-muted; + font-size: 0.875rem; + } + + &:focus { + outline: none; + border-color: $brand; + box-shadow: 0 0 0 3px rgba($brand, 0.1); + } + } + + .clear-btn { + position: absolute; + right: 0.5rem; + top: 50%; + transform: translateY(-50%); + background: rgba(0, 0, 0, 0.05); + border: none; + border-radius: 50%; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + + .material-symbols-outlined { + font-size: 16px; + color: $text-muted; + } + + &:hover, + &:active { + background: rgba(0, 0, 0, 0.1); + } + } +} + +.search-info { + text-align: center; + margin-top: 0.625rem; + font-size: 0.8125rem; + color: $text-muted; +} + +// ===== FILTER TABS - MOBILE OPTIMIZED ===== +.filter-tabs { + display: flex; + gap: 0.375rem; + overflow-x: auto; + padding: 0.5rem 0 1.25rem; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + scroll-snap-type: x mandatory; + + &::-webkit-scrollbar { + display: none; + } +} + +.tab { + display: flex; + align-items: center; + gap: 0.25rem; + padding: 0.5rem 0.75rem; + background: $white; + border: 2px solid $border; + border-radius: 50px; + font-size: 0.8125rem; + font-weight: 600; + color: $text-light; + cursor: pointer; + white-space: nowrap; + transition: all 0.2s; + flex-shrink: 0; + scroll-snap-align: start; + + .material-symbols-outlined { + font-size: 16px; + } + + &:hover, + &:active { + border-color: $brand; + color: $brand; + } + + &.active { + background: $brand; + border-color: $brand; + color: white; + } +} + +// ===== SERVICES ===== +.services { + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +// ===== CATEGORY - MOBILE OPTIMIZED ===== +.category { + background: $white; + border-radius: $radius; + padding: 1rem; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04); +} + +.category-header { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 1rem; + padding-bottom: 0.875rem; + border-bottom: 2px solid rgba(0, 0, 0, 0.04); + + h2 { + font-size: 1rem; + font-weight: 700; + color: $text; + margin-bottom: 0.125rem; + line-height: 1.2; + } + + p { + font-size: 0.75rem; + color: $text-muted; + line-height: 1.3; + } +} + +.category-icon { + width: 42px; + height: 42px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + .material-symbols-outlined { + font-size: 22px; + color: white; + } + + &[data-type="hardware"] { + background: linear-gradient(135deg, $c-hardware, darken($c-hardware, 8%)); + } + + &[data-type="software"] { + background: linear-gradient(135deg, $c-software, darken($c-software, 8%)); + } + + &[data-type="web"] { + background: linear-gradient(135deg, $c-web, darken($c-web, 8%)); + } + + &[data-type="netzwerk"] { + background: linear-gradient(135deg, $c-netzwerk, darken($c-netzwerk, 8%)); + } + + &[data-type="support"] { + background: linear-gradient(135deg, $c-support, darken($c-support, 8%)); + } +} + +// ===== SERVICE GRID - MOBILE OPTIMIZED ===== +.service-grid { + display: flex; + flex-direction: column; + gap: 0.625rem; +} + +// ===== SERVICE CARD - MOBILE OPTIMIZED ===== +.service-card { + display: flex; + align-items: flex-start; + gap: 0.75rem; + padding: 0.875rem; + background: $bg; + border: 2px solid rgba($brand, 0.08); + border-radius: $radius-sm; + cursor: pointer; + transition: all 0.2s; + text-align: left; + width: 100%; + -webkit-tap-highlight-color: transparent; + position: relative; + + // Subtle pulsing hint on first load (nur einmal) + &:first-child { + animation: subtleHint 2s ease-in-out 1s 1; + } + + &:hover, + &:active { + background: $white; + border-color: rgba($brand, 0.25); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + + .card-arrow { + opacity: 1; + transform: translateX(3px); + } + + .card-hint { + opacity: 1; + } + } +} + +@keyframes subtleHint { + 0%, 100% { border-color: rgba($brand, 0.08); } + 50% { border-color: rgba($brand, 0.3); } +} + +.card-hint { + position: absolute; + bottom: 0.5rem; + right: 0.75rem; + font-size: 0.625rem; + color: $brand; + opacity: 0.5; + transition: opacity 0.2s; + font-weight: 600; +} + +.service-icon { + font-size: 1.375rem; + line-height: 1; + flex-shrink: 0; +} + +.service-content { + flex: 1; + min-width: 0; + + h3 { + font-size: 0.875rem; + font-weight: 700; + color: $text; + margin-bottom: 0.1875rem; + line-height: 1.3; + } + + p { + font-size: 0.75rem; + color: $text-light; + line-height: 1.45; + margin-bottom: 0.375rem; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + } +} + +.card-arrow { + font-size: 18px; + color: $brand; + opacity: 0.7; + transform: translateX(0); + transition: all 0.2s; + flex-shrink: 0; + margin-top: 0.125rem; +} + +.tags { + display: flex; + flex-wrap: wrap; + gap: 0.25rem; + + span { + padding: 0.1875rem 0.4375rem; + background: rgba($brand, 0.08); + border-radius: 5px; + font-size: 0.625rem; + font-weight: 600; + color: $brand; + } +} + +// ===== NO RESULTS ===== +.no-results { + text-align: center; + padding: 2rem 1.25rem; + background: $white; + border: 2px dashed $border; + border-radius: $radius; + + .material-symbols-outlined { + font-size: 40px; + color: $text-muted; + margin-bottom: 0.75rem; + } + + h3 { + font-size: 1rem; + font-weight: 700; + color: $text; + margin-bottom: 0.375rem; + } + + p { + font-size: 0.875rem; + color: $text-light; + margin-bottom: 1rem; + max-width: 280px; + margin-left: auto; + margin-right: auto; + line-height: 1.4; + } +} + +// ===== CTA BOX - MOBILE OPTIMIZED ===== +.cta-box { + margin-top: 2rem; + padding: 1.25rem 1rem; + background: linear-gradient(135deg, rgba($brand, 0.05), rgba($brand, 0.02)); + border: 2px solid rgba($brand, 0.1); + border-radius: $radius; + text-align: center; + + h3 { + font-size: 1rem; + font-weight: 700; + color: $text; + margin-bottom: 0.25rem; + } + + > p { + font-size: 0.875rem; + color: $text-light; + margin-bottom: 1rem; + } +} + +.cta-buttons { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +// ===== BUTTONS - MOBILE OPTIMIZED ===== +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.75rem 1.25rem; + border-radius: 12px; + font-weight: 700; + font-size: 0.875rem; + border: none; + cursor: pointer; + transition: all 0.2s; + -webkit-tap-highlight-color: transparent; + + .material-symbols-outlined { + font-size: 18px; + } + + &.primary { + background: linear-gradient(135deg, $brand, $brand-light); + color: white; + box-shadow: 0 4px 16px rgba($brand, 0.25); + + &:hover, + &:active { + box-shadow: 0 6px 20px rgba($brand, 0.35); + } + } + + &.secondary { + background: $white; + color: $brand; + border: 2px solid $brand; + + &:hover, + &:active { + background: rgba($brand, 0.05); + } + } + + &.large { + padding: 0.875rem 1.5rem; + font-size: 0.9375rem; + } +} + +// ===== MATERIAL ICONS ===== +.material-symbols-outlined { + font-variation-settings: "FILL" 0, "wght" 500, "GRAD" 0, "opsz" 24; + line-height: 1; +} + + +// ============================================ +// MODAL - ZENTRIERT AUF ALLEN GERÄTEN +// ============================================ +.modal-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 1rem; + animation: fadeIn 0.25s ease; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.modal { + background: $white; + width: 100%; + max-width: 440px; + max-height: calc(100vh - 2rem); + max-height: calc(100dvh - 2rem); // Dynamic viewport height für mobile + overflow-y: auto; + border-radius: 20px; + position: relative; + animation: scaleIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); + + // Smooth scrolling + -webkit-overflow-scrolling: touch; + overscroll-behavior: contain; +} + +@keyframes scaleIn { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +.modal-close { + position: absolute; + top: 0.875rem; + right: 0.875rem; + background: rgba(0, 0, 0, 0.06); + border: none; + border-radius: 50%; + width: 34px; + height: 34px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s; + z-index: 1; + -webkit-tap-highlight-color: transparent; + + .material-symbols-outlined { + font-size: 18px; + color: $text-light; + } + + &:hover, + &:active { + background: rgba(0, 0, 0, 0.12); + } +} + +.modal-header { + display: flex; + align-items: center; + gap: 0.875rem; + padding: 1.25rem 1.25rem 0; + padding-right: 3rem; // Platz für Close Button + + h2 { + font-size: 1.125rem; + font-weight: 700; + color: $text; + margin: 0; + line-height: 1.3; + } +} + +.modal-category { + font-size: 0.6875rem; + font-weight: 600; + color: $text-muted; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.modal-icon { + width: 48px; + height: 48px; + border-radius: 14px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + .service-emoji { + font-size: 1.5rem; + } + + &[data-type="hardware"] { + background: linear-gradient(135deg, rgba($c-hardware, 0.15), rgba($c-hardware, 0.05)); + } + + &[data-type="software"] { + background: linear-gradient(135deg, rgba($c-software, 0.15), rgba($c-software, 0.05)); + } + + &[data-type="web"] { + background: linear-gradient(135deg, rgba($c-web, 0.15), rgba($c-web, 0.05)); + } + + &[data-type="netzwerk"] { + background: linear-gradient(135deg, rgba($c-netzwerk, 0.15), rgba($c-netzwerk, 0.05)); + } + + &[data-type="support"] { + background: linear-gradient(135deg, rgba($c-support, 0.15), rgba($c-support, 0.05)); + } +} + +.modal-body { + padding: 1rem 1.25rem; +} + +.modal-description { + font-size: 0.875rem; + line-height: 1.65; + color: $text-light; + margin-bottom: 1rem; +} + +.modal-tags { + display: flex; + flex-wrap: wrap; + gap: 0.375rem; + + span { + padding: 0.3125rem 0.625rem; + background: rgba(0, 0, 0, 0.04); + border-radius: 4px; + font-size: 0.6875rem; + font-weight: 500; + color: $text-muted; + // Bewusst flach und nicht-interaktiv + cursor: default; + user-select: none; + } +} + +.modal-actions { + padding: 1rem 1.25rem 1.25rem; + display: flex; + flex-direction: column; + gap: 0.625rem; + border-top: 1px solid rgba(0, 0, 0, 0.06); + background: rgba(0, 0, 0, 0.02); + border-radius: 0 0 20px 20px; + + .btn { + width: 100%; + } +} + + +// ============================================ +// RESPONSIVE - TABLET (640px+) +// ============================================ +@media (min-width: 640px) { + + .header { + margin-bottom: 2rem; + + h1 { + font-size: 2.25rem; + } + + > p { + font-size: 1.125rem; + margin-bottom: 1.5rem; + } + } + + .search-box { + max-width: 450px; + + input { + padding: 1rem 3rem 1rem 3.25rem; + font-size: 1rem; + + &::placeholder { + font-size: 1rem; + } + } + + > .material-symbols-outlined { + font-size: 22px; + left: 1rem; + } + } + + .filter-tabs { + justify-content: center; + flex-wrap: wrap; + padding: 1rem 0 1rem; + margin: 0; + gap: 0.5rem; + } + + .tab { + padding: 0.625rem 1rem; + font-size: 0.875rem; + + .material-symbols-outlined { + font-size: 18px; + } + } + + .services { + gap: 2rem; + } + + .category { + padding: 1.5rem; + } + + .category-header { + gap: 1rem; + margin-bottom: 1.25rem; + + h2 { + font-size: 1.25rem; + } + + p { + font-size: 0.875rem; + } + } + + .category-icon { + width: 56px; + height: 56px; + border-radius: 14px; + + .material-symbols-outlined { + font-size: 28px; + } + } + + .service-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + } + + .service-card { + padding: 1.125rem; + } + + .service-content { + h3 { + font-size: 1rem; + } + + p { + font-size: 0.875rem; + -webkit-line-clamp: 3; + } + } + + .service-icon { + font-size: 1.5rem; + } + + .tags span { + font-size: 0.75rem; + padding: 0.3rem 0.625rem; + } + + .card-arrow { + opacity: 0.6; + transform: translateX(0); + } + + .service-card:hover .card-arrow { + opacity: 1; + transform: translateX(3px); + } + + .cta-box { + padding: 2rem; + } + + .cta-buttons { + flex-direction: row; + justify-content: center; + gap: 0.75rem; + } + + .btn { + padding: 0.875rem 1.5rem; + font-size: 0.9375rem; + + &.large { + padding: 1rem 1.75rem; + font-size: 1rem; + } + } + + // Modal Tablet + .modal { + max-width: 480px; + } + + .modal-header { + padding: 1.5rem 1.5rem 0; + gap: 1rem; + + h2 { + font-size: 1.375rem; + } + } + + .modal-category { + font-size: 0.75rem; + } + + .modal-icon { + width: 56px; + height: 56px; + + .service-emoji { + font-size: 1.75rem; + } + } + + .modal-body { + padding: 1.25rem 1.5rem; + } + + .modal-description { + font-size: 0.9375rem; + margin-bottom: 1.25rem; + } + + .modal-tags span { + font-size: 0.75rem; + padding: 0.375rem 0.75rem; + } + + .modal-actions { + padding: 1.25rem 1.5rem 1.5rem; + flex-direction: row; + gap: 0.75rem; + + .btn { + width: auto; + flex: 1; + } + } + + .modal-close { + top: 1rem; + right: 1rem; + width: 36px; + height: 36px; + + .material-symbols-outlined { + font-size: 20px; + } + } +} + +// ============================================ +// RESPONSIVE - DESKTOP (900px+) +// ============================================ +@media (min-width: 900px) { + .header h1 { + font-size: 2.5rem; + } + + .service-grid { + grid-template-columns: repeat(3, 1fr); + } + + .category { + padding: 2rem; + } + + .modal { + max-width: 500px; + } + + .modal-header { + padding: 2rem 2rem 0; + + h2 { + font-size: 1.5rem; + } + } + + .modal-body { + padding: 1.5rem 2rem; + } + + .modal-actions { + padding: 1.5rem 2rem 2rem; + } +} + +// ===== SAFE AREA SUPPORT (Notch etc.) ===== +@supports (padding: max(0px)) { + .modal-backdrop { + padding: max(1rem, env(safe-area-inset-top)) max(1rem, env(safe-area-inset-right)) max(1rem, env(safe-area-inset-bottom)) max(1rem, env(safe-area-inset-left)); + } +} + +// ===== REDUCED MOTION ===== +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/components/services/services.component.spec.ts b/apps/frontend/src/app/components/services/services.component.spec.ts new file mode 100644 index 0000000..e0eb720 --- /dev/null +++ b/apps/frontend/src/app/components/services/services.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ServicesComponent } from './services.component'; + +describe('ServicesComponent', () => { + let component: ServicesComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ServicesComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ServicesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/frontend/src/app/components/services/services.component.ts b/apps/frontend/src/app/components/services/services.component.ts new file mode 100644 index 0000000..b532e9c --- /dev/null +++ b/apps/frontend/src/app/components/services/services.component.ts @@ -0,0 +1,176 @@ +import { Component, OnInit } from '@angular/core'; +import { Router, ActivatedRoute } from '@angular/router'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ServiceDataService, Service, ServiceCategory } from '../../shared/service-data.service'; +import { ApiService, ServiceCategory as ApiCategory } from '../../api/api.service'; +import { PageTitleComponent } from "../../shared/page-title/page-title.component"; + +@Component({ + selector: 'app-services', + standalone: true, + imports: [CommonModule, FormsModule, PageTitleComponent], + templateUrl: './services.component.html', + styleUrl: './services.component.scss' +}) +export class ServicesComponent implements OnInit { + + searchQuery = ''; + activeFilter = 'all'; + + // Data state + loading = true; + error = false; + isEmpty = false; + private _categories: ServiceCategory[] = []; + + // Modal state + selectedService: Service | null = null; + selectedCategory: ServiceCategory | null = null; + + constructor( + public router: Router, + private route: ActivatedRoute, + private serviceData: ServiceDataService, + private api: ApiService + ) {} + + ngOnInit(): void { + this.loadServices(); + + // Handle category query param from header dropdown + this.route.queryParams.subscribe(params => { + if (params['category']) { + this.activeFilter = params['category']; + } else { + this.activeFilter = 'all'; + } + }); + } + + loadServices(): void { + this.loading = true; + this.error = false; + this.isEmpty = false; + + this.api.getServicesCatalog().subscribe({ + next: (apiCategories) => { + if (apiCategories && apiCategories.length > 0) { + // Map API data to local format + this._categories = apiCategories.map(cat => ({ + id: cat.slug, + name: cat.name, + subtitle: cat.subtitle, + materialIcon: cat.materialIcon, + services: cat.services.map(svc => ({ + id: svc.slug, + icon: svc.icon, + title: svc.title, + description: svc.description, + longDescription: svc.longDescription, + tags: svc.tags, + keywords: svc.keywords + })) + })); + this.isEmpty = false; + } else { + this.isEmpty = true; + this._categories = []; + } + this.loading = false; + }, + error: (err) => { + console.error('Fehler beim Laden der Services:', err); + this.error = true; + this.loading = false; + } + }); + } + + // ===== GETTERS ===== + + get categories(): ServiceCategory[] { + return this._categories; + } + + get filters() { + // Dynamisch Filter aus den geladenen Kategorien erstellen + const dynamicFilters = [ + { id: 'all', name: 'Alle', icon: 'grid_view' }, + ...this._categories.map(cat => ({ + id: cat.id, + name: cat.name, + icon: cat.materialIcon + })) + ]; + return dynamicFilters; + } + + get filteredCategories(): ServiceCategory[] { + let cats = this.categories; + + // Filter by category + if (this.activeFilter !== 'all') { + cats = cats.filter(c => c.id === this.activeFilter); + } + + // Filter services by search + if (this.searchQuery.trim()) { + const query = this.searchQuery.toLowerCase(); + cats = cats.map(cat => ({ + ...cat, + services: cat.services.filter(s => + s.keywords.includes(query) || + s.title.toLowerCase().includes(query) || + s.tags.some(t => t.toLowerCase().includes(query)) + ) + })).filter(cat => cat.services.length > 0); + } + + return cats; + } + + get filteredCount(): number { + return this.filteredCategories.reduce((sum, cat) => sum + cat.services.length, 0); + } + + // ===== METHODS ===== + + setFilter(filter: string): void { + this.activeFilter = filter; + } + + clearSearch(): void { + this.searchQuery = ''; + this.activeFilter = 'all'; + } + + // ===== MODAL ===== + + openServiceModal(service: Service, category: ServiceCategory): void { + this.selectedService = service; + this.selectedCategory = category; + document.body.style.overflow = 'hidden'; + } + + closeModal(): void { + this.selectedService = null; + this.selectedCategory = null; + document.body.style.overflow = ''; + } + + onBackdropClick(event: MouseEvent): void { + if ((event.target as HTMLElement).classList.contains('modal-backdrop')) { + this.closeModal(); + } + } + + contactWithService(): void { + if (this.selectedService) { + this.router.navigate(['/contact'], { + queryParams: { service: this.selectedService.id } + }); + } + this.closeModal(); + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/components/vorgehen/vorgehen.component.html b/apps/frontend/src/app/components/vorgehen/vorgehen.component.html new file mode 100644 index 0000000..053c824 --- /dev/null +++ b/apps/frontend/src/app/components/vorgehen/vorgehen.component.html @@ -0,0 +1,285 @@ + + +
+ + +
+
+
+
+ route + Wie es abläuft +
+

Einfach, klar, keine Überraschungen

+

+ Von unserem ersten Gespräch bis zur fertigen Website - hier siehst du genau wie ich arbeite. +

+
+
+ 4 + Einfache Schritte +
+
+ 2-4 + Wochen fertig +
+
+ 100% + Transparent +
+
+
+
+
+ + +
+
+ + +
+
1
+
+
+ chat +
+

Wir reden (30 Min)

+

+ Du erzählst mir was du brauchst. Ich stelle ein paar Fragen. Am Ende weißt du genau was es kostet und wie + lange es dauert. +

+ +
+
+

Du sagst mir:

+
    +
  • Was du machst
  • +
  • Was auf die Website soll
  • +
  • Welchen Style du magst
  • +
  • Wann es fertig sein soll
  • +
+
+
+

Du bekommst:

+
    +
  • Ehrliche Einschätzung
  • +
  • Festen Preis
  • +
  • Klaren Zeitplan
  • +
  • Keine Pflicht
  • +
+
+
+ +
+ schedule + Dauer: 30 Minuten +
+
+
+ + +
+
2
+
+
+ palette +
+

Du siehst wie es aussieht

+

+ Ich mache einen ersten Entwurf. Du schaust es dir an und sagst was dir gefällt und was nicht. Ich ändere es + bis es passt. +

+ +
+
+

Du machst:

+
    +
  • Schickst mir deine Texte
  • +
  • Schickst mir Bilder/Logos
  • +
  • Gibst Feedback zum Entwurf
  • +
+
+
+

Ich mache:

+
    +
  • Ersten Design-Entwurf
  • +
  • Zeig dir verschiedene Versionen
  • +
  • Ändere es nach deinem Feedback
  • +
+
+
+ +
+ schedule + Dauer: 3-5 Tage +
+
+
+ + +
+
3
+
+
+ code +
+

Ich baue die Website

+

+ Jetzt wird's gemacht. Du bekommst regelmäßig Updates und kannst zwischendurch schauen wie es aussieht. +

+ +
+
+

Was passiert:

+
    +
  • Ich baue alle Seiten
  • +
  • Alles funktioniert auf Handy
  • +
  • Formulare werden eingebaut
  • +
  • Bei Google optimiert
  • +
+
+
+

Du kannst:

+
    +
  • Jederzeit schauen
  • +
  • Sagen wenn was fehlt
  • +
  • Kleine Änderungen machen
  • +
  • Mir Fragen stellen
  • +
+
+
+ +
+ schedule + Dauer: 1-2 Wochen +
+
+
+ + +
+
4
+
+
+ rocket_launch +
+

Website geht online

+

+ Letzte Checks, dann mache ich alles live. Du bekommst alle Zugänge und eine kurze Anleitung wie du Sachen + ändern kannst. +

+ +
+
+

Was ich mache:

+
    +
  • Alles nochmal checken
  • +
  • Website online stellen
  • +
  • Dir alles erklären
  • +
  • Dokument mit allen Infos
  • +
+
+
+

Du bekommst:

+
    +
  • Alle Zugänge
  • +
  • Einfache Anleitung
  • +
  • 3 Monate Support gratis
  • +
  • Hilfe bei Fragen
  • +
+
+
+ +
+ schedule + Dauer: 1-2 Tage +
+
+
+ +
+
+ + +
+
+

Warum ich so arbeite

+

Aus Erfahrung gelernt

+
+ +
+
+
+ visibility +
+

Du weißt immer Bescheid

+

Keine Geheimnisse. Du siehst was ich mache und wie weit ich bin.

+
+ +
+
+ handshake +
+

Keine Missverständnisse

+

Jeder weiß was er machen muss. Spart Zeit und Nerven.

+
+ +
+
+ schedule +
+

Realistische Zeiten

+

Ich verspreche nur was ich halten kann. Keine falschen Hoffnungen.

+
+ +
+
+ verified +
+

Saubere Übergabe

+

Am Ende hast du alles in der Hand und weißt wie alles funktioniert.

+
+
+
+ + +
+
+
+ rocket_launch +
+

Bereit für deine neue Website?

+

Lass uns in einem kurzen Gespräch klären, wie wir dir helfen können.

+ +
+
+ schedule + 30 Minuten +
+
+ workspace_premium + 100% kostenlos +
+
+ handshake + Unverbindlich +
+
+ +
+ + +

+ Noch unsicher? + + Mach den 2-Minuten-Fragebogen + +

+
+
+
+ +
\ No newline at end of file diff --git a/apps/frontend/src/app/components/vorgehen/vorgehen.component.scss b/apps/frontend/src/app/components/vorgehen/vorgehen.component.scss new file mode 100644 index 0000000..4f7f43d --- /dev/null +++ b/apps/frontend/src/app/components/vorgehen/vorgehen.component.scss @@ -0,0 +1,491 @@ +@import '../../utils/shared-styles.scss'; + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1.5rem; + margin-bottom: 4rem; +} + +.process-detail { + padding-block: 2rem; +} + +/* ===== HERO ===== */ +.hero-section { + background: linear-gradient(135deg, #f8fafc 0%, #e7eeff 100%); + padding: 3rem 0; + position: relative; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + inset: 0; + background: radial-gradient(circle at 20% 30%, rgba(37, 99, 235, 0.06) 0%, transparent 50%); + pointer-events: none; + } + + .hero-content { + position: relative; + z-index: 2; + text-align: center; + max-width: 800px; + margin: 0 auto; + display: grid; + gap: 1.25rem; + } + + .hero-badge { + display: inline-flex; + align-items: center; + gap: 0.5rem; + align-self: center; + background: linear-gradient(135deg, #2563eb, #3b82f6); + color: white; + padding: 0.625rem 1.5rem; + border-radius: 999px; + font-size: 0.875rem; + font-weight: 700; + box-shadow: 0 4px 16px rgba(37, 99, 235, 0.3); + width: fit-content; + margin: 0 auto; + } + + h1 { + font-size: clamp(2rem, 4vw, 3rem); + font-weight: 900; + color: $color-text-primary; + line-height: 1.2; + margin: 0; + } + + .hero-description { + font-size: clamp(1rem, 1.5vw, 1.125rem); + color: $color-text-secondary; + line-height: 1.6; + margin: 0; + } + + .hero-stats { + display: flex; + gap: 2rem; + justify-content: center; + flex-wrap: wrap; + margin-top: 1rem; + + .stat-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.25rem; + + .stat-number { + font-size: 2rem; + font-weight: 900; + background: linear-gradient(135deg, #2563eb, #3b82f6); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + line-height: 1; + } + + .stat-label { + font-size: 0.875rem; + color: $color-text-secondary; + font-weight: 600; + } + } + } +} + +/* ===== STEPS ===== */ +.steps-section { + .steps-wrapper { + display: grid; + gap: 2.5rem; + max-width: 900px; + margin: 0 auto; + } +} + +.step-card { + background: white; + border: 2px solid #e2e8f0; + border-radius: 20px; + padding: 2rem; + position: relative; + transition: all 0.3s ease; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.04); + + &:hover { + transform: translateY(-4px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.12); + border-color: #3b82f6; + + .step-number { + transform: scale(1.1); + } + } + + .step-number { + position: absolute; + top: -20px; + left: -1rem; + width: 48px; + height: 48px; + background: linear-gradient(135deg, #2563eb, #3b82f6); + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.5rem; + font-weight: 900; + box-shadow: 0 6px 20px rgba(37, 99, 235, 0.4); + transition: transform 0.3s ease; + } + + .step-content { + display: grid; + gap: 1.25rem; + } + + .step-icon { + width: 64px; + height: 64px; + background: linear-gradient(135deg, rgba(37, 99, 235, 0.1), rgba(59, 130, 246, 0.1)); + border-radius: 16px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 0.5rem; + + .material-symbols-outlined { + font-size: 36px; + color: #2563eb; + } + } + + h3 { + font-size: 1.5rem; + font-weight: 800; + color: $color-text-primary; + margin: 0; + } + + .step-description { + font-size: 1.0625rem; + color: $color-text-secondary; + line-height: 1.6; + margin: 0; + } + + .step-details { + display: grid; + gap: 1.25rem; + grid-template-columns: 1fr; + margin-top: 0.5rem; + + @media (min-width: 640px) { + grid-template-columns: 1fr 1fr; + } + } + + .detail-box { + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 1.25rem; + + h4 { + font-size: 0.9375rem; + font-weight: 700; + color: $color-text-primary; + margin: 0 0 0.75rem; + } + + ul { + list-style: none; + padding: 0; + margin: 0; + display: grid; + gap: 0.5rem; + + li { + font-size: 0.9375rem; + color: $color-text-secondary; + padding-left: 1.25rem; + position: relative; + + &::before { + content: '→'; + position: absolute; + left: 0; + color: #2563eb; + font-weight: 700; + } + } + } + } + + .step-time { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1.25rem; + background: linear-gradient(135deg, rgba(37, 99, 235, 0.1), rgba(59, 130, 246, 0.1)); + border: 1px solid rgba(37, 99, 235, 0.2); + border-radius: 999px; + font-size: 0.9375rem; + font-weight: 700; + color: #2563eb; + width: fit-content; + + .material-symbols-outlined { + font-size: 20px; + } + } +} + +/* ===== WHY ===== */ +.why-section { + .section-header { + text-align: center; + margin-bottom: 2.5rem; + + h2 { + font-size: clamp(1.75rem, 3vw, 2.5rem); + font-weight: 800; + color: $color-text-primary; + margin: 0 0 0.5rem; + } + + p { + font-size: 1.0625rem; + color: $color-text-secondary; + margin: 0; + } + } + + .why-grid { + display: grid; + gap: 1.5rem; + grid-template-columns: 1fr; + + @media (min-width: 640px) { + grid-template-columns: repeat(2, 1fr); + } + + @media (min-width: 1024px) { + grid-template-columns: repeat(4, 1fr); + } + } + + .why-card { + background: white; + border: 1px solid #e2e8f0; + border-radius: 16px; + padding: 1.75rem; + text-align: center; + transition: all 0.3s ease; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); + + &:hover { + transform: translateY(-6px); + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.1); + border-color: #3b82f6; + + .why-icon { + transform: scale(1.1); + } + } + + .why-icon { + width: 64px; + height: 64px; + margin: 0 auto 1rem; + background: linear-gradient(135deg, rgba(37, 99, 235, 0.1), rgba(59, 130, 246, 0.1)); + border-radius: 16px; + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.3s ease; + + .material-symbols-outlined { + font-size: 36px; + color: #2563eb; + } + } + + h3 { + font-size: 1.125rem; + font-weight: 700; + color: $color-text-primary; + margin: 0 0 0.5rem; + } + + p { + font-size: 0.9375rem; + color: $color-text-secondary; + line-height: 1.6; + margin: 0; + } + } +} + +/* ===== CTA ===== */ +.cta-section { + .cta-card { + background: linear-gradient(135deg, rgba(37, 99, 235, 0.05), rgba(59, 130, 246, 0.05)); + border: 2px solid rgba(37, 99, 235, 0.15); + border-radius: 24px; + padding: 3rem 2rem; + text-align: center; + + .cta-icon { + width: 80px; + height: 80px; + margin: 0 auto 1.5rem; + background: linear-gradient(135deg, #2563eb, #3b82f6); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 8px 24px rgba(37, 99, 235, 0.3); + + .material-symbols-outlined { + font-size: 48px; + color: white; + } + } + + h2 { + font-size: clamp(1.75rem, 3vw, 2.25rem); + font-weight: 800; + color: $color-text-primary; + margin: 0 0 0.75rem; + } + + >p { + font-size: 1.0625rem; + color: $color-text-secondary; + line-height: 1.6; + margin: 0 auto 1.5rem; + max-width: 600px; + } + + .cta-features { + display: flex; + gap: 1.5rem; + justify-content: center; + flex-wrap: wrap; + margin-bottom: 2rem; + + .feature-item { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.9375rem; + font-weight: 600; + color: $color-text-primary; + + .material-symbols-outlined { + font-size: 20px; + color: #10b981; + } + } + } + + .cta-actions { + display: flex; + gap: 1rem; + justify-content: center; + flex-wrap: wrap; + + @media (max-width: 639px) { + flex-direction: column; + + .btn { + width: 100%; + } + } + } + } +} + +/* ===== BUTTONS ===== */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.875rem 1.75rem; + border-radius: 12px; + font-weight: 700; + font-size: 0.9375rem; + transition: all 0.3s ease; + cursor: pointer; + border: none; + text-decoration: none; + + &--primary { + background: linear-gradient(135deg, #2563eb, #3b82f6); + color: white; + box-shadow: 0 4px 16px rgba(37, 99, 235, 0.25); + + &:hover { + transform: translateY(-2px); + box-shadow: 0 8px 24px rgba(37, 99, 235, 0.35); + } + } + + &--secondary { + background: white; + color: #2563eb; + border: 2px solid #2563eb; + + &:hover { + background: linear-gradient(135deg, rgba(37, 99, 235, 0.05), rgba(59, 130, 246, 0.05)); + transform: translateY(-2px); + } + } + + &--large { + padding: 1rem 2rem; + font-size: 1rem; + } +} + +@media (prefers-reduced-motion: reduce) { + * { + animation: none !important; + transition: none !important; + } +} + +.cta-alternative { + margin-top: 1.5rem; + text-align: center; + color: var(--text-muted); + font-size: 0.875rem; + + .link { + color: var(--primary-color); + text-decoration: none; + font-weight: 500; + display: inline-flex; + align-items: center; + gap: 0.25rem; + transition: gap 0.2s; + + .material-symbols-outlined { + font-size: 1rem; + } + + &:hover { + gap: 0.5rem; + text-decoration: underline; + } + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/components/vorgehen/vorgehen.component.spec.ts b/apps/frontend/src/app/components/vorgehen/vorgehen.component.spec.ts new file mode 100644 index 0000000..145b408 --- /dev/null +++ b/apps/frontend/src/app/components/vorgehen/vorgehen.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { VorgehenComponent } from './vorgehen.component'; + +describe('VorgehenComponent', () => { + let component: VorgehenComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [VorgehenComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(VorgehenComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/frontend/src/app/components/vorgehen/vorgehen.component.ts b/apps/frontend/src/app/components/vorgehen/vorgehen.component.ts new file mode 100644 index 0000000..3227845 --- /dev/null +++ b/apps/frontend/src/app/components/vorgehen/vorgehen.component.ts @@ -0,0 +1,84 @@ +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; +import { PageTitleComponent } from '../../shared/page-title/page-title.component'; +import { CommonModule } from '@angular/common'; +import { IconComponent } from "../../shared/icon/icon.component"; + +type Step = { + id: string; + title: string; + summary: string; + duration?: string; + output?: string; + you?: string[]; + me?: string[]; + icon: string; // inline SVG + done?: boolean; // für eventuelles Styling +}; + +@Component({ + selector: 'app-vorgehen', + standalone: true, + imports: [PageTitleComponent, CommonModule, IconComponent], + templateUrl: './vorgehen.component.html', + styleUrl: './vorgehen.component.scss' +}) +export class VorgehenComponent { + constructor(public router: Router) {} + + steps: Step[] = [ + { + id: 'kickoff', + title: 'Kickoff', + summary: 'Kurzes Gespräch: Ziel, Scope, Rahmenbedingungen. Danach klare To-dos.', + duration: '30–45 Min', + output: 'Kurzprotokoll & nächste Schritte', + you: ['Ziele & Rahmen (Budget/Timing)', 'Ansprechpartner & Entscheidungsweg'], + me: ['Machbarkeitseinschätzung', 'Risiken & Annahmen'], + icon: 'flag' + }, + { + id: 'scope', + title: 'Angebot & Scope', + summary: 'Konkreter Umfang, Aufwandsschätzung, Meilensteine, Schnittstellen.', + duration: '1–3 Tage', + output: 'Angebot / Statement of Work', + you: ['Feedback zum Scope', 'Zugang zu relevanten Unterlagen'], + me: ['Konkretes Angebot mit Milestones', 'Roadmap & Deliverables'], + icon: 'target' + }, + { + id: 'build', + title: 'Umsetzung', + summary: 'Iterativ entwickeln; kurze Feedbackschleifen. Kein Overengineering.', + duration: 'abhängig vom Umfang', + output: 'Inkremente, Doku, Tests', + you: ['Review der Inkremente', 'Zugänge/Accounts bei Bedarf'], + me: ['Frontend (Angular/TS)', 'API (NestJS/Node.js)', 'DB (PostgreSQL)'], + icon: 'code' + }, + { + id: 'handover', + title: 'Übergabe & Go-Live', + summary: 'Deployment, Smoke-Tests, Übergabedoku. Klarer Verantwortungswechsel.', + duration: '1–2 Tage', + output: 'Release + Übergabedokumentation', + you: ['Go-Live-Fenster', 'Finales OK'], + me: ['Deployment (Docker/CI/CD)', 'Monitoring-Checks', 'Übergabe-Call'], + icon: 'publish' + }, + { + id: 'support', + title: 'Support', + summary: 'Störungsbehebung nach vereinbarten Reaktionszeiten. Optionale Weiterentwicklung.', + duration: 'laufend', + output: 'SLA nach Bedarf', + you: ['Kontaktkanal (Mail/Phone)', 'Priorität des Tickets'], + me: ['Fehleranalyse & Fix', 'Kleine Verbesserungen nach Absprache'], + icon: 'support' + } + ]; + + trackById = (_: number, s: Step) => s.id; + +} diff --git a/apps/frontend/src/app/guards/admin.guard.ts b/apps/frontend/src/app/guards/admin.guard.ts new file mode 100644 index 0000000..107820f --- /dev/null +++ b/apps/frontend/src/app/guards/admin.guard.ts @@ -0,0 +1,64 @@ +import { inject } from '@angular/core'; +import { Router, CanActivateFn } from '@angular/router'; +import { AuthService, UserRole } from '../services/auth.service'; +import { ConfirmationService } from '../shared/confirmation/confirmation.service'; +import { catchError, map, of } from 'rxjs'; +import { HttpErrorResponse } from '@angular/common/http'; + +export const adminGuard: CanActivateFn = (route, state) => { + const authService = inject(AuthService); + const router = inject(Router); + const confirmationService = inject(ConfirmationService); + + // 1. SCHNELLER CHECK: Ist überhaupt ein Token vorhanden? + if (!authService.isLoggedIn()) { + router.navigate(['/login'], { + queryParams: { returnUrl: state.url } + }); + return false; + } + + // 2. BACKEND-VALIDIERUNG: Ist User wirklich Admin? + // Bei 429 verwendet verifyAdminStatus() die gecachten User-Daten + return authService.verifyAdminStatus().pipe( + map(isAdmin => { + if (isAdmin) { + return true; + } + + // User ist KEIN Admin → Fehlermeldung + Redirect + confirmationService.confirm({ + title: 'Keine Berechtigung', + message: 'Diese Seite ist nur für Administratoren zugänglich. Du hast keine Berechtigung, auf diesen Bereich zuzugreifen.', + confirmText: 'Zurück zur Startseite', + type: 'danger', + icon: 'block' + }); + + router.navigate(['/']); + return false; + }), + catchError((error: HttpErrorResponse) => { + console.error('Admin-Validierung fehlgeschlagen:', error); + + // Bei 429: Verwende gecachte User-Daten FALLS Admin-Status bereits vom Server bestätigt wurde + // HINWEIS: Das Backend prüft bei JEDEM API-Call nochmals - das hier ist nur UX + if (error.status === 429) { + const cachedUser = authService.getCurrentUser(); + if (cachedUser?.role === UserRole.ADMIN) { + console.warn('Rate-Limited: Verwende gecachten Admin-Status (Backend validiert trotzdem jeden Request)'); + return of(true); + } + // Kein gecachter Admin → kein Zugriff + return of(false); + } + + // Bei 401/403: Token ungültig → ausloggen + if (error.status === 401 || error.status === 403) { + authService.logout(); + } + + return of(false); + }) + ); +}; \ No newline at end of file diff --git a/apps/frontend/src/app/guards/auth.guard.ts b/apps/frontend/src/app/guards/auth.guard.ts new file mode 100644 index 0000000..a9dfb21 --- /dev/null +++ b/apps/frontend/src/app/guards/auth.guard.ts @@ -0,0 +1,17 @@ +import { inject } from '@angular/core'; +import { Router, CanActivateFn } from '@angular/router'; +import { AuthService } from '../services/auth.service'; + +export const authGuard: CanActivateFn = (route, state) => { + const authService = inject(AuthService); + const router = inject(Router); + + if (authService.isLoggedIn()) { + return true; + } + + router.navigate(['/login'], { + queryParams: { returnUrl: state.url } + }); + return false; +}; \ No newline at end of file diff --git a/apps/frontend/src/app/services/analytics.service.ts b/apps/frontend/src/app/services/analytics.service.ts new file mode 100644 index 0000000..cda2f90 --- /dev/null +++ b/apps/frontend/src/app/services/analytics.service.ts @@ -0,0 +1,206 @@ +import { Injectable, Inject, PLATFORM_ID } from '@angular/core'; +import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { isPlatformBrowser } from '@angular/common'; +import { Router, NavigationEnd } from '@angular/router'; +import { filter } from 'rxjs/operators'; +import { ConfigService } from './config.service'; +import { ConsentService } from './consent/consent.service'; + +// ===== ANALYTICS EVENT TYPES ===== +export type AnalyticsEventType = + | 'pageview' + | 'click' + | 'scroll' + | 'form_submit' + | 'conversion' + | 'error' + | 'custom'; + +export interface AnalyticsEvent { + type: AnalyticsEventType; + page: string; + referrer?: string; + userAgent?: string; + screenSize?: string; + timestamp: number; + sessionId: string; + metadata?: Record; +} + +@Injectable({ + providedIn: 'root' +}) +export class AnalyticsService { + private get API_URL(): string { + return `${this.configService.apiUrl}/analytics`; + } + private sessionId: string = ''; + private isBrowser: boolean; + + constructor( + private http: HttpClient, + private consentService: ConsentService, + private configService: ConfigService, + private router: Router, + @Inject(PLATFORM_ID) platformId: Object + ) { + this.isBrowser = isPlatformBrowser(platformId); + if (this.isBrowser) { + this.sessionId = this.getOrCreateSessionId(); + this.setupPageviewTracking(); + } + } + + /** + * Generiert oder lädt Session-ID + */ + private getOrCreateSessionId(): string { + const key = 'lub_session'; + let sessionId = sessionStorage.getItem(key); + + if (!sessionId) { + sessionId = this.generateSessionId(); + sessionStorage.setItem(key, sessionId); + } + + return sessionId; + } + + /** + * Generiert eine einfache Session-ID + */ + private generateSessionId(): string { + return Date.now().toString(36) + Math.random().toString(36).substring(2, 9); + } + + /** + * Automatisches Pageview-Tracking bei Navigation + */ + private setupPageviewTracking(): void { + this.router.events.pipe( + filter(event => event instanceof NavigationEnd) + ).subscribe((event: NavigationEnd) => { + this.trackPageview(event.urlAfterRedirects); + }); + + // Initial pageview + setTimeout(() => { + this.trackPageview(this.router.url); + }, 100); + } + + /** + * Prüft ob Analytics erlaubt ist + */ + private mayTrack(): boolean { + return this.consentService.isAllowed('analytics'); + } + + /** + * Tracked einen Pageview + */ + trackPageview(path: string): void { + if (!this.mayTrack() || !this.isBrowser) return; + + const event: AnalyticsEvent = { + type: 'pageview', + page: path, + referrer: document.referrer || undefined, + userAgent: navigator.userAgent, + screenSize: `${window.innerWidth}x${window.innerHeight}`, + timestamp: Date.now(), + sessionId: this.sessionId + }; + + this.sendEvent(event); + } + + /** + * Tracked ein Click-Event + */ + trackClick(elementId: string, metadata?: Record): void { + if (!this.mayTrack() || !this.isBrowser) return; + + const event: AnalyticsEvent = { + type: 'click', + page: this.router.url, + timestamp: Date.now(), + sessionId: this.sessionId, + metadata: { elementId, ...metadata } + }; + + this.sendEvent(event); + } + + /** + * Tracked eine Conversion (z.B. Kontaktformular) + */ + trackConversion(goal: string, metadata?: Record): void { + if (!this.mayTrack() || !this.isBrowser) return; + + const event: AnalyticsEvent = { + type: 'conversion', + page: this.router.url, + timestamp: Date.now(), + sessionId: this.sessionId, + metadata: { goal, ...metadata } + }; + + this.sendEvent(event); + } + + /** + * Tracked ein Custom Event + */ + trackEvent(eventName: string, metadata?: Record): void { + if (!this.mayTrack() || !this.isBrowser) return; + + const event: AnalyticsEvent = { + type: 'custom', + page: this.router.url, + timestamp: Date.now(), + sessionId: this.sessionId, + metadata: { eventName, ...metadata } + }; + + this.sendEvent(event); + } + + /** + * Tracked einen Fehler + */ + trackError(error: string, metadata?: Record): void { + if (!this.mayTrack() || !this.isBrowser) return; + + const event: AnalyticsEvent = { + type: 'error', + page: this.router.url, + timestamp: Date.now(), + sessionId: this.sessionId, + metadata: { error, ...metadata } + }; + + this.sendEvent(event); + } + + /** + * Sendet Event ans Backend + */ + private sendEvent(event: AnalyticsEvent): void { + const analyticsHeaders = this.consentService.getAnalyticsHeaders(); + console.log('[Analytics] Sending event:', event.type, event.page); + console.log('[Analytics] Consent headers:', analyticsHeaders); + console.log('[Analytics] API URL:', `${this.API_URL}/event`); + + const headers = new HttpHeaders({ + 'Content-Type': 'application/json', + ...analyticsHeaders + }); + + // Fire and forget - aber logge Fehler für Debugging + this.http.post(`${this.API_URL}/event`, event, { headers }).subscribe({ + next: () => console.log('[Analytics] Event sent successfully'), + error: (err) => console.error('[Analytics] Error sending event:', err) + }); + } +} diff --git a/apps/frontend/src/app/services/auth.interceptor.ts b/apps/frontend/src/app/services/auth.interceptor.ts new file mode 100644 index 0000000..1483073 --- /dev/null +++ b/apps/frontend/src/app/services/auth.interceptor.ts @@ -0,0 +1,83 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http'; +import { Observable, throwError } from 'rxjs'; +import { catchError, tap } from 'rxjs/operators'; +import { ToastService } from '../shared/toasts/toast.service'; + +@Injectable() +export class AuthInterceptor implements HttpInterceptor { + private toasts = inject(ToastService); + private rateLimitWarningShown = false; + private rateLimitToastShown = false; + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + // Token direkt aus localStorage holen (kein AuthService!) + const token = localStorage.getItem('access_token'); + + let request = req; + if (token) { + request = req.clone({ + setHeaders: { + Authorization: `Bearer ${token}` + } + }); + } + + return next.handle(request).pipe( + tap((event: any) => { + // Rate-Limit-Headers auswerten (wenn vorhanden) + if (event.headers) { + const remaining = event.headers.get('X-RateLimit-Remaining'); + const limit = event.headers.get('X-RateLimit-Limit'); + + // Warnung bei weniger als 20% verbleibenden Requests + if (remaining && limit) { + const remainingNum = parseInt(remaining, 10); + const limitNum = parseInt(limit, 10); + + if (remainingNum < limitNum * 0.2 && remainingNum > 0 && !this.rateLimitWarningShown) { + this.rateLimitWarningShown = true; + this.toasts.warning( + `Noch ${remainingNum} Anfragen übrig. Bitte warte kurz.`, + { duration: 5000 } + ); + // Reset nach 30 Sekunden + setTimeout(() => this.rateLimitWarningShown = false, 30000); + } + } + } + }), + catchError((error: HttpErrorResponse) => { + if (error.status === 429) { + // Rate Limit erreicht - zeige Toast nur wenn nicht schon einer angezeigt wird + // (verhindert Spam bei mehreren gleichzeitigen Requests) + if (!this.rateLimitToastShown) { + this.rateLimitToastShown = true; + + // Versuche Retry-After aus verschiedenen Quellen zu lesen + let seconds = 10; // Default + + // 1. Aus Header (bevorzugt) + const retryAfterHeader = error.headers?.get('Retry-After'); + if (retryAfterHeader) { + seconds = parseInt(retryAfterHeader, 10); + } + // 2. Aus Response Body (Fallback) + else if (error.error?.retryAfter) { + seconds = error.error.retryAfter; + } + + this.toasts.error( + `Zu viele Anfragen. Bitte warte ${seconds} Sekunden.`, + { duration: Math.min(seconds * 1000, 10000) } // Max 10 Sekunden Toast + ); + + // Reset nach der Wartezeit + setTimeout(() => this.rateLimitToastShown = false, seconds * 1000); + } + } + return throwError(() => error); + }) + ); + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/services/auth.service.ts b/apps/frontend/src/app/services/auth.service.ts new file mode 100644 index 0000000..75a1f28 --- /dev/null +++ b/apps/frontend/src/app/services/auth.service.ts @@ -0,0 +1,142 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { BehaviorSubject, catchError, map, Observable, of, tap } from 'rxjs'; +import { Router } from '@angular/router'; +import { ConfigService } from './config.service'; + +export enum UserRole { + USER = 'user', + ADMIN = 'admin' +} + +export interface User { + id: string; + email: string; + name: string; + role: UserRole; + wantsNewsletter?: boolean; + isVerified?: boolean; + createdAt: Date; + updatedAt?: Date; +} + +export interface LoginResponse { + access_token: string; + user: User; +} + +@Injectable({ + providedIn: 'root' +}) +export class AuthService { + private currentUserSubject = new BehaviorSubject(null); + public currentUser$ = this.currentUserSubject.asObservable(); + + private get API_URL(): string { + return this.configService.apiUrl; + } + + constructor( + private http: HttpClient, + private router: Router, + private configService: ConfigService + ) { + this.loadUserFromStorage(); + } + + private loadUserFromStorage() { + const token = localStorage.getItem('access_token'); + const userJson = localStorage.getItem('current_user'); + + if (token && userJson) { + try { + const user = JSON.parse(userJson); + this.currentUserSubject.next(user); + } catch (e) { + this.logout(); + } + } + } + + verifyAdminStatus(): Observable { + if (!this.getToken()) { + return of(false); + } + + // Prüfe ob wir gecachte User-Daten haben + const cachedUser = this.currentUserSubject.getValue(); + + return this.http.get(`${this.API_URL}/auth/me`).pipe( + map(user => { + localStorage.setItem('current_user', JSON.stringify(user)); + this.currentUserSubject.next(user); + return user.role === UserRole.ADMIN; + }), + catchError((error) => { + console.error('Admin-Validierung fehlgeschlagen:', error); + + // Bei 401: Token ungültig → ausloggen + if (error.status === 401) { + this.logout(); + return of(false); + } + + // Bei 429 (Rate Limit): Verwende gecachte User-Daten falls vorhanden + if (error.status === 429 && cachedUser) { + return of(cachedUser.role === UserRole.ADMIN); + } + + return of(false); + }) + ); + } + + register(email: string, name: string, password: string): Observable { + return this.http.post(`${this.API_URL}/auth/register`, { + email, + name, + password + }).pipe( + tap(response => this.handleAuthResponse(response)) + ); + } + + login(email: string, password: string): Observable { + return this.http.post(`${this.API_URL}/auth/login`, { + email, + password + }).pipe( + tap(response => this.handleAuthResponse(response)) + ); + } + + logout() { + localStorage.removeItem('access_token'); + localStorage.removeItem('current_user'); + this.currentUserSubject.next(null); + this.router.navigate(['/login']); + } + + getToken(): string | null { + return localStorage.getItem('access_token'); + } + + getCurrentUser(): User | null { + return this.currentUserSubject.value; + } + + isLoggedIn(): boolean { + return !!this.getToken() && !!this.getCurrentUser(); + } + + isAdmin(): boolean { + const user = this.getCurrentUser(); + return user?.role === UserRole.ADMIN; + } + + private handleAuthResponse(response: LoginResponse) { + localStorage.setItem('access_token', response.access_token); + localStorage.setItem('current_user', JSON.stringify(response.user)); + this.currentUserSubject.next(response.user); + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/services/config.service.ts b/apps/frontend/src/app/services/config.service.ts new file mode 100644 index 0000000..b398ecf --- /dev/null +++ b/apps/frontend/src/app/services/config.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { firstValueFrom } from 'rxjs'; + +export interface AppConfig { + apiUrl: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class ConfigService { + private config: AppConfig | null = null; + + constructor(private http: HttpClient) {} + + async loadConfig(): Promise { + try { + this.config = await firstValueFrom( + this.http.get('/config.json') + ); + console.log('Config loaded:', this.config); + } catch (error) { + console.error('Failed to load config, using fallback:', error); + // Fallback-Werte falls config.json nicht geladen werden kann + this.config = { + apiUrl: 'http://localhost:3000' + }; + } + } + + get apiUrl(): string { + return this.config?.apiUrl ?? 'http://localhost:3000'; + } + + getConfig(): AppConfig | null { + return this.config; + } +} diff --git a/apps/frontend/src/app/services/consent/consent.model.ts b/apps/frontend/src/app/services/consent/consent.model.ts new file mode 100644 index 0000000..0960fbc --- /dev/null +++ b/apps/frontend/src/app/services/consent/consent.model.ts @@ -0,0 +1,55 @@ +// ===== CONSENT MODEL ===== +// Single Source of Truth für Cookie/Tracking Consent + +export interface ConsentState { + necessary: true; // Immer true - technisch notwendig + functional: boolean; // Komfort-Features (Theme, Preferences) + analytics: boolean; // Nutzungsstatistiken + timestamp: number; // Wann wurde Consent gegeben + version: string; // Version der Consent-Policy +} + +export type ConsentCategory = 'necessary' | 'functional' | 'analytics'; + +export interface ConsentCategoryInfo { + id: ConsentCategory; + name: string; + description: string; + required: boolean; + icon: string; +} + +export const CONSENT_CATEGORIES: ConsentCategoryInfo[] = [ + { + id: 'necessary', + name: 'Notwendig', + description: 'Diese Cookies sind für die Grundfunktionen der Website erforderlich (z.B. Login, Sicherheit).', + required: true, + icon: 'lock' + }, + { + id: 'functional', + name: 'Funktional', + description: 'Ermöglicht erweiterte Funktionen wie Theme-Einstellungen und gespeicherte Präferenzen.', + required: false, + icon: 'tune' + }, + { + id: 'analytics', + name: 'Statistiken', + description: 'Hilft uns zu verstehen, wie Besucher die Website nutzen, um sie zu verbessern. Alle Daten sind anonymisiert.', + required: false, + icon: 'bar_chart' + } +]; + +export const DEFAULT_CONSENT: ConsentState = { + necessary: true, + functional: false, + analytics: false, + timestamp: 0, + version: '1.0' +}; + +export const CONSENT_VERSION = '1.0'; +export const CONSENT_STORAGE_KEY = 'lub_consent'; diff --git a/apps/frontend/src/app/services/consent/consent.service.ts b/apps/frontend/src/app/services/consent/consent.service.ts new file mode 100644 index 0000000..104c663 --- /dev/null +++ b/apps/frontend/src/app/services/consent/consent.service.ts @@ -0,0 +1,160 @@ +import { Injectable, PLATFORM_ID, Inject } from '@angular/core'; +import { isPlatformBrowser } from '@angular/common'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { + ConsentState, + ConsentCategory, + DEFAULT_CONSENT, + CONSENT_VERSION, + CONSENT_STORAGE_KEY +} from './consent.model'; + +@Injectable({ + providedIn: 'root' +}) +export class ConsentService { + private consentSubject = new BehaviorSubject(DEFAULT_CONSENT); + private showBannerSubject = new BehaviorSubject(false); + private isBrowser: boolean; + + consent$ = this.consentSubject.asObservable(); + showBanner$ = this.showBannerSubject.asObservable(); + + constructor(@Inject(PLATFORM_ID) platformId: Object) { + this.isBrowser = isPlatformBrowser(platformId); + this.loadConsent(); + } + + /** + * Lädt gespeicherten Consent aus localStorage + */ + private loadConsent(): void { + if (!this.isBrowser) return; + + try { + const stored = localStorage.getItem(CONSENT_STORAGE_KEY); + if (stored) { + const consent: ConsentState = JSON.parse(stored); + // Prüfe ob Version aktuell ist + if (consent.version === CONSENT_VERSION && consent.timestamp > 0) { + this.consentSubject.next(consent); + this.showBannerSubject.next(false); + return; + } + } + // Kein oder veralteter Consent -> Banner zeigen + this.showBannerSubject.next(true); + } catch { + this.showBannerSubject.next(true); + } + } + + /** + * Speichert Consent in localStorage + */ + private saveConsent(consent: ConsentState): void { + if (!this.isBrowser) return; + + try { + localStorage.setItem(CONSENT_STORAGE_KEY, JSON.stringify(consent)); + } catch (e) { + console.error('Consent konnte nicht gespeichert werden:', e); + } + } + + /** + * Aktueller Consent-Status + */ + get consent(): ConsentState { + return this.consentSubject.getValue(); + } + + /** + * Prüft ob eine Kategorie erlaubt ist + */ + isAllowed(category: ConsentCategory): boolean { + if (category === 'necessary') return true; + return this.consent[category] === true; + } + + /** + * Alle Cookies akzeptieren + */ + acceptAll(): void { + const consent: ConsentState = { + necessary: true, + functional: true, + analytics: true, + timestamp: Date.now(), + version: CONSENT_VERSION + }; + this.consentSubject.next(consent); + this.saveConsent(consent); + this.showBannerSubject.next(false); + } + + /** + * Nur notwendige Cookies akzeptieren + */ + acceptNecessaryOnly(): void { + const consent: ConsentState = { + necessary: true, + functional: false, + analytics: false, + timestamp: Date.now(), + version: CONSENT_VERSION + }; + this.consentSubject.next(consent); + this.saveConsent(consent); + this.showBannerSubject.next(false); + } + + /** + * Individuelle Auswahl speichern + */ + saveCustomConsent(functional: boolean, analytics: boolean): void { + const consent: ConsentState = { + necessary: true, + functional, + analytics, + timestamp: Date.now(), + version: CONSENT_VERSION + }; + this.consentSubject.next(consent); + this.saveConsent(consent); + this.showBannerSubject.next(false); + } + + /** + * Banner wieder anzeigen (für Einstellungen-Link im Footer) + */ + showSettings(): void { + this.showBannerSubject.next(true); + } + + /** + * Banner schließen ohne Änderungen + */ + closeBanner(): void { + this.showBannerSubject.next(false); + } + + /** + * Consent zurücksetzen (für Testzwecke) + */ + resetConsent(): void { + if (!this.isBrowser) return; + localStorage.removeItem(CONSENT_STORAGE_KEY); + this.consentSubject.next(DEFAULT_CONSENT); + this.showBannerSubject.next(true); + } + + /** + * Gibt Header für Analytics-Requests zurück + */ + getAnalyticsHeaders(): Record { + return this.isAllowed('analytics') + ? { 'X-Consent-Analytics': 'true' } + : {}; + } +} diff --git a/apps/frontend/src/app/shared/admin-notification-center/admin-notification-center.component.html b/apps/frontend/src/app/shared/admin-notification-center/admin-notification-center.component.html new file mode 100644 index 0000000..a05ce81 --- /dev/null +++ b/apps/frontend/src/app/shared/admin-notification-center/admin-notification-center.component.html @@ -0,0 +1,81 @@ +
+ + + + + +
diff --git a/apps/frontend/src/app/shared/admin-notification-center/admin-notification-center.component.scss b/apps/frontend/src/app/shared/admin-notification-center/admin-notification-center.component.scss new file mode 100644 index 0000000..8069112 --- /dev/null +++ b/apps/frontend/src/app/shared/admin-notification-center/admin-notification-center.component.scss @@ -0,0 +1,287 @@ +.notification-center { + position: relative; +} + +/* ===== BELL BUTTON ===== */ +.bell-btn { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + background: transparent; + border: none; + border-radius: 50%; + cursor: pointer; + transition: all .2s; + color: #64748b; + + &:hover { + background: rgba(37, 99, 235, .08); + color: #2563eb; + } + + &.has-notifications { + color: #2563eb; + } + + .ring { + animation: ring 2s ease-in-out infinite; + } +} + +@keyframes ring { + 0%, 100% { transform: rotate(0); } + 5%, 15% { transform: rotate(-12deg); } + 10%, 20% { transform: rotate(12deg); } + 25% { transform: rotate(0); } +} + +.badge { + position: absolute; + top: -2px; + right: -2px; + min-width: 18px; + height: 18px; + padding: 0 5px; + background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); + color: white; + font-size: .6875rem; + font-weight: 700; + border-radius: 9px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 6px rgba(239, 68, 68, .4); +} + +/* ===== DROPDOWN ===== */ +.dropdown { + position: absolute; + top: calc(100% + 8px); + right: 0; + width: 320px; + background: rgb(255, 255, 255); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-radius: 16px; + border: 1px solid rgba(255, 255, 255, .6); + box-shadow: + 0 8px 32px rgba(0, 0, 0, .08), + 0 0 0 1px rgba(0, 0, 0, .03), + inset 0 1px 0 rgba(255, 255, 255, .8); + z-index: 1000; + overflow: hidden; + + @media (max-width: 480px) { + position: fixed; + top: 60px; + right: 8px; + left: 8px; + width: auto; + } +} + +.dropdown-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: .875rem 1rem; + background: rgba(248, 250, 252, .6); + border-bottom: 1px solid rgba(226, 232, 240, .5); + + h4 { + margin: 0; + font-size: .9375rem; + font-weight: 700; + background: linear-gradient(135deg, #1e293b 0%, #475569 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + } + + .refresh-btn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + background: rgba(255, 255, 255, .8); + border: 1px solid rgba(226, 232, 240, .6); + border-radius: 8px; + color: #64748b; + cursor: pointer; + transition: all .2s; + + &:hover { + background: rgba(37, 99, 235, .08); + border-color: rgba(37, 99, 235, .3); + color: #2563eb; + } + + &:disabled { + opacity: .5; + cursor: not-allowed; + } + + app-icon { + font-size: 1rem; + } + + .spin { + animation: spin .8s linear infinite; + } + } +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.dropdown-content { + max-height: 400px; + overflow-y: auto; +} + +/* ===== SECTIONS ===== */ +.section { + padding: .5rem 0; + + & + .section { + border-top: 1px solid #e2e8f0; + } +} + +.section-header { + display: flex; + align-items: center; + gap: .5rem; + padding: .5rem 1rem; + font-size: .75rem; + font-weight: 600; + color: #64748b; + text-transform: uppercase; + letter-spacing: .03em; + + app-icon { + font-size: .875rem; + } + + .count { + margin-left: auto; + background: #e2e8f0; + color: #475569; + padding: .125rem .5rem; + border-radius: 10px; + font-weight: 700; + } +} + +/* ===== NOTIFICATION ITEM ===== */ +.notification-item { + display: flex; + align-items: center; + gap: .75rem; + padding: .625rem 1rem; + text-decoration: none; + color: inherit; + transition: all .15s; + cursor: pointer; + + &:hover { + background: #f8fafc; + } +} + +.item-icon { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: 10px; + flex-shrink: 0; + + app-icon { + font-size: 1.125rem; + } + + &.contact { + background: rgba(37, 99, 235, .1); + color: #2563eb; + } + + &.booking { + background: rgba(249, 115, 22, .1); + color: #ea580c; + } +} + +.item-content { + flex: 1; + min-width: 0; +} + +.item-title { + display: block; + font-size: .875rem; + font-weight: 600; + color: #1e293b; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.item-subtitle { + display: block; + font-size: .75rem; + color: #64748b; + margin-top: .125rem; +} + +/* ===== VIEW ALL ===== */ +.view-all { + display: block; + padding: .5rem 1rem; + font-size: .75rem; + font-weight: 600; + color: #2563eb; + text-decoration: none; + text-align: center; + transition: all .15s; + + &:hover { + background: rgba(37, 99, 235, .05); + } +} + +/* ===== EMPTY STATE ===== */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 2rem; + text-align: center; + + app-icon { + font-size: 2.5rem; + color: #22c55e; + margin-bottom: .75rem; + } + + p { + margin: 0; + font-size: 1rem; + font-weight: 700; + color: #1e293b; + } + + span { + font-size: .8125rem; + color: #64748b; + margin-top: .25rem; + } +} diff --git a/apps/frontend/src/app/shared/admin-notification-center/admin-notification-center.component.ts b/apps/frontend/src/app/shared/admin-notification-center/admin-notification-center.component.ts new file mode 100644 index 0000000..62a600d --- /dev/null +++ b/apps/frontend/src/app/shared/admin-notification-center/admin-notification-center.component.ts @@ -0,0 +1,114 @@ +import { Component, OnInit, OnDestroy, HostListener, ElementRef } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; +import { Subject, forkJoin, interval, of, merge } from 'rxjs'; +import { takeUntil, startWith, switchMap, tap, catchError } from 'rxjs/operators'; +import { ApiService, ContactRequest, Booking, BookingStatus } from '../../api/api.service'; +import { IconComponent } from '../icon/icon.component'; +import { NotificationRefreshService } from './notification-refresh.service'; + +@Component({ + selector: 'app-admin-notification-center', + standalone: true, + imports: [CommonModule, RouterModule, IconComponent], + templateUrl: './admin-notification-center.component.html', + styleUrls: ['./admin-notification-center.component.scss'] +}) +export class AdminNotificationCenterComponent implements OnInit, OnDestroy { + isOpen = false; + loading = false; + + unprocessedContacts: ContactRequest[] = []; + pendingBookings: Booking[] = []; + + private destroy$ = new Subject(); + private refreshInterval = 60000; // 1 Minute + + constructor( + private api: ApiService, + private elementRef: ElementRef, + private notificationRefresh: NotificationRefreshService + ) {} + + ngOnInit() { + // Initial load + periodic refresh + event-triggered refresh + merge( + interval(this.refreshInterval).pipe(startWith(0)), + this.notificationRefresh.onRefreshNeeded$ + ) + .pipe( + takeUntil(this.destroy$), + switchMap(() => this.loadNotifications()) + ) + .subscribe(); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + @HostListener('document:click', ['$event']) + onClickOutside(event: Event) { + if (!this.elementRef.nativeElement.contains(event.target)) { + this.isOpen = false; + } + } + + @HostListener('document:keydown.escape') + onEscapeKey() { + this.isOpen = false; + } + + toggle() { + this.isOpen = !this.isOpen; + if (this.isOpen) { + this.refresh(); + } + } + + refresh() { + this.loading = true; + this.loadNotifications().subscribe({ + next: () => this.loading = false, + error: () => this.loading = false + }); + } + + close() { + this.isOpen = false; + } + + private loadNotifications() { + return forkJoin({ + contacts: this.api.getUnprocessedContactRequests().pipe(catchError(() => of([]))), + bookings: this.api.getAllBookings().pipe(catchError(() => of([]))) + }).pipe( + tap(({ contacts, bookings }) => { + this.unprocessedContacts = contacts; + this.pendingBookings = bookings.filter(b => b.status === BookingStatus.PENDING); + }), + takeUntil(this.destroy$) + ); + } + + get totalCount(): number { + return this.unprocessedContacts.length + this.pendingBookings.length; + } + + getRelativeTime(date: Date | string): string { + const d = typeof date === 'string' ? new Date(date) : date; + const now = new Date(); + const diffMs = now.getTime() - d.getTime(); + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMins / 60); + const diffDays = Math.floor(diffHours / 24); + + if (diffMins < 1) return 'Gerade eben'; + if (diffMins < 60) return `vor ${diffMins} Min`; + if (diffHours < 24) return `vor ${diffHours} Std`; + if (diffDays === 1) return 'Gestern'; + if (diffDays < 7) return `vor ${diffDays} Tagen`; + return d.toLocaleDateString('de-DE'); + } +} diff --git a/apps/frontend/src/app/shared/admin-notification-center/notification-refresh.service.ts b/apps/frontend/src/app/shared/admin-notification-center/notification-refresh.service.ts new file mode 100644 index 0000000..d8aabd0 --- /dev/null +++ b/apps/frontend/src/app/shared/admin-notification-center/notification-refresh.service.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@angular/core'; +import { Subject } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class NotificationRefreshService { + private refreshTrigger$ = new Subject(); + + // Observable für Komponenten, die auf Änderungen reagieren wollen + onRefreshNeeded$ = this.refreshTrigger$.asObservable(); + + // Löst eine Aktualisierung aus + triggerRefresh(): void { + this.refreshTrigger$.next(); + } +} diff --git a/apps/frontend/src/app/shared/auth-required/auth-required.component.html b/apps/frontend/src/app/shared/auth-required/auth-required.component.html new file mode 100644 index 0000000..28ddddc --- /dev/null +++ b/apps/frontend/src/app/shared/auth-required/auth-required.component.html @@ -0,0 +1,31 @@ +
+
+
+ lock +
+ +

{{ title }}

+

{{ message }}

+ +
+ + + +
+ + +
+
\ No newline at end of file diff --git a/apps/frontend/src/app/shared/auth-required/auth-required.component.scss b/apps/frontend/src/app/shared/auth-required/auth-required.component.scss new file mode 100644 index 0000000..05a985f --- /dev/null +++ b/apps/frontend/src/app/shared/auth-required/auth-required.component.scss @@ -0,0 +1,146 @@ + @import '../../utils/shared-styles.scss'; + + .auth-required { + min-height: 60vh; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; + } + + .auth-card { + max-width: 520px; + width: 100%; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(30px); + border: 1px solid rgba(255, 255, 255, 0.8); + border-radius: 24px; + padding: 3rem 2.5rem; + text-align: center; + box-shadow: + 0 20px 60px rgba(0, 0, 0, 0.15), + 0 0 0 1px rgba(255, 255, 255, 0.5) inset; + position: relative; + + @media (max-width: 640px) { + padding: 2.5rem 1.75rem; + border-radius: 20px; + } + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, #2563eb, #3b82f6, #60a5fa); + animation: shimmer 3s ease-in-out infinite; + } + } + + @keyframes shimmer { + + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.6; + } + } + + .auth-icon { + width: 100px; + height: 100px; + margin: 0 auto 1.5rem; + background: linear-gradient(135deg, #2563eb, #1d4ed8); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 10px 40px rgba(37, 99, 235, 0.4); + position: relative; + + &::after { + content: ''; + position: absolute; + inset: -8px; + border: 2px solid rgba(37, 99, 235, 0.3); + border-radius: 50%; + animation: pulse 2s ease-in-out infinite; + } + + .material-symbols-outlined { + font-size: 48px; + color: white; + filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.2)); + } + } + + @keyframes pulse { + + 0%, + 100% { + transform: scale(1); + opacity: 0.8; + } + + 50% { + transform: scale(1.1); + opacity: 0.5; + } + } + + h2 { + font-size: clamp(1.5rem, 3vw + 1rem, 1.875rem); + font-weight: 900; + color: $color-text-primary; + margin-bottom: 0.75rem; + letter-spacing: -0.02em; + } + + p { + color: $color-text-secondary; + line-height: 1.6; + font-size: 1rem; + margin-bottom: 2rem; + max-width: 400px; + margin-left: auto; + margin-right: auto; + } + + .auth-actions { + display: flex; + flex-direction: column; + gap: 1rem; + + .btn { + width: 100%; + } + } + + .auth-footer { + margin-top: 1.5rem; + padding-top: 1.5rem; + border-top: 2px dashed rgba(0, 0, 0, 0.06); + } + + .ms-size-20 { + font-size: 20px; + } + + .ms-fill { + font-variation-settings: 'FILL' 1; + } + + @media (prefers-reduced-motion: reduce) { + + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } + } \ No newline at end of file diff --git a/apps/frontend/src/app/shared/auth-required/auth-required.component.spec.ts b/apps/frontend/src/app/shared/auth-required/auth-required.component.spec.ts new file mode 100644 index 0000000..49ecf31 --- /dev/null +++ b/apps/frontend/src/app/shared/auth-required/auth-required.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AuthRequiredComponent } from './auth-required.component'; + +describe('AuthRequiredComponent', () => { + let component: AuthRequiredComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AuthRequiredComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AuthRequiredComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/frontend/src/app/shared/auth-required/auth-required.component.ts b/apps/frontend/src/app/shared/auth-required/auth-required.component.ts new file mode 100644 index 0000000..1e7c58b --- /dev/null +++ b/apps/frontend/src/app/shared/auth-required/auth-required.component.ts @@ -0,0 +1,44 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; + +@Component({ + selector: 'app-auth-required', + standalone: true, + imports: [CommonModule, FormsModule, RouterLink], + templateUrl: './auth-required.component.html', + styleUrl: './auth-required.component.scss' +}) +export class AuthRequiredComponent { + @Input() title: string = 'Anmeldung erforderlich'; + @Input() message: string = 'Um diese Funktion zu nutzen, musst du angemeldet sein.'; + @Input() returnUrl: string = '/'; + @Input() showBackButton: boolean = false; + + fullReturnUrl: string = '/'; + + constructor( + private router: Router, + private route: ActivatedRoute + ) { } + + ngOnInit(): void { + const urlTree = this.router.parseUrl(this.router.url); + const path = urlTree.root.children['primary']?.segments.map(s => s.path).join('/') || this.returnUrl; + const queryParams = urlTree.queryParams; + + if (Object.keys(queryParams).length > 0) { + const queryString = Object.entries(queryParams) + .map(([key, value]) => `${key}=${value}`) + .join('&'); + this.fullReturnUrl = `/${path}?${queryString}`; + } else { + this.fullReturnUrl = `/${path}`; + } + } + + goBack(): void { + window.history.back(); + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/shared/confirmation/confirmation.component.html b/apps/frontend/src/app/shared/confirmation/confirmation.component.html new file mode 100644 index 0000000..45167a9 --- /dev/null +++ b/apps/frontend/src/app/shared/confirmation/confirmation.component.html @@ -0,0 +1,30 @@ + \ No newline at end of file diff --git a/apps/frontend/src/app/shared/confirmation/confirmation.component.scss b/apps/frontend/src/app/shared/confirmation/confirmation.component.scss new file mode 100644 index 0000000..2e70966 --- /dev/null +++ b/apps/frontend/src/app/shared/confirmation/confirmation.component.scss @@ -0,0 +1,353 @@ +@import '../../utils/shared-styles.scss'; + +/* ===== ANIMATIONS ===== */ +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-30px) scale(0.95); + } + + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* ===== BACKDROP ===== */ +.modal-backdrop { + position: fixed; + inset: 0; + backdrop-filter: blur(3px); + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + animation: fadeIn 0.2s ease-out; + + // Wichtig: Verhindert dass Backdrop selbst scrollt + overflow: hidden; +} + +/* ===== CONTAINER ===== */ +.modal-container { + position: relative; + width: 100%; + max-width: 440px; + background: rgba(255, 255, 255, 0.98); + backdrop-filter: blur(40px) saturate(180%); + border: 1px solid rgba(255, 255, 255, 0.8); + border-radius: 24px; + padding: 2rem; + box-shadow: + 0 25px 50px rgba(0, 0, 0, 0.15), + 0 0 0 1px rgba(255, 255, 255, 0.5) inset; + display: flex; + flex-direction: column; + gap: 1.5rem; + animation: slideIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + + // Wichtig: Modal bleibt immer in der Mitte + margin: auto; + max-height: calc(100vh - 2rem); + overflow-y: auto; + + @media (max-width: 480px) { + padding: 1.5rem; + gap: 1.25rem; + border-radius: 20px; + } +} + +/* ===== ICON ===== */ +.modal-icon { + width: 64px; + height: 64px; + border-radius: 20px; + display: flex; + align-items: center; + justify-content: center; + align-self: center; + position: relative; + transition: transform 0.3s ease; + flex-shrink: 0; + + @media (max-width: 480px) { + width: 56px; + height: 56px; + border-radius: 16px; + } + + &::after { + content: ''; + position: absolute; + inset: -6px; + border-radius: 24px; + opacity: 0.3; + animation: pulse 2s ease-in-out infinite; + } + + .material-symbols-outlined { + font-size: 36px; + position: relative; + z-index: 1; + filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.15)); + + @media (max-width: 480px) { + font-size: 32px; + } + } + + /* Type Variants */ + &--warning { + background: linear-gradient(135deg, rgba(245, 158, 11, 0.15), rgba(251, 191, 36, 0.15)); + border: 2px solid rgba(245, 158, 11, 0.3); + + &::after { + border: 2px solid rgba(245, 158, 11, 0.4); + } + + .material-symbols-outlined { + color: #f59e0b; + } + } + + &--danger { + background: linear-gradient(135deg, rgba(239, 68, 68, 0.15), rgba(248, 113, 113, 0.15)); + border: 2px solid rgba(239, 68, 68, 0.3); + + &::after { + border: 2px solid rgba(239, 68, 68, 0.4); + } + + .material-symbols-outlined { + color: #ef4444; + } + } + + &--info { + background: linear-gradient(135deg, rgba(37, 99, 235, 0.15), rgba(59, 130, 246, 0.15)); + border: 2px solid rgba(37, 99, 235, 0.3); + + &::after { + border: 2px solid rgba(37, 99, 235, 0.4); + } + + .material-symbols-outlined { + color: #2563eb; + } + } + + &--success { + background: linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(52, 211, 153, 0.15)); + border: 2px solid rgba(16, 185, 129, 0.3); + + &::after { + border: 2px solid rgba(16, 185, 129, 0.4); + } + + .material-symbols-outlined { + color: #10b981; + } + } +} + +@keyframes pulse { + + 0%, + 100% { + transform: scale(1); + opacity: 0.3; + } + + 50% { + transform: scale(1.05); + opacity: 0.15; + } +} + +/* ===== CONTENT ===== */ +.modal-content { + text-align: center; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.modal-title { + font-size: 1.5rem; + font-weight: 800; + color: $color-text-primary; + margin: 0; + letter-spacing: -0.02em; + line-height: 1.2; + + @media (max-width: 480px) { + font-size: 1.35rem; + } +} + +.modal-message { + font-size: 1rem; + color: $color-text-secondary; + margin: 0; + line-height: 1.6; + + @media (max-width: 480px) { + font-size: 0.9375rem; + } +} + +/* ===== ACTIONS ===== */ +.modal-actions { + display: flex; + gap: 0.75rem; + margin-top: 0.5rem; + + @media (max-width: 480px) { + flex-direction: column-reverse; + gap: 0.625rem; + } + + .btn { + flex: 1; + justify-content: center; + padding: 0.875rem 1.5rem; + font-size: 0.9375rem; + font-weight: 700; + border-radius: 12px; + transition: all 0.2s ease; + position: relative; + overflow: hidden; + + @media (max-width: 480px) { + padding: 0.875rem 1.25rem; + font-size: 0.9rem; + } + + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } + + .spinner { + width: 18px; + height: 18px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-top-color: white; + border-radius: 50%; + animation: spin 0.6s linear infinite; + display: inline-block; + } + } + + .btn--ghost { + background: rgba(0, 0, 0, 0.04); + color: $color-text-secondary; + border: 2px solid rgba(0, 0, 0, 0.08); + + &:hover:not(:disabled) { + background: rgba(0, 0, 0, 0.08); + color: $color-text-primary; + border-color: rgba(0, 0, 0, 0.12); + } + + &:active:not(:disabled) { + transform: scale(0.98); + } + } + + .btn--warning { + background: linear-gradient(135deg, #f59e0b, #fbbf24); + color: white; + border: none; + box-shadow: 0 4px 16px rgba(245, 158, 11, 0.3); + + &:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(245, 158, 11, 0.4); + } + + &:active:not(:disabled) { + transform: translateY(0); + } + } + + .btn--danger { + background: linear-gradient(135deg, #ef4444, #f87171); + color: white; + border: none; + box-shadow: 0 4px 16px rgba(239, 68, 68, 0.3); + + &:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(239, 68, 68, 0.4); + } + + &:active:not(:disabled) { + transform: translateY(0); + } + } + + .btn--info { + background: linear-gradient(135deg, #2563eb, #3b82f6); + color: white; + border: none; + box-shadow: 0 4px 16px rgba(37, 99, 235, 0.3); + + &:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(37, 99, 235, 0.4); + } + + &:active:not(:disabled) { + transform: translateY(0); + } + } + + .btn--success { + background: linear-gradient(135deg, #10b981, #34d399); + color: white; + border: none; + box-shadow: 0 4px 16px rgba(16, 185, 129, 0.3); + + &:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(16, 185, 129, 0.4); + } + + &:active:not(:disabled) { + transform: translateY(0); + } + } +} + +/* ===== ACCESSIBILITY ===== */ +@media (prefers-reduced-motion: reduce) { + + .modal-backdrop, + .modal-container, + .modal-icon::after { + animation: none !important; + } + + .btn { + transition: none !important; + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/shared/confirmation/confirmation.component.spec.ts b/apps/frontend/src/app/shared/confirmation/confirmation.component.spec.ts new file mode 100644 index 0000000..4b7169e --- /dev/null +++ b/apps/frontend/src/app/shared/confirmation/confirmation.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ConfirmationComponent } from './confirmation.component'; + +describe('ConfirmationComponent', () => { + let component: ConfirmationComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ConfirmationComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ConfirmationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/frontend/src/app/shared/confirmation/confirmation.component.ts b/apps/frontend/src/app/shared/confirmation/confirmation.component.ts new file mode 100644 index 0000000..6454e84 --- /dev/null +++ b/apps/frontend/src/app/shared/confirmation/confirmation.component.ts @@ -0,0 +1,67 @@ +import { CommonModule } from '@angular/common'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +export interface ConfirmationConfig { + title: string; + message: string; + confirmText?: string; + cancelText?: string; + type?: 'warning' | 'danger' | 'info' | 'success'; + icon?: string; +} + +@Component({ + selector: 'app-confirmation', + standalone: true, + imports: [CommonModule, FormsModule], + templateUrl: './confirmation.component.html', + styleUrl: './confirmation.component.scss' +}) +export class ConfirmationComponent { + @Input() isOpen = false; + @Input() config: ConfirmationConfig = { + title: 'Bestätigung', + message: 'Möchtest du fortfahren?', + type: 'info' + }; + @Input() loading = false; + + @Output() confirmed = new EventEmitter(); + @Output() cancelled = new EventEmitter(); + @Output() closed = new EventEmitter(); + + getDefaultIcon(): string { + switch (this.config.type) { + case 'warning': return 'warning'; + case 'danger': return 'delete'; + case 'success': return 'check_circle'; + default: return 'help'; + } + } + + onConfirm(): void { + if (!this.loading) { + this.confirmed.emit(); + } + } + + onCancel(): void { + if (!this.loading) { + this.isOpen = false; + this.cancelled.emit(); + } + } + + onBackdropClick(): void { + if (!this.loading) { + this.isOpen = false; + this.cancelled.emit(); + } + } + + close(): void { + this.isOpen = false; + this.closed.emit(); + } +} diff --git a/apps/frontend/src/app/shared/confirmation/confirmation.service.ts b/apps/frontend/src/app/shared/confirmation/confirmation.service.ts new file mode 100644 index 0000000..03a6a59 --- /dev/null +++ b/apps/frontend/src/app/shared/confirmation/confirmation.service.ts @@ -0,0 +1,58 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { ConfirmationConfig } from './confirmation.component'; + +interface ConfirmationState { + isOpen: boolean; + config: ConfirmationConfig; + resolve?: (value: boolean) => void; +} + +@Injectable({ + providedIn: 'root' +}) +export class ConfirmationService { + private stateSubject = new BehaviorSubject({ + isOpen: false, + config: { + title: 'Bestätigung', + message: 'Möchtest du fortfahren?', + type: 'info' + } + }); + + state$ = this.stateSubject.asObservable(); + + confirm(config: ConfirmationConfig): Promise { + return new Promise((resolve) => { + this.stateSubject.next({ + isOpen: true, + config, + resolve + }); + }); + } + + handleConfirm(): void { + const state = this.stateSubject.value; + if (state.resolve) { + state.resolve(true); + } + this.close(); + } + + handleCancel(): void { + const state = this.stateSubject.value; + if (state.resolve) { + state.resolve(false); + } + this.close(); + } + + private close(): void { + this.stateSubject.next({ + ...this.stateSubject.value, + isOpen: false + }); + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/shared/cookie-banner/cookie-banner.component.html b/apps/frontend/src/app/shared/cookie-banner/cookie-banner.component.html new file mode 100644 index 0000000..9bf293d --- /dev/null +++ b/apps/frontend/src/app/shared/cookie-banner/cookie-banner.component.html @@ -0,0 +1,87 @@ + + diff --git a/apps/frontend/src/app/shared/cookie-banner/cookie-banner.component.scss b/apps/frontend/src/app/shared/cookie-banner/cookie-banner.component.scss new file mode 100644 index 0000000..f0dd813 --- /dev/null +++ b/apps/frontend/src/app/shared/cookie-banner/cookie-banner.component.scss @@ -0,0 +1,480 @@ +// ===== COOKIE BANNER - CLEAN & MOBILE FIRST ===== + +@import '../../utils/shared-styles.scss'; + +// ===== VARIABLES ===== +$banner-bg: rgba(255, 255, 255, 0.98); +$banner-radius: 20px; +$banner-shadow: 0 -8px 40px rgba(0, 0, 0, 0.15); + +// ===== OVERLAY ===== +.cookie-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + display: flex; + align-items: flex-end; + justify-content: center; + z-index: 9999; + padding: 0; + animation: fadeIn 0.3s ease; + + @media (min-width: 640px) { + align-items: center; + padding: 1rem; + } +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes fadeOut { + from { opacity: 1; } + to { opacity: 0; } +} + +// ===== BANNER ===== +.cookie-banner { + background: $banner-bg; + width: 100%; + max-width: 100%; + max-height: 85vh; + max-height: 85dvh; + overflow-y: auto; + border-radius: $banner-radius $banner-radius 0 0; + box-shadow: $banner-shadow; + animation: slideUp 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); + display: flex; + flex-direction: column; + + @media (min-width: 640px) { + max-width: 480px; + border-radius: $banner-radius; + animation: scaleIn 0.3s ease; + } + + &.expanded { + max-height: 90vh; + max-height: 90dvh; + } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(100%); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes scaleIn { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes slideDown { + from { + opacity: 1; + transform: translateY(0); + } + to { + opacity: 0; + transform: translateY(100%); + } +} + +@keyframes scaleOut { + from { + opacity: 1; + transform: scale(1); + } + to { + opacity: 0; + transform: scale(0.95); + } +} + +// ===== OVERLAY CLOSING STATE ===== +.cookie-overlay.closing { + animation: fadeOut 0.3s ease forwards; +} + +.cookie-banner.closing { + animation: slideDown 0.3s cubic-bezier(0.4, 0, 1, 1) forwards; + + @media (min-width: 640px) { + animation: scaleOut 0.3s ease forwards; + } +} + +// ===== HEADER ===== +.banner-header { + display: flex; + align-items: center; + gap: 0.875rem; + padding: 1.25rem 1.25rem 0; +} + +.banner-icon { + width: 48px; + height: 48px; + background: $gradient-primary; + border-radius: 14px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + .material-symbols-outlined { + font-size: 24px; + color: white; + } +} + +.banner-title-area { + h2 { + margin: 0; + font-size: 1.125rem; + font-weight: 700; + color: $color-text-primary; + line-height: 1.3; + } +} + +.banner-subtitle { + margin: 0.125rem 0 0; + font-size: 0.8125rem; + color: $color-text-tertiary; +} + +// ===== CONTENT ===== +.banner-content { + padding: 1rem 1.25rem; +} + +.banner-text { + margin: 0; + font-size: 0.875rem; + line-height: 1.6; + color: $color-text-secondary; +} + +// ===== COOKIE DETAILS ===== +.cookie-details { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.cookie-category { + background: $color-gray-50; + border: 2px solid transparent; + border-radius: 12px; + padding: 0.875rem; + transition: all 0.2s; + + &.enabled { + background: rgba($color-brand-primary, 0.04); + border-color: rgba($color-brand-primary, 0.15); + } + + &.required { + background: $color-gray-50; + border-color: $color-gray-200; + } +} + +.category-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.75rem; +} + +.category-info { + display: flex; + align-items: center; + gap: 0.625rem; +} + +.category-icon { + font-size: 20px; + color: $color-brand-primary; +} + +.category-name { + font-size: 0.875rem; + font-weight: 600; + color: $color-text-primary; +} + +.category-badge { + display: inline-block; + margin-left: 0.5rem; + padding: 0.125rem 0.5rem; + background: $color-gray-200; + border-radius: 4px; + font-size: 0.625rem; + font-weight: 600; + color: $color-text-tertiary; + text-transform: uppercase; + letter-spacing: 0.02em; +} + +.category-description { + margin: 0.5rem 0 0; + font-size: 0.75rem; + line-height: 1.5; + color: $color-text-tertiary; +} + +// ===== TOGGLE SWITCH ===== +.toggle-switch { + position: relative; + width: 44px; + height: 26px; + background: $color-gray-300; + border: none; + border-radius: 13px; + cursor: pointer; + transition: all 0.25s; + flex-shrink: 0; + + &:disabled { + cursor: not-allowed; + opacity: 0.6; + } + + &.active { + background: $color-brand-primary; + + .toggle-knob { + transform: translateX(18px); + } + } + + &:not(:disabled):hover { + background: $color-gray-400; + + &.active { + background: $color-brand-dark; + } + } +} + +.toggle-knob { + position: absolute; + top: 3px; + left: 3px; + width: 20px; + height: 20px; + background: white; + border-radius: 50%; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); + transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); +} + +// ===== ACTIONS ===== +.banner-actions { + padding: 0.75rem 1.25rem 1rem; + display: flex; + flex-direction: column; + gap: 0.625rem; +} + +.action-row { + display: flex; + gap: 0.5rem; + + &.primary-actions { + .btn { + flex: 1; + } + } +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.75rem 1rem; + border-radius: 12px; + font-size: 0.875rem; + font-weight: 600; + border: none; + cursor: pointer; + transition: all 0.2s; + -webkit-tap-highlight-color: transparent; + + .material-symbols-outlined { + font-size: 18px; + } +} + +.btn-accept { + background: $gradient-primary; + color: white; + box-shadow: 0 4px 12px rgba($color-brand-primary, 0.25); + + &:hover { + box-shadow: 0 6px 16px rgba($color-brand-primary, 0.35); + transform: translateY(-1px); + } + + &:active { + transform: translateY(0); + } +} + +.btn-reject { + background: $color-gray-100; + color: $color-text-secondary; + border: 1px solid $color-gray-200; + + &:hover { + background: $color-gray-200; + color: $color-text-primary; + } +} + +.btn-save { + background: $color-brand-primary; + color: white; + width: 100%; + + &:hover { + background: $color-brand-dark; + } +} + +.btn-details { + display: flex; + align-items: center; + justify-content: center; + gap: 0.375rem; + padding: 0.625rem; + background: transparent; + border: none; + font-size: 0.8125rem; + font-weight: 500; + color: $color-text-tertiary; + cursor: pointer; + transition: color 0.2s; + + .material-symbols-outlined { + font-size: 18px; + } + + &:hover { + color: $color-brand-primary; + } +} + +// ===== FOOTER ===== +.banner-footer { + padding: 0.75rem 1.25rem 1rem; + border-top: 1px solid $color-gray-100; + display: flex; + justify-content: center; + align-items: center; + gap: 0.5rem; + font-size: 0.75rem; + + a { + color: $color-text-tertiary; + text-decoration: none; + transition: color 0.2s; + + &:hover { + color: $color-brand-primary; + text-decoration: underline; + } + } + + .separator { + color: $color-gray-300; + } +} + +// ===== MATERIAL ICONS ===== +.material-symbols-outlined { + font-variation-settings: "FILL" 0, "wght" 500, "GRAD" 0, "opsz" 24; + line-height: 1; +} + +// ===== SAFE AREA (iPhone Notch) ===== +@supports (padding: max(0px)) { + .cookie-banner { + padding-bottom: max(0px, env(safe-area-inset-bottom)); + } +} + +// ===== REDUCED MOTION ===== +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} + +// ===== TABLET+ ADJUSTMENTS ===== +@media (min-width: 640px) { + .banner-header { + padding: 1.5rem 1.5rem 0; + } + + .banner-icon { + width: 52px; + height: 52px; + + .material-symbols-outlined { + font-size: 26px; + } + } + + .banner-title-area h2 { + font-size: 1.25rem; + } + + .banner-content { + padding: 1.25rem 1.5rem; + } + + .banner-text { + font-size: 0.9375rem; + } + + .cookie-category { + padding: 1rem; + } + + .category-name { + font-size: 0.9375rem; + } + + .category-description { + font-size: 0.8125rem; + } + + .banner-actions { + padding: 1rem 1.5rem 1.25rem; + } + + .banner-footer { + padding: 1rem 1.5rem 1.25rem; + } +} diff --git a/apps/frontend/src/app/shared/cookie-banner/cookie-banner.component.ts b/apps/frontend/src/app/shared/cookie-banner/cookie-banner.component.ts new file mode 100644 index 0000000..44d3870 --- /dev/null +++ b/apps/frontend/src/app/shared/cookie-banner/cookie-banner.component.ts @@ -0,0 +1,107 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; +import { Subscription } from 'rxjs'; +import { ConsentService } from '../../services/consent/consent.service'; +import { CONSENT_CATEGORIES, ConsentCategoryInfo } from '../../services/consent/consent.model'; + +@Component({ + selector: 'app-cookie-banner', + standalone: true, + imports: [CommonModule, RouterModule], + templateUrl: './cookie-banner.component.html', + styleUrls: ['./cookie-banner.component.scss'] +}) +export class CookieBannerComponent implements OnInit, OnDestroy { + showBanner = false; + showDetails = false; + isClosing = false; + categories = CONSENT_CATEGORIES; + + // Für individuelle Auswahl + functionalEnabled = false; + analyticsEnabled = false; + + private subscription?: Subscription; + + constructor(public consentService: ConsentService) {} + + ngOnInit(): void { + this.subscription = this.consentService.showBanner$.subscribe(show => { + if (show) { + this.isClosing = false; + this.showBanner = true; + // Aktuelle Werte laden falls vorhanden + const consent = this.consentService.consent; + this.functionalEnabled = consent.functional; + this.analyticsEnabled = consent.analytics; + } else { + // Wenn Banner geschlossen wird, Animation abspielen + if (this.showBanner) { + this.closeBannerWithAnimation(); + } + } + }); + } + + ngOnDestroy(): void { + this.subscription?.unsubscribe(); + } + + private closeBannerWithAnimation(): void { + this.isClosing = true; + setTimeout(() => { + this.showBanner = false; + this.isClosing = false; + }, 300); // Match animation duration + } + + acceptAll(): void { + this.isClosing = true; + setTimeout(() => { + this.consentService.acceptAll(); + this.showBanner = false; + this.isClosing = false; + }, 300); + } + + rejectAll(): void { + this.isClosing = true; + setTimeout(() => { + this.consentService.acceptNecessaryOnly(); + this.showBanner = false; + this.isClosing = false; + }, 300); + } + + toggleDetails(): void { + this.showDetails = !this.showDetails; + } + + saveCustom(): void { + this.isClosing = true; + setTimeout(() => { + this.consentService.saveCustomConsent( + this.functionalEnabled, + this.analyticsEnabled + ); + this.showBanner = false; + this.isClosing = false; + }, 300); + } + + toggleCategory(category: string): void { + if (category === 'functional') { + this.functionalEnabled = !this.functionalEnabled; + } else if (category === 'analytics') { + this.analyticsEnabled = !this.analyticsEnabled; + } + } + + isCategoryEnabled(category: ConsentCategoryInfo): boolean { + if (category.id === 'necessary') return true; + if (category.id === 'functional') return this.functionalEnabled; + if (category.id === 'analytics') return this.analyticsEnabled; + return false; + } +} diff --git a/apps/frontend/src/app/shared/footer/footer.component.html b/apps/frontend/src/app/shared/footer/footer.component.html new file mode 100644 index 0000000..7bb91a7 --- /dev/null +++ b/apps/frontend/src/app/shared/footer/footer.component.html @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/apps/frontend/src/app/shared/footer/footer.component.scss b/apps/frontend/src/app/shared/footer/footer.component.scss new file mode 100644 index 0000000..8a0e894 --- /dev/null +++ b/apps/frontend/src/app/shared/footer/footer.component.scss @@ -0,0 +1,99 @@ +@import '../../utils/shared-styles.scss'; + +.site-footer { + position: relative; + background: rgba(255, 255, 255, 0.7); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-top: 1px solid rgba(255, 255, 255, 0.3); + box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.06); + + // Optional: Gradient overlay für mehr Tiefe + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient( + 135deg, + rgba(59, 130, 246, 0.03) 0%, + rgba(147, 51, 234, 0.03) 100% + ); + pointer-events: none; + } + + .footer__inner { + position: relative; + z-index: 1; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding-block: 1.5rem; + + @media (max-width: 600px) { + flex-direction: column; + text-align: center; + gap: 0.75rem; + } + } + + .footer__copy { + font-size: 0.875rem; + color: $color-text-secondary; + font-weight: 500; + } + + .footer__nav { + display: flex; + gap: 2rem; + + @media (max-width: 600px) { + gap: 1.5rem; + } + + a, .cookie-link { + position: relative; + font-size: 0.875rem; + font-weight: 500; + color: $color-text-primary; + text-decoration: none; + padding: 0.5rem 1rem; + border-radius: 8px; + transition: all 200ms ease; + cursor: pointer; + background: transparent; + border: none; + font-family: inherit; + + &:hover { + color: $color-brand-primary; + background: rgba(59, 130, 246, 0.08); + } + + &:focus-visible { + outline: 2px solid $color-brand-primary; + outline-offset: 2px; + } + + // Optional: Subtle underline effect + &::after { + content: ''; + position: absolute; + bottom: 0.25rem; + left: 1rem; + right: 1rem; + height: 1px; + background: $color-brand-primary; + transform: scaleX(0); + transition: transform 200ms ease; + } + + &:hover::after { + transform: scaleX(1); + } + } + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/shared/footer/footer.component.spec.ts b/apps/frontend/src/app/shared/footer/footer.component.spec.ts new file mode 100644 index 0000000..3f93915 --- /dev/null +++ b/apps/frontend/src/app/shared/footer/footer.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FooterComponent } from './footer.component'; + +describe('FooterComponent', () => { + let component: FooterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FooterComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(FooterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/frontend/src/app/shared/footer/footer.component.ts b/apps/frontend/src/app/shared/footer/footer.component.ts new file mode 100644 index 0000000..22429d3 --- /dev/null +++ b/apps/frontend/src/app/shared/footer/footer.component.ts @@ -0,0 +1,20 @@ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { Router, RouterLink } from '@angular/router'; +import { ConsentService } from '../../services/consent/consent.service'; + +@Component({ + selector: 'app-footer', + standalone: true, + imports: [CommonModule, RouterLink], + templateUrl: './footer.component.html', + styleUrl: './footer.component.scss' +}) +export class FooterComponent { + constructor(public router: Router, private consentService: ConsentService) {} + year = new Date().getFullYear(); + + openCookieSettings(): void { + this.consentService.showSettings(); + } +} diff --git a/apps/frontend/src/app/shared/header/header.component.html b/apps/frontend/src/app/shared/header/header.component.html new file mode 100644 index 0000000..7592a0a --- /dev/null +++ b/apps/frontend/src/app/shared/header/header.component.html @@ -0,0 +1,102 @@ + \ No newline at end of file diff --git a/apps/frontend/src/app/shared/header/header.component.scss b/apps/frontend/src/app/shared/header/header.component.scss new file mode 100644 index 0000000..3907825 --- /dev/null +++ b/apps/frontend/src/app/shared/header/header.component.scss @@ -0,0 +1,669 @@ +@import '../../utils/shared-styles.scss'; + +/* ===== SAFARI/iOS NUCLEAR TOUCH FIX ===== */ +/* Safari hat einen Bug mit backdrop-filter + Touch Events */ +/* Diese Fixes sind AGGRESSIV aber notwendig */ + +.site-header { + /* ALLE klickbaren Elemente brauchen diese Fixes */ + a, button, [role="button"], .brand-btn, .nav-toggle, .dropdown-item { + -webkit-tap-highlight-color: rgba(0,0,0,0) !important; + -webkit-touch-callout: none !important; + touch-action: manipulation !important; + cursor: pointer !important; + /* Eigener Compositing-Layer für jedes Element */ + -webkit-transform: translate3d(0,0,0); + transform: translate3d(0,0,0); + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + /* Verhindert dass Safari Events "optimiert" */ + will-change: transform; + } +} + +.site-header { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; + background: rgba(255, 255, 255, 0.95); + border-bottom: 1px solid rgba(0, 0, 0, 0.06); + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.04), + 0 0 0 1px rgba(255, 255, 255, 0.5) inset; + transition: all 0.3s ease; + padding-top: env(safe-area-inset-top); + /* Stacking Context isolieren */ + isolation: isolate; + + /* backdrop-filter NUR auf Desktop - auf Mobile ist es der Touch-Bug-Verursacher! */ + @media (min-width: 900px) { + -webkit-backdrop-filter: blur(20px) saturate(160%); + backdrop-filter: blur(20px) saturate(160%); + background: rgba(255, 255, 255, 0.85); + } + + .header__inner { + display: flex; + align-items: center; + gap: 1rem; + min-height: 56px; + padding: 0.5rem 1rem; + + @media (min-width: 900px) { + gap: 2rem; + min-height: 72px; + padding: 0 1.5rem; + } + } +} + +/* ===== LOGO ===== */ + +.brand-btn { + display: flex; + align-items: center; + justify-content: center; + padding: 0; + margin: 0; + background: transparent; + border: none; + cursor: pointer; + flex-shrink: 0; + /* Safari Touch Fix - KRITISCH */ + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; + touch-action: manipulation; + -webkit-appearance: none; + appearance: none; + -webkit-user-select: none; + user-select: none; + position: relative; + z-index: 1; + + &:focus-visible { + outline: 2px solid $color-brand-primary; + outline-offset: 4px; + border-radius: $radius-sm; + } +} + +.brand { + height: 32px; + width: auto; + object-fit: contain; + pointer-events: none; /* Klicks gehen zum Parent-Button */ + transition: transform 0.3s ease; + flex-shrink: 0; + + .brand-btn:hover & { + transform: scale(1.05); + } + + @media (min-width: 900px) { + height: 44px; + } +} + +/* ===== NAVIGATION ===== */ + +.nav { + display: none; + flex: 1; + + @media (min-width: 900px) { + display: flex; + align-items: center; + justify-content: space-between; + } + + /* Mobile Menu Open */ + &.open { + display: flex; + flex-direction: column; + position: fixed; + top: calc(env(safe-area-inset-top) + 56px); + left: 0; + right: 0; + /* KEIN backdrop-filter auf Mobile! Verursacht Touch-Bugs in Safari */ + background: rgba(255, 255, 255, 0.98); + border-bottom: 1px solid rgba(0, 0, 0, 0.06); + padding: 0.75rem; + gap: 0.5rem; + box-shadow: $shadow-glass; + animation: slideDown 0.3s ease; + isolation: isolate; + + .nav__main, + .nav__auth { + flex-direction: column; + width: 100%; + gap: 0.375rem; + } + + .nav__main { + padding-bottom: 0.75rem; + border-bottom: 1px solid $color-gray-200; + } + + .nav__auth { + padding-top: 0.5rem; + } + + a, button { + width: 100%; + justify-content: flex-start; + padding: 0.875rem 1rem; + } + + .nav-link--icon { + width: 100%; + height: auto; + padding: 0.875rem 1rem; + border-radius: $radius-md; + + &::after { + content: attr(title); + margin-left: 0.5rem; + font-weight: 500; + font-family: inherit; + font-size: 0.9375rem; + } + } + + /* Hide notification in mobile nav (shown in header__mobile-actions) */ + app-admin-notification-center { + display: none; + } + } +} + +.nav__main { + display: flex; + align-items: center; + gap: 0.25rem; + + a { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.875rem; + min-height: 44px; /* iOS minimum touch target */ + border-radius: $radius-md; + font-size: 0.9375rem; + font-weight: 500; + color: $color-text-secondary; + text-decoration: none; + transition: all 0.2s ease; + white-space: nowrap; + /* Safari Touch Fix */ + -webkit-tap-highlight-color: transparent; + touch-action: manipulation; + position: relative; + z-index: 1; /* Über dem backdrop-filter Layer */ + + &:hover { + color: $color-brand-primary; + background: rgba(37, 99, 235, 0.06); + } + + &.active { + color: $color-brand-primary; + font-weight: 600; + background: $gradient-subtle; + } + + &:focus-visible { + outline: none; + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2); + } + } +} + +.nav__auth { + display: flex; + align-items: center; + gap: 0.5rem; + + app-admin-notification-center { + display: flex; + align-items: center; + } +} + +/* ===== ICON BUTTONS (Logged In) ===== */ + +.nav-link--icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 44px; /* iOS minimum touch target */ + height: 44px; /* iOS minimum touch target */ + padding: 0; + border-radius: $radius-full; + color: $color-brand-primary; // Always colored + text-decoration: none; + border: 2px solid transparent; + background: transparent; + cursor: pointer; + transition: all 0.2s ease; + /* Safari Touch Fix */ + -webkit-tap-highlight-color: transparent; + touch-action: manipulation; + position: relative; + z-index: 1; + + &:hover { + color: $color-brand-primary; + background: rgba(37, 99, 235, 0.08); + border-color: rgba(37, 99, 235, 0.15); + transform: translateY(-1px); + } + + &.active { + color: $color-brand-primary; + background: $gradient-subtle; + border-color: rgba(37, 99, 235, 0.2); + } + + &:focus-visible { + outline: none; + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2); + } +} + +/* ===== SPECIAL NAV LINKS ===== */ + +.nav-link--primary { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.5rem 1.25rem; + min-height: 44px; /* iOS minimum touch target */ + border-radius: $radius-md; + font-size: 0.9375rem; + font-weight: 600; + text-decoration: none; + background: $gradient-primary; + color: white; + box-shadow: $shadow-brand; + transition: all 0.2s ease; + /* Safari Touch Fix */ + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; + touch-action: manipulation; + position: relative; + z-index: 1; + + &:hover { + box-shadow: $shadow-brand-hover; + transform: translateY(-2px); + background: $gradient-primary-hover; + } + + &:focus-visible { + outline: none; + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.3), $shadow-brand; + } +} + +.nav-link--admin { + color: #ea580c !important; // Always orange + + &:hover { + color: #c2410c !important; + background: rgba(234, 88, 12, 0.1) !important; + border-color: rgba(234, 88, 12, 0.25) !important; + } + + &.active { + color: #c2410c !important; + background: rgba(234, 88, 12, 0.12) !important; + border-color: rgba(234, 88, 12, 0.3) !important; + } +} + +/* Admin Link in Main Navigation */ +.nav-link--admin-main { + color: #ea580c !important; + + .material-symbols-outlined { + color: #ea580c !important; + } + + &:hover { + color: #c2410c !important; + background: rgba(234, 88, 12, 0.08) !important; + + .material-symbols-outlined { + color: #c2410c !important; + } + } + + &.active { + color: #c2410c !important; + background: rgba(234, 88, 12, 0.12) !important; + font-weight: 600; + + .material-symbols-outlined { + color: #c2410c !important; + } + } +} + +.nav-link--logout { + color: #dc2626 !important; // Always red + + &:hover { + color: #b91c1c !important; + background: rgba(239, 68, 68, 0.1) !important; + border-color: rgba(239, 68, 68, 0.25) !important; + } +} + +/* ===== MOBILE ACTIONS ===== */ + +.header__mobile-actions { + display: flex; + align-items: center; + gap: 0.5rem; + margin-left: auto; + + @media (min-width: 900px) { + display: none; + } + + app-admin-notification-center { + display: flex; + align-items: center; + /* Bell vertical centering tweak */ + position: relative; + top: 4px; + + .notification-bell { + vertical-align: middle; + } + } +} + +.nav-toggle { + display: flex; + align-items: center; + justify-content: center; + width: 44px; /* iOS minimum touch target */ + height: 44px; /* iOS minimum touch target */ + padding: 0; + background: transparent; + border: 2px solid $color-gray-200; + border-radius: $radius-md; + color: $color-text-primary; + cursor: pointer; + transition: all 0.2s ease; + /* Safari Touch Fix - KRITISCH für Hamburger Menu */ + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; + touch-action: manipulation; + position: relative; + z-index: 10; /* Muss über allem sein */ + -webkit-appearance: none; + appearance: none; + + @media (min-width: 900px) { + display: none; + } + + &:hover { + border-color: $color-brand-primary; + background: rgba(37, 99, 235, 0.06); + color: $color-brand-primary; + } + + &:focus-visible { + outline: none; + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2); + } + + .material-symbols-outlined { + font-size: 22px; + } +} + +/* ===== SERVICES DROPDOWN ===== */ + +.nav-dropdown-container { + position: relative; + /* Wichtig: Container soll NICHT größer sein als sein Inhalt */ + display: inline-flex; + flex-direction: column; + align-items: flex-start; + + // Desktop: Hover funktioniert auch + @media (min-width: 900px) { + &:hover .nav-dropdown { + opacity: 1; + visibility: visible; + transform: translateY(0); + } + } +} + +.nav-dropdown-trigger { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.875rem; + min-height: 44px; /* iOS minimum touch target */ + border-radius: $radius-md; + font-size: 0.9375rem; + font-weight: 500; + color: $color-text-secondary; + background: transparent; + border: none; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; + /* Safari Touch Fix */ + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; + touch-action: manipulation; + -webkit-appearance: none; + appearance: none; + position: relative; + z-index: 1; + + &:hover { + color: $color-brand-primary; + background: rgba(37, 99, 235, 0.06); + } + + &.active { + color: $color-brand-primary; + font-weight: 600; + background: $gradient-subtle; + } + + .dropdown-arrow { + transition: transform 0.2s ease; + } +} + +.dropdown-open .nav-dropdown-trigger { + color: $color-brand-primary; + background: rgba(37, 99, 235, 0.06); + + .dropdown-arrow { + transform: rotate(180deg); + } +} + +.nav-dropdown { + // Desktop Styles + @media (min-width: 900px) { + position: absolute; + top: calc(100% + 0.5rem); + left: 0; + min-width: 220px; + background: white; + border-radius: $radius-lg; + box-shadow: + 0 10px 40px rgba(0, 0, 0, 0.12), + 0 0 0 1px rgba(0, 0, 0, 0.05); + padding: 0.5rem; + opacity: 0; + visibility: hidden; + transform: translateY(-8px); + transition: all 0.2s ease; + z-index: 100; + + // Show on click + .dropdown-open & { + opacity: 1; + visibility: visible; + transform: translateY(0); + } + } + + // Mobile Styles: Inline expanded + @media (max-width: 899px) { + display: flex; + flex-direction: column; + gap: 0.25rem; + padding: 0.5rem 0 0.5rem 1rem; + margin-top: 0.25rem; + border-left: 2px solid rgba($color-brand-primary, 0.2); + margin-left: 1rem; + } +} + +.dropdown-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.625rem 0.875rem; + min-height: 44px; /* iOS minimum touch target */ + border-radius: $radius-md; + font-size: 0.875rem; + font-weight: 500; + color: $color-text-secondary; + text-decoration: none; + cursor: pointer; + transition: all 0.15s ease; + background: transparent; + border: none; + width: 100%; + text-align: left; + /* Safari Touch Fix */ + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; + touch-action: manipulation; + -webkit-appearance: none; + appearance: none; + position: relative; + z-index: 1; + + .material-symbols-outlined { + font-size: 18px; + color: $color-text-tertiary; + transition: color 0.15s; + } + + &:hover { + background: rgba($color-brand-primary, 0.06); + color: $color-brand-primary; + + .material-symbols-outlined { + color: $color-brand-primary; + } + } + + &--all { + font-weight: 600; + color: $color-brand-primary; + + .material-symbols-outlined { + color: $color-brand-primary; + } + } +} + +.dropdown-divider { + height: 1px; + background: $color-gray-200; + margin: 0.375rem 0.5rem; +} + +// Mobile Menu anpassen +.nav.open { + .nav-dropdown-container { + width: 100%; + /* WICHTIG: Nicht die anderen Elemente überdecken! */ + position: relative; + z-index: auto; + } + + .nav-dropdown-trigger { + width: 100%; + justify-content: flex-start; + padding: 0.875rem 1rem; + } + + .nav-dropdown { + animation: slideDown 0.2s ease; + /* Dropdown innerhalb des Flows, nicht overlay */ + position: relative; + z-index: auto; + } +} + +/* ===== BACKDROP ===== */ + +.backdrop { + position: fixed; + inset: 0; + /* KEIN backdrop-filter! Verursacht Touch-Bugs in Safari */ + background: rgba(15, 23, 42, 0.6); + z-index: -1; + animation: fadeIn 0.3s ease; + height: 2000px; + cursor: pointer; + -webkit-tap-highlight-color: transparent; + touch-action: manipulation; +} + +/* ===== ANIMATIONS ===== */ + +@keyframes slideDown { + from { + transform: translateY(-10px); + opacity: 0; + } + + to { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +/* ===== MOTION PREFERENCES ===== */ + +@media (prefers-reduced-motion: reduce) { + + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/shared/header/header.component.spec.ts b/apps/frontend/src/app/shared/header/header.component.spec.ts new file mode 100644 index 0000000..204ed6e --- /dev/null +++ b/apps/frontend/src/app/shared/header/header.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HeaderComponent } from './header.component'; + +describe('HeaderComponent', () => { + let component: HeaderComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [HeaderComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(HeaderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/frontend/src/app/shared/header/header.component.ts b/apps/frontend/src/app/shared/header/header.component.ts new file mode 100644 index 0000000..eb3a306 --- /dev/null +++ b/apps/frontend/src/app/shared/header/header.component.ts @@ -0,0 +1,187 @@ +import { CommonModule, DOCUMENT } from '@angular/common'; +import { Component, Inject, OnInit, OnDestroy, HostListener } from '@angular/core'; +import { Router, RouterModule } from '@angular/router'; +import { AuthService, User, UserRole } from '../../services/auth.service'; +import { ToastService } from '../toasts/toast.service'; +import { ConfirmationService } from '../confirmation/confirmation.service'; +import { AdminNotificationCenterComponent } from '../admin-notification-center/admin-notification-center.component'; +import { ApiService, ServiceCategory } from '../../api/api.service'; +import { Subscription } from 'rxjs'; + +interface NavCategory { + id: string; + name: string; + icon: string; +} + +@Component({ + selector: 'app-header', + standalone: true, + imports: [RouterModule, CommonModule, AdminNotificationCenterComponent], + templateUrl: './header.component.html', + styleUrl: './header.component.scss' +}) +export class HeaderComponent implements OnInit, OnDestroy { + open = false; + user: User | null = null; + UserRole = UserRole; + + // Services Dropdown + servicesDropdownOpen = false; + serviceCategories: NavCategory[] = []; + private categoriesSub?: Subscription; + private routerSub?: Subscription; + + constructor( + @Inject(DOCUMENT) private doc: Document, + public router: Router, + private authService: AuthService, + private toasts: ToastService, + private confirmationService: ConfirmationService, + private api: ApiService + ) { } + + ngOnInit(): void { + this.authService.currentUser$.subscribe(user => { + this.user = user; + }); + + // Load service categories for dropdown + this.loadServiceCategories(); + + // Reset dropdown on EVERY route change + this.routerSub = this.router.events.subscribe(() => { + this.servicesDropdownOpen = false; + this.open = false; + this.doc.body.style.overflow = ''; + this.doc.body.style.touchAction = ''; + }); + } + + ngOnDestroy(): void { + this.categoriesSub?.unsubscribe(); + this.routerSub?.unsubscribe(); + } + + private loadServiceCategories(): void { + this.categoriesSub = this.api.getServicesCatalog().subscribe({ + next: (categories) => { + this.serviceCategories = categories.map(cat => ({ + id: cat.slug, + name: cat.name, + icon: cat.materialIcon + })); + }, + error: () => { + // Fallback - zeige Dropdown trotzdem, aber ohne Kategorien + this.serviceCategories = []; + } + }); + } + + // Close dropdown when clicking outside + // KEIN touchend - das stört den Burger-Button! + @HostListener('document:click', ['$event']) + onDocumentClick(event: Event): void { + const target = event.target as HTMLElement; + // Wenn Klick NICHT im Dropdown-Container UND NICHT im nav-toggle ist + if (!target.closest('.nav-dropdown-container') && !target.closest('.nav-toggle')) { + this.servicesDropdownOpen = false; + } + } + + toggleServicesDropdown(event: Event): void { + event.preventDefault(); + event.stopPropagation(); + this.servicesDropdownOpen = !this.servicesDropdownOpen; + } + + navigateToCategory(categoryId: string): void { + this.router.navigate(['/services'], { + queryParams: { category: categoryId } + }); + this.servicesDropdownOpen = false; + this.closeMenu(); + } + + navigateToServices(): void { + this.router.navigate(['/services']); + this.servicesDropdownOpen = false; + this.closeMenu(); + } + + // Public damit es im Template verwendet werden kann + closeMenu(): void { + this.open = false; + this.servicesDropdownOpen = false; + this.doc.body.style.overflow = ''; + this.doc.body.style.touchAction = ''; + } + + // Safari Touch Fix: Event explizit behandeln + toggle(event?: Event) { + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + + // IMMER Dropdown resetten - egal ob öffnen oder schließen + this.servicesDropdownOpen = false; + + // Dann toggle das Menu + this.open = !this.open; + this.doc.body.style.overflow = this.open ? 'hidden' : ''; + this.doc.body.style.touchAction = this.open ? 'none' : ''; + } + + // Safari Touch Fix: Event explizit behandeln + routeTo(route: string, event?: Event) { + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + this.router.navigate([route]); + this.open = false; + this.servicesDropdownOpen = false; + this.doc.body.style.overflow = ''; + this.doc.body.style.touchAction = ''; + } + + // GEÄNDERT! + async logout() { + const confirmed = await this.confirmationService.confirm({ + title: 'Abmelden', + message: 'Möchtest du dich wirklich abmelden?', + confirmText: 'Ja, abmelden', + cancelText: 'Abbrechen', + type: 'danger', + icon: 'logout' + }); + + if (confirmed) { + this.authService.logout(); + this.toasts.success('Erfolgreich abgemeldet.'); + this.router.navigate(['/']); + + this.open = false; + this.doc.body.style.overflow = ''; + this.doc.body.style.touchAction = ''; + } + } + + getRoleBadgeClass(): string { + return this.user?.role === UserRole.ADMIN ? 'badge--admin' : 'badge--user'; + } + + getRoleLabel(): string { + return this.user?.role === UserRole.ADMIN ? 'Admin' : 'User'; + } + + isAdmin(): boolean { + return this.user?.role === UserRole.ADMIN; + } + + isInAdminArea(): boolean { + return this.router.url.startsWith('/admin'); + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/shared/icon/icon.component.html b/apps/frontend/src/app/shared/icon/icon.component.html new file mode 100644 index 0000000..c1ee7f0 --- /dev/null +++ b/apps/frontend/src/app/shared/icon/icon.component.html @@ -0,0 +1 @@ + diff --git a/apps/frontend/src/app/shared/icon/icon.component.scss b/apps/frontend/src/app/shared/icon/icon.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/apps/frontend/src/app/shared/icon/icon.component.spec.ts b/apps/frontend/src/app/shared/icon/icon.component.spec.ts new file mode 100644 index 0000000..61f155b --- /dev/null +++ b/apps/frontend/src/app/shared/icon/icon.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { IconComponent } from './icon.component'; + +describe('IconComponent', () => { + let component: IconComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [IconComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(IconComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/frontend/src/app/shared/icon/icon.component.ts b/apps/frontend/src/app/shared/icon/icon.component.ts new file mode 100644 index 0000000..b805428 --- /dev/null +++ b/apps/frontend/src/app/shared/icon/icon.component.ts @@ -0,0 +1,15 @@ +import { Component, Input } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-icon', + standalone: true, + imports: [CommonModule], + templateUrl: './icon.component.html', + styleUrl: './icon.component.scss' +}) +export class IconComponent { + @Input() icon!: string; + @Input() size!: string; + @Input() color!: string; +} diff --git a/apps/frontend/src/app/shared/page-title/page-title.component.html b/apps/frontend/src/app/shared/page-title/page-title.component.html new file mode 100644 index 0000000..e23711c --- /dev/null +++ b/apps/frontend/src/app/shared/page-title/page-title.component.html @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/apps/frontend/src/app/shared/page-title/page-title.component.scss b/apps/frontend/src/app/shared/page-title/page-title.component.scss new file mode 100644 index 0000000..2f3232f --- /dev/null +++ b/apps/frontend/src/app/shared/page-title/page-title.component.scss @@ -0,0 +1,89 @@ +@import '../../utils/shared-styles.scss'; + +.page-header { + margin-bottom: 2rem; + text-align: center; + margin-top: 2rem; + animation: fadeSlideIn 0.6s ease-out; + + @media (min-width: 768px) { + margin-bottom: 2.5rem; + } +} + +.page-title { + position: relative; + display: inline-block; + margin: 0; + padding: 0; + font-size: clamp(1.75rem, 4vw + 1rem, 2.5rem); + font-weight: 900; + color: $color-text-primary; + letter-spacing: -0.02em; + line-height: 1.2; + + .title-accent { + position: absolute; + bottom: -8px; + left: 50%; + transform: translateX(-50%); + width: 60px; + height: 4px; + background: $gradient-primary; + border-radius: $radius-full; + box-shadow: 0 2px 8px rgba(37, 99, 235, 0.3); + animation: scaleIn 0.4s ease-out 0.3s both; + } +} + +.page-subtitle { + margin: 1rem auto 0; + max-width: 600px; + font-size: clamp(1rem, 1.5vw + 0.5rem, 1.125rem); + color: $color-text-secondary; + line-height: 1.6; + animation: fadeIn 0.5s ease-out 0.2s both; +} + +/* ===== ANIMATIONS ===== */ + +@keyframes fadeSlideIn { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes scaleIn { + from { + transform: translateX(-50%) scaleX(0); + opacity: 0; + } + to { + transform: translateX(-50%) scaleX(1); + opacity: 1; + } +} + +/* ===== MOTION PREFERENCES ===== */ + +@media (prefers-reduced-motion: reduce) { + .page-header, + .page-subtitle, + .title-accent { + animation: none !important; + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/shared/page-title/page-title.component.spec.ts b/apps/frontend/src/app/shared/page-title/page-title.component.spec.ts new file mode 100644 index 0000000..aa0ef96 --- /dev/null +++ b/apps/frontend/src/app/shared/page-title/page-title.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PageTitleComponent } from './page-title.component'; + +describe('PageTitleComponent', () => { + let component: PageTitleComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PageTitleComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PageTitleComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/frontend/src/app/shared/page-title/page-title.component.ts b/apps/frontend/src/app/shared/page-title/page-title.component.ts new file mode 100644 index 0000000..4ebf172 --- /dev/null +++ b/apps/frontend/src/app/shared/page-title/page-title.component.ts @@ -0,0 +1,15 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'app-page-title', + standalone: true, + imports: [CommonModule], + templateUrl: './page-title.component.html', + styleUrl: './page-title.component.scss' +}) +export class PageTitleComponent { + @Input() title: string = ''; + @Input() subtitle?: string; + +} diff --git a/apps/frontend/src/app/shared/seo.service.ts b/apps/frontend/src/app/shared/seo.service.ts new file mode 100644 index 0000000..8381bbc --- /dev/null +++ b/apps/frontend/src/app/shared/seo.service.ts @@ -0,0 +1,128 @@ +import { DOCUMENT } from '@angular/common'; +import { Inject, Injectable } from '@angular/core'; +import { Meta, Title } from '@angular/platform-browser'; + +export interface SeoOptions { + title?: string; + description?: string; + robots?: string; // e.g. "index,follow" + url?: string; // canonical and og:url + image?: string; // absolute or relative + type?: string; // og:type + structuredData?: object | object[]; // JSON-LD +} + +@Injectable({ providedIn: 'root' }) +export class SeoService { + private readonly siteName = 'Leonards & Brandenburger IT'; + private readonly defaultDescription = + 'IT-Dienstleistungen, Webentwicklung und SEO aus einer Hand. Leonards & Brandenburger IT – pragmatisch, transparent und zuverlässig.'; + private readonly defaultImage = '/assets/LM_Logos/Logo1.png'; + + constructor( + private title: Title, + private meta: Meta, + @Inject(DOCUMENT) private doc: Document + ) {} + + update(options: SeoOptions = {}): void { + const origin = this.getOrigin(); + const url = options.url || this.doc.location.href; + const image = this.toAbsoluteUrl(options.image || this.defaultImage, origin); + const title = options.title || this.siteName; + const description = options.description || this.defaultDescription; + const robots = options.robots || 'index,follow'; + const type = options.type || 'website'; + + // Title + this.title.setTitle(title); + + // Basic + this.meta.updateTag({ name: 'description', content: description }); + this.meta.updateTag({ name: 'robots', content: robots }); + this.meta.updateTag({ name: 'theme-color', content: '#0d6efd' }); + + // Canonical + this.setCanonical(url); + + // Open Graph + this.meta.updateTag({ property: 'og:type', content: type }); + this.meta.updateTag({ property: 'og:site_name', content: this.siteName }); + this.meta.updateTag({ property: 'og:locale', content: 'de_DE' }); + this.meta.updateTag({ property: 'og:title', content: title }); + this.meta.updateTag({ property: 'og:description', content: description }); + this.meta.updateTag({ property: 'og:image', content: image }); + this.meta.updateTag({ property: 'og:url', content: url }); + + // Twitter + this.meta.updateTag({ name: 'twitter:card', content: 'summary_large_image' }); + this.meta.updateTag({ name: 'twitter:title', content: title }); + this.meta.updateTag({ name: 'twitter:description', content: description }); + this.meta.updateTag({ name: 'twitter:image', content: image }); + + // JSON-LD + if (options.structuredData) { + this.setJsonLd(options.structuredData); + } else { + // Set default Organization JSON-LD + this.setJsonLd([ + { + '@context': 'https://schema.org', + '@type': 'Organization', + name: this.siteName, + url: origin, + logo: this.toAbsoluteUrl(this.defaultImage, origin), + }, + { + '@context': 'https://schema.org', + '@type': 'WebSite', + name: this.siteName, + url: origin, + }, + ]); + } + } + + setCanonical(url: string): void { + const head = this.doc.head || this.doc.getElementsByTagName('head')[0]; + const linkRel = 'canonical'; + let link: HTMLLinkElement | null = head.querySelector(`link[rel="${linkRel}"]`); + if (!link) { + link = this.doc.createElement('link'); + link.setAttribute('rel', linkRel); + head.appendChild(link); + } + link.setAttribute('href', url); + } + + setJsonLd(data: object | object[]): void { + const head = this.doc.head || this.doc.getElementsByTagName('head')[0]; + const id = 'structured-data'; + let script = this.doc.getElementById(id) as HTMLScriptElement | null; + if (!script) { + script = this.doc.createElement('script'); + script.type = 'application/ld+json'; + script.id = id; + head.appendChild(script); + } + script.text = JSON.stringify(data); + } + + private getOrigin(): string { + try { + // In browser + return (this.doc.defaultView?.location?.origin || this.doc.location.origin); + } catch { + return ''; + } + } + + private toAbsoluteUrl(url: string, origin: string): string { + if (!url) return origin; + if (/^https?:\/\//i.test(url)) return url; + if (url.startsWith('//')) return `${this.doc.location.protocol}${url}`; + // Ensure leading slash + const path = url.startsWith('/') ? url : `/${url}`; + return `${origin}${path}`; + } +} diff --git a/apps/frontend/src/app/shared/service-data.service.ts b/apps/frontend/src/app/shared/service-data.service.ts new file mode 100644 index 0000000..329f088 --- /dev/null +++ b/apps/frontend/src/app/shared/service-data.service.ts @@ -0,0 +1,573 @@ +import { Injectable } from '@angular/core'; + +// ===== INTERFACES ===== +export interface Service { + id: string; + icon: string; + title: string; + description: string; + longDescription: string; + tags: string[]; + keywords: string; +} + +export interface ServiceCategory { + id: string; + name: string; + subtitle: string; + materialIcon: string; + services: Service[]; +} + +export interface FilterOption { + id: string; + name: string; + icon: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class ServiceDataService { + + // ===== ALL SERVICES DATA ===== + private categories: ServiceCategory[] = [ + { + id: 'hardware', + name: 'Hardware', + subtitle: 'Computer, Laptops & Geräte', + materialIcon: 'memory', + services: [ + { + id: 'pc-reparatur', + icon: '🔧', + title: 'PC & Laptop Reparatur', + description: 'Startet nicht, stürzt ab, wird heiß – wir finden das Problem.', + longDescription: 'Dein Computer macht Probleme? Egal ob er nicht mehr startet, ständig abstürzt, überhitzt oder merkwürdige Geräusche macht – wir diagnostizieren das Problem und beheben es. Wir reparieren sowohl Desktop-PCs als auch Laptops aller Marken.', + tags: ['Fehlerdiagnose', 'Komponentenaustausch'], + keywords: 'pc reparatur computer laptop notebook reparieren kaputt defekt' + }, + { + id: 'hardware-upgrade', + icon: '⚡', + title: 'Hardware-Aufrüstung', + description: 'Mehr RAM, größere SSD, neue Grafikkarte – schneller machen.', + longDescription: 'Dein Rechner ist zu langsam? Oft hilft ein gezieltes Upgrade: Mehr Arbeitsspeicher für flüssiges Multitasking, eine SSD für schnelleres Booten und Laden, oder eine bessere Grafikkarte für Gaming und Videobearbeitung. Wir beraten dich, was sich lohnt.', + tags: ['RAM', 'SSD', 'Grafikkarte'], + keywords: 'aufrüstung upgrade ram ssd festplatte arbeitsspeicher schneller' + }, + { + id: 'datenrettung', + icon: '💾', + title: 'Datenrettung', + description: 'Festplatte kaputt? Daten gelöscht? Wir retten was geht.', + longDescription: 'Wichtige Dateien verschwunden oder Festplatte defekt? Wir versuchen deine Daten zu retten – von HDDs, SSDs, USB-Sticks und SD-Karten. Je früher du dich meldest, desto besser die Chancen. Keine Rettung, keine Kosten.', + tags: ['HDD', 'SSD', 'USB-Stick'], + keywords: 'datenrettung daten retten festplatte kaputt backup wiederherstellen recovery' + }, + { + id: 'pc-zusammenbau', + icon: '🖥️', + title: 'PC-Zusammenbau', + description: 'Wir bauen deinen Wunsch-PC zusammen.', + longDescription: 'Du willst einen maßgeschneiderten PC? Wir stellen die Komponenten zusammen, bauen alles sauber auf, installieren das Betriebssystem und testen alles durch. Ob Gaming-Monster, leise Workstation oder kompakter Office-PC.', + tags: ['Gaming-PC', 'Workstation', 'Office'], + keywords: 'zusammenbau pc bauen computer custom gaming workstation zusammenstellen' + }, + { + id: 'kaufberatung', + icon: '🛒', + title: 'Kaufberatung', + description: 'Welcher PC oder Laptop passt zu dir? Ehrliche Beratung.', + longDescription: 'Überfordert von der Auswahl? Wir beraten dich herstellerunabhängig und ehrlich. Kein Upselling, keine Provision – nur die Empfehlung, die zu deinem Budget und deinen Anforderungen passt.', + tags: ['Laptop', 'PC', 'Preis-Leistung'], + keywords: 'kaufberatung hardware beratung welcher pc laptop kaufen' + }, + { + id: 'geraete-einrichtung', + icon: '📦', + title: 'Geräte-Einrichtung', + description: 'Neues Gerät? Wir richten alles ein.', + longDescription: 'Neuen PC oder Laptop gekauft? Wir kümmern uns um alles: Windows einrichten, Programme installieren, Drucker verbinden, Daten vom alten Gerät übertragen. Du bekommst ein fertiges, einsatzbereites System.', + tags: ['Windows', 'Datenumzug', 'Software'], + keywords: 'einrichtung setup neuer pc laptop einrichten installieren konfigurieren' + }, + { + id: 'reinigung-wartung', + icon: '🧹', + title: 'Reinigung & Wartung', + description: 'Staub raus, neue Wärmeleitpaste – leise und kühl.', + longDescription: 'Nach ein paar Jahren sammelt sich Staub an, die Wärmeleitpaste trocknet aus – der PC wird laut und heiß. Wir reinigen alles gründlich, tragen neue Wärmeleitpaste auf und tauschen bei Bedarf Lüfter aus.', + tags: ['Entstaubung', 'Wärmeleitpaste'], + keywords: 'reinigung pc reinigen staub lüfter wärmeleitpaste thermal wartung' + }, + { + id: 'display-reparatur', + icon: '🖼️', + title: 'Display-Reparatur', + description: 'Laptop-Display kaputt? Wir tauschen es aus.', + longDescription: 'Display gesprungen, Pixelfehler oder Bildausfall? Wir tauschen Laptop-Displays aus und reparieren auch Scharniere und Displaykabel. Die meisten Reparaturen sind innerhalb weniger Tage erledigt.', + tags: ['Displaytausch', 'Scharnier'], + keywords: 'display bildschirm monitor reparatur kaputt austausch' + }, + { + id: 'drucker-scanner', + icon: '🖨️', + title: 'Drucker & Scanner', + description: 'Drucker einrichten, WLAN-Druck konfigurieren.', + longDescription: 'Drucker will nicht? Wir richten deinen Drucker ein – lokal oder im Netzwerk, per Kabel oder WLAN. Auch Scanner und Multifunktionsgeräte. Inklusive Treiber-Installation und Testdruck.', + tags: ['WLAN-Drucker', 'Scanner'], + keywords: 'drucker einrichtung scanner multifunktion wlan drucker installieren' + }, + { + id: 'smartphone-tablet', + icon: '📱', + title: 'Smartphone & Tablet', + description: 'Display-Tausch, Akku-Wechsel, Datenübertragung.', + longDescription: 'Handy-Display kaputt oder Akku schwach? Wir reparieren Smartphones und Tablets. Außerdem helfen wir beim Umzug auf ein neues Gerät – alle Daten, Kontakte und Apps sicher übertragen.', + tags: ['Display', 'Akku', 'Daten'], + keywords: 'smartphone handy tablet reparatur display akku tauschen' + }, + { + id: 'nas-speicher', + icon: '💿', + title: 'NAS & Speicher', + description: 'NAS einrichten, RAID konfigurieren, Backup-Strategien.', + longDescription: 'Eigene Cloud zuhause? Wir richten NAS-Systeme von Synology, QNAP und anderen ein. RAID-Konfiguration, Benutzer anlegen, Backup einrichten, Fernzugriff – alles aus einer Hand.', + tags: ['Synology', 'QNAP', 'RAID'], + keywords: 'nas netzwerkspeicher speicher server storage synology qnap' + } + ] + }, + { + id: 'software', + name: 'Software', + subtitle: 'Programme, Apps & Automatisierung', + materialIcon: 'code', + services: [ + { + id: 'desktop-anwendungen', + icon: '💻', + title: 'Desktop-Anwendungen', + description: 'Individuelle Windows-Programme nach deinen Wünschen.', + longDescription: 'Du brauchst ein Programm, das genau das macht, was du willst? Wir entwickeln individuelle Desktop-Anwendungen für Windows – von kleinen Tools bis zu komplexen Business-Anwendungen.', + tags: ['Windows', 'Cross-Platform'], + keywords: 'desktop anwendung programm windows software entwickeln programmieren' + }, + { + id: 'mobile-apps', + icon: '📲', + title: 'Mobile Apps', + description: 'Apps für iOS und Android mit Flutter.', + longDescription: 'Eine App für dein Business? Mit Flutter entwickeln wir Apps, die auf iPhone und Android laufen – mit einer Codebasis. Schneller, günstiger und einfacher zu warten als native Entwicklung.', + tags: ['Flutter', 'iOS', 'Android'], + keywords: 'app mobile ios android smartphone tablet flutter' + }, + { + id: 'automatisierungen', + icon: '🤖', + title: 'Automatisierungen', + description: 'Wiederkehrende Aufgaben automatisch erledigen lassen.', + longDescription: 'Jeden Tag die gleichen Klicks? Wir automatisieren wiederkehrende Aufgaben mit Scripts, Bots und Workflows. Daten übertragen, Reports erstellen, E-Mails versenden – alles auf Autopilot.', + tags: ['Scripts', 'Bots', 'Workflows'], + keywords: 'automatisierung script skript automatisch automatisieren' + }, + { + id: 'api-entwicklung', + icon: '🔌', + title: 'API-Entwicklung', + description: 'Systeme verbinden, Daten austauschen.', + longDescription: 'Deine Systeme sollen miteinander reden? Wir entwickeln REST-APIs und Schnittstellen, die deine Anwendungen verbinden. Sauber dokumentiert und sicher.', + tags: ['REST-API', 'NestJS', 'Node.js'], + keywords: 'api schnittstelle integration rest backend' + }, + { + id: 'datenbanken', + icon: '🗄️', + title: 'Datenbanken', + description: 'Datenbanken designen, optimieren, verwalten.', + longDescription: 'Daten sind das Fundament. Wir designen Datenbankstrukturen, optimieren langsame Queries und migrieren bestehende Datenbanken. PostgreSQL, MySQL, MongoDB – wir kennen sie alle.', + tags: ['PostgreSQL', 'MySQL', 'MongoDB'], + keywords: 'datenbank sql database mysql postgresql mongodb' + }, + { + id: 'excel-vba', + icon: '📊', + title: 'Excel & VBA', + description: 'Makros, VBA-Scripts, komplexe Formeln.', + longDescription: 'Excel kann mehr als du denkst. Wir erstellen Makros und VBA-Scripts, die dir stundenlange Arbeit ersparen. Komplexe Formeln, automatische Reports, Datenvalidierung.', + tags: ['Makros', 'VBA', 'Formeln'], + keywords: 'excel makro vba access tabelle automatisieren' + }, + { + id: 'business-software', + icon: '🏢', + title: 'Business-Software', + description: 'Lager, Kunden, Aufträge – individuelle Lösungen.', + longDescription: 'Standardsoftware passt nicht? Wir entwickeln individuelle Lösungen für Lagerverwaltung, Kundenverwaltung, Auftragsabwicklung – genau auf deine Prozesse zugeschnitten.', + tags: ['Warenwirtschaft', 'CRM'], + keywords: 'erp crm system business software warenwirtschaft' + }, + { + id: 'daten-import-export', + icon: '🔄', + title: 'Daten-Import & Export', + description: 'Daten zwischen Systemen austauschen und migrieren.', + longDescription: 'Daten müssen von A nach B? Wir schreiben Import/Export-Routinen, konvertieren Formate und migrieren Datenbestände. CSV, XML, JSON, Excel – kein Problem.', + tags: ['CSV', 'XML', 'JSON'], + keywords: 'schnittstelle import export datenaustausch csv xml json' + } + ] + }, + { + id: 'web', + name: 'Web', + subtitle: 'Websites & Web-Apps', + materialIcon: 'language', + services: [ + { + id: 'firmenwebsites', + icon: '🌐', + title: 'Firmenwebsites', + description: 'Professionelle Websites für Unternehmen.', + longDescription: 'Dein digitales Aushängeschild. Wir bauen moderne, schnelle Websites, die auf allen Geräten gut aussehen. Für Unternehmen, Handwerker, Freiberufler – individuell gestaltet, nicht von der Stange.', + tags: ['Responsive', 'Modern', 'Schnell'], + keywords: 'website webseite homepage firmenwebsite internetseite erstellen' + }, + { + id: 'landingpages', + icon: '🎯', + title: 'Landingpages', + description: 'Conversion-optimierte Seiten für Kampagnen.', + longDescription: 'Eine Seite, ein Ziel. Wir bauen Landingpages, die konvertieren – für Produktlaunches, Kampagnen oder Lead-Generierung. Klare Struktur, überzeugender Text, schnelle Ladezeit.', + tags: ['Conversion', 'Marketing'], + keywords: 'landingpage landing page marketing conversion' + }, + { + id: 'web-applikationen', + icon: '⚡', + title: 'Web-Applikationen', + description: 'Komplexe Browser-Anwendungen mit Angular.', + longDescription: 'Mehr als eine Website – eine vollwertige Anwendung im Browser. Dashboards, interne Tools, Kundenportale. Mit Angular, TypeScript und modernen Technologien.', + tags: ['Angular', 'TypeScript', 'SPA'], + keywords: 'web app webanwendung angular react frontend browser' + }, + { + id: 'online-shops', + icon: '🛍️', + title: 'Online-Shops', + description: 'E-Commerce Lösungen von einfach bis komplex.', + longDescription: 'Verkaufen im Internet? Wir bauen Online-Shops – von einfachen Shopify-Lösungen bis zu individuellen E-Commerce-Plattformen. Payment, Versand, Bestandsverwaltung inklusive.', + tags: ['Shopify', 'WooCommerce'], + keywords: 'online shop e-commerce webshop shopify woocommerce' + }, + { + id: 'buchungssysteme', + icon: '📅', + title: 'Buchungssysteme', + description: 'Online-Terminbuchung und Reservierungen.', + longDescription: 'Kunden sollen online buchen können? Wir entwickeln Buchungssysteme für Termine, Kurse, Ressourcen. Mit Kalenderansicht, automatischen Bestätigungen und Erinnerungen.', + tags: ['Terminbuchung', 'Kalender'], + keywords: 'buchungssystem terminbuchung kalender online buchen reservierung' + }, + { + id: 'wordpress-cms', + icon: '📝', + title: 'WordPress & CMS', + description: 'WordPress-Seiten, Themes, Plugins.', + longDescription: 'WordPress ist der Klassiker – und wir kennen ihn in- und auswendig. Neue Seiten aufsetzen, Themes anpassen, Plugins entwickeln, bestehende Seiten reparieren.', + tags: ['WordPress', 'Themes', 'Blog'], + keywords: 'wordpress cms content management blog' + }, + { + id: 'seo-optimierung', + icon: '📈', + title: 'SEO-Optimierung', + description: 'Google-Rankings verbessern.', + longDescription: 'Gefunden werden bei Google. Wir optimieren deine Website technisch: Ladezeit, Struktur, Meta-Tags, Schema Markup. Damit du bei relevanten Suchanfragen oben stehst.', + tags: ['On-Page SEO', 'Core Web Vitals'], + keywords: 'seo suchmaschine google optimierung ranking' + }, + { + id: 'hosting-domains', + icon: '☁️', + title: 'Hosting & Domains', + description: 'Domain, Hosting, SSL, DNS – alles einrichten.', + longDescription: 'Das technische Fundament. Wir registrieren Domains, richten Hosting ein, konfigurieren DNS und SSL-Zertifikate. Damit deine Website sicher und erreichbar ist.', + tags: ['Domain', 'SSL', 'DNS'], + keywords: 'hosting domain webspace server ssl' + }, + { + id: 'website-wartung', + icon: '🛠️', + title: 'Website-Wartung', + description: 'Updates, Backups, Security-Patches.', + longDescription: 'Eine Website braucht Pflege. Wir kümmern uns um Updates, Backups, Sicherheits-Patches und kleine Änderungen. Damit du dich um dein Business kümmern kannst.', + tags: ['Updates', 'Backups', 'Security'], + keywords: 'website wartung pflege update aktualisierung' + } + ] + }, + { + id: 'netzwerk', + name: 'Netzwerk', + subtitle: 'WLAN, Server & Cloud', + materialIcon: 'lan', + services: [ + { + id: 'wlan-einrichtung', + icon: '📶', + title: 'WLAN-Einrichtung', + description: 'WLAN optimieren, Reichweite verbessern, Mesh.', + longDescription: 'WLAN zu langsam oder Funklöcher? Wir analysieren dein Netzwerk und optimieren es. Access Points platzieren, Mesh-Systeme einrichten, Kanäle optimieren.', + tags: ['Mesh', 'Access Points', '5GHz'], + keywords: 'wlan wifi wireless funk netzwerk einrichten langsam reichweite' + }, + { + id: 'netzwerk-verkabelung', + icon: '🔗', + title: 'Netzwerk-Verkabelung', + description: 'LAN-Verkabelung planen und umsetzen.', + longDescription: 'Kabel ist King. Wir planen und verlegen Netzwerkkabel, richten Switches ein und dokumentieren alles sauber. Cat6, Cat7 – für stabiles, schnelles Internet.', + tags: ['Cat6/Cat7', 'Switches'], + keywords: 'netzwerk lan kabel ethernet switch router verkabelung' + }, + { + id: 'router-firewall', + icon: '🌐', + title: 'Router & Firewall', + description: 'Router, Firewall-Regeln, Port-Forwarding.', + longDescription: 'Das Tor zum Internet. Wir konfigurieren Router und Firewalls, richten Port-Forwarding ein und sorgen für Sicherheit. FritzBox, pfSense, Ubiquiti – wir kennen sie alle.', + tags: ['FritzBox', 'pfSense', 'VPN'], + keywords: 'router firewall fritz fritzbox konfiguration internet' + }, + { + id: 'vpn-loesungen', + icon: '🔒', + title: 'VPN-Lösungen', + description: 'Sichere Fernzugriffe, Homeoffice-Anbindung.', + longDescription: 'Sicher von überall arbeiten. Wir richten VPN-Verbindungen ein – für Homeoffice, Außendienst oder Standortvernetzung. WireGuard, OpenVPN, IPSec.', + tags: ['WireGuard', 'OpenVPN'], + keywords: 'vpn virtual private network fernzugriff remote tunnel' + }, + { + id: 'server-administration', + icon: '🖥️', + title: 'Server-Administration', + description: 'Windows Server und Linux einrichten und warten.', + longDescription: 'Server sind unser Ding. Windows Server oder Linux – wir installieren, konfigurieren und warten. Updates, Monitoring, Troubleshooting.', + tags: ['Windows Server', 'Linux', 'Debian'], + keywords: 'server windows linux debian ubuntu einrichten administrieren' + }, + { + id: 'active-directory', + icon: '👥', + title: 'Active Directory', + description: 'Domänen, Benutzer, Gruppen, Richtlinien.', + longDescription: 'Zentrale Benutzerverwaltung für Unternehmen. Wir richten Active Directory ein, verwalten Benutzer und Gruppen, konfigurieren Gruppenrichtlinien.', + tags: ['AD', 'GPO', 'Benutzer'], + keywords: 'active directory ad domäne benutzer gruppen rechte' + }, + { + id: 'cloud-loesungen', + icon: '☁️', + title: 'Cloud-Lösungen', + description: 'Microsoft 365, Azure, AWS einrichten.', + longDescription: 'Ab in die Cloud. Wir richten Microsoft 365 ein, migrieren Daten zu Azure oder AWS und verwalten Cloud-Infrastruktur. Hybrid oder Full Cloud.', + tags: ['Microsoft 365', 'Azure', 'AWS'], + keywords: 'cloud microsoft 365 office azure aws google cloud' + }, + { + id: 'backup-strategien', + icon: '💾', + title: 'Backup-Strategien', + description: 'Backup-Konzepte, automatische Sicherungen.', + longDescription: 'Daten sind Gold wert. Wir entwickeln Backup-Konzepte nach der 3-2-1 Regel, richten automatische Sicherungen ein und testen die Wiederherstellung.', + tags: ['3-2-1 Regel', 'Cloud-Backup'], + keywords: 'backup datensicherung sicherung restore wiederherstellung' + }, + { + id: 'virtualisierung', + icon: '📦', + title: 'Virtualisierung', + description: 'VMs, Hyper-V, Proxmox, Docker.', + longDescription: 'Mehr aus der Hardware rausholen. Wir richten Virtualisierung ein – Hyper-V, Proxmox, VMware. Oder Container mit Docker für moderne Anwendungen.', + tags: ['Hyper-V', 'Proxmox', 'Docker'], + keywords: 'virtualisierung vm vmware hyper-v proxmox docker container' + }, + { + id: 'it-sicherheit', + icon: '🛡️', + title: 'IT-Sicherheit', + description: 'Firewall, Virenschutz, Security-Audits.', + longDescription: 'Sicherheit ist kein Zustand, sondern ein Prozess. Wir prüfen deine IT auf Schwachstellen, richten Firewalls und Virenschutz ein, implementieren 2FA.', + tags: ['Firewall', 'Antivirus', '2FA'], + keywords: 'sicherheit security firewall antivirus virenschutz malware' + }, + { + id: 'email-systeme', + icon: '📧', + title: 'E-Mail-Systeme', + description: 'Exchange, IMAP/SMTP einrichten.', + longDescription: 'E-Mail ist Kommunikation Nr. 1. Wir richten E-Mail-Server ein, migrieren Postfächer, konfigurieren Spam-Filter und sorgen für zuverlässige Zustellung.', + tags: ['Exchange', 'IMAP', 'Spam-Filter'], + keywords: 'e-mail mail exchange postfach mailserver imap smtp' + }, + { + id: 'monitoring', + icon: '📊', + title: 'Monitoring', + description: 'Server und Netzwerk überwachen, Alerts.', + longDescription: 'Probleme erkennen, bevor sie eskalieren. Wir richten Monitoring ein – für Server, Netzwerk, Dienste. Mit Dashboards und Alerts bei Problemen.', + tags: ['Uptime', 'Alerts', 'Grafana'], + keywords: 'monitoring überwachung netzwerk server nagios zabbix' + } + ] + }, + { + id: 'support', + name: 'Support', + subtitle: 'Hilfe remote oder vor Ort', + materialIcon: 'support_agent', + services: [ + { + id: 'remote-support', + icon: '🖱️', + title: 'Remote-Support', + description: 'Schnelle Hilfe per Fernwartung.', + longDescription: 'Problem schildern, wir schalten uns drauf. Per TeamViewer oder AnyDesk helfen wir dir sofort – ohne Anfahrt, ohne Wartezeit. Die schnellste Lösung für die meisten Probleme.', + tags: ['TeamViewer', 'AnyDesk'], + keywords: 'remote support fernwartung fernzugriff teamviewer hilfe' + }, + { + id: 'vor-ort-service', + icon: '🚗', + title: 'Vor-Ort-Service', + description: 'Wir kommen zu dir – im Westerwald und Umgebung.', + longDescription: 'Manchmal muss man vor Ort sein. Wir kommen zu dir – im Westerwald, Altenkirchen und Umgebung. Für Hardware-Probleme, Netzwerk-Einrichtung oder wenn Remote nicht reicht.', + tags: ['Westerwald', 'Altenkirchen'], + keywords: 'vor ort vor-ort vorort service techniker kommen westerwald' + }, + { + id: 'wartungsvertraege', + icon: '📋', + title: 'Wartungsverträge', + description: 'Regelmäßige Wartung, bevorzugter Support.', + longDescription: 'Planbare IT-Kosten und bevorzugter Support. Mit einem Wartungsvertrag kümmern wir uns regelmäßig um deine Systeme und du hast einen festen Ansprechpartner.', + tags: ['Regelmäßig', 'Priorität'], + keywords: 'wartung wartungsvertrag regelmäßig service monatlich' + }, + { + id: 'schulungen', + icon: '🎓', + title: 'Schulungen', + description: 'Einweisungen, IT-Grundlagen, Workshops.', + longDescription: 'Wissen ist Macht. Wir schulen dich und dein Team – in neuer Software, IT-Grundlagen oder speziellen Themen. Einzeln oder als Workshop.', + tags: ['Einweisung', 'Workshop'], + keywords: 'schulung training einweisung lernen erklären workshop' + }, + { + id: 'software-installation', + icon: '📀', + title: 'Software-Installation', + description: 'Programme installieren und konfigurieren.', + longDescription: 'Neue Software soll aufs System? Wir installieren und konfigurieren Programme, sorgen für die richtigen Einstellungen und weisen dich ein.', + tags: ['Installation', 'Konfiguration'], + keywords: 'installation software installieren programm einrichten' + }, + { + id: 'updates-patches', + icon: '🔄', + title: 'Updates & Patches', + description: 'Betriebssystem, Treiber, Security-Patches.', + longDescription: 'Aktuell bleiben ist wichtig. Wir bringen deine Systeme auf den neuesten Stand – Windows-Updates, Treiber, Security-Patches. Kontrolliert und ohne böse Überraschungen.', + tags: ['Windows Update', 'Treiber'], + keywords: 'update aktualisierung windows treiber patch' + }, + { + id: 'virenentfernung', + icon: '🦠', + title: 'Virenentfernung', + description: 'Malware, Trojaner, Adware – sauber machen.', + longDescription: 'System verseucht? Wir entfernen Viren, Trojaner, Adware und andere Schadsoftware. Gründlich und nachhaltig – damit dein System wieder sauber läuft.', + tags: ['Malware', 'Trojaner'], + keywords: 'virus malware trojaner entfernen reinigen säubern infiziert' + }, + { + id: 'performance-optimierung', + icon: '🚀', + title: 'Performance-Optimierung', + description: 'PC zu langsam? Wir machen ihn wieder flott.', + longDescription: 'Rechner lahm? Wir finden die Ursache: Autostart aufräumen, Bloatware entfernen, Festplatte bereinigen, Dienste optimieren. Danach läuft er wieder.', + tags: ['Autostart', 'Bereinigung'], + keywords: 'performance langsam optimieren schneller tuning beschleunigen' + }, + { + id: 'it-beratung', + icon: '💡', + title: 'IT-Beratung', + description: 'Welche Lösung passt? Ehrliche Beratung.', + longDescription: 'Nicht sicher, was du brauchst? Wir beraten dich herstellerunabhängig und ehrlich. Keine versteckten Interessen – nur die Lösung, die für dich passt.', + tags: ['Strategie', 'Neutral'], + keywords: 'beratung consulting it-beratung strategie konzept planen' + }, + { + id: 'notfall-support', + icon: '🚨', + title: 'Notfall-Support', + description: 'Server down? Hilfe auch außerhalb der Geschäftszeiten.', + longDescription: 'IT-Notfall wartet nicht auf Bürozeiten. Bei dringenden Problemen helfen wir auch außerhalb der regulären Zeiten. Server down, Datenverlust, Hackerangriff – wir sind da.', + tags: ['24/7', 'Notfall'], + keywords: 'notfall notdienst dringend schnell sofort hilfe' + } + ] + } + ]; + + // ===== FILTER OPTIONS ===== + private filters: FilterOption[] = [ + { id: 'all', name: 'Alle', icon: '' }, + { id: 'hardware', name: 'Hardware', icon: 'memory' }, + { id: 'software', name: 'Software', icon: 'code' }, + { id: 'web', name: 'Web', icon: 'language' }, + { id: 'netzwerk', name: 'Netzwerk', icon: 'lan' }, + { id: 'support', name: 'Support', icon: 'support_agent' } + ]; + + // ===== PUBLIC METHODS ===== + + getCategories(): ServiceCategory[] { + return this.categories; + } + + getFilters(): FilterOption[] { + return this.filters; + } + + getServiceById(id: string): Service | undefined { + for (const cat of this.categories) { + const service = cat.services.find(s => s.id === id); + if (service) return service; + } + return undefined; + } + + /** + * Gibt die Service-ID (Slug) zurück - wird direkt ans Backend gesendet + * @param serviceId Die ID des Services aus dem Frontend + * @returns Der Slug für das Backend (identisch mit serviceId) + */ + getServiceSlug(serviceId: string): string { + return serviceId || 'allgemeine-anfrage'; + } + + getCategoryByServiceId(serviceId: string): ServiceCategory | undefined { + return this.categories.find(cat => + cat.services.some(s => s.id === serviceId) + ); + } + + getAllServices(): Service[] { + return this.categories.flatMap(cat => cat.services); + } +} \ No newline at end of file diff --git a/apps/frontend/src/app/shared/toasts/toast-container.component.scss b/apps/frontend/src/app/shared/toasts/toast-container.component.scss new file mode 100644 index 0000000..12a2105 --- /dev/null +++ b/apps/frontend/src/app/shared/toasts/toast-container.component.scss @@ -0,0 +1,269 @@ +/* ===== TOAST CONTAINER ===== */ +:host { + position: fixed; + top: 0; + right: 0; + pointer-events: none; + z-index: 9999; + width: 100%; + max-width: 380px; + padding: 1rem; + padding-top: 80px; +} + +.toast-wrap { + display: flex; + flex-direction: column; + gap: .625rem; + align-items: flex-end; +} + +/* ===== TOAST CARD ===== */ +.toast { + --toast-color: #2563eb; + + pointer-events: auto; + display: flex; + align-items: flex-start; + gap: .75rem; + + width: 100%; + padding: .875rem 1rem; + + background: rgba(255, 255, 255, .92); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, .6); + border-radius: 14px; + + box-shadow: + 0 8px 32px rgba(0, 0, 0, .08), + 0 2px 8px rgba(0, 0, 0, .04), + inset 0 1px 0 rgba(255, 255, 255, .8); + + position: relative; + overflow: hidden; + + transform: translateX(120%); + opacity: 0; + animation: toast-in .4s cubic-bezier(.16, 1, .3, 1) forwards; + + /* Subtle accent line */ + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: var(--toast-color); + border-radius: 14px 14px 0 0; + } +} + +/* ===== ICON ===== */ +.toast-icon { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: 10px; + background: rgba(var(--toast-color-rgb, 37, 99, 235), .1); + color: var(--toast-color); + flex-shrink: 0; + + app-icon { + font-size: 1.25rem; + } +} + +/* ===== CONTENT ===== */ +.toast-content { + flex: 1; + min-width: 0; + padding-top: .125rem; +} + +.toast-message { + display: block; + font-size: .875rem; + font-weight: 500; + line-height: 1.45; + color: #1e293b; + word-break: break-word; +} + +.toast-action { + display: inline-flex; + align-items: center; + margin-top: .5rem; + padding: .375rem .75rem; + background: transparent; + border: 1.5px solid var(--toast-color); + border-radius: 8px; + color: var(--toast-color); + font-size: .75rem; + font-weight: 600; + cursor: pointer; + transition: all .2s; + + &:hover { + background: var(--toast-color); + color: white; + } +} + +/* ===== CLOSE ===== */ +.toast-close { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + margin: -.125rem -.25rem 0 0; + background: transparent; + border: none; + border-radius: 8px; + color: #94a3b8; + cursor: pointer; + transition: all .15s; + flex-shrink: 0; + + app-icon { + font-size: 1rem; + } + + &:hover { + background: rgba(0, 0, 0, .06); + color: #64748b; + } +} + +/* ===== PROGRESS ===== */ +.toast-progress { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 3px; + background: var(--toast-color); + opacity: .25; + transform-origin: left; + animation: progress linear forwards; +} + +/* ===== VARIANTS ===== */ +.toast.success { + --toast-color: #22c55e; + --toast-color-rgb: 34, 197, 94; +} + +.toast.error { + --toast-color: #ef4444; + --toast-color-rgb: 239, 68, 68; +} + +.toast.warning { + --toast-color: #f59e0b; + --toast-color-rgb: 245, 158, 11; +} + +.toast.info { + --toast-color: #3b82f6; + --toast-color-rgb: 59, 130, 246; +} + +/* ===== CLOSING ===== */ +.toast.closing { + animation: toast-out .25s cubic-bezier(.4, 0, 1, 1) forwards; +} + +/* ===== ANIMATIONS ===== */ +@keyframes toast-in { + from { + opacity: 0; + transform: translateX(120%) scale(.9); + } + to { + opacity: 1; + transform: translateX(0) scale(1); + } +} + +@keyframes toast-out { + from { + opacity: 1; + transform: translateX(0) scale(1); + } + to { + opacity: 0; + transform: translateX(120%) scale(.9); + } +} + +@keyframes progress { + from { transform: scaleX(1); } + to { transform: scaleX(0); } +} + +/* ===== MOBILE ===== */ +@media (max-width: 480px) { + :host { + top: auto; + bottom: 0; + left: 0; + right: 0; + max-width: 100%; + padding: .75rem; + padding-bottom: calc(env(safe-area-inset-bottom, 0px) + .75rem); + } + + .toast-wrap { + align-items: stretch; + } + + .toast { + border-radius: 12px; + + &::before { + border-radius: 12px 12px 0 0; + } + } + + @keyframes toast-in { + from { + opacity: 0; + transform: translateY(100%) scale(.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } + } + + @keyframes toast-out { + from { + opacity: 1; + transform: translateY(0) scale(1); + } + to { + opacity: 0; + transform: translateY(100%) scale(.95); + } + } +} + +/* ===== REDUCED MOTION ===== */ +@media (prefers-reduced-motion: reduce) { + .toast, + .toast.closing { + animation: none !important; + opacity: 1 !important; + transform: none !important; + } + + .toast-progress { + display: none; + } +} diff --git a/apps/frontend/src/app/shared/toasts/toast-container.component.ts b/apps/frontend/src/app/shared/toasts/toast-container.component.ts new file mode 100644 index 0000000..35f0d6f --- /dev/null +++ b/apps/frontend/src/app/shared/toasts/toast-container.component.ts @@ -0,0 +1,153 @@ +import { Component, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ToastService } from './toast.service'; +import { ToastOptions } from './toast.model'; +import { IconComponent } from '../icon/icon.component'; + +type InternalToast = Required & { + closing?: boolean; + timeout?: any; + touchX?: number; + offsetX?: number; +}; + +@Component({ + selector: 'app-toast-container', + standalone: true, + imports: [CommonModule, IconComponent], + template: ` +
+
+ + + + + +
+ {{ t.message }} + +
+ + + + + +
+
+
+ `, + styleUrls: ['./toast-container.component.scss'] +}) +export class ToastContainerComponent { + toasts = signal([]); + + private iconMap: Record = { + success: 'check_circle', + error: 'error', + warning: 'warning', + info: 'info' + }; + + constructor(private svc: ToastService) { + this.svc.stream$.subscribe((evt: any) => { + if (evt?.close) { this._closeById(evt.id); return; } + const t = this._materialize(evt as ToastOptions); + this.toasts.update(list => [t, ...list]); + this._armTimer(t); + }); + } + + getIcon(type: string): string { + return this.iconMap[type] || 'info'; + } + + transform(t: InternalToast) { + const x = t.offsetX ?? 0; + const damp = Math.max(0, 1 - Math.min(Math.abs(x) / 160, 1)); + return `translate3d(${x}px,0,0) scale(${0.98 + 0.02 * damp})`; + } + + dismiss(t: InternalToast) { + this._clearTimer(t); + t.closing = true; + setTimeout(() => this._remove(t.id), 200); + } + + onAction(t: InternalToast) { + try { t.onAction?.(); } catch { } + this.dismiss(t); + } + + onPointerDown(ev: PointerEvent, t: InternalToast) { + t.touchX = ev.clientX; + t.offsetX = 0; + (ev.currentTarget as HTMLElement).setPointerCapture(ev.pointerId); + this._clearTimer(t); + } + + onPointerMove(ev: PointerEvent, t: InternalToast) { + if (t.touchX == null) return; + t.offsetX = ev.clientX - t.touchX; + this.toasts.update(v => [...v]); + } + + onPointerUp(ev: PointerEvent, t: InternalToast) { + (ev.currentTarget as HTMLElement).releasePointerCapture(ev.pointerId); + const shouldDismiss = Math.abs(t.offsetX ?? 0) > 80; + if (shouldDismiss) { this.dismiss(t); } + else { t.offsetX = 0; this._armTimer(t); this.toasts.update(v => [...v]); } + t.touchX = undefined; + } + + private _materialize(opts: ToastOptions): InternalToast { + return { + id: opts.id!, + message: opts.message, + type: opts.type ?? 'info', + duration: opts.duration ?? 3500, + dismissible: opts.dismissible ?? true, + actionLabel: opts.actionLabel ?? '', + onAction: opts.onAction ?? (() => { }), + closing: false, + timeout: undefined, + touchX: undefined, + offsetX: 0 + }; + } + + private _armTimer(t: InternalToast) { + if (!t.duration || t.duration <= 0) return; + t.timeout = setTimeout(() => this.dismiss(t), t.duration); + } + + private _clearTimer(t: InternalToast) { + if (t.timeout) { clearTimeout(t.timeout); t.timeout = undefined; } + } + + private _remove(id: string) { + this.toasts.update(list => list.filter(x => x.id !== id)); + } + + private _closeById(id: string) { + const t = this.toasts().find(x => x.id === id); + if (t) this.dismiss(t); + } +} diff --git a/apps/frontend/src/app/shared/toasts/toast.model.ts b/apps/frontend/src/app/shared/toasts/toast.model.ts new file mode 100644 index 0000000..7d0aadc --- /dev/null +++ b/apps/frontend/src/app/shared/toasts/toast.model.ts @@ -0,0 +1,12 @@ +// src/app/toasts/toast.model.ts +export type ToastType = 'success' | 'error' | 'warning' | 'info'; + +export interface ToastOptions { + id?: string; + message: string; + type?: ToastType; + duration?: number; // ms, default 3500 + dismissible?: boolean; // default true + actionLabel?: string; // optional Button + onAction?: () => void; // callback für Action +} diff --git a/apps/frontend/src/app/shared/toasts/toast.service.ts b/apps/frontend/src/app/shared/toasts/toast.service.ts new file mode 100644 index 0000000..923e0da --- /dev/null +++ b/apps/frontend/src/app/shared/toasts/toast.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@angular/core'; +import { Subject } from 'rxjs'; +import { ToastOptions } from './toast.model'; + +@Injectable({ providedIn: 'root' }) +export class ToastService { + private _stream = new Subject(); + stream$ = this._stream.asObservable(); + + show(opts: ToastOptions) { + const id = opts.id ?? (globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2)); + this._stream.next({ + ...opts, + id, + duration: opts.duration ?? 3500, + dismissible: opts.dismissible ?? true, + }); + return id; + } + + success(message: string, opts: Partial = {}) { return this.show({ message, type: 'success', ...opts }); } + error(message: string, opts: Partial = {}) { return this.show({ message, type: 'error', ...opts }); } + warning(message: string, opts: Partial = {}) { return this.show({ message, type: 'warning', ...opts }); } + info(message: string, opts: Partial = {}) { return this.show({ message, type: 'info', ...opts }); } + + close(id: string) { + this._stream.next({ id, close: true } as any); + } +} diff --git a/apps/frontend/src/app/utils/shared-styles.scss b/apps/frontend/src/app/utils/shared-styles.scss new file mode 100644 index 0000000..0694f13 --- /dev/null +++ b/apps/frontend/src/app/utils/shared-styles.scss @@ -0,0 +1,520 @@ +// ===== MODERNE FARBPALETTE MIT GLASSMORPHISM ===== + +// Primäre Hintergründe +$color-background-main: #f8fafc; // Sehr helles Blaugrau +$color-background-secondary: #f1f5f9; // Etwas dunkler für Kontrast +$color-background-elevated: #ffffff; // Weiß für Cards + +// Text +$color-text-primary: #0f172a; // Tiefes Slate +$color-text-secondary: #64748b; // Mittleres Slate +$color-text-tertiary: #94a3b8; // Helles Slate für Subtexte + +// Branding mit modernem Gradient-System +$color-brand-primary: #2563eb; // Kräftiges Blau +$color-brand-secondary: #3b82f6; // Helleres Blau +$color-brand-dark: #1e40af; // Dunkles Blau +$color-brand-light: #60a5fa; // Sehr helles Blau +$color-brand-bg: #eff6ff; // Subtiler Blau-Hintergrund + +// Gradient Definitionen +$gradient-primary: linear-gradient(135deg, $color-brand-primary 0%, $color-brand-secondary 100%); +$gradient-primary-hover: linear-gradient(135deg, $color-brand-dark 0%, $color-brand-primary 100%); +$gradient-subtle: linear-gradient(135deg, rgba(37, 99, 235, 0.05) 0%, rgba(59, 130, 246, 0.05) 100%); +$gradient-bg: linear-gradient(135deg, #f8fafc 0%, #e7eeff 100%); + +// Glassmorphism +$glass-bg: rgba(255, 255, 255, 0.95); +$glass-bg-light: rgba(255, 255, 255, 0.7); +$glass-border: rgba(255, 255, 255, 0.8); +$glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.08); +$glass-shadow-hover: 0 16px 48px rgba(0, 0, 0, 0.12); + +// Neutrale Farben +$color-white: #ffffff; +$color-black: #000000; +$color-gray-50: #f8fafc; +$color-gray-100: #f1f5f9; +$color-gray-200: #e2e8f0; +$color-gray-300: #cbd5e1; +$color-gray-400: #94a3b8; +$color-gray-500: #64748b; +$color-gray-600: #475569; +$color-gray-700: #334155; +$color-gray-800: #1e293b; +$color-gray-900: #0f172a; + +// Legacy Support (für bestehenden Code) +$color-gray-dark: $color-gray-700; +$color-gray: $color-gray-500; +$color-gray-light: $color-gray-200; + +// Statusfarben +$color-success: #10b981; // Modernes Grün +$color-success-light: #d1fae5; +$color-warning: #f59e0b; // Warmes Orange +$color-warning-light: #fef3c7; +$color-error: #ef4444; // Klares Rot +$color-error-light: #fee2e2; +$color-info: #06b6d4; // Cyan +$color-info-light: #cffafe; + +// ===== SHADOWS & EFFECTS ===== +$shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05); +$shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.06); +$shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); +$shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.1); +$shadow-xl: 0 16px 48px rgba(0, 0, 0, 0.12); +$shadow-2xl: 0 24px 64px rgba(0, 0, 0, 0.15); + +// Glassmorphism Shadows +$shadow-glass: 0 8px 32px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(255, 255, 255, 0.5) inset; +$shadow-glass-hover: 0 16px 48px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(255, 255, 255, 0.8) inset; + +// Brand Shadows +$shadow-brand: 0 4px 16px rgba(37, 99, 235, 0.25); +$shadow-brand-hover: 0 8px 24px rgba(37, 99, 235, 0.35); + +// ===== BORDER RADIUS ===== +$radius-xs: 6px; +$radius-sm: 8px; +$radius-md: 12px; +$radius-lg: 16px; +$radius-xl: 20px; +$radius-2xl: 24px; +$radius-full: 9999px; + +// ===== TRANSITIONS ===== +$transition-fast: 0.15s ease; +$transition-base: 0.3s ease; +$transition-slow: 0.4s ease; + +// ===== SPACING SYSTEM ===== +$spacing-xs: 0.25rem; // 4px +$spacing-sm: 0.5rem; // 8px +$spacing-md: 0.75rem; // 12px +$spacing-lg: 1rem; // 16px +$spacing-xl: 1.5rem; // 24px +$spacing-2xl: 2rem; // 32px +$spacing-3xl: 3rem; // 48px +$spacing-4xl: 4rem; // 64px + +// ===== UTILITY CLASSES ===== + +// Flexbox +.flex { + display: flex; +} + +.flex-center { + display: flex; + justify-content: center; + align-items: center; +} + +.flex-between { + display: flex; + justify-content: space-between; + align-items: center; +} + +.flex-col { + display: flex; + flex-direction: column; +} + +// Pointer +.pointer { + cursor: pointer !important; +} + +// ===== MODERNE BUTTONS ===== + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.75rem 1.5rem; + min-height: 44px; /* iOS minimum touch target */ + border-radius: $radius-md; + font-weight: 600; + font-size: 0.9375rem; + line-height: 1; + border: none; + cursor: pointer; + transition: all $transition-base; + position: relative; + overflow: hidden; + text-decoration: none; + white-space: nowrap; + /* GPU Acceleration für Safari */ + -webkit-transform: translateZ(0); + transform: translateZ(0); + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + /* Touch-Optimierung */ + -webkit-tap-highlight-color: transparent; + touch-action: manipulation; + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; + } + + // Primary Button mit Gradient + &--primary { + background: $gradient-primary; + color: $color-white; + box-shadow: $shadow-brand; + + &:hover { + background: $gradient-primary-hover; + box-shadow: $shadow-brand-hover; + transform: translateY(-2px); + } + + &:active { + transform: translateY(0); + box-shadow: $shadow-brand; + } + + &:focus-visible { + outline: none; + box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.2); + } + } + + // Ghost Button mit Glassmorphism + &--ghost { + background: transparent; + color: $color-brand-primary; + border: 2px solid $color-brand-primary; + box-shadow: none; + + &:hover { + background: $color-brand-primary; + color: $color-white; + transform: translateY(-2px); + box-shadow: $shadow-brand; + } + + &:active { + transform: translateY(0); + } + + &:focus-visible { + outline: none; + box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.2); + } + } + + // Secondary Button mit Glassmorphism + &--secondary { + background: $glass-bg; + color: $color-brand-primary; + border: 1px solid rgba(37, 99, 235, 0.2); + backdrop-filter: blur(10px); + box-shadow: $shadow-glass; + + &:hover { + background: $color-white; + border-color: $color-brand-primary; + transform: translateY(-2px); + box-shadow: $shadow-glass-hover; + } + + &:active { + transform: translateY(0); + } + + &:focus-visible { + outline: none; + box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.2); + } + } + + // Tertiary (Text Button) + &--tertiary { + background: transparent; + color: $color-brand-primary; + padding: 0.5rem 1rem; + + &:hover { + background: rgba(37, 99, 235, 0.06); + } + + &:focus-visible { + outline: none; + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15); + } + } + + // Size Variations + &--small { + padding: 0.5rem 1rem; + font-size: 0.875rem; + } + + &--large { + padding: 0.875rem 1.75rem; + font-size: 1rem; + } + + &--xl { + padding: 1rem 2rem; + font-size: 1.0625rem; + border-radius: $radius-lg; + } + + // Full Width + &--full { + width: 100%; + } +} + +// ===== GLASSMORPHISM CARDS ===== + +.card { + background: $glass-bg; + backdrop-filter: blur(20px); + border: 1px solid $glass-border; + border-radius: $radius-lg; + padding: 1.5rem; + box-shadow: $shadow-glass; + transition: all $transition-base; + + &:hover { + transform: translateY(-4px); + box-shadow: $shadow-glass-hover; + } + + &--solid { + background: $color-white; + backdrop-filter: none; + } + + &--bordered { + border: 2px solid $color-gray-200; + } +} + +// ===== LINKS ===== + +.link { + color: $color-brand-primary; + text-decoration: none; + font-weight: 600; + transition: color $transition-fast; + cursor: pointer; + + &:hover { + color: $color-brand-dark; + text-decoration: underline; + } + + &:focus-visible { + outline: none; + border-radius: $radius-sm; + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.18); + } +} + +// ===== BADGES & PILLS ===== + +.badge { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.375rem 0.75rem; + border-radius: $radius-full; + font-size: 0.875rem; + font-weight: 600; + line-height: 1; + + &--primary { + background: $gradient-primary; + color: $color-white; + box-shadow: $shadow-brand; + } + + &--secondary { + background: $color-gray-100; + color: $color-gray-700; + } + + &--success { + background: $color-success-light; + color: darken($color-success, 10%); + } + + &--warning { + background: $color-warning-light; + color: darken($color-warning, 10%); + } + + &--error { + background: $color-error-light; + color: darken($color-error, 10%); + } + + &--glass { + background: $glass-bg-light; + backdrop-filter: blur(10px); + border: 1px solid $glass-border; + color: $color-text-primary; + } +} + +// ===== INPUT STYLES (für Formulare) ===== + +.input { + width: 100%; + padding: 0.75rem 1rem; + border: 2px solid $color-gray-200; + border-radius: $radius-md; + font-size: 0.9375rem; + color: $color-text-primary; + background: $color-white; + transition: all $transition-fast; + + &:hover { + border-color: $color-gray-300; + } + + &:focus { + outline: none; + border-color: $color-brand-primary; + box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.1); + } + + &::placeholder { + color: $color-text-tertiary; + } + + &--glass { + background: $glass-bg; + backdrop-filter: blur(10px); + border-color: $glass-border; + } +} + +.textarea { + @extend .input; + min-height: 100px; + resize: vertical; +} + +// ===== DIVIDER ===== + +.divider { + height: 1px; + background: linear-gradient(90deg, transparent, $color-gray-200, transparent); + border: none; + margin: 2rem 0; +} + +// ===== TEXT UTILITIES ===== + +.text-primary { + color: $color-text-primary; +} + +.text-secondary { + color: $color-text-secondary; +} + +.text-tertiary { + color: $color-text-tertiary; +} + +.text-brand { + color: $color-brand-primary; +} + +.gradient-text { + background: $gradient-primary; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +// ===== SPACING UTILITIES ===== + +.mt-xs { margin-top: $spacing-xs; } +.mt-sm { margin-top: $spacing-sm; } +.mt-md { margin-top: $spacing-md; } +.mt-lg { margin-top: $spacing-lg; } +.mt-xl { margin-top: $spacing-xl; } + +.mb-xs { margin-bottom: $spacing-xs; } +.mb-sm { margin-bottom: $spacing-sm; } +.mb-md { margin-bottom: $spacing-md; } +.mb-lg { margin-bottom: $spacing-lg; } +.mb-xl { margin-bottom: $spacing-xl; } + +.gap-xs { gap: $spacing-xs; } +.gap-sm { gap: $spacing-sm; } +.gap-md { gap: $spacing-md; } +.gap-lg { gap: $spacing-lg; } +.gap-xl { gap: $spacing-xl; } + +// ===== PAGE ENTRANCE ANIMATIONS ===== + +@keyframes pageSlideUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes pageFadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes pageScaleIn { + from { + opacity: 0; + transform: scale(0.96); + } + to { + opacity: 1; + transform: scale(1); + } +} + +.page-animate { + animation: pageSlideUp 0.5s cubic-bezier(0.22, 1, 0.36, 1) forwards; +} + +.page-animate-fade { + animation: pageFadeIn 0.4s ease-out forwards; +} + +.page-animate-scale { + animation: pageScaleIn 0.45s cubic-bezier(0.22, 1, 0.36, 1) forwards; +} + +// Staggered children animation +.page-animate-stagger { + > * { + opacity: 0; + animation: pageSlideUp 0.5s cubic-bezier(0.22, 1, 0.36, 1) forwards; + + @for $i from 1 through 10 { + &:nth-child(#{$i}) { + animation-delay: #{$i * 0.08}s; + } + } + } +} \ No newline at end of file diff --git a/apps/frontend/src/index.html b/apps/frontend/src/index.html new file mode 100644 index 0000000..f5da137 --- /dev/null +++ b/apps/frontend/src/index.html @@ -0,0 +1,41 @@ + + + + + Leonards & Brandenburger IT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/frontend/src/main.ts b/apps/frontend/src/main.ts new file mode 100644 index 0000000..35b00f3 --- /dev/null +++ b/apps/frontend/src/main.ts @@ -0,0 +1,6 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { AppComponent } from './app/app.component'; + +bootstrapApplication(AppComponent, appConfig) + .catch((err) => console.error(err)); diff --git a/apps/frontend/src/robots.txt b/apps/frontend/src/robots.txt new file mode 100644 index 0000000..7ce14f6 --- /dev/null +++ b/apps/frontend/src/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://Leonards & Brandenburger IT.de/sitemap.xml \ No newline at end of file diff --git a/apps/frontend/src/sitemap.xml b/apps/frontend/src/sitemap.xml new file mode 100644 index 0000000..bb046d1 --- /dev/null +++ b/apps/frontend/src/sitemap.xml @@ -0,0 +1,69 @@ + + + + https://Leonards & Brandenburger IT.de/ + weekly + 1.0 + 2026-01-26 + + + https://Leonards & Brandenburger IT.de/about + monthly + 0.6 + 2026-01-26 + + + https://Leonards & Brandenburger IT.de/booking + monthly + 0.6 + 2026-01-26 + + + https://Leonards & Brandenburger IT.de/contact + monthly + 0.6 + 2026-01-26 + + + https://Leonards & Brandenburger IT.de/faq + monthly + 0.6 + 2026-01-26 + + + https://Leonards & Brandenburger IT.de/imprint + monthly + 0.6 + 2026-01-26 + + + https://Leonards & Brandenburger IT.de/it-services + monthly + 0.6 + 2026-01-26 + + + https://Leonards & Brandenburger IT.de/policy + monthly + 0.6 + 2026-01-26 + + + https://Leonards & Brandenburger IT.de/process + monthly + 0.6 + 2026-01-26 + + + https://Leonards & Brandenburger IT.de/services + weekly + 0.8 + 2026-01-26 + + + https://Leonards & Brandenburger IT.de/survey + monthly + 0.6 + 2026-01-26 + + \ No newline at end of file diff --git a/apps/frontend/src/styles.scss b/apps/frontend/src/styles.scss new file mode 100644 index 0000000..e60c418 --- /dev/null +++ b/apps/frontend/src/styles.scss @@ -0,0 +1,221 @@ +/* ===== GLOBAL STYLES - CLEAN & MINIMAL ===== */ + +@import './app/utils/shared-styles.scss'; + +/* ===== RESET ===== */ +*, *::before, *::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + font-size: 16px; + scroll-behavior: smooth; + -webkit-font-smoothing: antialiased; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + font-size: 1rem; + line-height: 1.6; + color: $color-text-primary; + background: $gradient-bg; + background-attachment: fixed; + min-height: 100vh; + overflow-x: hidden; +} + +/* ===== TYPOGRAPHY ===== */ +h1, h2, h3, h4, h5, h6 { + margin: 0; + font-weight: 700; + line-height: 1.2; + color: $color-text-primary; +} + +h1 { font-size: clamp(1.75rem, 5vw, 3rem); font-weight: 800; } +h2 { font-size: clamp(1.5rem, 4vw, 2.25rem); font-weight: 700; } +h3 { font-size: clamp(1.25rem, 3vw, 1.75rem); } +h4 { font-size: clamp(1.1rem, 2.5vw, 1.35rem); } + +p { margin: 0; } + +a { + color: inherit; + text-decoration: none; +} + +img, svg { + max-width: 100%; + height: auto; + display: block; +} + +/* ===== CONTAINER ===== */ +.container { + width: 100%; + max-width: 700px; + margin: 0 auto; + padding: 0 1rem; + + @media (min-width: 900px) { + max-width: 1000px; + padding: 0 1.5rem; + } + + @media (min-width: 1200px) { + max-width: 1400px; + padding: 0 2rem; + } +} + +/* ===== SCROLLBAR ===== */ +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: $color-gray-100; +} + +::-webkit-scrollbar-thumb { + background: $color-brand-primary; + border-radius: 4px; +} + +* { + scrollbar-width: thin; + scrollbar-color: $color-brand-primary $color-gray-100; +} + +/* ===== SELECTION ===== */ +::selection { + background: rgba(37, 99, 235, 0.2); +} + +/* ===== FOCUS STATES ===== */ +*:focus-visible { + outline: 2px solid $color-brand-primary; + outline-offset: 2px; +} + +/* ===== MATERIAL ICONS ===== */ +.material-symbols-outlined { + font-family: 'Material Symbols Outlined'; + font-size: 24px; + line-height: 1; + display: inline-flex; + align-items: center; + justify-content: center; + font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; +} + +/* ===== GLASS PANEL ===== */ +.glass-panel { + background: $glass-bg; + border: 1px solid $glass-border; + border-radius: $radius-xl; + box-shadow: $shadow-glass; + padding: 1.5rem; + + @media (min-width: 641px) { + backdrop-filter: blur(20px); + padding: 2rem; + } +} + +/* ===== FORM ELEMENTS ===== */ +input[type="text"], +input[type="email"], +input[type="password"], +input[type="tel"], +input[type="number"], +textarea, +select { + width: 100%; + padding: 0.75rem 1rem; + border: 2px solid $color-gray-200; + border-radius: $radius-md; + font-size: 1rem; + font-family: inherit; + color: $color-text-primary; + background: $color-white; + transition: border-color 0.2s; + + &:focus { + outline: none; + border-color: $color-brand-primary; + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); + } + + &::placeholder { + color: $color-text-tertiary; + } +} + +textarea { + min-height: 120px; + resize: vertical; +} + +label { + display: block; + font-size: 0.9375rem; + font-weight: 600; + margin-bottom: 0.5rem; +} + +/* ===== UTILITIES ===== */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} + +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.text-center { text-align: center; } + +/* ===== ANIMATIONS ===== */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.spinner { + width: 32px; + height: 32px; + border: 3px solid $color-gray-200; + border-top-color: $color-brand-primary; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +/* ===== REDUCED MOTION ===== */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} + +/* ===== MOBILE PERFORMANCE ===== */ +@media (max-width: 640px) { + *, *::before, *::after { + backdrop-filter: none !important; + } +} \ No newline at end of file diff --git a/apps/frontend/styles.scss b/apps/frontend/styles.scss new file mode 100644 index 0000000..a2e5c2a --- /dev/null +++ b/apps/frontend/styles.scss @@ -0,0 +1,264 @@ +/* ===== GLOBAL STYLES - CLEAN & MINIMAL ===== */ + +@import './app/utils/shared-styles.scss'; + +/* ===== SAFARI & iOS GLOBAL HOTFIX ===== */ +/* Behebt Touch-Probleme, Button-Verzögerungen und Tap-Highlight auf Safari/iOS */ + +/* Entfernt 300ms Touch-Delay auf allen interaktiven Elementen */ +a, button, input, select, textarea, [role="button"], [tabindex] { + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; + touch-action: manipulation; /* Entfernt 300ms delay, erlaubt nur pan/zoom */ +} + +/* Fix für Safari Button-Styling */ +button, [type="button"], [type="submit"], [type="reset"] { + -webkit-appearance: none; + appearance: none; +} + +/* GPU-beschleunigte Transforms für alle Buttons (verhindert Ruckeln) */ +.btn, button, a.btn, [role="button"] { + -webkit-transform: translateZ(0); + transform: translateZ(0); + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + will-change: transform, box-shadow; +} + +/* Fix: Material Icons Platzhalter (verhindert Layout-Shift beim Laden) */ +.material-symbols-outlined { + width: 24px; + height: 24px; + min-width: 24px; + font-display: block; /* Zeigt nichts bis Font geladen */ +} + +/* Font-Loading Fallback (verhindert FOUT - Flash of Unstyled Text) */ +html { + font-display: swap; +} + +/* ===== RESET ===== */ +*, *::before, *::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + font-size: 16px; + scroll-behavior: smooth; + -webkit-font-smoothing: antialiased; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + font-size: 1rem; + line-height: 1.6; + color: $color-text-primary; + background: $gradient-bg; + background-attachment: fixed; + min-height: 100vh; + overflow-x: hidden; +} + +/* ===== TYPOGRAPHY ===== */ +h1, h2, h3, h4, h5, h6 { + margin: 0; + font-weight: 700; + line-height: 1.2; + color: $color-text-primary; +} + +h1 { font-size: clamp(1.75rem, 5vw, 3rem); font-weight: 800; } +h2 { font-size: clamp(1.5rem, 4vw, 2.25rem); font-weight: 700; } +h3 { font-size: clamp(1.25rem, 3vw, 1.75rem); } +h4 { font-size: clamp(1.1rem, 2.5vw, 1.35rem); } + +p { margin: 0; } + +a { + color: inherit; + text-decoration: none; +} + +img, svg { + max-width: 100%; + height: auto; + display: block; +} + +/* ===== CONTAINER ===== */ +.container { + width: 100%; + max-width: 700px; + margin: 0 auto; + padding: 0 1rem; + + @media (min-width: 900px) { + max-width: 900px; + padding: 0 1.5rem; + } + + @media (min-width: 1200px) { + max-width: 1100px; + padding: 0 2rem; + } +} + +/* ===== SCROLLBAR ===== */ +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: $color-gray-100; +} + +::-webkit-scrollbar-thumb { + background: $color-brand-primary; + border-radius: 4px; +} + +* { + scrollbar-width: thin; + scrollbar-color: $color-brand-primary $color-gray-100; +} + +/* ===== SELECTION ===== */ +::selection { + background: rgba(37, 99, 235, 0.2); +} + +/* ===== FOCUS STATES ===== */ +*:focus-visible { + outline: 2px solid $color-brand-primary; + outline-offset: 2px; +} + +/* ===== MATERIAL ICONS ===== */ +.material-symbols-outlined { + font-family: 'Material Symbols Outlined'; + font-size: 24px; + width: 24px; + height: 24px; + min-width: 24px; + line-height: 1; + display: inline-flex; + align-items: center; + justify-content: center; + font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; + /* Verhindert Layout-Shift beim Font-Laden */ + font-display: block; +} + +/* ===== GLASS PANEL ===== */ +.glass-panel { + background: $glass-bg; + border: 1px solid $glass-border; + border-radius: $radius-xl; + box-shadow: $shadow-glass; + padding: 1.5rem; + + @media (min-width: 641px) { + backdrop-filter: blur(20px); + padding: 2rem; + } +} + +/* ===== FORM ELEMENTS ===== */ +input[type="text"], +input[type="email"], +input[type="password"], +input[type="tel"], +input[type="number"], +textarea, +select { + width: 100%; + padding: 0.75rem 1rem; + border: 2px solid $color-gray-200; + border-radius: $radius-md; + font-size: 1rem; + font-family: inherit; + color: $color-text-primary; + background: $color-white; + transition: border-color 0.2s; + + &:focus { + outline: none; + border-color: $color-brand-primary; + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); + } + + &::placeholder { + color: $color-text-tertiary; + } +} + +textarea { + min-height: 120px; + resize: vertical; +} + +label { + display: block; + font-size: 0.9375rem; + font-weight: 600; + margin-bottom: 0.5rem; +} + +/* ===== UTILITIES ===== */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} + +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.text-center { text-align: center; } + +/* ===== ANIMATIONS ===== */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.spinner { + width: 32px; + height: 32px; + border: 3px solid $color-gray-200; + border-top-color: $color-brand-primary; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +/* ===== REDUCED MOTION ===== */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} + +/* ===== MOBILE PERFORMANCE ===== */ +@media (max-width: 640px) { + *, *::before, *::after { + backdrop-filter: none !important; + } +} \ No newline at end of file diff --git a/apps/frontend/tools/generate-pwa-icons.mjs b/apps/frontend/tools/generate-pwa-icons.mjs new file mode 100644 index 0000000..d2b5a8c --- /dev/null +++ b/apps/frontend/tools/generate-pwa-icons.mjs @@ -0,0 +1,110 @@ +/** + * PWA Icon Generator Script + * Generiert Placeholder-Icons für die PWA + * + * Verwendung: node tools/generate-pwa-icons.mjs + * + * Für echte Icons: Ersetze die generierten Dateien mit deinen + * eigenen Icons oder nutze https://www.pwabuilder.com/imageGenerator + */ + +import { writeFileSync, mkdirSync, existsSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const ICONS_DIR = join(__dirname, '../public/assets/icons'); + +const SIZES = [72, 96, 128, 144, 152, 192, 384, 512]; +const BRAND_COLOR = '#C2410C'; +const BG_COLOR = '#ffffff'; + +// Stelle sicher dass der Ordner existiert +if (!existsSync(ICONS_DIR)) { + mkdirSync(ICONS_DIR, { recursive: true }); +} + +/** + * Generiert ein einfaches SVG-Icon als Placeholder + */ +function generateSvgIcon(size) { + const padding = size * 0.15; + const innerSize = size - (padding * 2); + const fontSize = size * 0.35; + + return ` + + + + L&B + +`; +} + +/** + * Konvertiert SVG zu PNG mit Canvas (vereinfachte Base64 Version) + * Hinweis: Für echte PNG-Generierung würde man sharp oder canvas npm packages verwenden + */ +function generatePlaceholderPng(size) { + // Da wir keine externen Dependencies haben, generieren wir eine Info-Datei + const infoContent = `PWA Icon Placeholder - ${size}x${size}px + +WICHTIG: Ersetze diese Datei mit einem echten PNG-Icon! + +Empfohlene Tools: +- PWA Builder: https://www.pwabuilder.com/imageGenerator +- Maskable App: https://maskable.app/editor +- Real Favicon Generator: https://realfavicongenerator.net/ + +Icon-Anforderungen: +- Größe: ${size}x${size} Pixel +- Format: PNG mit Transparenz +- Safe Zone für Maskable: 80% des Icons sollte im inneren Kreis sein +`; + return infoContent; +} + +// Generiere SVG Icons (können direkt verwendet werden) +console.log('🎨 Generiere PWA Icons...\n'); + +SIZES.forEach(size => { + const svgPath = join(ICONS_DIR, `icon-${size}x${size}.svg`); + const svg = generateSvgIcon(size); + writeFileSync(svgPath, svg); + console.log(` ✓ icon-${size}x${size}.svg`); +}); + +// Erstelle eine README +const readme = `# PWA Icons + +Diese SVG-Icons wurden automatisch generiert und dienen als Placeholder. + +## Für die Produktion: + +1. Erstelle ein quadratisches Logo (mindestens 512x512px) +2. Gehe zu https://www.pwabuilder.com/imageGenerator +3. Lade dein Logo hoch +4. Lade die generierten Icons herunter +5. Ersetze die SVG-Dateien hier mit den PNG-Dateien + +## Benötigte Größen: +${SIZES.map(s => `- icon-${s}x${s}.png`).join('\n')} + +## Icon-Tipps: +- Verwende PNG mit Transparenz +- Halte wichtige Elemente in der "Safe Zone" (innere 80%) +- Teste mit https://maskable.app/editor + +## Schnelle Alternative: +Die SVG-Icons funktionieren auch, aber PNGs sind kompatibler. +Ändere in manifest.webmanifest die Endungen von .png zu .svg. +`; + +writeFileSync(join(ICONS_DIR, 'README.md'), readme); + +console.log('\n✅ PWA Icons generiert!\n'); +console.log('📝 Nächste Schritte:'); +console.log(' 1. Öffne public/assets/icons/README.md für Anweisungen'); +console.log(' 2. Ersetze die SVGs mit echten PNGs für beste Kompatibilität'); +console.log(' 3. Oder ändere manifest.webmanifest zu .svg Endungen\n'); diff --git a/apps/frontend/tools/generate-seo-assets.mjs b/apps/frontend/tools/generate-seo-assets.mjs new file mode 100644 index 0000000..5302746 --- /dev/null +++ b/apps/frontend/tools/generate-seo-assets.mjs @@ -0,0 +1,125 @@ +import { writeFileSync, existsSync, readFileSync, mkdirSync } from "node:fs"; +import { dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +/** + * Domain setzen – per ENV überschreibbar: + * BASE_URL=https://Leonards & Brandenburger IT.de npm run build + */ +const BASE_URL = (process.env.BASE_URL || "https://Leonards & Brandenburger IT.de").replace(/\/+$/, ""); + +/** + * Statische, öffentliche Routen (aus deiner Liste, aber ohne Admin/Auth/Preview). + * Passe diese Liste an, falls du noch mehr veröffentlichen willst. + */ +const STATIC_ROUTES = [ + "/", // Home + "/services", + "/about", + "/contact", + "/imprint", + "/process", + "/faq", + "/policy", + "/booking", + "/survey", + "/it-services" +]; + +/** + * Dynamische Services: /services/:slug + * Lies optionale Slugs aus src/data/service-slugs.json + * Formatbeispiel: + * { "slugs": ["one-pager","all-in-one","large-website","seo-optimization","full-stack-development"] } + */ +function readServiceSlugs() { + try { + const p = "src/data/service-slugs.json"; + if (!existsSync(p)) return []; + const json = JSON.parse(readFileSync(p, "utf8")); + const arr = Array.isArray(json) ? json : json.slugs; + return Array.isArray(arr) ? arr.filter(Boolean) : []; + } catch { + return []; + } +} + +const serviceSlugs = readServiceSlugs(); +const DYNAMIC_ROUTES = serviceSlugs.map(slug => `/services/${slug}`); + +/** + * Final: Alle URLs, doppelte raus, sortiert + */ +const allPaths = Array.from(new Set([...STATIC_ROUTES, ...DYNAMIC_ROUTES])); +allPaths.sort((a, b) => a.localeCompare(b)); + +/** + * lastmod: heute im YYYY-MM-DD + */ +const today = new Date().toISOString().slice(0, 10); + +/** + * Prioritäten & changefreq: simple Heuristik + */ +function getMeta(path) { + if (path === "/") return { priority: "1.0", changefreq: "weekly" }; + if (path.startsWith("/services/")) return { priority: "0.7", changefreq: "monthly" }; + if (path === "/services") return { priority: "0.8", changefreq: "weekly" }; + return { priority: "0.6", changefreq: "monthly" }; +} + +/** + * sitemap.xml bauen (ohne externes Paket) + */ +function buildSitemapXml() { + const urlsXml = allPaths.map(p => { + const { priority, changefreq } = getMeta(p); + const loc = `${BASE_URL}${p === "/" ? "/" : p}`; + return [ + " ", + ` ${loc}`, + ` ${changefreq}`, + ` ${priority}`, + ` ${today}`, + " " + ].join("\n"); + }).join("\n"); + + return [ + ``, + ``, + urlsXml, + `` + ].join("\n"); +} + +/** + * robots.txt bauen (mit absoluter Sitemap-URL) + */ +function buildRobotsTxt() { + return [ + `User-agent: *`, + `Allow: /`, + ``, + `Sitemap: ${BASE_URL}/sitemap.xml` + ].join("\n"); +} + +/** + * Dateien nach src/ schreiben (werden per assets ins Webroot kopiert) + */ +function ensureDirFor(filePath) { + const dir = filePath.split("/").slice(0, -1).join("/"); + if (dir && !existsSync(dir)) mkdirSync(dir, { recursive: true }); +} + +function writeFile(path, content) { + ensureDirFor(path); + writeFileSync(path, content, "utf8"); + console.log(`✓ ${path} geschrieben`); +} + +writeFile("src/sitemap.xml", buildSitemapXml()); +writeFile("src/robots.txt", buildRobotsTxt()); diff --git a/apps/frontend/tsconfig.app.json b/apps/frontend/tsconfig.app.json new file mode 100644 index 0000000..3775b37 --- /dev/null +++ b/apps/frontend/tsconfig.app.json @@ -0,0 +1,15 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/apps/frontend/tsconfig.json b/apps/frontend/tsconfig.json new file mode 100644 index 0000000..a8bb65b --- /dev/null +++ b/apps/frontend/tsconfig.json @@ -0,0 +1,33 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "bundler", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "lib": [ + "ES2022", + "dom" + ] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/apps/frontend/tsconfig.spec.json b/apps/frontend/tsconfig.spec.json new file mode 100644 index 0000000..5fb748d --- /dev/null +++ b/apps/frontend/tsconfig.spec.json @@ -0,0 +1,15 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..3b41469 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,20 @@ +services: + db: + image: postgres:16 + container_name: lub_db_dev + environment: + POSTGRES_USER: app + POSTGRES_PASSWORD: secret + POSTGRES_DB: appdb + ports: + - "5432:5432" + volumes: + - pgdata_dev:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U app -d appdb"] + interval: 5s + timeout: 3s + retries: 10 + +volumes: + pgdata_dev: \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e12bc2d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,48 @@ +services: + db: + image: postgres:16 + container_name: lub_db + environment: + POSTGRES_USER: app + POSTGRES_PASSWORD: secret + POSTGRES_DB: appdb + ports: + - "15432:5432" # ← Außen 15432 statt 5432 + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U app -d appdb"] + interval: 5s + timeout: 3s + retries: 10 + restart: unless-stopped + + backend: + build: + context: . + dockerfile: Dockerfile.backend + container_name: lub_backend + ports: + - "13000:3000" # ← Außen 13000 statt 3000 + env_file: + - ./apps/backend/.env.production + depends_on: + db: + condition: service_healthy + restart: unless-stopped + + frontend: + build: + context: . + dockerfile: Dockerfile.frontend + container_name: lub_frontend + ports: + - "1111:80" # ← Außen 1111 statt 80 (DAS WILLST DU!) + volumes: + - ./apps/frontend/public/config.production.json:/usr/share/nginx/html/config.json:ro + depends_on: + - backend + restart: unless-stopped + +volumes: + pgdata: \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3f23d63 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,24597 @@ +{ + "name": "lub-project", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lub-project", + "version": "1.0.0", + "workspaces": [ + "apps/frontend", + "apps/backend" + ], + "dependencies": { + "rxjs": "~7.8.0" + }, + "devDependencies": { + "concurrently": "^8.2.2" + } + }, + "apps/backend": { + "name": "leonards-media-api", + "version": "0.0.1", + "license": "UNLICENSED", + "dependencies": { + "@emailjs/nodejs": "^5.0.2", + "@nestjs-modules/mailer": "^2.0.2", + "@nestjs/common": "^10.0.0", + "@nestjs/config": "^4.0.2", + "@nestjs/core": "^10.0.0", + "@nestjs/jwt": "^11.0.0", + "@nestjs/passport": "^11.0.5", + "@nestjs/platform-express": "^10.4.22", + "@nestjs/throttler": "^6.5.0", + "@nestjs/typeorm": "^11.0.0", + "bcrypt": "^6.0.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.2", + "googleapis": "^163.0.0", + "helmet": "^8.1.0", + "nodemailer": "^7.0.9", + "openai": "^6.2.0", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "pg": "^8.16.3", + "reflect-metadata": "^0.2.0", + "rxjs": "^7.8.1", + "sqlite3": "^5.1.7", + "typeorm": "^0.3.27" + }, + "devDependencies": { + "@nestjs/cli": "^10.0.0", + "@nestjs/schematics": "^10.0.0", + "@nestjs/testing": "^10.0.0", + "@types/bcrypt": "^6.0.0", + "@types/express": "^4.17.17", + "@types/jest": "^29.5.2", + "@types/node": "^20.3.1", + "@types/passport-jwt": "^4.0.1", + "@types/supertest": "^6.0.0", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "eslint": "^8.42.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "jest": "^29.5.0", + "prettier": "^3.0.0", + "source-map-support": "^0.5.21", + "supertest": "^7.0.0", + "ts-jest": "^29.1.0", + "ts-loader": "^9.4.3", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.1.3" + } + }, + "apps/frontend": { + "name": "website-base-v2", + "version": "0.0.0", + "dependencies": { + "@angular/animations": "^18.2.0", + "@angular/common": "^18.2.0", + "@angular/compiler": "^18.2.0", + "@angular/core": "^18.2.0", + "@angular/forms": "^18.2.0", + "@angular/platform-browser": "^18.2.0", + "@angular/platform-browser-dynamic": "^18.2.0", + "@angular/router": "^18.2.0", + "@popperjs/core": "^2.11.8", + "jspdf": "^4.0.0", + "tslib": "^2.3.0", + "zone.js": "~0.14.10" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^18.2.8", + "@angular/cli": "^18.2.8", + "@angular/compiler-cli": "^18.2.0", + "@types/jasmine": "~5.1.0", + "@types/jspdf": "^1.3.3", + "jasmine-core": "~5.2.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "typescript": "~5.5.2" + } + }, + "apps/frontend/node_modules/@angular-devkit/build-angular": { + "version": "18.2.21", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.21.tgz", + "integrity": "sha512-0pJfURFpEUV2USgZ2TL3nNAaJmF9bICx9OVddBoC+F9FeOpVKxkcVIb+c8Km5zHFo1iyVtPZ6Rb25vFk9Zm/ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1802.21", + "@angular-devkit/build-webpack": "0.1802.21", + "@angular-devkit/core": "18.2.21", + "@angular/build": "18.2.21", + "@babel/core": "7.26.10", + "@babel/generator": "7.26.10", + "@babel/helper-annotate-as-pure": "7.25.9", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-transform-async-generator-functions": "7.26.8", + "@babel/plugin-transform-async-to-generator": "7.25.9", + "@babel/plugin-transform-runtime": "7.26.10", + "@babel/preset-env": "7.26.9", + "@babel/runtime": "7.26.10", + "@discoveryjs/json-ext": "0.6.1", + "@ngtools/webpack": "18.2.21", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.20", + "babel-loader": "9.1.3", + "browserslist": "^4.21.5", + "copy-webpack-plugin": "12.0.2", + "critters": "0.0.24", + "css-loader": "7.1.2", + "esbuild-wasm": "0.23.0", + "fast-glob": "3.3.2", + "http-proxy-middleware": "3.0.5", + "https-proxy-agent": "7.0.5", + "istanbul-lib-instrument": "6.0.3", + "jsonc-parser": "3.3.1", + "karma-source-map-support": "1.4.0", + "less": "4.2.0", + "less-loader": "12.2.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.3.1", + "magic-string": "0.30.11", + "mini-css-extract-plugin": "2.9.0", + "mrmime": "2.0.0", + "open": "10.1.0", + "ora": "5.4.1", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "4.0.2", + "piscina": "4.6.1", + "postcss": "8.4.41", + "postcss-loader": "8.1.1", + "resolve-url-loader": "5.0.0", + "rxjs": "7.8.1", + "sass": "1.77.6", + "sass-loader": "16.0.0", + "semver": "7.6.3", + "source-map-loader": "5.0.0", + "source-map-support": "0.5.21", + "terser": "5.31.6", + "tree-kill": "1.2.2", + "tslib": "2.6.3", + "watchpack": "2.4.1", + "webpack": "5.94.0", + "webpack-dev-middleware": "7.4.2", + "webpack-dev-server": "5.2.2", + "webpack-merge": "6.0.1", + "webpack-subresource-integrity": "5.1.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "esbuild": "0.23.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^18.0.0", + "@angular/localize": "^18.0.0", + "@angular/platform-server": "^18.0.0", + "@angular/service-worker": "^18.0.0", + "@web/test-runner": "^0.18.0", + "browser-sync": "^3.0.2", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "karma": "^6.3.0", + "ng-packagr": "^18.0.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=5.4 <5.6" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@web/test-runner": { + "optional": true + }, + "browser-sync": { + "optional": true + }, + "jest": { + "optional": true + }, + "jest-environment-jsdom": { + "optional": true + }, + "karma": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "protractor": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "apps/frontend/node_modules/@angular/build": { + "version": "18.2.21", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.21.tgz", + "integrity": "sha512-uvq3qP4cByJrUkV1ri0v3x6LxOFt4fDKiQdNwbQAqdxtfRs3ssEIoCGns4t89sTWXv6VZWBNDcDIKK9/Fa9mmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1802.21", + "@babel/core": "7.25.2", + "@babel/helper-annotate-as-pure": "7.24.7", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-syntax-import-attributes": "7.24.7", + "@inquirer/confirm": "3.1.22", + "@vitejs/plugin-basic-ssl": "1.1.0", + "browserslist": "^4.23.0", + "critters": "0.0.24", + "esbuild": "0.23.0", + "fast-glob": "3.3.2", + "https-proxy-agent": "7.0.5", + "listr2": "8.2.4", + "lmdb": "3.0.13", + "magic-string": "0.30.11", + "mrmime": "2.0.0", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "4.0.2", + "piscina": "4.6.1", + "rollup": "4.22.4", + "sass": "1.77.6", + "semver": "7.6.3", + "vite": "~5.4.17", + "watchpack": "2.4.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^18.0.0", + "@angular/localize": "^18.0.0", + "@angular/platform-server": "^18.0.0", + "@angular/service-worker": "^18.0.0", + "less": "^4.2.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=5.4 <5.6" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "less": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "apps/frontend/node_modules/@angular/build/node_modules/@babel/core": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", + "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" + } + }, + "apps/frontend/node_modules/@angular/build/node_modules/@babel/core/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" + } + }, + "apps/frontend/node_modules/@angular/build/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "apps/frontend/node_modules/@angular/build/node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", + "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "apps/frontend/node_modules/@angular/build/node_modules/@vitejs/plugin-basic-ssl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.1.0.tgz", + "integrity": "sha512-wO4Dk/rm8u7RNhOf95ZzcEmC9rYOncYgvq4z3duaJrCgjN8BxAnDVyndanfcJZ0O6XZzHz6Q0hTimxTg8Y9g/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.6.0" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + } + }, + "apps/frontend/node_modules/@angular/build/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" + }, + "apps/frontend/node_modules/@angular/build/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 + } + } + }, + "apps/frontend/node_modules/@angular/build/node_modules/vite/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" + } + }, + "apps/frontend/node_modules/@angular/build/node_modules/vite/node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "apps/frontend/node_modules/@angular/compiler-cli": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.14.tgz", + "integrity": "sha512-BmmjyrFSBSYkm0tBSqpu4cwnJX/b/XvhM36mj2k8jah3tNS5zLDDx5w6tyHmaPJa/1D95MlXx2h6u7K9D+Mhew==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "7.25.2", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^4.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js", + "ngcc": "bundles/ngcc/index.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/compiler": "18.2.14", + "typescript": ">=5.4 <5.6" + } + }, + "apps/frontend/node_modules/@angular/compiler-cli/node_modules/@babel/core": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", + "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" + } + }, + "apps/frontend/node_modules/@angular/compiler-cli/node_modules/@babel/core/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" + }, + "apps/frontend/node_modules/@angular/compiler-cli/node_modules/@babel/core/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" + } + }, + "apps/frontend/node_modules/@babel/runtime": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "apps/frontend/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" + } + }, + "apps/frontend/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" + } + }, + "apps/frontend/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" + } + }, + "apps/frontend/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" + } + }, + "apps/frontend/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" + } + }, + "apps/frontend/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" + } + }, + "apps/frontend/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" + } + }, + "apps/frontend/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" + } + }, + "apps/frontend/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" + } + }, + "apps/frontend/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" + } + }, + "apps/frontend/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" + } + }, + "apps/frontend/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" + } + }, + "apps/frontend/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" + } + }, + "apps/frontend/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" + } + }, + "apps/frontend/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" + } + }, + "apps/frontend/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" + } + }, + "apps/frontend/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" + } + }, + "apps/frontend/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" + } + }, + "apps/frontend/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" + } + }, + "apps/frontend/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" + } + }, + "apps/frontend/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" + } + }, + "apps/frontend/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" + } + }, + "apps/frontend/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" + } + }, + "apps/frontend/node_modules/@ngtools/webpack": { + "version": "18.2.21", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.21.tgz", + "integrity": "sha512-mfLT7lXbyJRlsazuPyuF5AGsMcgzRJRwsDlgxFbiy1DBlaF1chRFsXrKYj1gQ/WXQWNcEd11aedU0Rt+iCNDVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^18.0.0", + "typescript": ">=5.4 <5.6", + "webpack": "^5.54.0" + } + }, + "apps/frontend/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "apps/frontend/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "license": "0BSD" + }, + "apps/frontend/node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "apps/frontend/node_modules/webpack": { + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.7.1", + "acorn-import-attributes": "^1.9.5", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.1802.21", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.21.tgz", + "integrity": "sha512-+Ll+xtpKwZ3iLWN/YypvnCZV/F0MVbP+/7ZpMR+Xv/uB0OmribhBVj9WGaCd9I/bGgoYBw8wBV/NFNCKkf0k3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "18.2.21", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1802.21", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.21.tgz", + "integrity": "sha512-2jSVRhA3N4Elg8OLcBktgi+CMSjlAm/bBQJE6TQYbdQWnniuT7JAWUHA/iPf7MYlQE5qj4rnAni1CI/c1Bk4HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1802.21", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^5.0.2" + } + }, + "node_modules/@angular-devkit/core": { + "version": "18.2.21", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.21.tgz", + "integrity": "sha512-Lno6GNbJME85wpc/uqn+wamBxvfZJZFYSH8+oAkkyjU/hk8r5+X8DuyqsKAa0m8t46zSTUsonHsQhVe5vgrZeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "18.2.21", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.21.tgz", + "integrity": "sha512-yuC2vN4VL48JhnsaOa9J/o0Jl+cxOklRNQp5J2/ypMuRROaVCrZAPiX+ChSHh++kHYMpj8+ggNrrUwRNfMKACQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "18.2.21", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.11", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-17.3.11.tgz", + "integrity": "sha512-kcOMqp+PHAKkqRad7Zd7PbpqJ0LqLaNZdY1+k66lLWmkEBozgq8v4ASn/puPWf9Bo0HpCiK+EzLf0VHE8Z/y6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "ansi-colors": "4.1.3", + "inquirer": "9.2.15", + "symbol-observable": "4.0.0", + "yargs-parser": "21.1.1" + }, + "bin": { + "schematics": "bin/schematics.js" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@angular-devkit/core": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.11.tgz", + "integrity": "sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/@angular-devkit/schematics": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.11.tgz", + "integrity": "sha512-I5wviiIqiFwar9Pdk30Lujk8FczEEc18i22A5c6Z9lbmhPQdTroDnEQdsfXjy404wPe8H62s0I15o4pmMGfTYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "jsonc-parser": "3.2.1", + "magic-string": "0.30.8", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/inquirer": { + "version": "9.2.15", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.15.tgz", + "integrity": "sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ljharb/through": "^2.3.12", + "ansi-escapes": "^4.3.2", + "chalk": "^5.3.0", + "cli-cursor": "^3.1.0", + "cli-width": "^4.1.0", + "external-editor": "^3.1.0", + "figures": "^3.2.0", + "lodash": "^4.17.21", + "mute-stream": "1.0.0", + "ora": "^5.4.1", + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/magic-string": { + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/picomatch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", + "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@angular-devkit/schematics-cli/node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@angular-devkit/schematics-cli/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@angular/animations": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.14.tgz", + "integrity": "sha512-Kp/MWShoYYO+R3lrrZbZgszbbLGVXHB+39mdJZwnIuZMDkeL3JsIBlSOzyJRTnpS1vITc+9jgHvP/6uKbMrW1Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/core": "18.2.14" + } + }, + "node_modules/@angular/cli": { + "version": "18.2.21", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.21.tgz", + "integrity": "sha512-efweY4p8awRTbHs+HKdg6s44hl7Y0gdVlXYi3HeY8Z5JDC0abbka0K6sA/MrV9AXvn/5ovxYbxiL3AsOApjTpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1802.21", + "@angular-devkit/core": "18.2.21", + "@angular-devkit/schematics": "18.2.21", + "@inquirer/prompts": "5.3.8", + "@listr2/prompt-adapter-inquirer": "2.0.15", + "@schematics/angular": "18.2.21", + "@yarnpkg/lockfile": "1.1.0", + "ini": "4.1.3", + "jsonc-parser": "3.3.1", + "listr2": "8.2.4", + "npm-package-arg": "11.0.3", + "npm-pick-manifest": "9.1.0", + "pacote": "18.0.6", + "resolve": "1.22.8", + "semver": "7.6.3", + "symbol-observable": "4.0.0", + "yargs": "17.7.2" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/common": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.14.tgz", + "integrity": "sha512-ZPRswzaVRiqcfZoowuAM22Hr2/z10ajWOUoFDoQ9tWqz/fH/773kJv2F9VvePIekgNPCzaizqv9gF6tGNqaAwg==", + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/core": "18.2.14", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.14.tgz", + "integrity": "sha512-Mpq3v/mztQzGAQAAFV+wAI1hlXxZ0m8eDBgaN2kD3Ue+r4S6bLm1Vlryw0iyUnt05PcFIdxPT6xkcphq5pl6lw==", + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/core": "18.2.14" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + } + } + }, + "node_modules/@angular/core": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.14.tgz", + "integrity": "sha512-BIPrCs93ZZTY9ym7yfoTgAQ5rs706yoYeAdrgc8kh/bDbM9DawxKlgeKBx2FLt09Y0YQ1bFhKVp0cV4gDEaMxQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.14.10" + } + }, + "node_modules/@angular/forms": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.14.tgz", + "integrity": "sha512-fZVwXctmBJa5VdopJae/T9MYKPXNd04+6j4k/6X819y+9fiyWLJt2QicSc5Rc+YD9mmhXag3xaljlrnotf9VGA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "18.2.14", + "@angular/core": "18.2.14", + "@angular/platform-browser": "18.2.14", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.14.tgz", + "integrity": "sha512-W+JTxI25su3RiZVZT3Yrw6KNUCmOIy7OZIZ+612skPgYK2f2qil7VclnW1oCwG896h50cMJU/lnAfxZxefQgyQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/animations": "18.2.14", + "@angular/common": "18.2.14", + "@angular/core": "18.2.14" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.14.tgz", + "integrity": "sha512-QOv+o89u8HLN0LG8faTIVHKBxfkOBHVDB0UuXy19+HJofWZGGvho+vGjV0/IAkhZnMC4Sxdoy/mOHP2ytALX3A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "18.2.14", + "@angular/compiler": "18.2.14", + "@angular/core": "18.2.14", + "@angular/platform-browser": "18.2.14" + } + }, + "node_modules/@angular/router": { + "version": "18.2.14", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.14.tgz", + "integrity": "sha512-v/gweh8MBjjDfh1QssuyjISa+6SVVIvIZox7MaMs81RkaoVHwS9grDtPud1pTKHzms2KxSVpvwwyvkRJQplueg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "18.2.14", + "@angular/core": "18.2.14", + "@angular/platform-browser": "18.2.14", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", + "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/core/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/@babel/core/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/@babel/generator": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", + "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.10", + "@babel/types": "^7.26.10", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/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/@babel/helper-create-class-features-plugin": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/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/@babel/helper-create-regexp-features-plugin": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/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/@babel/helper-define-polyfill-provider": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", + "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "debug": "^4.4.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.10" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "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/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", + "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", + "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", + "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.26.8.tgz", + "integrity": "sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-remap-async-to-generator": "^7.25.9", + "@babel/traverse": "^7.26.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", + "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", + "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", + "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", + "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", + "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", + "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/template": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", + "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.28.6.tgz", + "integrity": "sha512-5suVoXjC14lUN6ZL9OLKIHCNVWCrqGqlmEp/ixdXjvgnEl/kauLvvMO/Xw9NyMc95Joj1AeLVPVMvibBgSoFlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", + "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", + "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", + "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz", + "integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", + "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", + "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", + "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", + "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", + "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", + "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", + "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.6.tgz", + "integrity": "sha512-eZhoEZHYQLL5uc1gS5e9/oTknS0sSSAtd5TkKMUp3J+S/CaUjagc0kOUPsEbDmMeva0nC3WWl4SxVY6+OBuxfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", + "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.26.10.tgz", + "integrity": "sha512-NWaL2qG6HRpONTnj4JvDU6th4jYeZOJgu3QhmFTCihib0ermtOJqktA5BduGm3suhhVe9EMP9c9+mfJ/I9slqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.26.5", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/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/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", + "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", + "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", + "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.9.tgz", + "integrity": "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.8", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.26.0", + "@babel/plugin-syntax-import-attributes": "^7.26.0", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.25.9", + "@babel/plugin-transform-async-generator-functions": "^7.26.8", + "@babel/plugin-transform-async-to-generator": "^7.25.9", + "@babel/plugin-transform-block-scoped-functions": "^7.26.5", + "@babel/plugin-transform-block-scoping": "^7.25.9", + "@babel/plugin-transform-class-properties": "^7.25.9", + "@babel/plugin-transform-class-static-block": "^7.26.0", + "@babel/plugin-transform-classes": "^7.25.9", + "@babel/plugin-transform-computed-properties": "^7.25.9", + "@babel/plugin-transform-destructuring": "^7.25.9", + "@babel/plugin-transform-dotall-regex": "^7.25.9", + "@babel/plugin-transform-duplicate-keys": "^7.25.9", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-dynamic-import": "^7.25.9", + "@babel/plugin-transform-exponentiation-operator": "^7.26.3", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-for-of": "^7.26.9", + "@babel/plugin-transform-function-name": "^7.25.9", + "@babel/plugin-transform-json-strings": "^7.25.9", + "@babel/plugin-transform-literals": "^7.25.9", + "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", + "@babel/plugin-transform-member-expression-literals": "^7.25.9", + "@babel/plugin-transform-modules-amd": "^7.25.9", + "@babel/plugin-transform-modules-commonjs": "^7.26.3", + "@babel/plugin-transform-modules-systemjs": "^7.25.9", + "@babel/plugin-transform-modules-umd": "^7.25.9", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-new-target": "^7.25.9", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.26.6", + "@babel/plugin-transform-numeric-separator": "^7.25.9", + "@babel/plugin-transform-object-rest-spread": "^7.25.9", + "@babel/plugin-transform-object-super": "^7.25.9", + "@babel/plugin-transform-optional-catch-binding": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9", + "@babel/plugin-transform-private-methods": "^7.25.9", + "@babel/plugin-transform-private-property-in-object": "^7.25.9", + "@babel/plugin-transform-property-literals": "^7.25.9", + "@babel/plugin-transform-regenerator": "^7.25.9", + "@babel/plugin-transform-regexp-modifiers": "^7.26.0", + "@babel/plugin-transform-reserved-words": "^7.25.9", + "@babel/plugin-transform-shorthand-properties": "^7.25.9", + "@babel/plugin-transform-spread": "^7.25.9", + "@babel/plugin-transform-sticky-regex": "^7.25.9", + "@babel/plugin-transform-template-literals": "^7.26.8", + "@babel/plugin-transform-typeof-symbol": "^7.26.7", + "@babel/plugin-transform-unicode-escapes": "^7.25.9", + "@babel/plugin-transform-unicode-property-regex": "^7.25.9", + "@babel/plugin-transform-unicode-regex": "^7.25.9", + "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.40.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/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/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.1.tgz", + "integrity": "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@css-inline/css-inline": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@css-inline/css-inline/-/css-inline-0.14.1.tgz", + "integrity": "sha512-u4eku+hnPqqHIGq/ZUQcaP0TrCbYeLIYBaK7qClNRGZbnh8RC4gVxLEIo8Pceo1nOK9E5G4Lxzlw5KnXcvflfA==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@css-inline/css-inline-android-arm-eabi": "0.14.1", + "@css-inline/css-inline-android-arm64": "0.14.1", + "@css-inline/css-inline-darwin-arm64": "0.14.1", + "@css-inline/css-inline-darwin-x64": "0.14.1", + "@css-inline/css-inline-linux-arm-gnueabihf": "0.14.1", + "@css-inline/css-inline-linux-arm64-gnu": "0.14.1", + "@css-inline/css-inline-linux-arm64-musl": "0.14.1", + "@css-inline/css-inline-linux-x64-gnu": "0.14.1", + "@css-inline/css-inline-linux-x64-musl": "0.14.1", + "@css-inline/css-inline-win32-x64-msvc": "0.14.1" + } + }, + "node_modules/@css-inline/css-inline-android-arm-eabi": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@css-inline/css-inline-android-arm-eabi/-/css-inline-android-arm-eabi-0.14.1.tgz", + "integrity": "sha512-LNUR8TY4ldfYi0mi/d4UNuHJ+3o8yLQH9r2Nt6i4qeg1i7xswfL3n/LDLRXvGjBYqeEYNlhlBQzbPwMX1qrU6A==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@css-inline/css-inline-android-arm64": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@css-inline/css-inline-android-arm64/-/css-inline-android-arm64-0.14.1.tgz", + "integrity": "sha512-tH5us0NYGoTNBHOUHVV7j9KfJ4DtFOeTLA3cM0XNoMtArNu2pmaaBMFJPqECzavfXkLc7x5Z22UPZYjoyHfvCA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@css-inline/css-inline-darwin-arm64": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@css-inline/css-inline-darwin-arm64/-/css-inline-darwin-arm64-0.14.1.tgz", + "integrity": "sha512-QE5W1YRIfRayFrtrcK/wqEaxNaqLULPI0gZB4ArbFRd3d56IycvgBasDTHPre5qL2cXCO3VyPx+80XyHOaVkag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@css-inline/css-inline-darwin-x64": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@css-inline/css-inline-darwin-x64/-/css-inline-darwin-x64-0.14.1.tgz", + "integrity": "sha512-mAvv2sN8awNFsbvBzlFkZPbCNZ6GCWY5/YcIz7V5dPYw+bHHRbjnlkNTEZq5BsDxErVrMIGvz05PGgzuNvZvdQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@css-inline/css-inline-linux-arm-gnueabihf": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@css-inline/css-inline-linux-arm-gnueabihf/-/css-inline-linux-arm-gnueabihf-0.14.1.tgz", + "integrity": "sha512-AWC44xL0X7BgKvrWEqfSqkT2tJA5kwSGrAGT+m0gt11wnTYySvQ6YpX0fTY9i3ppYGu4bEdXFjyK2uY1DTQMHA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@css-inline/css-inline-linux-arm64-gnu": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@css-inline/css-inline-linux-arm64-gnu/-/css-inline-linux-arm64-gnu-0.14.1.tgz", + "integrity": "sha512-drj0ciiJgdP3xKXvNAt4W+FH4KKMs8vB5iKLJ3HcH07sNZj58Sx++2GxFRS1el3p+GFp9OoYA6dgouJsGEqt0Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@css-inline/css-inline-linux-arm64-musl": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@css-inline/css-inline-linux-arm64-musl/-/css-inline-linux-arm64-musl-0.14.1.tgz", + "integrity": "sha512-FzknI+st8eA8YQSdEJU9ykcM0LZjjigBuynVF5/p7hiMm9OMP8aNhWbhZ8LKJpKbZrQsxSGS4g9Vnr6n6FiSdQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@css-inline/css-inline-linux-x64-gnu": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@css-inline/css-inline-linux-x64-gnu/-/css-inline-linux-x64-gnu-0.14.1.tgz", + "integrity": "sha512-yubbEye+daDY/4vXnyASAxH88s256pPati1DfVoZpU1V0+KP0BZ1dByZOU1ktExurbPH3gZOWisAnBE9xon0Uw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@css-inline/css-inline-linux-x64-musl": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@css-inline/css-inline-linux-x64-musl/-/css-inline-linux-x64-musl-0.14.1.tgz", + "integrity": "sha512-6CRAZzoy1dMLPC/tns2rTt1ZwPo0nL/jYBEIAsYTCWhfAnNnpoLKVh5Nm+fSU3OOwTTqU87UkGrFJhObD/wobQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@css-inline/css-inline-win32-x64-msvc": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@css-inline/css-inline-win32-x64-msvc/-/css-inline-win32-x64-msvc-0.14.1.tgz", + "integrity": "sha512-nzotGiaiuiQW78EzsiwsHZXbxEt6DiMUFcDJ6dhiliomXxnlaPyBfZb6/FMBgRJOf6sknDt/5695OttNmbMYzg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.1.tgz", + "integrity": "sha512-boghen8F0Q8D+0/Q1/1r6DUEieUJ8w2a1gIknExMSHBsJFOr2+0KUfHiVYBvucPwl3+RU5PFBK833FjFCh3BhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.17.0" + } + }, + "node_modules/@emailjs/nodejs": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@emailjs/nodejs/-/nodejs-5.0.2.tgz", + "integrity": "sha512-+BrO5rF0msaoEeSKxoa64weUkczd3fTXRCB8/EnKaIRzC8B1EyLDZfr9vYK5TNG/U3Eco9cpjMNnA1IlUBkmOw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", + "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", + "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", + "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", + "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", + "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", + "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", + "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", + "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", + "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", + "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", + "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", + "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", + "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", + "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", + "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", + "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", + "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", + "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz", + "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", + "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", + "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", + "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", + "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", + "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "license": "MIT", + "optional": true + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@inquirer/checkbox": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-2.5.0.tgz", + "integrity": "sha512-sMgdETOfi2dUHT8r7TT1BTKOwNvdDGFDXYWtQ2J69SvlYNntk9I/gJe7r5yvMwwsuKnYbuRs3pNhx4tgNck5aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/figures": "^1.0.5", + "@inquirer/type": "^1.5.3", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/confirm": { + "version": "3.1.22", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-3.1.22.tgz", + "integrity": "sha512-gsAKIOWBm2Q87CDfs9fEo7wJT3fwWIJfnDGMn9Qy74gBnNFOACDNfhUzovubbJjWnKLGBln7/NcSmZwj5DuEXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.0.10", + "@inquirer/type": "^1.5.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz", + "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "@types/mute-stream": "^0.0.4", + "@types/node": "^22.5.5", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^1.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core/node_modules/@inquirer/type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz", + "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==", + "dev": true, + "license": "MIT", + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core/node_modules/@types/node": { + "version": "22.19.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", + "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@inquirer/core/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/editor": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-2.2.0.tgz", + "integrity": "sha512-9KHOpJ+dIL5SZli8lJ6xdaYLPPzB8xB9GZItg39MBybzhxA16vxmszmQFrRwbOA918WA2rvu8xhDEg/p6LXKbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/expand": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-2.3.0.tgz", + "integrity": "sha512-qnJsUcOGCSG1e5DTOErmv2BPQqrtT6uzqn1vI/aYGiPKq+FgslGZmtdnXbhuI7IlT7OByDoEEqdnhUnVR2hhLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-2.3.0.tgz", + "integrity": "sha512-XfnpCStx2xgh1LIRqPXrTNEEByqQWoxsWYzNRSEUxJ5c6EQlhMogJ3vHKu8aXuTacebtaZzMAHwEL0kAflKOBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/number": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-1.1.0.tgz", + "integrity": "sha512-ilUnia/GZUtfSZy3YEErXLJ2Sljo/mf9fiKc08n18DdwdmDbOzRcTv65H1jjDvlsAuvdFXf4Sa/aL7iw/NanVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/password": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-2.2.0.tgz", + "integrity": "sha512-5otqIpgsPYIshqhgtEwSspBQE40etouR8VIxzpJkv9i0dVHIpyhiivbkH9/dGiMLdyamT54YRdGJLfl8TFnLHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/prompts": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-5.3.8.tgz", + "integrity": "sha512-b2BudQY/Si4Y2a0PdZZL6BeJtl8llgeZa7U2j47aaJSCeAl1e4UI7y8a9bSkO3o/ZbZrgT5muy/34JbsjfIWxA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@inquirer/checkbox": "^2.4.7", + "@inquirer/confirm": "^3.1.22", + "@inquirer/editor": "^2.1.22", + "@inquirer/expand": "^2.1.22", + "@inquirer/input": "^2.2.9", + "@inquirer/number": "^1.0.10", + "@inquirer/password": "^2.1.22", + "@inquirer/rawlist": "^2.2.4", + "@inquirer/search": "^1.0.7", + "@inquirer/select": "^2.4.7" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/rawlist": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-2.3.0.tgz", + "integrity": "sha512-zzfNuINhFF7OLAtGHfhwOW2TlYJyli7lOUoJUXw/uyklcwalV6WRXBXtFIicN8rTRK1XTiPWB4UY+YuW8dsnLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/search": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-1.1.0.tgz", + "integrity": "sha512-h+/5LSj51dx7hp5xOn4QFnUaKeARwUCLs6mIhtkJ0JYPBLmEYjdHSYh7I6GrLg9LwpJ3xeX0FZgAG1q0QdCpVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/figures": "^1.0.5", + "@inquirer/type": "^1.5.3", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/select": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-2.5.0.tgz", + "integrity": "sha512-YmDobTItPP3WcEI86GvPo+T2sRHkxxOq/kXmsBjHS5BVXUgvgZ5AfJjkvQvZr03T81NnI3KrrRuMzeuYUQRFOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/figures": "^1.0.5", + "@inquirer/type": "^1.5.3", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz", + "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/console/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@jest/reporters/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/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/@jest/transform/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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/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==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "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==", + "devOptional": 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/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/buffers": { + "version": "17.65.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.65.0.tgz", + "integrity": "sha512-eBrIXd0/Ld3p9lpDDlMaMn6IEfWqtHMD+z61u0JrIiPzsV1r7m6xDZFRxJyvIFTEO+SWdYF9EiQbXZGd8BzPfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-core": { + "version": "4.56.9", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.56.9.tgz", + "integrity": "sha512-BUkXXWL3I7VZ34cpmP7WSttmP5o+z+lxi3teYMnEcUOKBu7DhCFxCesOevw+UATUewMHRMUtsmFYxOxgV7SQwg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.56.9", + "@jsonjoy.com/fs-node-utils": "4.56.9", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-fsa": { + "version": "4.56.9", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.56.9.tgz", + "integrity": "sha512-g15wwrvRRsy73p/b93XltxMkARyh3efxZNkrKbiocUNaPnHF+iDXQ1IlBwsTi5zxijdCYOsmVuyEdBX87tLqlw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.56.9", + "@jsonjoy.com/fs-node-builtins": "4.56.9", + "@jsonjoy.com/fs-node-utils": "4.56.9", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node": { + "version": "4.56.9", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.56.9.tgz", + "integrity": "sha512-YiI2iqVMi/Ro9BcqWWLQBv939gje748pC4t376M/goQoLaM0sItsj0bBTiQr4eXyLsLdGw10n/F/kH5/snBe7g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.56.9", + "@jsonjoy.com/fs-node-builtins": "4.56.9", + "@jsonjoy.com/fs-node-utils": "4.56.9", + "@jsonjoy.com/fs-print": "4.56.9", + "glob-to-regex.js": "^1.0.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-builtins": { + "version": "4.56.9", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.56.9.tgz", + "integrity": "sha512-q9MEsySAwyhgy1GT1FKfnKJ1a8bJmzbQnMGQA94F663C/wycrSgRrM33byzTAwn6FBRzMfTvABANkYvkOeYGhw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-to-fsa": { + "version": "4.56.9", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.56.9.tgz", + "integrity": "sha512-rOnn9FBLY+JWy0UDSXaYXY45j7FxfRJepRW5pZvNbdAzHYFZ0/M3OQ1+RfZsMYgWeMkaN9pGhOsIj/A7P9pAXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-fsa": "4.56.9", + "@jsonjoy.com/fs-node-builtins": "4.56.9", + "@jsonjoy.com/fs-node-utils": "4.56.9" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-utils": { + "version": "4.56.9", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.56.9.tgz", + "integrity": "sha512-UMUirCu0jDPyJEsfllKX1SmK9E7ww2VltWiq2qBCy3ZcyHqDuHswPycrxLTwGrLJnGiHPW9f7LOniP7enl9jYQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.56.9" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-print": { + "version": "4.56.9", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.56.9.tgz", + "integrity": "sha512-Op6rXFnmhHHAClNvHFGx9zALHgZfyPdPBd0WIf/MBr4DEoShhAj0MZxg0jMO7foqleq2YSNNCNBMFGkmY43wAQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-utils": "4.56.9", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot": { + "version": "4.56.9", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.56.9.tgz", + "integrity": "sha512-nMxEvDku2bCdCCNLkjd9hjPyUng8mLIfok8yAQ0zHNbZqeE44K5CSXnT0o3TGzv/zWynM49rUlF95ZjlNazFAQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^17.65.0", + "@jsonjoy.com/fs-node-utils": "4.56.9", + "@jsonjoy.com/json-pack": "^17.65.0", + "@jsonjoy.com/util": "^17.65.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { + "version": "17.65.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.65.0.tgz", + "integrity": "sha512-Xrh7Fm/M0QAYpekSgmskdZYnFdSGnsxJ/tHaolA4bNwWdG9i65S8m83Meh7FOxyJyQAdo4d4J97NOomBLEfkDQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { + "version": "17.65.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.65.0.tgz", + "integrity": "sha512-7MXcRYe7n3BG+fo3jicvjB0+6ypl2Y/bQp79Sp7KeSiiCgLqw4Oled6chVv07/xLVTdo3qa1CD0VCCnPaw+RGA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { + "version": "17.65.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.65.0.tgz", + "integrity": "sha512-e0SG/6qUCnVhHa0rjDJHgnXnbsacooHVqQHxspjvlYQSkHm+66wkHw6Gql+3u/WxI/b1VsOdUi0M+fOtkgKGdQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "17.65.0", + "@jsonjoy.com/buffers": "17.65.0", + "@jsonjoy.com/codegen": "17.65.0", + "@jsonjoy.com/json-pointer": "17.65.0", + "@jsonjoy.com/util": "17.65.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { + "version": "17.65.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.65.0.tgz", + "integrity": "sha512-uhTe+XhlIZpWOxgPcnO+iSCDgKKBpwkDVTyYiXX9VayGV8HSFVJM67M6pUE71zdnXF1W0Da21AvnhlmdwYPpow==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/util": "17.65.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { + "version": "17.65.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.65.0.tgz", + "integrity": "sha512-cWiEHZccQORf96q2y6zU3wDeIVPeidmGqd9cNKJRYoVHTV0S1eHPy5JTbHpMnGfDvtvujQwQozOqgO9ABu6h0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "17.65.0", + "@jsonjoy.com/codegen": "17.65.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.2.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.2", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-2.0.15.tgz", + "integrity": "sha512-MZrGem/Ujjd4cPTLYDfCZK2iKKeiO/8OX13S6jqxldLs0Prf2aGqVlJ77nMBqMv7fzqgXEgjrNHLXcKR8l9lOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/type": "^1.5.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 6" + } + }, + "node_modules/@ljharb/through": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.14.tgz", + "integrity": "sha512-ajBvlKpWucBB17FuQYUShqpqy8GRgYEpJW0vWJbUu1CV9lWyrDCapy0lScU8T8Z6qn49sSwJB3+M+evYIdGg+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.0.13.tgz", + "integrity": "sha512-uiKPB0Fv6WEEOZjruu9a6wnW/8jrjzlZbxXscMB8kuCJ1k6kHpcBnuvaAWcqhbI7rqX5GKziwWEdD+wi2gNLfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.0.13.tgz", + "integrity": "sha512-bEVIIfK5mSQoG1R19qA+fJOvCB+0wVGGnXHT3smchBVahYBdlPn2OsZZKzlHWfb1E+PhLBmYfqB5zQXFP7hJig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.0.13.tgz", + "integrity": "sha512-Yml1KlMzOnXj/tnW7yX8U78iAzTk39aILYvCPbqeewAq1kSzl+w59k/fiVkTBfvDi/oW/5YRxL+Fq+Y1Fr1r2Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.0.13.tgz", + "integrity": "sha512-afbVrsMgZ9dUTNUchFpj5VkmJRxvht/u335jUJ7o23YTbNbnpmXif3VKQGCtnjSh+CZaqm6N3CPG8KO3zwyZ1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.0.13.tgz", + "integrity": "sha512-vOtxu0xC0SLdQ2WRXg8Qgd8T32ak4SPqk5zjItRszrJk2BdeXqfGxBJbP7o4aOvSPSmSSv46Lr1EP4HXU8v7Kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.0.13.tgz", + "integrity": "sha512-UCrMJQY/gJnOl3XgbWRZZUvGGBuKy6i0YNSptgMzHBjs+QYDYR1Mt/RLTOPy4fzzves65O1EDmlL//OzEqoLlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@nestjs-modules/mailer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@nestjs-modules/mailer/-/mailer-2.0.2.tgz", + "integrity": "sha512-+z4mADQasg0H1ZaGu4zZTuKv2pu+XdErqx99PLFPzCDNTN/q9U59WPgkxVaHnsvKHNopLj5Xap7G4ZpptduoYw==", + "license": "MIT", + "dependencies": { + "@css-inline/css-inline": "0.14.1", + "glob": "10.3.12" + }, + "optionalDependencies": { + "@types/ejs": "^3.1.5", + "@types/mjml": "^4.7.4", + "@types/pug": "^2.0.10", + "ejs": "^3.1.10", + "handlebars": "^4.7.8", + "liquidjs": "^10.11.1", + "mjml": "^4.15.3", + "preview-email": "^3.0.19", + "pug": "^3.0.2" + }, + "peerDependencies": { + "@nestjs/common": ">=7.0.9", + "@nestjs/core": ">=7.0.9", + "@types/ejs": ">=3.0.3", + "@types/mjml": ">=4.7.4", + "@types/pug": ">=2.0.6", + "ejs": ">=3.1.2", + "handlebars": ">=4.7.6", + "liquidjs": ">=10.8.2", + "mjml": ">=4.15.3", + "nodemailer": ">=6.4.6", + "preview-email": ">=3.0.19", + "pug": ">=3.0.1" + } + }, + "node_modules/@nestjs/cli": { + "version": "10.4.9", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.9.tgz", + "integrity": "sha512-s8qYd97bggqeK7Op3iD49X2MpFtW4LVNLAwXFkfbRxKME6IYT7X0muNTJ2+QfI8hpbNx9isWkrLWIp+g5FOhiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "@angular-devkit/schematics-cli": "17.3.11", + "@nestjs/schematics": "^10.0.1", + "chalk": "4.1.2", + "chokidar": "3.6.0", + "cli-table3": "0.6.5", + "commander": "4.1.1", + "fork-ts-checker-webpack-plugin": "9.0.2", + "glob": "10.4.5", + "inquirer": "8.2.6", + "node-emoji": "1.11.0", + "ora": "5.4.1", + "tree-kill": "1.2.2", + "tsconfig-paths": "4.2.0", + "tsconfig-paths-webpack-plugin": "4.2.0", + "typescript": "5.7.2", + "webpack": "5.97.1", + "webpack-node-externals": "3.0.0" + }, + "bin": { + "nest": "bin/nest.js" + }, + "engines": { + "node": ">= 16.14" + }, + "peerDependencies": { + "@swc/cli": "^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0", + "@swc/core": "^1.3.62" + }, + "peerDependenciesMeta": { + "@swc/cli": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/@angular-devkit/core": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.11.tgz", + "integrity": "sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/@angular-devkit/schematics": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.11.tgz", + "integrity": "sha512-I5wviiIqiFwar9Pdk30Lujk8FczEEc18i22A5c6Z9lbmhPQdTroDnEQdsfXjy404wPe8H62s0I15o4pmMGfTYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "jsonc-parser": "3.2.1", + "magic-string": "0.30.8", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@nestjs/cli/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@nestjs/cli/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@nestjs/cli/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@nestjs/cli/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/@nestjs/cli/node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nestjs/cli/node_modules/magic-string": { + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@nestjs/cli/node_modules/picomatch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", + "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@nestjs/cli/node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@nestjs/cli/node_modules/webpack": { + "version": "5.97.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", + "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/@nestjs/common": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.22.tgz", + "integrity": "sha512-fxJ4v85nDHaqT1PmfNCQ37b/jcv2OojtXTaK1P2uAXhzLf9qq6WNUOFvxBrV4fhQek1EQoT1o9oj5xAZmv3NRw==", + "license": "MIT", + "peer": true, + "dependencies": { + "file-type": "20.4.1", + "iterare": "1.2.1", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.2.tgz", + "integrity": "sha512-McMW6EXtpc8+CwTUwFdg6h7dYcBUpH5iUILCclAsa+MbCEvC9ZKu4dCHRlJqALuhjLw97pbQu62l4+wRwGeZqA==", + "license": "MIT", + "dependencies": { + "dotenv": "16.4.7", + "dotenv-expand": "12.0.1", + "lodash": "4.17.21" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/config/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/@nestjs/core": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.22.tgz", + "integrity": "sha512-6IX9+VwjiKtCjx+mXVPncpkQ5ZjKfmssOZPFexmT+6T9H9wZ3svpYACAo7+9e7Nr9DZSoRZw3pffkJP7Z0UjaA==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@nuxtjs/opencollective": "0.3.2", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "3.3.0", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nestjs/jwt": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.2.tgz", + "integrity": "sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "9.0.10", + "jsonwebtoken": "9.0.3" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" + } + }, + "node_modules/@nestjs/passport": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-11.0.5.tgz", + "integrity": "sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "passport": "^0.5.0 || ^0.6.0 || ^0.7.0" + } + }, + "node_modules/@nestjs/platform-express": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.22.tgz", + "integrity": "sha512-ySSq7Py/DFozzZdNDH67m/vHoeVdphDniWBnl6q5QVoXldDdrZIHLXLRMPayTDh5A95nt7jjJzmD4qpTbNQ6tA==", + "license": "MIT", + "peer": true, + "dependencies": { + "body-parser": "1.20.4", + "cors": "2.8.5", + "express": "4.22.1", + "multer": "2.0.2", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0" + } + }, + "node_modules/@nestjs/schematics": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.2.3.tgz", + "integrity": "sha512-4e8gxaCk7DhBxVUly2PjYL4xC2ifDFexCqq1/u4TtivLGXotVk0wHdYuPYe1tHTHuR1lsOkRbfOCpkdTnigLVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "comment-json": "4.2.5", + "jsonc-parser": "3.3.1", + "pluralize": "8.0.0" + }, + "peerDependencies": { + "typescript": ">=4.8.2" + } + }, + "node_modules/@nestjs/schematics/node_modules/@angular-devkit/core": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.11.tgz", + "integrity": "sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@nestjs/schematics/node_modules/@angular-devkit/core/node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nestjs/schematics/node_modules/@angular-devkit/schematics": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.11.tgz", + "integrity": "sha512-I5wviiIqiFwar9Pdk30Lujk8FczEEc18i22A5c6Z9lbmhPQdTroDnEQdsfXjy404wPe8H62s0I15o4pmMGfTYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "17.3.11", + "jsonc-parser": "3.2.1", + "magic-string": "0.30.8", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@nestjs/schematics/node_modules/@angular-devkit/schematics/node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nestjs/schematics/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@nestjs/schematics/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@nestjs/schematics/node_modules/magic-string": { + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@nestjs/schematics/node_modules/picomatch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", + "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@nestjs/testing": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.22.tgz", + "integrity": "sha512-HO9aPus3bAedAC+jKVAA8jTdaj4fs5M9fing4giHrcYV2txe9CvC1l1WAjwQ9RDhEHdugjY4y+FZA/U/YqPZrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + } + } + }, + "node_modules/@nestjs/throttler": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-6.5.0.tgz", + "integrity": "sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "reflect-metadata": "^0.1.13 || ^0.2.0" + } + }, + "node_modules/@nestjs/typeorm": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.0.tgz", + "integrity": "sha512-SOeUQl70Lb2OfhGkvnh4KXWlsd+zA08RuuQgT7kKbzivngxzSo1Oc7Usu5VxCxACQC9wc2l9esOHILSJeK7rJA==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0", + "reflect-metadata": "^0.1.13 || ^0.2.0", + "rxjs": "^7.2.0", + "typeorm": "^0.3.0" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "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/@npmcli/agent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", + "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.8.tgz", + "integrity": "sha512-liASfw5cqhjNW9UFd+ruwwdEf/lbOAQjLL2XY2dFW/bkJheXDYZgOyul/4gVvEV4BWkTXjYGmDqMw9uegdbJNQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^7.0.0", + "ini": "^4.1.3", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^9.0.0", + "proc-log": "^4.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.1.0.tgz", + "integrity": "sha512-c8UuGLeZpm69BryRykLuKRyKFZYJsZSCT4aVY5ds4omyZqJ172ApzgfKJ5eV/r3HgLdUYgFVe54KSFVjKoe27w==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", + "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.2.1.tgz", + "integrity": "sha512-f7zYC6kQautXHvNbLEWgD/uGu1+xCn9izgqBfgItWSx22U0ZDekxN08A1vM8cTxj/cRVe0Q94Ode+tdoYmIOOQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^5.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^7.0.0", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^6.0.0", + "proc-log": "^4.0.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.2.tgz", + "integrity": "sha512-xhfYPXoV5Dy4UkY0D+v2KkwvnDfiA/8Mt3sWCGI/hM03NsYIH8ZaG6QzS9x7pje5vHZBZJ2v6VRFVTWACnqcmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-2.0.1.tgz", + "integrity": "sha512-YgsR5jCQZhVmTJvjduTOIHph0L73pK8xwMVaDY0PatySqVM9AZj93jpoXYSJqfHFxFkN9dmqTw6OiqExsS3LPw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-8.1.0.tgz", + "integrity": "sha512-y7efHHwghQfk28G2z3tlZ67pLG0XdfYbcVG26r7YIXALRsrVQcTq4/tdenSmdOrEsNahIYA/eh8aEVROWGFUDg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/package-json": "^5.0.0", + "@npmcli/promise-spawn": "^7.0.0", + "node-gyp": "^10.0.0", + "proc-log": "^4.0.0", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/run-script/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@nuxtjs/opencollective": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz", + "integrity": "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "consola": "^2.15.0", + "node-fetch": "^2.6.1" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "license": "MIT", + "optional": true + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", + "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", + "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", + "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", + "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", + "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", + "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", + "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", + "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", + "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", + "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", + "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", + "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", + "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", + "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", + "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", + "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@schematics/angular": { + "version": "18.2.21", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.21.tgz", + "integrity": "sha512-5Ai+NEflQZi67y4NsQ3o04iEp7zT0/BUFVCrJ3CueU3uYQGs8jrN1Lk6tvQ9c5HzGcTDrMXuTrCswyR9o6ecpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "18.2.21", + "@angular-devkit/schematics": "18.2.21", + "jsonc-parser": "3.3.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@selderee/plugin-htmlparser2": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", + "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "domhandler": "^5.0.3", + "selderee": "^0.11.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/@sigstore/bundle": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.3.2.tgz", + "integrity": "sha512-wueKWDk70QixNLB363yHc2D2ItTgYiMTdPwK8D9dKQMR3ZQ0c35IxP5xnwQ8cNLoCgCRcHf14kE+CLIvNX1zmA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.2" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-1.1.0.tgz", + "integrity": "sha512-JzBqdVIyqm2FRQCulY6nbQzMpJJpSiJ8XXWMhtOX9eKgaXXpfNOF53lzQEjIydlStnd/eFtuC1dW4VYdD93oRg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.3.3.tgz", + "integrity": "sha512-RpacQhBlwpBWd7KEJsRKcBQalbV28fvkxwTOJIqhIuDysMMaJW47V4OqW30iJB9uRpqOSxxEAQFdr8tTattReQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.3.2.tgz", + "integrity": "sha512-5Vz5dPVuunIIvC5vBb0APwo7qKA4G9yM48kPWJT+OEERs40md5GoUR1yedwpekWZ4m0Hhw44m6zU+ObsON+iDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^2.3.2", + "@sigstore/core": "^1.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "make-fetch-happen": "^13.0.1", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.4.tgz", + "integrity": "sha512-44vtsveTPUpqhm9NCrbU8CWLe3Vck2HO1PNLw7RIajbB7xhtn5RBPm1VNSCMwqGYHhDsBJG8gDF0q4lgydsJvw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.2", + "tuf-js": "^2.2.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-1.2.1.tgz", + "integrity": "sha512-8iKx79/F73DKbGfRf7+t4dqrc0bRr0thdPrxAtCKWRm/F0tG71i6O1rvlnScncJLLBZHn3h8M3c1BSUAb9yu8g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^2.3.2", + "@sigstore/core": "^1.1.0", + "@sigstore/protobuf-specs": "^0.3.2" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sqltools/formatter": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", + "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", + "license": "MIT" + }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-2.0.1.tgz", + "integrity": "sha512-92F7/SFyufn4DXsha9+QfKnN03JGqtMFMXgSHbZOo8JG59WkTni7UzAouNQDf7AuP9OAMxVOPQcqG3sB7w+kkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "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/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ejs": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz", + "integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.17", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", + "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jasmine": { + "version": "5.1.15", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.15.tgz", + "integrity": "sha512-ZAC8KjmV2MJxbNTrwXFN+HKeajpXQZp6KpPiR6Aa4XvaEnjP6qh23lL/Rqb7AYzlp3h/rcwDrQ7Gg7q28cQTQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/jspdf": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/jspdf/-/jspdf-1.3.3.tgz", + "integrity": "sha512-DqwyAKpVuv+7DniCp2Deq1xGvfdnKSNgl9Agun2w6dFvR5UKamiv4VfYUgcypd8S9ojUyARFIlZqBrYrBMQlew==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mjml": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/@types/mjml/-/mjml-4.7.4.tgz", + "integrity": "sha512-vyi1vzWgMzFMwZY7GSZYX0GU0dmtC8vLHwpgk+NWmwbwRSrlieVyJ9sn5elodwUfklJM7yGl0zQeet1brKTWaQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/mjml-core": "*" + } + }, + "node_modules/@types/mjml-core": { + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/@types/mjml-core/-/mjml-core-4.15.2.tgz", + "integrity": "sha512-Q7SxFXgoX979HP57DEVsRI50TV8x1V4lfCA4Up9AvfINDM5oD/X9ARgfoyX1qS987JCnDLv85JjkqAjt3hZSiQ==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "20.19.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", + "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, + "node_modules/@types/pug": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz", + "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, + "node_modules/@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.1.tgz", + "integrity": "sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.53.1", + "@typescript-eslint/type-utils": "8.53.1", + "@typescript-eslint/utils": "8.53.1", + "@typescript-eslint/visitor-keys": "8.53.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.53.1", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.1.tgz", + "integrity": "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.53.1", + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/typescript-estree": "8.53.1", + "@typescript-eslint/visitor-keys": "8.53.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.1.tgz", + "integrity": "sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.53.1", + "@typescript-eslint/types": "^8.53.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.1.tgz", + "integrity": "sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/visitor-keys": "8.53.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.1.tgz", + "integrity": "sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.53.1.tgz", + "integrity": "sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/typescript-estree": "8.53.1", + "@typescript-eslint/utils": "8.53.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.1.tgz", + "integrity": "sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.1.tgz", + "integrity": "sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.53.1", + "@typescript-eslint/tsconfig-utils": "8.53.1", + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/visitor-keys": "8.53.1", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.1.tgz", + "integrity": "sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.53.1", + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/typescript-estree": "8.53.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.1.tgz", + "integrity": "sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/@zone-eu/mailsplit": { + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/@zone-eu/mailsplit/-/mailsplit-5.4.8.tgz", + "integrity": "sha512-eEyACj4JZ7sjzRvy26QhLgKEMWwQbsw1+QZnlLX+/gihcNH07lVPOcnwf5U6UAL7gkc//J3jVd76o/WS+taUiA==", + "license": "(MIT OR EUPL-1.1+)", + "optional": true, + "dependencies": { + "libbase64": "1.3.0", + "libmime": "5.3.7", + "libqp": "2.1.1" + } + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "devOptional": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/alce": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/alce/-/alce-1.2.0.tgz", + "integrity": "sha512-XppPf2S42nO2WhvKzlwzlfcApcXHzjlod30pKmcWjRgLOtqoe5DMuqdiYoM6AgyXksc6A6pV4v1L/WW217e57w==", + "license": "MIT", + "optional": true, + "dependencies": { + "esprima": "^1.2.0", + "estraverse": "^1.5.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/alce/node_modules/esprima": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.5.tgz", + "integrity": "sha512-S9VbPDU0adFErpDai3qDkjq8+G05ONtKzcyNrPKg/ZKa+tf879nX2KexNU95b31UoTJjRLInNBHHHjFPoCd7lQ==", + "optional": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/alce/node_modules/estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha512-25w1fMXQrGdoquWnScXZGckOv+Wes+JDnuN/+7ex3SauFRS72r2lFDec0EKPt2YD1wUJ/IrfEex+9yp4hfSOJA==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "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/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "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==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/app-root-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", + "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/assert-never": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.4.0.tgz", + "integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==", + "license": "MIT", + "optional": true + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT", + "optional": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "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.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-jest/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-loader": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", + "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-loader/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/babel-loader/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/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/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", + "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.7", + "@babel/helper-define-polyfill-provider": "^0.6.5", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/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/babel-plugin-polyfill-corejs3": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", + "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.3", + "core-js-compat": "^3.40.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", + "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-walk": { + "version": "3.0.0-canary-5", + "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", + "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/types": "^7.9.6" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.17", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.17.tgz", + "integrity": "sha512-agD0MgJFUP/4nvjqzIB29zRPUuCF7Ge6mEv9s8dHrtYD7QWXRcx75rOADE/d5ah1NI+0vkDl0yorDd5U852IQQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/bonjour-service": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "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", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", + "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==", + "license": "MIT", + "optional": true, + "dependencies": { + "no-case": "^2.2.0", + "upper-case": "^1.1.1" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001765", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz", + "integrity": "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==", + "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/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/canvg/node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/character-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", + "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-regex": "^1.0.3" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "devOptional": 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/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/chokidar/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT", + "peer": true + }, + "node_modules/class-validator": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz", + "integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.20" + } + }, + "node_modules/clean-css": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", + "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", + "license": "MIT", + "optional": true, + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/clean-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "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/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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/comment-json": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz", + "integrity": "sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "core-util-is": "^1.0.3", + "esprima": "^4.0.1", + "has-own-prop": "^2.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true, + "license": "ISC" + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC", + "optional": true + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/connect/node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/connect/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/connect/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/consola": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", + "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", + "license": "MIT" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC", + "optional": true + }, + "node_modules/constantinople": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", + "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/parser": "^7.6.0", + "@babel/types": "^7.6.1" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", + "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.1", + "globby": "^14.0.0", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/copy-webpack-plugin/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/copy-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/core-js": { + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz", + "integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz", + "integrity": "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/critters": { + "version": "0.0.24", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.24.tgz", + "integrity": "sha512-Oyqew0FGM0wYUSNqR0L6AteO5MpMoUU0rhKRieXeiKs+PmRTxiJMyaunYB2KF6fQ3dzChXKCpbFOEJx3OQ1v/Q==", + "deprecated": "Ownership of Critters has moved to the Nuxt team, who will be maintaining the project going forward. If you'd like to keep using Critters, please switch to the actively-maintained fork at https://github.com/danielroe/beasties", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "chalk": "^4.1.0", + "css-select": "^5.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.2", + "htmlparser2": "^8.0.2", + "postcss": "^8.4.23", + "postcss-media-query-parser": "^0.2.3" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/css-loader": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", + "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.27.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "devOptional": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "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/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dedent": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", + "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.4.0.tgz", + "integrity": "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT", + "optional": true + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/display-notification": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/display-notification/-/display-notification-2.0.0.tgz", + "integrity": "sha512-TdmtlAcdqy1NU+j7zlkDdMnCL878zriLaBmoD9quOoq1ySSSGv03l0hXK5CvIFZlIfFI/hizqdQuW+Num7xuhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-applescript": "^1.0.0", + "run-applescript": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/display-notification/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "license": "MIT", + "optional": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/display-notification/node_modules/execa": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz", + "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==", + "license": "MIT", + "optional": true, + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/display-notification/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/display-notification/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/display-notification/node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "license": "MIT", + "optional": true, + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/display-notification/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/display-notification/node_modules/run-applescript": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-3.2.0.tgz", + "integrity": "sha512-Ep0RsvAjnRcBX1p5vogbaBdAGu/8j/ewpvGqnQYunnLd9SM0vWcPJewPKNnWFggf0hF0pwIgwV5XK7qQ7UZ8Qg==", + "license": "MIT", + "optional": true, + "dependencies": { + "execa": "^0.10.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/display-notification/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/display-notification/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "license": "MIT", + "optional": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/display-notification/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/display-notification/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, + "node_modules/display-notification/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/doctypes": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", + "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==", + "license": "MIT", + "optional": true + }, + "node_modules/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "devOptional": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", + "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "devOptional": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.1.tgz", + "integrity": "sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==", + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/editorconfig": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", + "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "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/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding-japanese": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.2.0.tgz", + "integrity": "sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io": { + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz", + "integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.2.tgz", + "integrity": "sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "punycode": "^1.4.1", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", + "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.0", + "@esbuild/android-arm": "0.23.0", + "@esbuild/android-arm64": "0.23.0", + "@esbuild/android-x64": "0.23.0", + "@esbuild/darwin-arm64": "0.23.0", + "@esbuild/darwin-x64": "0.23.0", + "@esbuild/freebsd-arm64": "0.23.0", + "@esbuild/freebsd-x64": "0.23.0", + "@esbuild/linux-arm": "0.23.0", + "@esbuild/linux-arm64": "0.23.0", + "@esbuild/linux-ia32": "0.23.0", + "@esbuild/linux-loong64": "0.23.0", + "@esbuild/linux-mips64el": "0.23.0", + "@esbuild/linux-ppc64": "0.23.0", + "@esbuild/linux-riscv64": "0.23.0", + "@esbuild/linux-s390x": "0.23.0", + "@esbuild/linux-x64": "0.23.0", + "@esbuild/netbsd-x64": "0.23.0", + "@esbuild/openbsd-arm64": "0.23.0", + "@esbuild/openbsd-x64": "0.23.0", + "@esbuild/sunos-x64": "0.23.0", + "@esbuild/win32-arm64": "0.23.0", + "@esbuild/win32-ia32": "0.23.0", + "@esbuild/win32-x64": "0.23.0" + } + }, + "node_modules/esbuild-wasm": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.23.0.tgz", + "integrity": "sha512-6jP8UmWy6R6TUUV8bMuC3ZyZ6lZKI56x0tkxyCIqWwRRJ/DgeQKneh/Oid5EoGoPFLrGNkz47ZEtWAYuiY/u9g==", + "dev": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-goat": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz", + "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-applescript": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/escape-string-applescript/-/escape-string-applescript-1.0.0.tgz", + "integrity": "sha512-4/hFwoYaC6TkpDn9A3pTC52zQPArFeXuIfhUtCGYdauTzXVP9H3BDr3oO/QzQehMpLDC7srvYgfwvImPFGfvBA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", + "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.12" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/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/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extend-object": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/extend-object/-/extend-object-1.0.0.tgz", + "integrity": "sha512-0dHDIXC7y7LDmCh/lp1oYkmv73K25AMugQI07r8eFopkW6f7Ufn1q+ETMsJjnV9Am14SlElkqy3O92r6xEaxPw==", + "license": "MIT", + "optional": true + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "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.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "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/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "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/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-type": { + "version": "20.4.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.4.1.tgz", + "integrity": "sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.6", + "strtok3": "^10.2.0", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fixpack": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fixpack/-/fixpack-4.0.0.tgz", + "integrity": "sha512-5SM1+H2CcuJ3gGEwTiVo/+nd/hYpNj9Ch3iMDOQ58ndY+VGQ2QdvaUTkd3otjZvYnd/8LF/HkJ5cx7PBq0orCQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "alce": "1.2.0", + "chalk": "^3.0.0", + "detect-indent": "^6.0.0", + "detect-newline": "^3.1.0", + "extend-object": "^1.0.0", + "rc": "^1.2.8" + }, + "bin": { + "fixpack": "bin/fixpack" + } + }, + "node_modules/fixpack/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fixpack/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "optional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.0.2.tgz", + "integrity": "sha512-Uochze2R8peoN1XqlSi/rGUkDQpRogtLFocP9+PGu68zk1BDAKXfdeCdyVZpgTk8V8WFVQXdEz426VKjXLO1Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "cosmiconfig": "^8.2.0", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">=12.13.0", + "yarn": ">=1.0.0" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "webpack": "^5.11.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "devOptional": true, + "license": "ISC" + }, + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, + "node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gaxios/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/gaxios/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-port": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", + "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regex.js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/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/globby/node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/googleapis": { + "version": "163.0.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-163.0.0.tgz", + "integrity": "sha512-5c94aCqw5awp2YbWXXlDJT5CnDGr7F/Aw43XHKFb5vQI0CcoZgx9bSImdIAYst2mOIWtnP62+HBBrEw15budNw==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.2.0", + "googleapis-common": "^8.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/googleapis-common": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-8.0.1.tgz", + "integrity": "sha512-eCzNACUXPb1PW5l0ULTzMHaL/ltPRADoPgjBlT8jWsTbxkCp6siv+qKJ/1ldaybCthGwsYFYallF7u9AkU4L+A==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^7.0.0-rc.4", + "google-auth-library": "^10.1.0", + "qs": "^6.7.0", + "url-template": "^2.0.8" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-own-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz", + "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC", + "optional": true + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "optional": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-minifier": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-4.0.0.tgz", + "integrity": "sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==", + "license": "MIT", + "optional": true, + "dependencies": { + "camel-case": "^3.0.0", + "clean-css": "^4.2.1", + "commander": "^2.19.0", + "he": "^1.2.0", + "param-case": "^2.1.1", + "relateurl": "^0.2.7", + "uglify-js": "^3.5.1" + }, + "bin": { + "html-minifier": "cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/html-minifier/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT", + "optional": true + }, + "node_modules/html-to-text": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", + "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@selderee/plugin-htmlparser2": "^0.11.0", + "deepmerge": "^4.3.1", + "dom-serializer": "^2.0.0", + "htmlparser2": "^8.0.2", + "selderee": "^0.11.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "optional": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "devOptional": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "devOptional": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-middleware": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.5.tgz", + "integrity": "sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.15", + "debug": "^4.3.6", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.3", + "is-plain-object": "^5.0.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.18" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.5.tgz", + "integrity": "sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "license": "ISC", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "devOptional": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/inquirer": { + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", + "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^6.0.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/inquirer/node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "node_modules/inquirer/node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inquirer/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-expression": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", + "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", + "license": "MIT", + "optional": true, + "dependencies": { + "acorn": "^7.1.1", + "object-assign": "^4.1.1" + } + }, + "node_modules/is-expression/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "license": "MIT", + "optional": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "devOptional": 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-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/is-network-error": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", + "integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT", + "optional": true + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "engines": { + "node": ">=6" + } + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jasmine-core": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.2.0.tgz", + "integrity": "sha512-tSAtdrvWybZkQmmaIoDgnvHG8ORUNw5kEVlO5CvrXj02Jjr9TZrmjFq7FUiOUzJiOP2wLGYT6PgrQgQF4R1xiw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest-config/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest-runtime/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.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-beautify": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", + "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "license": "MIT", + "optional": true, + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.4.2", + "js-cookie": "^3.0.5", + "nopt": "^7.2.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-beautify/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", + "optional": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-beautify/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "optional": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-stringify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", + "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==", + "license": "MIT", + "optional": true + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "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/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jspdf": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.0.0.tgz", + "integrity": "sha512-w12U97Z6edKd2tXDn3LzTLg7C7QLJlx0BPfM3ecjK2BckUl9/81vZ+r5gK4/3KQdhAcEZhENUxRhtgYBj75MqQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.2.4", + "html2canvas": "^1.0.0-rc.5" + } + }, + "node_modules/jstransformer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", + "integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-promise": "^2.0.0", + "promise": "^7.0.1" + } + }, + "node_modules/juice": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/juice/-/juice-10.0.1.tgz", + "integrity": "sha512-ZhJT1soxJCkOiO55/mz8yeBKTAJhRzX9WBO+16ZTqNTONnnVlUPyVBIzQ7lDRjaBdTbid+bAnyIon/GM3yp4cA==", + "license": "MIT", + "optional": true, + "dependencies": { + "cheerio": "1.0.0-rc.12", + "commander": "^6.1.0", + "mensch": "^0.3.4", + "slick": "^1.12.2", + "web-resource-inliner": "^6.0.1" + }, + "bin": { + "juice": "bin/juice" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/juice/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/karma": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", + "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.7.2", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/karma-chrome-launcher": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz", + "integrity": "sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "which": "^1.2.1" + } + }, + "node_modules/karma-chrome-launcher/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/karma-coverage": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", + "integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.0.5", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/karma-coverage/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma-coverage/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/karma-coverage/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/karma-coverage/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/karma-jasmine": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", + "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jasmine-core": "^4.1.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "karma": "^6.0.0" + } + }, + "node_modules/karma-jasmine-html-reporter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.1.0.tgz", + "integrity": "sha512-sPQE1+nlsn6Hwb5t+HHwyy0A1FNCVKuL1192b+XNauMYWThz2kweiBVW1DqloRpVvZIJkIoHVB7XRpK78n1xbQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "jasmine-core": "^4.0.0 || ^5.0.0", + "karma": "^6.0.0", + "karma-jasmine": "^5.0.0" + } + }, + "node_modules/karma-jasmine/node_modules/jasmine-core": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.1.tgz", + "integrity": "sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map-support": "^0.5.5" + } + }, + "node_modules/karma/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/karma/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/karma/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/karma/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/karma/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/karma/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/launch-editor": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz", + "integrity": "sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1", + "shell-quote": "^1.8.3" + } + }, + "node_modules/leac": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", + "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/leonards-media-api": { + "resolved": "apps/backend", + "link": true + }, + "node_modules/less": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", + "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/less-loader": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-12.2.0.tgz", + "integrity": "sha512-MYUxjSQSBUQmowc0l5nPieOYwMzGPUaTzB6inNW/bdPEG9zOL3eAAD1Qw5ZxSPk7we5dMojHwNODYMV1hq4EVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "less": "^3.5.0 || ^4.0.0", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/less/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/less/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/less/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/libbase64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-1.3.0.tgz", + "integrity": "sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==", + "license": "MIT", + "optional": true + }, + "node_modules/libmime": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.7.tgz", + "integrity": "sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw==", + "license": "MIT", + "optional": true, + "dependencies": { + "encoding-japanese": "2.2.0", + "iconv-lite": "0.6.3", + "libbase64": "1.3.0", + "libqp": "2.1.1" + } + }, + "node_modules/libmime/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.12.34", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.34.tgz", + "integrity": "sha512-v/Ip8k8eYdp7bINpzqDh46V/PaQ8sK+qi97nMQgjZzFlb166YFqlR/HVI+MzsI9JqcyyVWCOipmmretiaSyQyw==", + "license": "MIT" + }, + "node_modules/libqp": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/libqp/-/libqp-2.1.1.tgz", + "integrity": "sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow==", + "license": "MIT", + "optional": true + }, + "node_modules/license-webpack-plugin": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", + "dev": true, + "license": "ISC", + "dependencies": { + "webpack-sources": "^3.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-sources": { + "optional": true + } + } + }, + "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/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/liquidjs": { + "version": "10.24.0", + "resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.24.0.tgz", + "integrity": "sha512-TAUNAdgwaAXjjcUFuYVJm9kOVH7zc0mTKxsG9t9Lu4qdWjB2BEblyVIYpjWcmJLMGgiYqnGNJjpNMHx0gp/46A==", + "license": "MIT", + "optional": true, + "dependencies": { + "commander": "^10.0.0" + }, + "bin": { + "liquid": "bin/liquid.js", + "liquidjs": "bin/liquid.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/liquidjs" + } + }, + "node_modules/liquidjs/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/listr2": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.4.tgz", + "integrity": "sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lmdb": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.0.13.tgz", + "integrity": "sha512-UGe+BbaSUQtAMZobTb4nHvFMrmvuAQKSeaqAX2meTEQjfsbpl5sxdHD8T72OnwD4GU9uwNhYXIVe4QGs8N9Zyw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "msgpackr": "^1.10.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.4.1", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.0.13", + "@lmdb/lmdb-darwin-x64": "3.0.13", + "@lmdb/lmdb-linux-arm": "3.0.13", + "@lmdb/lmdb-linux-arm64": "3.0.13", + "@lmdb/lmdb-linux-x64": "3.0.13", + "@lmdb/lmdb-win32-x64": "3.0.13" + } + }, + "node_modules/lmdb/node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", + "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.2.0.tgz", + "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==", + "license": "MIT", + "optional": true + }, + "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/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/mailparser": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.9.1.tgz", + "integrity": "sha512-6vHZcco3fWsDMkf4Vz9iAfxvwrKNGbHx0dV1RKVphQ/zaNY34Buc7D37LSa09jeSeybWzYcTPjhiZFxzVRJedA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@zone-eu/mailsplit": "5.4.8", + "encoding-japanese": "2.2.0", + "he": "1.2.0", + "html-to-text": "9.0.5", + "iconv-lite": "0.7.0", + "libmime": "5.3.7", + "linkify-it": "5.0.0", + "nodemailer": "7.0.11", + "punycode.js": "2.3.1", + "tlds": "1.261.0" + } + }, + "node_modules/mailparser/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mailparser/node_modules/nodemailer": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.11.tgz", + "integrity": "sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw==", + "license": "MIT-0", + "optional": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/make-fetch-happen": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", + "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^2.0.0", + "cacache": "^18.0.0", + "http-cache-semantics": "^4.1.1", + "is-lambda": "^1.0.1", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/mensch": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz", + "integrity": "sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==", + "license": "MIT", + "optional": true + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "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/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "devOptional": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.0.tgz", + "integrity": "sha512-Zs1YsZVfemekSZG+44vBsYTLQORkPMwnlv+aehcxK/NLKC+EGhDB39/YePYYqx/sTk6NnYpuqikhSn7+JIevTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/mjml": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml/-/mjml-4.18.0.tgz", + "integrity": "sha512-rQM4aqFRrNvV1k733e8hJSopBjZvoSdBpRYzNTMAN+As0jqJsO5eN0wTT2IFtfe4PREzzu5b06RkPiUQdd0IIg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "mjml-cli": "4.18.0", + "mjml-core": "4.18.0", + "mjml-migrate": "4.18.0", + "mjml-preset-core": "4.18.0", + "mjml-validator": "4.18.0" + }, + "bin": { + "mjml": "bin/mjml" + } + }, + "node_modules/mjml-accordion": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-accordion/-/mjml-accordion-4.18.0.tgz", + "integrity": "sha512-9PUmy2JxIOGgAaVHvgVYX21nVAo3o/+wJckTTF/YTLGAqB+nm+44buxRzaXxVk7qXRwbCNfE8c8mlGVNh7vB1g==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-body": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-body/-/mjml-body-4.18.0.tgz", + "integrity": "sha512-34AwX70/7NkRIajPsa5j6NySRiNrlLatTKhiLwTVFiVtrEFlfCcbeMNmdVixI3Ldvs8209ZC6euaAnXDRyR1zw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-button": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-button/-/mjml-button-4.18.0.tgz", + "integrity": "sha512-ZsWMI0j7EcFCMqbqdVwMWhmsVc03FhmypWXokKopGhwySn4IAB4AOURonRmFrO7k6sDeQ+iJ9QtTu7jA+S8wmg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-carousel": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-carousel/-/mjml-carousel-4.18.0.tgz", + "integrity": "sha512-wY4g1CHCOoVSZuar7CLFon/qkPbICu71IT+6pa4BDwkAiaAMAemZPyy+a+iIUgdc8kHgSuHGsGf6PQzBSMWRZA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-cli": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-cli/-/mjml-cli-4.18.0.tgz", + "integrity": "sha512-N6CnA4o/q/VRnGPxTzvVnjAEcF7WUVVQGYfS9SPAp0qwyf7RysMmewdS9yN8GwXwZV6L2sKdn+3ANNi2FNsJ7w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "chokidar": "^3.0.0", + "glob": "^10.3.10", + "html-minifier": "^4.0.0", + "js-beautify": "^1.6.14", + "lodash": "^4.17.21", + "minimatch": "^9.0.3", + "mjml-core": "4.18.0", + "mjml-migrate": "4.18.0", + "mjml-parser-xml": "4.18.0", + "mjml-validator": "4.18.0", + "yargs": "^17.7.2" + }, + "bin": { + "mjml-cli": "bin/mjml" + } + }, + "node_modules/mjml-column": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-column/-/mjml-column-4.18.0.tgz", + "integrity": "sha512-0QZ1whxbHUmJaRT8tW+wmr3fWZ/kpsHKAd24c7Z/N1Otm/U2G0T/FFEFJ6cB25X6ZN0K40QZ8L9gdLfiSVuRbA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-core": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-core/-/mjml-core-4.18.0.tgz", + "integrity": "sha512-yey72LszXvIo5p0R6DB+YU8er/nP2wPsqpLKQCB0H8vG0WRT1sbSUvnCUOkKGn7subuyWDTdzHKbQO3XYIOmvg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "cheerio": "1.0.0-rc.12", + "detect-node": "^2.0.4", + "html-minifier": "^4.0.0", + "js-beautify": "^1.6.14", + "juice": "^10.0.0", + "lodash": "^4.17.21", + "mjml-migrate": "4.18.0", + "mjml-parser-xml": "4.18.0", + "mjml-validator": "4.18.0" + } + }, + "node_modules/mjml-divider": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-divider/-/mjml-divider-4.18.0.tgz", + "integrity": "sha512-FmGUVJqi4RYroh7y85vDx0aUKZgECkxHtMQ4pkLGQbZ2g93/Qt0Ek88DVCNJ5XwUAQQkE/TvrGMLHp3CIqpQ9Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-group": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-group/-/mjml-group-4.18.0.tgz", + "integrity": "sha512-28ABkXsKljBqj7XCC8GkQ94xz8HEU2XTyD+9LTlkDafzGp/MGJb8DcLh/7IkxCwqkQWyeMiDNLf1djsQ909Vxw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-head": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-head/-/mjml-head-4.18.0.tgz", + "integrity": "sha512-DS0adpIAsVMDIk2DOsHzjg+RNjQU0fF8jiVP9BmdRHVGrLPmpL9wIHZk2KvsKvZe7VaXXBijFt3DZ5/CQ/+D7Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-head-attributes": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-head-attributes/-/mjml-head-attributes-4.18.0.tgz", + "integrity": "sha512-nLzix1wrMnojE0RPGhk4iKqSRwHKjie2EPzgKT7CDzfqN+Ref03E5Q19x3cQTLgxvq3C3CnvCQBfnhoS3Eakug==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-head-breakpoint": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-head-breakpoint/-/mjml-head-breakpoint-4.18.0.tgz", + "integrity": "sha512-k6rwff+7i+vTQYJ/CjBfE20qNqPaW60IRH2x2oEPuCzmwDmoVWOcplJIuotSqIAdfwF9hLkICknisp1BpczVlQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-head-font": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-head-font/-/mjml-head-font-4.18.0.tgz", + "integrity": "sha512-ao8HB5nf+Dmxw4GO6lMMOlnj1lNZONai0GC9RobrZgPlghZw6hpURWGpkON7pQcy6XnOHwYwkV7Go/npzA2i7w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-head-html-attributes": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-head-html-attributes/-/mjml-head-html-attributes-4.18.0.tgz", + "integrity": "sha512-xaQE1rthe0RrNotwEr71X1tE+QQ489Yc0ynMm3oNMrohDI/TaCeazx8GAHPMM7VLduDA8D4A5wkZ6PuEvlJu4w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-head-preview": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-head-preview/-/mjml-head-preview-4.18.0.tgz", + "integrity": "sha512-2JvYqhbLyU/+Te6/1AXxzTNoHYCDYhXOVZP7wMvU4t7K34pXqyRUNO405atyHUY1MRafrl6RJ8cIx0x5vUX7PA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-head-style": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-head-style/-/mjml-head-style-4.18.0.tgz", + "integrity": "sha512-nEwDHkAqY3Fm7QWeAZc/a7MakZpXh6THfrE8/AWrfpgzTHrD/wihNUc09ztNpr6z/K1+JWgQfSF2BRc+X3P46g==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-head-title": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-head-title/-/mjml-head-title-4.18.0.tgz", + "integrity": "sha512-0Hm8o50rPMUQLSCOOa4D4pz9NajmCDccLvBYE4fwKdeUXjSJ6bwAYeMpveel8oNZMDUVJ4Hx+PskisEGHMHM2w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-hero": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-hero/-/mjml-hero-4.18.0.tgz", + "integrity": "sha512-rujm0ROM4QGWw77vnl3NaVaCKXrT4xTSHeAnkHKiY5AuRf6HPTgEtutq5pdel/y6Q9GrmxvN3HRESum7tpJCJw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-image": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-image/-/mjml-image-4.18.0.tgz", + "integrity": "sha512-e09NkoYwvzMcTv7V6H5doWD6Te2E1y2EvOLQJoXKVdQpDwyBWGdfnZke0scJGdA58HLAB+0mLYogpLwmfLaP5Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-migrate": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-migrate/-/mjml-migrate-4.18.0.tgz", + "integrity": "sha512-qfNCgW9zhJIsbPyXFA5RT/WY4mlje3N0WhHHOsHc0nY89Q01DenyslUy9nLLGXwi4K5FHS58oCjwWbMhwDcj1w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "js-beautify": "^1.6.14", + "lodash": "^4.17.21", + "mjml-core": "4.18.0", + "mjml-parser-xml": "4.18.0", + "yargs": "^17.7.2" + }, + "bin": { + "migrate": "lib/cli.js" + } + }, + "node_modules/mjml-navbar": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-navbar/-/mjml-navbar-4.18.0.tgz", + "integrity": "sha512-uho/MS2tfNAe+V9u2X7NoCco34MDbdp30ETA8009Qo1VCP/D8lZ+s69WGRPu6hvN/Y2pzBgZly++CMg3qFZqBQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-parser-xml": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-parser-xml/-/mjml-parser-xml-4.18.0.tgz", + "integrity": "sha512-sHSsZg4afY1heThuJzxa1Kvfh/QzB7/9P5fFUHeVnnxb07ZTXnhXWA6YbobdND5/l9+5yjN5/UgqDZm3tIT4Uw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "detect-node": "2.1.0", + "htmlparser2": "^9.1.0", + "lodash": "^4.17.21" + } + }, + "node_modules/mjml-parser-xml/node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/mjml-preset-core": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-preset-core/-/mjml-preset-core-4.18.0.tgz", + "integrity": "sha512-x3l8vMVtsaqM/jauMeZIN7HFD2t5A28J4U0o4849yIlRxiWguLFV5l3BL8Byol+YLkoLuT9PjaZs9RYv+FGfeg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "mjml-accordion": "4.18.0", + "mjml-body": "4.18.0", + "mjml-button": "4.18.0", + "mjml-carousel": "4.18.0", + "mjml-column": "4.18.0", + "mjml-divider": "4.18.0", + "mjml-group": "4.18.0", + "mjml-head": "4.18.0", + "mjml-head-attributes": "4.18.0", + "mjml-head-breakpoint": "4.18.0", + "mjml-head-font": "4.18.0", + "mjml-head-html-attributes": "4.18.0", + "mjml-head-preview": "4.18.0", + "mjml-head-style": "4.18.0", + "mjml-head-title": "4.18.0", + "mjml-hero": "4.18.0", + "mjml-image": "4.18.0", + "mjml-navbar": "4.18.0", + "mjml-raw": "4.18.0", + "mjml-section": "4.18.0", + "mjml-social": "4.18.0", + "mjml-spacer": "4.18.0", + "mjml-table": "4.18.0", + "mjml-text": "4.18.0", + "mjml-wrapper": "4.18.0" + } + }, + "node_modules/mjml-raw": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-raw/-/mjml-raw-4.18.0.tgz", + "integrity": "sha512-F/kViAwXm3ccPP52kw++/mHQbcYbYYxC8JH15TZxH8GLVZkX5CGKgcBrHhDK7WoIlfEIsVRZ6IZdlHjH8vgyxw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-section": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-section/-/mjml-section-4.18.0.tgz", + "integrity": "sha512-bB8My9zvIEkTOxej+TrjEeaeRT0lsypGeRADtdrRZXeqUClkkuCnCXlsNKSLGT8ZRqjUqWRc5z8ubDOvGk2+Gg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-social": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-social/-/mjml-social-4.18.0.tgz", + "integrity": "sha512-iAQc9g59L6L3VHDd55BxeIvk/zHkxflxmvuyYyOOvpmmKAvUBC//ULfpxiiM4yupofsThqFfrO+wc8d4kTRkbQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-spacer": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-spacer/-/mjml-spacer-4.18.0.tgz", + "integrity": "sha512-FK/0f5IBiONgaRpwNBs7G8EbLdAbmYqcIfHR8O8tP4LipAChLQKHO9vX3vrRMGLBZZNTESLObcFSVWmA40Mfpw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-table": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-table/-/mjml-table-4.18.0.tgz", + "integrity": "sha512-vJysCPUL3CHcsQDAFpW+skzBtY0RYsmMBYswI4WX0B05GLKlOjXqpYOwcmAupWeGoBVL5r/t28ynu2PqnOlN3w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-text": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-text/-/mjml-text-4.18.0.tgz", + "integrity": "sha512-hBLmF3JgveUKktKQFWHqHAr7qr92j1CxAvq7mtpDUgiWgyPFzqRX8mUsFYgZ7DmRxG4UE+Kzpt8/YFd9+E98lw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0" + } + }, + "node_modules/mjml-validator": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-validator/-/mjml-validator-4.18.0.tgz", + "integrity": "sha512-JmpWAsNTUlAxJOz2zHYfF8Vod8OzM3Qp5JXtrVw5tivZQzq88ZfqVGuqsas51z0pp1/ilfD4lC17YGfGwKGyhA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4" + } + }, + "node_modules/mjml-wrapper": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/mjml-wrapper/-/mjml-wrapper-4.18.0.tgz", + "integrity": "sha512-TZeOvLjIhXEK60rjWNiYhEYNlv5GKYahE+96ifcT5OGkWkRA0DsQDfp+6VI32OS5VxsfKq2h/UdERPlQijjpAQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "lodash": "^4.17.21", + "mjml-core": "4.18.0", + "mjml-section": "4.18.0" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/msgpackr": { + "version": "1.11.8", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.8.tgz", + "integrity": "sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/needle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/nice-napi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", + "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "!win32" + ], + "dependencies": { + "node-addon-api": "^3.0.0", + "node-gyp-build": "^4.2.2" + } + }, + "node_modules/nice-napi/node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "license": "MIT", + "optional": true + }, + "node_modules/no-case": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", + "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "lower-case": "^1.1.1" + } + }, + "node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", + "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.3.1.tgz", + "integrity": "sha512-Pp3nFHBThHzVtNY7U6JfPjvT/DTE8+o/4xKsLQtBoU+j2HLsGlhcfzflAoUreaJbNmYnX+LlLi0qjV8kpyO6xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^13.0.0", + "nopt": "^7.0.0", + "proc-log": "^4.1.0", + "semver": "^7.3.5", + "tar": "^6.2.1", + "which": "^4.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemailer": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.12.tgz", + "integrity": "sha512-H+rnK5bX2Pi/6ms3sN4/jRQvYSMltV6vqup/0SFOrxYYY/qoNvhXPlYq3e+Pm9RFJRwrMGbMIwi81M4dxpomhA==", + "license": "MIT-0", + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-bundled": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.1.tgz", + "integrity": "sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-install-checks": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", + "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz", + "integrity": "sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^7.0.0", + "proc-log": "^4.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-packlist": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.2.tgz", + "integrity": "sha512-shYrPFIS/JLP4oQmAwDyk5HcyysKW8/JLTEA32S0Z5TzvpaeeX2yMFfoK1fjEBnCBvVyIB/Jj/GBFdm0wsgzbA==", + "dev": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^6.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.1.0.tgz", + "integrity": "sha512-nkc+3pIIhqHVQr085X9d2JzPzLyjzQS96zbruppqC9aZRm/x8xx6xhI98gHtsfELP2bE+loHq8ZaHFHhe+NauA==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^11.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-17.1.0.tgz", + "integrity": "sha512-5+bKQRH0J1xG1uZ1zMNvxW0VEyoNWgJpY9UDuluPFLKDfJ9u2JmmjmTJV1srBGQOROfdBMiVvnH2Zvpbm+xkVA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^2.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^13.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minizlib": "^2.1.2", + "npm-package-arg": "^11.0.0", + "proc-log": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "devOptional": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openai": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.16.0.tgz", + "integrity": "sha512-fZ1uBqjFUjXzbGc35fFtYKEOxd20kd9fDpFeqWtsOZWiubY8CZ1NAlXHW3iathaFvqmNtCWMIsosCuyeI7Joxg==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ordered-binary": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.1.tgz", + "integrity": "sha512-QkCdPooczexPLiXIrbVOPYkR3VO3T6v2OyKRkR1Xbhpy7/LAVXwahnRCgRp78Oe/Ehf0C/HATAxfSr6eA1oX+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-event": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz", + "integrity": "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "p-timeout": "^3.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "license": "MIT", + "optional": true, + "dependencies": { + "p-finally": "^1.0.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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/p-wait-for": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-wait-for/-/p-wait-for-3.2.0.tgz", + "integrity": "sha512-wpgERjNkLrBiFmkMEjuZJEWKKDrNfHCKA1OhyN1wg1FrLkULbviEy6py1AyJUgZ72YWFbZ38FIpnqvVqAlDUwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "p-timeout": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/pacote": { + "version": "18.0.6", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-18.0.6.tgz", + "integrity": "sha512-+eK3G27SMwsB8kLIuj4h1FUhHtwiEUo21Tw8wNjmvdlpOEr613edv+8FUsTj/4F/VN5ywGE19X18N7CC2EJk6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^5.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/package-json": "^5.1.0", + "@npmcli/promise-spawn": "^7.0.0", + "@npmcli/run-script": "^8.0.0", + "cacache": "^18.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^11.0.0", + "npm-packlist": "^8.0.0", + "npm-pick-manifest": "^9.0.0", + "npm-registry-fetch": "^17.0.0", + "proc-log": "^4.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^2.2.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, + "node_modules/param-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", + "integrity": "sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==", + "license": "MIT", + "optional": true, + "dependencies": { + "no-case": "^2.2.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", + "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.3.0", + "parse5": "^7.0.0", + "parse5-sax-parser": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", + "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseley": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", + "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", + "license": "MIT", + "optional": true, + "dependencies": { + "leac": "^0.6.0", + "peberminta": "^0.9.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "license": "MIT", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, + "node_modules/peberminta": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", + "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, + "node_modules/pg": { + "version": "8.17.2", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.17.2.tgz", + "integrity": "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw==", + "license": "MIT", + "peer": true, + "dependencies": { + "pg-connection-string": "^2.10.1", + "pg-pool": "^3.11.0", + "pg-protocol": "^1.11.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.10.1.tgz", + "integrity": "sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.11.0.tgz", + "integrity": "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz", + "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "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/piscina": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.6.1.tgz", + "integrity": "sha512-z30AwWGtQE+Apr+2WBZensP2lIvwoaMcOPkQlIEmSGMJNUvaYACylPYrQM6wSdUNJlnDVMSpLv7xTMJqlVshOA==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "nice-napi": "^1.0.2" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/pkg-dir/node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.4.41", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", + "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "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", + "peer": true, + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-loader": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", + "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cosmiconfig": "^9.0.0", + "jiti": "^1.20.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/postcss-loader/node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "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/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/preview-email": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/preview-email/-/preview-email-3.1.1.tgz", + "integrity": "sha512-nrdhnt+E9ClJ4khk9rNzqgsxubH7xSJSKoqXx/7aed2eghegNGNWkSGOelNgFgUtMz3LmKGks0waH2NuXWWmPg==", + "license": "MIT", + "optional": true, + "dependencies": { + "ci-info": "^3.8.0", + "display-notification": "2.0.0", + "fixpack": "^4.0.0", + "get-port": "5.1.1", + "mailparser": "^3.9.1", + "nodemailer": "^7.0.12", + "open": "7", + "p-event": "4.2.0", + "p-wait-for": "3.2.0", + "pug": "^3.0.3", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/preview-email/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "optional": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/preview-email/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/preview-email/node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "license": "MIT", + "optional": true, + "dependencies": { + "asap": "~2.0.3" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "license": "ISC", + "optional": true + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/pug": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.3.tgz", + "integrity": "sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==", + "license": "MIT", + "optional": true, + "dependencies": { + "pug-code-gen": "^3.0.3", + "pug-filters": "^4.0.0", + "pug-lexer": "^5.0.1", + "pug-linker": "^4.0.0", + "pug-load": "^3.0.0", + "pug-parser": "^6.0.0", + "pug-runtime": "^3.0.1", + "pug-strip-comments": "^2.0.0" + } + }, + "node_modules/pug-attrs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz", + "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", + "license": "MIT", + "optional": true, + "dependencies": { + "constantinople": "^4.0.1", + "js-stringify": "^1.0.2", + "pug-runtime": "^3.0.0" + } + }, + "node_modules/pug-code-gen": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.3.tgz", + "integrity": "sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==", + "license": "MIT", + "optional": true, + "dependencies": { + "constantinople": "^4.0.1", + "doctypes": "^1.1.0", + "js-stringify": "^1.0.2", + "pug-attrs": "^3.0.0", + "pug-error": "^2.1.0", + "pug-runtime": "^3.0.1", + "void-elements": "^3.1.0", + "with": "^7.0.0" + } + }, + "node_modules/pug-code-gen/node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pug-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz", + "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==", + "license": "MIT", + "optional": true + }, + "node_modules/pug-filters": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz", + "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==", + "license": "MIT", + "optional": true, + "dependencies": { + "constantinople": "^4.0.1", + "jstransformer": "1.0.0", + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0", + "resolve": "^1.15.1" + } + }, + "node_modules/pug-lexer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz", + "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==", + "license": "MIT", + "optional": true, + "dependencies": { + "character-parser": "^2.2.0", + "is-expression": "^4.0.0", + "pug-error": "^2.0.0" + } + }, + "node_modules/pug-linker": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz", + "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", + "license": "MIT", + "optional": true, + "dependencies": { + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0" + } + }, + "node_modules/pug-load": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz", + "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "object-assign": "^4.1.1", + "pug-walk": "^2.0.0" + } + }, + "node_modules/pug-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz", + "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", + "license": "MIT", + "optional": true, + "dependencies": { + "pug-error": "^2.0.0", + "token-stream": "1.0.0" + } + }, + "node_modules/pug-runtime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz", + "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==", + "license": "MIT", + "optional": true + }, + "node_modules/pug-strip-comments": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz", + "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "pug-error": "^2.0.0" + } + }, + "node_modules/pug-walk": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", + "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true, + "license": "MIT" + }, + "node_modules/regex-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.1.tgz", + "integrity": "sha512-yXLRqatcCuKtVHsWrNg0JL3l1zGfdXeEvDa0bdu4tCDQw0RpMDZsqbkyRTUnKMR0tXF627V2oEWjBEaEdqTwtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "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-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/resolve-url-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "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/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "devOptional": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "devOptional": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/rollup": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", + "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.22.4", + "@rollup/rollup-android-arm64": "4.22.4", + "@rollup/rollup-darwin-arm64": "4.22.4", + "@rollup/rollup-darwin-x64": "4.22.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", + "@rollup/rollup-linux-arm-musleabihf": "4.22.4", + "@rollup/rollup-linux-arm64-gnu": "4.22.4", + "@rollup/rollup-linux-arm64-musl": "4.22.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", + "@rollup/rollup-linux-riscv64-gnu": "4.22.4", + "@rollup/rollup-linux-s390x-gnu": "4.22.4", + "@rollup/rollup-linux-x64-gnu": "4.22.4", + "@rollup/rollup-linux-x64-musl": "4.22.4", + "@rollup/rollup-win32-arm64-msvc": "4.22.4", + "@rollup/rollup-win32-ia32-msvc": "4.22.4", + "@rollup/rollup-win32-x64-msvc": "4.22.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "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/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.77.6", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.6.tgz", + "integrity": "sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-loader": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.0.tgz", + "integrity": "sha512-n13Z+3rU9A177dk4888czcVFiC8CL9dii4qpXWUg3YIIgZEvi9TCFKjOQcbK0kJM7DJu9VucrZFddvNfYCPwtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/sax": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", + "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/selderee": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", + "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", + "license": "MIT", + "optional": true, + "dependencies": { + "parseley": "^0.12.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true, + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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", + "optional": true + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sigstore": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.3.1.tgz", + "integrity": "sha512-8G+/XDU8wNsJOQS5ysDVO0Etg9/2uA5gR9l4ZwijjlwxBcrU6RPfwi2+jJmbP+Ap1Hlp/nVAaEO4Fj22/SL2gQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^2.3.2", + "@sigstore/core": "^1.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "@sigstore/sign": "^2.3.2", + "@sigstore/tuf": "^2.3.4", + "@sigstore/verify": "^1.2.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "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/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "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": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slick": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", + "integrity": "sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==", + "license": "MIT (http://mootools.net/license.txt)", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", + "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.18.3" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/sockjs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "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/source-map-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", + "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, + "node_modules/source-map-loader/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/sql-highlight": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sql-highlight/-/sql-highlight-6.1.0.tgz", + "integrity": "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==", + "funding": [ + "https://github.com/scriptcoded/sql-highlight?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/scriptcoded" + } + ], + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/sqlite3/node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/sqlite3/node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true + }, + "node_modules/sqlite3/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/sqlite3/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/sqlite3/node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sqlite3/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/sqlite3/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sqlite3/node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/sqlite3/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/sqlite3/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sqlite3/node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "license": "ISC", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sqlite3/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/sqlite3/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sqlite3/node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/sqlite3/node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/sqlite3/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sqlite3/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/sqlite3/node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/sqlite3/node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/sqlite3/node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sqlite3/node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/sqlite3/node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/sqlite3/node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "license": "ISC", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/sqlite3/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/ssri": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/streamroller/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/streamroller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/streamroller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "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/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strtok3": { + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", + "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz", + "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/terser": { + "version": "5.31.6", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz", + "integrity": "sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/thingies": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.5.0.tgz", + "integrity": "sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tlds": { + "version": "1.261.0", + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz", + "integrity": "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==", + "license": "MIT", + "optional": true, + "bin": { + "tlds": "bin.js" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", + "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==", + "license": "MIT", + "optional": true + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tree-dump": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", + "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-jest": { + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-loader": { + "version": "9.5.4", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.4.tgz", + "integrity": "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths-webpack-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/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", + "peer": true + }, + "node_modules/tuf-js": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.2.1.tgz", + "integrity": "sha512-GwIJau9XaA8nLVbUXsN3IlFi7WmQ48gBUrl3FTkkL/XLu/POhBzfmX9hd33FNMX1qAsfl6ozO1iMmW9NC8YniA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "2.0.1", + "debug": "^4.3.4", + "make-fetch-happen": "^13.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-assert": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", + "dev": true, + "license": "MIT" + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/typeorm": { + "version": "0.3.28", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.28.tgz", + "integrity": "sha512-6GH7wXhtfq2D33ZuRXYwIsl/qM5685WZcODZb7noOOcRMteM9KF2x2ap3H0EBjnSV0VO4gNAfJT5Ukp0PkOlvg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@sqltools/formatter": "^1.2.5", + "ansis": "^4.2.0", + "app-root-path": "^3.1.0", + "buffer": "^6.0.3", + "dayjs": "^1.11.19", + "debug": "^4.4.3", + "dedent": "^1.7.0", + "dotenv": "^16.6.1", + "glob": "^10.5.0", + "reflect-metadata": "^0.2.2", + "sha.js": "^2.4.12", + "sql-highlight": "^6.1.0", + "tslib": "^2.8.1", + "uuid": "^11.1.0", + "yargs": "^17.7.2" + }, + "bin": { + "typeorm": "cli.js", + "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js", + "typeorm-ts-node-esm": "cli-ts-node-esm.js" + }, + "engines": { + "node": ">=16.13.0" + }, + "funding": { + "url": "https://opencollective.com/typeorm" + }, + "peerDependencies": { + "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@sap/hana-client": "^2.14.22", + "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", + "ioredis": "^5.0.4", + "mongodb": "^5.8.0 || ^6.0.0", + "mssql": "^9.1.1 || ^10.0.0 || ^11.0.0 || ^12.0.0", + "mysql2": "^2.2.5 || ^3.0.1", + "oracledb": "^6.3.0", + "pg": "^8.5.1", + "pg-native": "^3.0.0", + "pg-query-stream": "^4.0.0", + "redis": "^3.1.1 || ^4.0.0 || ^5.0.14", + "sql.js": "^1.4.0", + "sqlite3": "^5.0.3", + "ts-node": "^10.7.0", + "typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@google-cloud/spanner": { + "optional": true + }, + "@sap/hana-client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "mssql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "pg-query-stream": { + "optional": true + }, + "redis": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "ts-node": { + "optional": true + }, + "typeorm-aurora-data-api-driver": { + "optional": true + } + } + }, + "node_modules/typeorm/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "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": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/typeorm/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/typeorm/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/typeorm/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.41", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.41.tgz", + "integrity": "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT", + "optional": true + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==", + "license": "MIT", + "optional": true + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uri-js/node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", + "license": "BSD" + }, + "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==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/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/valid-data-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", + "integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/validator": { + "version": "13.15.26", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", + "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", + "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/web-resource-inliner": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-6.0.1.tgz", + "integrity": "sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-colors": "^4.1.1", + "escape-goat": "^3.0.0", + "htmlparser2": "^5.0.0", + "mime": "^2.4.6", + "node-fetch": "^2.6.0", + "valid-data-url": "^3.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/web-resource-inliner/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", + "optional": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/dom-serializer/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/domhandler": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", + "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "domelementtype": "^2.0.1" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/domutils/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "optional": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/web-resource-inliner/node_modules/htmlparser2": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-5.0.1.tgz", + "integrity": "sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^3.3.0", + "domutils": "^2.4.2", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/fb55/htmlparser2?sponsor=1" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/webpack": { + "version": "5.104.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", + "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.4", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.4.4", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", + "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^4.6.0", + "mime-types": "^2.1.31", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware/node_modules/memfs": { + "version": "4.56.9", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.56.9.tgz", + "integrity": "sha512-Fo+xSG0MhtaPyEi7B2AEj4omBen3kb5RCeFKaM/YVsxgO8vkcpX0tkheRIoCGqXw9oAnFQRe1oWuR1xq4oE17A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.56.9", + "@jsonjoy.com/fs-fsa": "4.56.9", + "@jsonjoy.com/fs-node": "4.56.9", + "@jsonjoy.com/fs-node-builtins": "4.56.9", + "@jsonjoy.com/fs-node-to-fsa": "4.56.9", + "@jsonjoy.com/fs-node-utils": "4.56.9", + "@jsonjoy.com/fs-print": "4.56.9", + "@jsonjoy.com/fs-snapshot": "^4.56.9", + "@jsonjoy.com/json-pack": "^1.11.0", + "@jsonjoy.com/util": "^1.9.0", + "glob-to-regex.js": "^1.0.1", + "thingies": "^2.5.0", + "tree-dump": "^1.0.3", + "tslib": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/webpack-dev-middleware/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack-dev-server": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz", + "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/express-serve-static-core": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "express": "^4.21.2", + "graceful-fs": "^4.2.6", + "http-proxy-middleware": "^2.0.9", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/ipaddr.js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", + "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-dev-server/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-node-externals": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz", + "integrity": "sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-subresource-integrity": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "typed-assert": "^1.0.8" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", + "webpack": "^5.12.0" + }, + "peerDependenciesMeta": { + "html-webpack-plugin": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack/node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/website-base-v2": { + "resolved": "apps/frontend", + "link": true + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/with": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", + "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/parser": "^7.9.6", + "@babel/types": "^7.9.6", + "assert-never": "^1.2.1", + "babel-walk": "3.0.0-canary-5" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "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": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zone.js": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.10.tgz", + "integrity": "sha512-YGAhaO7J5ywOXW6InXNlLmfU194F8lVgu7bRntUF3TiG8Y3nBK0x1UJJuHUP/e8IyihkjCYqhCScpSwnlaSRkQ==", + "license": "MIT", + "peer": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d3d4a6a --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "lub-project", + "version": "1.0.0", + "private": true, + "workspaces": [ + "apps/frontend", + "apps/backend" + ], + "scripts": { + "dev:f": "npm run start -w apps/frontend -- --host 0.0.0.0", + "dev:b": "npm run start:dev -w apps/backend -- --host 0.0.0.0", + "dev": "concurrently \"npm run dev:backend\" \"npm run dev:frontend -- --host 0.0.0.0\"", + "build:frontend": "npm run build -w apps/frontend", + "build:backend": "npm run build -w apps/backend", + "build:all": "npm run build:backend && npm run build:frontend" + }, + "dependencies": { + "rxjs": "~7.8.0" + }, + "devDependencies": { + "concurrently": "^8.2.2" + }, + "overrides": { + "rxjs": "~7.8.0" + } +} \ No newline at end of file