commit 2bb9aab82c214c61dc4abdf3942e31edced09a4f Author: nadja Date: Tue Aug 25 20:29:14 2026 +0200 init push diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a6aa602 --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# WICHTIG: Erstelle eine Kopie als ".env" und ändere die Werte! + +# Geheimnis für JWT-Tokens (mind. 32 Zeichen) +JWT_SECRET=super-langes-geheimnis-hier-aendern + +# Admin-Account (wird beim ersten Start erstellt) +ADMIN_USER=admin +ADMIN_PASS=DEIN-SICHERES-PASSWORT + +# Ollama KI-Modell (optional, Standard: mistral) +# OLLAMA_MODEL=mistral diff --git a/README.md b/README.md new file mode 100644 index 0000000..9928c27 --- /dev/null +++ b/README.md @@ -0,0 +1,249 @@ +# TikTok Rezepte + +Deine Rezepte, immer griffbereit. Speichere, organisiere und durchsuche Rezepte aus TikTok-Videos. Mit KI-gestützter Extraktion von Zutaten und Zubereitungsschritten. + +## Features + +- **TikTok-Extraktion** — URL einfügen, Rezept wird automatisch erkannt +- **KI-Unterstützung** — Ollama extrahiert Zutaten und Schritte aus dem Video-Titel (optional) +- **Suche & Filter** — Rezepte nach Name, Zutaten oder Kategorie durchsuchen +- **Einkaufsliste** — Zutaten mit Haken abhaken beim Kochen +- **Schritt für Schritt** — Zubereitung mit Fortschrittsanzeige +- **Invite-System** — Geschlossene Registrierung, nur eingeladene Nutzer +- **Admin-Dashboard** — Einladungen verwalten, Nutzer einsehen und löschen +- **Profil** — Username und Passwort ändern +- **Responsive** — Funktioniert auf Desktop, Tablet und Handy + +## Screenshots + +| Startseite | Rezept hinzufügen | Admin Dashboard | +|---|---|---| +| Rezepte als Karten mit Suche und Kategorien | TikTok-URL + KI-Extraktion | Einladungen + Nutzerverwaltung | + +## Tech Stack + +| Bereich | Technologie | +|---|---| +| Frontend | React 18, Vite, Tailwind CSS, React Router | +| Backend | Node.js, Express, bcryptjs, jsonwebtoken | +| Datenbank | JSON-Datei (keine Datenbank-Installation nötig) | +| KI | Ollama mit Mistral-Modell (optional) | +| Deployment | Docker Compose (Nginx + Node.js + Ollama) | + +## Projektstruktur + +``` +tiktok-rezepte/ +├── docker-compose.yml # Docker Deployment +├── .env.example # Umgebungsvariablen-Vorlage +│ +├── backend/ # Express API Server +│ ├── Dockerfile +│ ├── server.js # Einstiegspunkt +│ ├── db.js # JSON-Datenbank +│ ├── routes/ +│ │ ├── auth.js # Authentifizierung & Benutzerverwaltung +│ │ └── recipes.js # Rezepte CRUD + TikTok-Extraktion +│ ├── services/ +│ │ ├── tiktok.js # TikTok oEmbed API +│ │ └── ai.js # Ollama KI-Integration +│ └── prompts/ +│ └── extractRecipe.js # System-Prompt für KI-Extraktion +│ +└── frontend/ # React SPA + ├── Dockerfile + ├── nginx.conf # Nginx-Konfiguration + ├── vite.config.js # Vite + Dev-Server Proxy + └── src/ + ├── App.jsx # Routen & Navigation + ├── context/ + │ └── AuthContext.jsx # Auth-Zustand (JWT) + ├── pages/ + │ ├── Home.jsx # Rezeptübersicht mit Suche + │ ├── AddRecipe.jsx # TikTok-URL + KI-Extraktion + │ ├── RecipeDetail.jsx # Rezept ansehen/bearbeiten + │ ├── Login.jsx # Anmeldung + │ ├── Register.jsx # Registrierung (Invite) + │ ├── Profile.jsx # Profil bearbeiten + │ └── Admin.jsx # Admin Dashboard + └── components/ + ├── RecipeCard.jsx # Rezept-Karte + ├── IngredientList.jsx # Zutaten (Lesen/Bearbeiten) + ├── StepList.jsx # Schritte (Lesen/Bearbeiten) + └── CategoryBadge.jsx # Kategorie-Badge +``` + +## Schnellstart (lokal) + +### Voraussetzungen + +- Node.js 18+ +- (Optional) Ollama für KI-Extraktion + +### Installation + +```bash +# Repository klonen +git clone /dein-user/tiktok-rezepte.git +cd tiktok-rezepte + +# Backend installieren +cd backend +npm install + +# Frontend installieren +cd ../frontend +npm install +``` + +### Starten + +```bash +# Backend starten (Port 3001) +cd backend +npm run dev + +# Frontend starten (Port 5173, in anderem Terminal) +cd frontend +npm run dev +``` + +Oder über das Root-Projekt: +```bash +npm run dev +``` + +Die App ist dann unter `http://localhost:5173` erreichbar. + +### Standard-Login + +| Benutzername | Passwort | +|---|---| +| `admin` | `admin123` | + +> **Wichtig:** Passwort nach dem ersten Login ändern! + +### Ollama (optional) + +Für KI-gestützte Rezept-Extraktion: + +```bash +# Ollama installieren (https://ollama.com) +ollama pull mistral +``` + +Ohne Ollama funktioniert die App vollständig — TikTok-Titel werden übernommen, Zutaten und Schritte werden manuell ausgefüllt. + +## Docker Deployment + +### Voraussetzungen + +- Docker & Docker Compose +- (Optional) Synology NAS mit Container Manager + +### Schritte + +```bash +# 1. .env-Datei anlegen +cp .env.example .env +# Werte in .env ändern (JWT_SECRET, ADMIN_PASS)! + +# 2. Docker starten +docker compose up -d + +# 3. Ollama Modell herunterladen +docker exec -it ollama pull mistral +``` + +### Umgebungsvariablen + +| Variable | Beschreibung | Standard | +|---|---|---| +| `JWT_SECRET` | Geheimnis für JWT-Tokens | `tiktok-rezepte-secret-key-...` | +| `ADMIN_USER` | Initialer Admin-Username | `admin` | +| `ADMIN_PASS` | Initial admin password | `admin123` | +| `PORT` | Backend-Port | `3001` | +| `OLLAMA_URL` | Ollama API-URL | `http://localhost:11434` | +| `OLLAMA_MODEL` | KI-Modell | `mistral` | +| `DB_PATH` | Pfad zur Datenbank-Datei | `./data.json` | + +### Synology NAS mit Reverse Proxy + +1. Code nach Gitea pushen +2. Container Manager → Projekt → "Aus Git-Repository" +3. Gitea-URL angeben, `docker-compose.yml` wählen +4. Environment-Variablen setzen +5. Deploy starten +6. Systemsteuerung → Login-Dienste → Reverse Proxy einrichten +7. SSL-Zertifikat über Let's Encrypt beantragen + +## API Endpoints + +### Authentifizierung (`/api/auth`) + +| Methode | Pfad | Beschreibung | +|---|---|---| +| POST | `/register` | Registrierung (Invite-Token erforderlich) | +| POST | `/login` | Anmeldung, gibt JWT zurück | +| GET | `/me` | Aktuellen Benutzer abrufen | +| POST | `/invite` | Einladung erstellen (Admin) | +| GET | `/invites` | Alle Einladungen anzeigen (Admin) | +| GET | `/users` | Alle Benutzer anzeigen (Admin) | +| DELETE | `/users/:id` | Benutzer löschen (Admin) | +| PUT | `/credentials` | Username/Passwort ändern | + +### Rezepte (`/api/recipes`) + +| Methode | Pfad | Beschreibung | +|---|---|---| +| GET | `/` | Rezepte auflisten (optional: `?search=&category=`) | +| GET | `/categories` | Kategorien mit Anzahl | +| GET | `/ai-status` | Ollama-Verfügbarkeit prüfen | +| GET | `/:id` | Einzelnes Rezept | +| POST | `/` | Rezept erstellen | +| PUT | `/:id` | Rezept aktualisieren | +| DELETE | `/:id` | Rezept löschen | +| POST | `/from-tiktok` | Rezept aus TikTok-URL extrahieren | + +## Kategorien + +| Kategorie | Emoji | +|---|---| +| Frühstück | 🍳 | +| Hauptgericht | 🍽️ | +| Dessert | 🍰 | +| Snack | 🍿 | +| Getränk | ☕ | +| Sonstiges | 📋 | + +## Wartung + +### Daten sichern + +Die gesamte Datenbank liegt in einer einzigen Datei: +```bash +# Bei Docker: +cp data/data.json data/data.json.backup + +# Bei lokaler Entwicklung: +cp backend/data.json backend/data.json.backup +``` + +### Datenbank zurücksetzen + +```bash +# Vorsicht: Alle Rezepte, User und Einladungen gehen verloren! +rm data/data.json +# Beim Neustart wird die Admin-Config neu erstellt +``` + +### Logs anzeigen + +```bash +docker compose logs -f backend +docker compose logs -f nginx +``` + +## Lizenz + +MIT diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..a23c580 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,4 @@ +node_modules +data.json +recipes.db* +.env diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..de97399 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,12 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev + +COPY . . + +EXPOSE 3001 + +CMD ["node", "server.js"] diff --git a/backend/data.json b/backend/data.json new file mode 100644 index 0000000..86479b2 --- /dev/null +++ b/backend/data.json @@ -0,0 +1,42 @@ +{ + "users": [ + { + "id": 1, + "username": "Duda", + "password": "$2a$10$CZau8N7du7NSSn5Vvzb7dOma089slkoX.2rqjryod3AlZ2DOY9uAe", + "is_admin": 1, + "created_at": "2026-08-25 15:45:56" + }, + { + "id": 3, + "username": "admin", + "password": "$2a$10$nEk2A5poqbBKGYHOeJ/Bkub1LqTohLeanHNhxHbXMtIRQ3.mb4jwS", + "is_admin": 1, + "created_at": "2026-08-25 18:18:57" + } + ], + "recipes": [], + "invite_tokens": [ + { + "id": 1, + "token": "50015008cb92902ba41e716702b90467", + "created_by": 1, + "used_by": null, + "created_at": "2026-08-25 15:50:10", + "used_at": null + }, + { + "id": 2, + "token": "1c781250ad364d850b6e9084d3fd3d9c", + "created_by": 1, + "used_by": 2, + "created_at": "2026-08-25 16:02:44", + "used_at": "2026-08-25 16:03:26" + } + ], + "_counters": { + "users": 3, + "recipes": 1, + "invite_tokens": 2 + } +} \ No newline at end of file diff --git a/backend/db.js b/backend/db.js new file mode 100644 index 0000000..6caf599 --- /dev/null +++ b/backend/db.js @@ -0,0 +1,202 @@ +import { readFileSync, writeFileSync, existsSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const DB_FILE = process.env.DB_PATH || join(__dirname, 'data.json'); + +function loadDB() { + if (existsSync(DB_FILE)) { + return JSON.parse(readFileSync(DB_FILE, 'utf-8')); + } + return { users: [], recipes: [], invite_tokens: [], _counters: { users: 0, recipes: 0, invite_tokens: 0 } }; +} + +function saveDB(data) { + writeFileSync(DB_FILE, JSON.stringify(data, null, 2)); +} + +let db = loadDB(); + +function nextId(table) { + db._counters[table] = (db._counters[table] || 0) + 1; + return db._counters[table]; +} + +function now() { + return new Date().toISOString().replace('T', ' ').slice(0, 19); +} + +export function getDb() { + return db; +} + +export function getAllRecipes(search, category) { + let results = [...db.recipes]; + + if (search) { + const term = search.toLowerCase(); + results = results.filter(r => + (r.name && r.name.toLowerCase().includes(term)) || + (r.description && r.description.toLowerCase().includes(term)) || + (r.ingredients && r.ingredients.toLowerCase().includes(term)) + ); + } + + if (category && category !== 'Alle') { + results = results.filter(r => r.category === category); + } + + results.sort((a, b) => (b.created_at || '').localeCompare(a.created_at || '')); + return results; +} + +export function getRecipeById(id) { + return db.recipes.find(r => r.id === Number(id)) || undefined; +} + +export function createRecipe(recipe) { + const id = nextId('recipes'); + const nowStr = now(); + const row = { + id, + name: recipe.name, + description: recipe.description || '', + ingredients: JSON.stringify(recipe.ingredients || []), + steps: JSON.stringify(recipe.steps || []), + category: recipe.category || 'Sonstiges', + tiktok_url: recipe.tiktok_url || null, + tiktok_author: recipe.tiktok_author || null, + thumbnail_url: recipe.thumbnail_url || null, + user_id: recipe.user_id || null, + created_at: nowStr, + updated_at: nowStr, + }; + db.recipes.push(row); + saveDB(db); + return row; +} + +export function updateRecipe(id, recipe) { + const idx = db.recipes.findIndex(r => r.id === Number(id)); + if (idx === -1) return undefined; + db.recipes[idx] = { + ...db.recipes[idx], + name: recipe.name, + description: recipe.description || '', + ingredients: JSON.stringify(recipe.ingredients || []), + steps: JSON.stringify(recipe.steps || []), + category: recipe.category || 'Sonstiges', + tiktok_url: recipe.tiktok_url || null, + tiktok_author: recipe.tiktok_author || null, + thumbnail_url: recipe.thumbnail_url || null, + updated_at: now(), + }; + saveDB(db); + return db.recipes[idx]; +} + +export function deleteRecipe(id) { + const idx = db.recipes.findIndex(r => r.id === Number(id)); + if (idx === -1) return; + db.recipes.splice(idx, 1); + saveDB(db); +} + +export function getCategories() { + const counts = {}; + for (const r of db.recipes) { + counts[r.category] = (counts[r.category] || 0) + 1; + } + return Object.entries(counts) + .map(([category, count]) => ({ category, count })) + .sort((a, b) => b.count - a.count); +} + +export function getUserByUsername(username) { + return db.users.find(u => u.username === username) || undefined; +} + +export function getUserById(id) { + return db.users.find(u => u.id === Number(id)) || undefined; +} + +export function createUser(username, hashedPassword, isAdmin = false) { + const id = nextId('users'); + const user = { + id, + username, + password: hashedPassword, + is_admin: isAdmin ? 1 : 0, + created_at: now(), + }; + db.users.push(user); + saveDB(db); + return { id, username, is_admin: user.is_admin, created_at: user.created_at }; +} + +export function updateUserCredentials(userId, { username, password }) { + const user = db.users.find(u => u.id === Number(userId)); + if (!user) return null; + if (username) user.username = username; + if (password) user.password = password; + saveDB(db); + return { id: user.id, username: user.username, is_admin: user.is_admin, created_at: user.created_at }; +} + +export function createInviteToken(token, createdBy) { + const id = nextId('invite_tokens'); + db.invite_tokens.push({ + id, + token, + created_by: createdBy, + used_by: null, + created_at: now(), + used_at: null, + }); + saveDB(db); +} + +export function getInviteToken(token) { + return db.invite_tokens.find(i => i.token === token) || undefined; +} + +export function useInviteToken(token, userId) { + const inv = db.invite_tokens.find(i => i.token === token); + if (inv) { + inv.used_by = userId; + inv.used_at = now(); + saveDB(db); + } +} + +export function getInvitesByUser(userId) { + return db.invite_tokens + .filter(i => i.created_by === Number(userId)) + .sort((a, b) => (b.created_at || '').localeCompare(a.created_at || '')) + .map(i => { + const usedByUser = i.used_by ? db.users.find(u => u.id === i.used_by) : null; + return { ...i, used_by_username: usedByUser ? usedByUser.username : null }; + }); +} + +export function getAllUsers() { + return db.users + .map(u => ({ id: u.id, username: u.username, is_admin: u.is_admin, created_at: u.created_at })) + .sort((a, b) => (b.created_at || '').localeCompare(a.created_at || '')); +} + +export function deleteUser(id) { + const idx = db.users.findIndex(u => u.id === Number(id)); + if (idx === -1) return false; + db.users.splice(idx, 1); + db.invite_tokens = db.invite_tokens.filter(i => i.created_by !== Number(id)); + saveDB(db); + return true; +} + +export function getInviteStats() { + const total = db.invite_tokens.length; + const used = db.invite_tokens.filter(i => i.used_by !== null).length; + return { total, used, available: total - used }; +} diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..361e575 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,1073 @@ +{ + "name": "tiktok-recipe-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tiktok-recipe-backend", + "version": "1.0.0", + "dependencies": { + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "express": "^4.21.0", + "jsonwebtoken": "^9.0.2", + "node-fetch": "^3.3.2" + } + }, + "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/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/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "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.15.1", + "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/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/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/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/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/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/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/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/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/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/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/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/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/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-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/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/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "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.15.1", + "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/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/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/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/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/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/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/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-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/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/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/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/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/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/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/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/jsonwebtoken/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/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/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.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/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/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/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/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/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/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "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/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-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/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/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/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/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "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/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/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/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/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "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/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/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/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "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.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "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/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/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/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/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/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/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/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" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..a94f1bc --- /dev/null +++ b/backend/package.json @@ -0,0 +1,16 @@ +{ + "name": "tiktok-recipe-backend", + "version": "1.0.0", + "type": "module", + "scripts": { + "start": "node server.js", + "dev": "node --watch server.js" + }, + "dependencies": { + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "express": "^4.21.0", + "jsonwebtoken": "^9.0.2", + "node-fetch": "^3.3.2" + } +} diff --git a/backend/prompts/extractRecipe.js b/backend/prompts/extractRecipe.js new file mode 100644 index 0000000..9f15f2e --- /dev/null +++ b/backend/prompts/extractRecipe.js @@ -0,0 +1,25 @@ +export const SYSTEM_PROMPT = `Du bist ein Assistent der aus TikTok-Videobeschreibungen Rezepte extrahiert. +Du antwortest NUR mit validem JSON, keinem anderen Text. + +Das JSON muss dieses Schema haben: +{ + "name": "Name des Rezepts", + "description": "Kurze Beschreibung des Gerichts", + "category": "Frühstück|Hauptgericht|Dessert|Snack|Getränk|Sonstiges", + "ingredients": [ + { "name": "Zutat", "amount": "Menge", "unit": "Einheit" } + ], + "steps": [ + "Schritt 1 Beschreibung", + "Schritt 2 Beschreibung" + ] +} + +Regeln: +- Extrahiere alle Zutaten mit Menge und Einheit aus dem Text +- Wenn keine Menge angegeben ist, setze amount auf "" +- Wenn keine Einheit angegeben ist, setze unit auf "" +- Kategorie muss eines dieser sein: Frühstück, Hauptgericht, Dessert, Snack, Getränk, Sonstiges +- steps ist eine sortierte Liste der Zubereitungsschritte +- Wenn du die Zubereitung nicht erkennen kannst, gib einen sinnvollen Standard basierend auf dem Rezeptnamen +- Gib NUR das JSON-Objekt zurück, kein Markdown, kein Code-Block`; diff --git a/backend/recipes.db-shm b/backend/recipes.db-shm new file mode 100644 index 0000000..30570f8 Binary files /dev/null and b/backend/recipes.db-shm differ diff --git a/backend/recipes.db-wal b/backend/recipes.db-wal new file mode 100644 index 0000000..de519f7 Binary files /dev/null and b/backend/recipes.db-wal differ diff --git a/backend/routes/auth.js b/backend/routes/auth.js new file mode 100644 index 0000000..f9663d4 --- /dev/null +++ b/backend/routes/auth.js @@ -0,0 +1,214 @@ +import { Router } from 'express'; +import bcrypt from 'bcryptjs'; +import jwt from 'jsonwebtoken'; +import crypto from 'crypto'; +import { + getUserByUsername, getUserById, createUser, updateUserCredentials, deleteUser, + createInviteToken, getInviteToken, useInviteToken, + getInvitesByUser, getAllUsers, getInviteStats +} from '../db.js'; + +const router = Router(); +const JWT_SECRET = process.env.JWT_SECRET || 'tiktok-rezepte-secret-key-change-in-production'; +const JWT_EXPIRES_IN = '7d'; + +function authMiddleware(req, res, next) { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ error: 'Nicht authentifiziert' }); + } + try { + const token = authHeader.split(' ')[1]; + const decoded = jwt.verify(token, JWT_SECRET); + const user = getUserByUsername(decoded.username); + if (!user) return res.status(401).json({ error: 'Benutzer nicht gefunden' }); + req.user = user; + next(); + } catch { + res.status(401).json({ error: 'Ungültiges Token' }); + } +} + +function adminMiddleware(req, res, next) { + if (!req.user || !req.user.is_admin) { + return res.status(403).json({ error: 'Keine Berechtigung' }); + } + next(); +} + +router.post('/register', async (req, res) => { + try { + const { username, password, inviteToken } = req.body; + + if (!username || !password || !inviteToken) { + return res.status(400).json({ error: 'Benutzername, Passwort und Einladungscode erforderlich' }); + } + + if (username.length < 3) { + return res.status(400).json({ error: 'Benutzername muss mindestens 3 Zeichen lang sein' }); + } + + if (password.length < 6) { + return res.status(400).json({ error: 'Passwort muss mindestens 6 Zeichen lang sein' }); + } + + const invite = getInviteToken(inviteToken); + if (!invite) { + return res.status(400).json({ error: 'Ungültiger Einladungscode' }); + } + if (invite.used_by) { + return res.status(400).json({ error: 'Einladungscode wurde bereits verwendet' }); + } + + const existing = getUserByUsername(username); + if (existing) { + return res.status(409).json({ error: 'Benutzername ist bereits vergeben' }); + } + + const hashedPassword = await bcrypt.hash(password, 10); + const user = createUser(username, hashedPassword); + useInviteToken(inviteToken, user.id); + + const token = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN }); + + res.status(201).json({ user, token }); + } catch (error) { + res.status(500).json({ error: 'Registrierung fehlgeschlagen' }); + } +}); + +router.post('/login', async (req, res) => { + try { + const { username, password } = req.body; + + if (!username || !password) { + return res.status(400).json({ error: 'Benutzername und Passwort erforderlich' }); + } + + const user = getUserByUsername(username); + if (!user) { + return res.status(401).json({ error: 'Ungültige Anmeldedaten' }); + } + + const valid = await bcrypt.compare(password, user.password); + if (!valid) { + return res.status(401).json({ error: 'Ungültige Anmeldedaten' }); + } + + const token = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN }); + + res.json({ + user: { id: user.id, username: user.username, is_admin: !!user.is_admin, created_at: user.created_at }, + token, + }); + } catch (error) { + res.status(500).json({ error: 'Anmeldung fehlgeschlagen' }); + } +}); + +router.get('/me', (req, res) => { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ error: 'Nicht authentifiziert' }); + } + try { + const token = authHeader.split(' ')[1]; + const decoded = jwt.verify(token, JWT_SECRET); + const user = getUserByUsername(decoded.username); + if (!user) return res.status(401).json({ error: 'Benutzer nicht gefunden' }); + res.json({ id: user.id, username: user.username, is_admin: !!user.is_admin, created_at: user.created_at }); + } catch { + res.status(401).json({ error: 'Ungültiges Token' }); + } +}); + +router.post('/invite', authMiddleware, adminMiddleware, (req, res) => { + try { + const { count = 1 } = req.body; + const amount = Math.min(Math.max(parseInt(count) || 1, 1), 20); + const tokens = []; + + for (let i = 0; i < amount; i++) { + const token = crypto.randomBytes(16).toString('hex'); + createInviteToken(token, req.user.id); + tokens.push(token); + } + + res.json({ tokens }); + } catch (error) { + res.status(500).json({ error: 'Einladung fehlgeschlagen' }); + } +}); + +router.get('/invites', authMiddleware, adminMiddleware, (req, res) => { + try { + const invites = getInvitesByUser(req.user.id); + const stats = getInviteStats(); + res.json({ invites, stats }); + } catch (error) { + res.status(500).json({ error: 'Fehler beim Laden der Einladungen' }); + } +}); + +router.get('/users', authMiddleware, adminMiddleware, (req, res) => { + try { + const users = getAllUsers(); + res.json(users); + } catch (error) { + res.status(500).json({ error: 'Fehler beim Laden der Benutzer' }); + } +}); + +router.delete('/users/:id', authMiddleware, adminMiddleware, (req, res) => { + try { + const userId = Number(req.params.id); + if (userId === req.user.id) { + return res.status(400).json({ error: 'Du kannst dich nicht selbst löschen' }); + } + const user = getUserById(userId); + if (!user) return res.status(404).json({ error: 'Benutzer nicht gefunden' }); + deleteUser(userId); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ error: 'Fehler beim Löschen' }); + } +}); + +router.put('/credentials', authMiddleware, async (req, res) => { + try { + const { currentPassword, newUsername, newPassword } = req.body; + + const user = getUserByUsername(req.user.username); + if (!user) return res.status(404).json({ error: 'Benutzer nicht gefunden' }); + + if (currentPassword) { + const valid = await bcrypt.compare(currentPassword, user.password); + if (!valid) return res.status(401).json({ error: 'Aktuelles Passwort ist falsch' }); + } + + if (newUsername && newUsername !== user.username) { + const exists = getUserByUsername(newUsername); + if (exists) return res.status(409).json({ error: 'Benutzername ist bereits vergeben' }); + } + + const updates = {}; + if (newUsername) updates.username = newUsername; + if (newPassword) { + if (newPassword.length < 6) return res.status(400).json({ error: 'Neues Passwort muss mindestens 6 Zeichen lang sein' }); + updates.password = await bcrypt.hash(newPassword, 10); + } + + if (Object.keys(updates).length === 0) { + return res.status(400).json({ error: 'Keine Änderungen angegeben' }); + } + + const updated = updateUserCredentials(user.id, updates); + const token = jwt.sign({ id: updated.id, username: updated.username }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN }); + + res.json({ user: updated, token }); + } catch (error) { + res.status(500).json({ error: 'Fehler beim Aktualisieren' }); + } +}); + +export default router; diff --git a/backend/routes/recipes.js b/backend/routes/recipes.js new file mode 100644 index 0000000..dbaf316 --- /dev/null +++ b/backend/routes/recipes.js @@ -0,0 +1,91 @@ +import { Router } from 'express'; +import { + getAllRecipes, getRecipeById, createRecipe, + updateRecipe, deleteRecipe, getCategories +} from '../db.js'; +import { getTikTokData } from '../services/tiktok.js'; +import { isOllamaAvailable, extractRecipe } from '../services/ai.js'; + +const router = Router(); + +router.get('/', (req, res) => { + const { search, category } = req.query; + const recipes = getAllRecipes(search, category); + res.json(recipes.map(r => ({ + ...r, + ingredients: JSON.parse(r.ingredients), + steps: JSON.parse(r.steps), + }))); +}); + +router.get('/categories', (req, res) => { + res.json(getCategories()); +}); + +router.get('/ai-status', async (req, res) => { + const available = await isOllamaAvailable(); + res.json({ ollama: available }); +}); + +router.get('/:id', (req, res) => { + const recipe = getRecipeById(req.params.id); + if (!recipe) return res.status(404).json({ error: 'Rezept nicht gefunden' }); + res.json({ + ...recipe, + ingredients: JSON.parse(recipe.ingredients), + steps: JSON.parse(recipe.steps), + }); +}); + +router.post('/', (req, res) => { + const recipe = createRecipe(req.body); + res.status(201).json(recipe); +}); + +router.put('/:id', (req, res) => { + const existing = getRecipeById(req.params.id); + if (!existing) return res.status(404).json({ error: 'Rezept nicht gefunden' }); + const updated = updateRecipe(req.params.id, req.body); + res.json(updated); +}); + +router.delete('/:id', (req, res) => { + const existing = getRecipeById(req.params.id); + if (!existing) return res.status(404).json({ error: 'Rezept nicht gefunden' }); + deleteRecipe(req.params.id); + res.json({ success: true }); +}); + +router.post('/from-tiktok', async (req, res) => { + try { + const { url } = req.body; + if (!url) return res.status(400).json({ error: 'URL erforderlich' }); + + const tiktokData = await getTikTokData(url); + + const ollamaAvailable = await isOllamaAvailable(); + let extracted = null; + let aiStatus = ollamaAvailable ? 'available' : 'unavailable'; + + if (ollamaAvailable && tiktokData.title) { + try { + extracted = await extractRecipe(tiktokData.title); + aiStatus = 'success'; + } catch (e) { + console.error('Ollama Extraktion fehlgeschlagen:', e.message); + aiStatus = 'error'; + } + } + + res.json({ + tiktok: tiktokData, + ollama: ollamaAvailable, + aiStatus, + extracted, + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +export default router; diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 0000000..a707556 --- /dev/null +++ b/backend/server.js @@ -0,0 +1,30 @@ +import express from 'express'; +import cors from 'cors'; +import bcrypt from 'bcryptjs'; +import recipesRouter from './routes/recipes.js'; +import authRouter from './routes/auth.js'; +import { getUserByUsername, createUser } from './db.js'; + +const app = express(); +const PORT = process.env.PORT || 3001; + +app.use(cors()); +app.use(express.json()); + +app.use('/api/auth', authRouter); +app.use('/api/recipes', recipesRouter); + +const adminUser = process.env.ADMIN_USER || 'admin'; +const adminPass = process.env.ADMIN_PASS || 'admin123'; + +const existing = getUserByUsername(adminUser); +if (!existing) { + const hashed = bcrypt.hashSync(adminPass, 10); + createUser(adminUser, hashed, true); + console.log(`Admin erstellt: ${adminUser} / ${adminPass}`); + console.log('WICHTIG: Passwort nach dem ersten Login ändern!'); +} + +app.listen(PORT, () => { + console.log(`Backend läuft auf http://localhost:${PORT}`); +}); diff --git a/backend/services/ai.js b/backend/services/ai.js new file mode 100644 index 0000000..0116960 --- /dev/null +++ b/backend/services/ai.js @@ -0,0 +1,39 @@ +import { SYSTEM_PROMPT } from '../prompts/extractRecipe.js'; + +const OLLAMA_URL = process.env.OLLAMA_URL || 'http://localhost:11434'; +const MODEL = process.env.OLLAMA_MODEL || 'mistral'; + +export async function isOllamaAvailable() { + try { + const res = await fetch(`${OLLAMA_URL}/api/tags`, { signal: AbortSignal.timeout(3000) }); + return res.ok; + } catch { + return false; + } +} + +export async function extractRecipe(description) { + const response = await fetch(`${OLLAMA_URL}/api/generate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model: MODEL, + system: SYSTEM_PROMPT, + prompt: `Extrahiere ein Rezept aus dieser TikTok-Videobeschreibung:\n\n${description}`, + stream: false, + }), + }); + + if (!response.ok) { + throw new Error(`Ollama Fehler: ${response.status}`); + } + + const data = await response.json(); + let text = data.response.trim(); + + if (text.startsWith('```')) { + text = text.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, ''); + } + + return JSON.parse(text); +} diff --git a/backend/services/tiktok.js b/backend/services/tiktok.js new file mode 100644 index 0000000..b5800ed --- /dev/null +++ b/backend/services/tiktok.js @@ -0,0 +1,20 @@ +import fetch from 'node-fetch'; + +export async function getTikTokData(url) { + const oembedUrl = `https://www.tiktok.com/oembed?url=${encodeURIComponent(url)}`; + const response = await fetch(oembedUrl); + + if (!response.ok) { + throw new Error(`TikTok oEmbed Fehler: ${response.status}`); + } + + const data = await response.json(); + + return { + title: data.title || '', + author: data.author_name || '', + authorUrl: data.author_url || '', + thumbnail: data.thumbnail_url || '', + embedHtml: data.html || '', + }; +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7b25fc5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,33 @@ +services: + nginx: + build: ./frontend + ports: + - "80:80" + depends_on: + - backend + restart: unless-stopped + + backend: + build: ./backend + environment: + - PORT=3001 + - DB_PATH=/app/data/data.json + - OLLAMA_URL=http://ollama:11434 + - OLLAMA_MODEL=${OLLAMA_MODEL:-mistral} + - JWT_SECRET=${JWT_SECRET} + - ADMIN_USER=${ADMIN_USER:-admin} + - ADMIN_PASS=${ADMIN_PASS} + volumes: + - ./data:/app/data + depends_on: + - ollama + restart: unless-stopped + + ollama: + image: ollama/ollama + volumes: + - ollama_data:/root/.ollama + restart: unless-stopped + +volumes: + ollama_data: diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..717a47c --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,18 @@ +FROM node:20-alpine AS build + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +COPY . . +RUN npm run build + +FROM nginx:alpine + +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/dist/assets/index-CV-7lATv.css b/frontend/dist/assets/index-CV-7lATv.css new file mode 100644 index 0000000..9b3044d --- /dev/null +++ b/frontend/dist/assets/index-CV-7lATv.css @@ -0,0 +1 @@ +/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-800:oklch(44.4% .177 26.899);--color-orange-50:oklch(98% .016 73.684);--color-orange-300:oklch(83.7% .128 66.29);--color-orange-400:oklch(75% .183 55.934);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-yellow-100:oklch(97.3% .071 103.193);--color-yellow-800:oklch(47.6% .114 61.907);--color-green-100:oklch(96.2% .044 156.743);--color-green-800:oklch(44.8% .119 151.328);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-800:oklch(42.4% .199 265.638);--color-pink-100:oklch(94.8% .028 342.258);--color-pink-800:oklch(45.9% .187 3.815);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-white:#fff;--spacing:.25rem;--container-2xl:42rem;--container-6xl:72rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--leading-tight:1.25;--radius-lg:.5rem;--radius-xl:.75rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.mx-auto{margin-inline:auto}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mb-1{margin-bottom:var(--spacing)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.flex{display:flex}.grid{display:grid}.inline-block{display:inline-block}.h-4{height:calc(var(--spacing) * 4)}.h-6{height:calc(var(--spacing) * 6)}.h-48{height:calc(var(--spacing) * 48)}.h-full{height:100%}.min-h-screen{min-height:100vh}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-6{width:calc(var(--spacing) * 6)}.w-14{width:calc(var(--spacing) * 14)}.w-16{width:calc(var(--spacing) * 16)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-6xl{max-width:var(--container-6xl)}.flex-1{flex:1}.flex-shrink-0{flex-shrink:0}.resize-none{resize:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:var(--spacing)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.overflow-hidden{overflow:hidden}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-orange-300{border-color:var(--color-orange-300)}.border-orange-500{border-color:var(--color-orange-500)}.border-red-300{border-color:var(--color-red-300)}.bg-blue-100{background-color:var(--color-blue-100)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-green-100{background-color:var(--color-green-100)}.bg-orange-500{background-color:var(--color-orange-500)}.bg-pink-100{background-color:var(--color-pink-100)}.bg-red-100{background-color:var(--color-red-100)}.bg-white{background-color:var(--color-white)}.bg-yellow-100{background-color:var(--color-yellow-100)}.object-cover{object-fit:cover}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-12{padding-block:calc(var(--spacing) * 12)}.pt-2{padding-top:calc(var(--spacing) * 2)}.text-center{text-align:center}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.whitespace-nowrap{white-space:nowrap}.text-blue-800{color:var(--color-blue-800)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-green-800{color:var(--color-green-800)}.text-orange-500{color:var(--color-orange-500)}.text-orange-600{color:var(--color-orange-600)}.text-pink-800{color:var(--color-pink-800)}.text-red-400{color:var(--color-red-400)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-red-800{color:var(--color-red-800)}.text-white{color:var(--color-white)}.text-yellow-800{color:var(--color-yellow-800)}.line-through{text-decoration-line:line-through}.accent-orange-500{accent-color:var(--color-orange-500)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media(hover:hover){.hover\:border-orange-300:hover{border-color:var(--color-orange-300)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-orange-50:hover{background-color:var(--color-orange-50)}.hover\:bg-orange-600:hover{background-color:var(--color-orange-600)}.hover\:bg-red-50:hover{background-color:var(--color-red-50)}.hover\:text-gray-600:hover{color:var(--color-gray-600)}.hover\:text-orange-600:hover{color:var(--color-orange-600)}.hover\:text-red-600:hover{color:var(--color-red-600)}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.focus\:border-transparent:focus{border-color:#0000}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-orange-400:focus{--tw-ring-color:var(--color-orange-400)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:bg-orange-300:disabled{background-color:var(--color-orange-300)}@media(min-width:40rem){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:64rem){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}}body{background-color:var(--color-gray-50);color:var(--color-gray-900)}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000} diff --git a/frontend/dist/assets/index-CeRXDRLQ.js b/frontend/dist/assets/index-CeRXDRLQ.js new file mode 100644 index 0000000..dd5929f --- /dev/null +++ b/frontend/dist/assets/index-CeRXDRLQ.js @@ -0,0 +1,67 @@ +function jd(u,a){for(var s=0;sp[v]})}}}return Object.freeze(Object.defineProperty(u,Symbol.toStringTag,{value:"Module"}))}(function(){const a=document.createElement("link").relList;if(a&&a.supports&&a.supports("modulepreload"))return;for(const v of document.querySelectorAll('link[rel="modulepreload"]'))p(v);new MutationObserver(v=>{for(const x of v)if(x.type==="childList")for(const w of x.addedNodes)w.tagName==="LINK"&&w.rel==="modulepreload"&&p(w)}).observe(document,{childList:!0,subtree:!0});function s(v){const x={};return v.integrity&&(x.integrity=v.integrity),v.referrerPolicy&&(x.referrerPolicy=v.referrerPolicy),v.crossOrigin==="use-credentials"?x.credentials="include":v.crossOrigin==="anonymous"?x.credentials="omit":x.credentials="same-origin",x}function p(v){if(v.ep)return;v.ep=!0;const x=s(v);fetch(v.href,x)}})();function hc(u){return u&&u.__esModule&&Object.prototype.hasOwnProperty.call(u,"default")?u.default:u}var Bi={exports:{}},_r={},$i={exports:{}},J={};/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Ya;function Rd(){if(Ya)return J;Ya=1;var u=Symbol.for("react.element"),a=Symbol.for("react.portal"),s=Symbol.for("react.fragment"),p=Symbol.for("react.strict_mode"),v=Symbol.for("react.profiler"),x=Symbol.for("react.provider"),w=Symbol.for("react.context"),L=Symbol.for("react.forward_ref"),E=Symbol.for("react.suspense"),C=Symbol.for("react.memo"),N=Symbol.for("react.lazy"),k=Symbol.iterator;function I(h){return h===null||typeof h!="object"?null:(h=k&&h[k]||h["@@iterator"],typeof h=="function"?h:null)}var D={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},X=Object.assign,W={};function O(h,P,Z){this.props=h,this.context=P,this.refs=W,this.updater=Z||D}O.prototype.isReactComponent={},O.prototype.setState=function(h,P){if(typeof h!="object"&&typeof h!="function"&&h!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,h,P,"setState")},O.prototype.forceUpdate=function(h){this.updater.enqueueForceUpdate(this,h,"forceUpdate")};function K(){}K.prototype=O.prototype;function le(h,P,Z){this.props=h,this.context=P,this.refs=W,this.updater=Z||D}var b=le.prototype=new K;b.constructor=le,X(b,O.prototype),b.isPureReactComponent=!0;var ue=Array.isArray,ke=Object.prototype.hasOwnProperty,Le={current:null},Oe={key:!0,ref:!0,__self:!0,__source:!0};function $e(h,P,Z){var q,te={},ne=null,ae=null;if(P!=null)for(q in P.ref!==void 0&&(ae=P.ref),P.key!==void 0&&(ne=""+P.key),P)ke.call(P,q)&&!Oe.hasOwnProperty(q)&&(te[q]=P[q]);var oe=arguments.length-2;if(oe===1)te.children=Z;else if(1>>1,P=F[h];if(0>>1;hv(te,B))nev(ae,te)?(F[h]=ae,F[ne]=B,h=ne):(F[h]=te,F[q]=B,h=q);else if(nev(ae,B))F[h]=ae,F[ne]=B,h=ne;else break e}}return G}function v(F,G){var B=F.sortIndex-G.sortIndex;return B!==0?B:F.id-G.id}if(typeof performance=="object"&&typeof performance.now=="function"){var x=performance;u.unstable_now=function(){return x.now()}}else{var w=Date,L=w.now();u.unstable_now=function(){return w.now()-L}}var E=[],C=[],N=1,k=null,I=3,D=!1,X=!1,W=!1,O=typeof setTimeout=="function"?setTimeout:null,K=typeof clearTimeout=="function"?clearTimeout:null,le=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function b(F){for(var G=s(C);G!==null;){if(G.callback===null)p(C);else if(G.startTime<=F)p(C),G.sortIndex=G.expirationTime,a(E,G);else break;G=s(C)}}function ue(F){if(W=!1,b(F),!X)if(s(E)!==null)X=!0,Ae(ke);else{var G=s(C);G!==null&&ye(ue,G.startTime-F)}}function ke(F,G){X=!1,W&&(W=!1,K($e),$e=-1),D=!0;var B=I;try{for(b(G),k=s(E);k!==null&&(!(k.expirationTime>G)||F&&!se());){var h=k.callback;if(typeof h=="function"){k.callback=null,I=k.priorityLevel;var P=h(k.expirationTime<=G);G=u.unstable_now(),typeof P=="function"?k.callback=P:k===s(E)&&p(E),b(G)}else p(E);k=s(E)}if(k!==null)var Z=!0;else{var q=s(C);q!==null&&ye(ue,q.startTime-G),Z=!1}return Z}finally{k=null,I=B,D=!1}}var Le=!1,Oe=null,$e=-1,it=5,ut=-1;function se(){return!(u.unstable_now()-utF||125h?(F.sortIndex=B,a(C,F),s(E)===null&&F===s(C)&&(W?(K($e),$e=-1):W=!0,ye(ue,B-h))):(F.sortIndex=P,a(E,F),X||D||(X=!0,Ae(ke))),F},u.unstable_shouldYield=se,u.unstable_wrapCallback=function(F){var G=I;return function(){var B=I;I=G;try{return F.apply(this,arguments)}finally{I=B}}}})(Wi)),Wi}var ba;function Id(){return ba||(ba=1,Vi.exports=Od()),Vi.exports}/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var ec;function Dd(){if(ec)return Ge;ec=1;var u=Zi(),a=Id();function s(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),E=Object.prototype.hasOwnProperty,C=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,N={},k={};function I(e){return E.call(k,e)?!0:E.call(N,e)?!1:C.test(e)?k[e]=!0:(N[e]=!0,!1)}function D(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function X(e,t,n,r){if(t===null||typeof t>"u"||D(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function W(e,t,n,r,l,o,i){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=o,this.removeEmptyString=i}var O={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){O[e]=new W(e,0,!1,e,null,!1,!1)}),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];O[t]=new W(t,1,!1,e[1],null,!1,!1)}),["contentEditable","draggable","spellCheck","value"].forEach(function(e){O[e]=new W(e,2,!1,e.toLowerCase(),null,!1,!1)}),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){O[e]=new W(e,2,!1,e,null,!1,!1)}),"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){O[e]=new W(e,3,!1,e.toLowerCase(),null,!1,!1)}),["checked","multiple","muted","selected"].forEach(function(e){O[e]=new W(e,3,!0,e,null,!1,!1)}),["capture","download"].forEach(function(e){O[e]=new W(e,4,!1,e,null,!1,!1)}),["cols","rows","size","span"].forEach(function(e){O[e]=new W(e,6,!1,e,null,!1,!1)}),["rowSpan","start"].forEach(function(e){O[e]=new W(e,5,!1,e.toLowerCase(),null,!1,!1)});var K=/[\-:]([a-z])/g;function le(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(K,le);O[t]=new W(t,1,!1,e,null,!1,!1)}),"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(K,le);O[t]=new W(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)}),["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(K,le);O[t]=new W(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)}),["tabIndex","crossOrigin"].forEach(function(e){O[e]=new W(e,1,!1,e.toLowerCase(),null,!1,!1)}),O.xlinkHref=new W("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach(function(e){O[e]=new W(e,1,!1,e.toLowerCase(),null,!0,!0)});function b(e,t,n,r){var l=O.hasOwnProperty(t)?O[t]:null;(l!==null?l.type!==0:r||!(2c||l[i]!==o[c]){var f=` +`+l[i].replace(" at new "," at ");return e.displayName&&f.includes("")&&(f=f.replace("",e.displayName)),f}while(1<=i&&0<=c);break}}}finally{Z=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?P(e):""}function te(e){switch(e.tag){case 5:return P(e.type);case 16:return P("Lazy");case 13:return P("Suspense");case 19:return P("SuspenseList");case 0:case 2:case 15:return e=q(e.type,!1),e;case 11:return e=q(e.type.render,!1),e;case 1:return e=q(e.type,!0),e;default:return""}}function ne(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Oe:return"Fragment";case Le:return"Portal";case it:return"Profiler";case $e:return"StrictMode";case Ye:return"Suspense";case st:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case se:return(e.displayName||"Context")+".Consumer";case ut:return(e._context.displayName||"Context")+".Provider";case ge:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case vt:return t=e.displayName||null,t!==null?t:ne(e.type)||"Memo";case Ae:t=e._payload,e=e._init;try{return ne(e(t))}catch{}}return null}function ae(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return ne(t);case 8:return t===$e?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function oe(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function pe(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Xe(e){var t=pe(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,o=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(i){r=""+i,o.call(this,i)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(i){r=""+i},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function Rr(e){e._valueTracker||(e._valueTracker=Xe(e))}function eu(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=pe(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Lr(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function Ql(e,t){var n=t.checked;return B({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function tu(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=oe(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function nu(e,t){t=t.checked,t!=null&&b(e,"checked",t,!1)}function Kl(e,t){nu(e,t);var n=oe(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?Gl(e,t.type,n):t.hasOwnProperty("defaultValue")&&Gl(e,t.type,oe(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function ru(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function Gl(e,t,n){(t!=="number"||Lr(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var $n=Array.isArray;function hn(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l"+t.valueOf().toString()+"",t=zr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function An(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Vn={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},zc=["Webkit","ms","Moz","O"];Object.keys(Vn).forEach(function(e){zc.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Vn[t]=Vn[e]})});function au(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Vn.hasOwnProperty(e)&&Vn[e]?(""+t).trim():t+"px"}function cu(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,l=au(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,l):e[n]=l}}var Tc=B({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function Zl(e,t){if(t){if(Tc[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(s(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(s(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(s(61))}if(t.style!=null&&typeof t.style!="object")throw Error(s(62))}}function Jl(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var ql=null;function bl(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var eo=null,mn=null,vn=null;function fu(e){if(e=cr(e)){if(typeof eo!="function")throw Error(s(280));var t=e.stateNode;t&&(t=el(t),eo(e.stateNode,e.type,t))}}function du(e){mn?vn?vn.push(e):vn=[e]:mn=e}function pu(){if(mn){var e=mn,t=vn;if(vn=mn=null,fu(e),t)for(e=0;e>>=0,e===0?32:31-(Wc(e)/Hc|0)|0}var Fr=64,Mr=4194304;function Kn(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Ur(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,l=e.suspendedLanes,o=e.pingedLanes,i=n&268435455;if(i!==0){var c=i&~l;c!==0?r=Kn(c):(o&=i,o!==0&&(r=Kn(o)))}else i=n&~l,i!==0?r=Kn(i):o!==0&&(r=Kn(o));if(r===0)return 0;if(t!==0&&t!==r&&(t&l)===0&&(l=r&-r,o=t&-t,l>=o||l===16&&(o&4194240)!==0))return t;if((r&4)!==0&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function Gn(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-at(t),e[t]=n}function Yc(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=tr),Au=" ",Vu=!1;function Wu(e,t){switch(e){case"keyup":return Ef.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Hu(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var xn=!1;function _f(e,t){switch(e){case"compositionend":return Hu(t);case"keypress":return t.which!==32?null:(Vu=!0,Au);case"textInput":return e=t.data,e===Au&&Vu?null:e;default:return null}}function Nf(e,t){if(xn)return e==="compositionend"||!xo&&Wu(e,t)?(e=Du(),Wr=po=It=null,xn=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=Ju(n)}}function bu(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?bu(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function es(){for(var e=window,t=Lr();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Lr(e.document)}return t}function ko(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function Df(e){var t=es(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&bu(n.ownerDocument.documentElement,n)){if(r!==null&&ko(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var l=n.textContent.length,o=Math.min(r.start,l);r=r.end===void 0?o:Math.min(r.end,l),!e.extend&&o>r&&(l=r,r=o,o=l),l=qu(n,o);var i=qu(n,r);l&&i&&(e.rangeCount!==1||e.anchorNode!==l.node||e.anchorOffset!==l.offset||e.focusNode!==i.node||e.focusOffset!==i.offset)&&(t=t.createRange(),t.setStart(l.node,l.offset),e.removeAllRanges(),o>r?(e.addRange(t),e.extend(i.node,i.offset)):(t.setEnd(i.node,i.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,wn=null,Eo=null,or=null,Co=!1;function ts(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Co||wn==null||wn!==Lr(r)||(r=wn,"selectionStart"in r&&ko(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),or&&lr(or,r)||(or=r,r=Jr(Eo,"onSelect"),0_n||(e.current=Fo[_n],Fo[_n]=null,_n--)}function ce(e,t){_n++,Fo[_n]=e.current,e.current=t}var Ut={},Ie=Mt(Ut),Ve=Mt(!1),en=Ut;function Nn(e,t){var n=e.type.contextTypes;if(!n)return Ut;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},o;for(o in n)l[o]=t[o];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function We(e){return e=e.childContextTypes,e!=null}function tl(){de(Ve),de(Ie)}function vs(e,t,n){if(Ie.current!==Ut)throw Error(s(168));ce(Ie,t),ce(Ve,n)}function gs(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(s(108,ae(e)||"Unknown",l));return B({},n,r)}function nl(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Ut,en=Ie.current,ce(Ie,e),ce(Ve,Ve.current),!0}function ys(e,t,n){var r=e.stateNode;if(!r)throw Error(s(169));n?(e=gs(e,t,en),r.__reactInternalMemoizedMergedChildContext=e,de(Ve),de(Ie),ce(Ie,e)):de(Ve),ce(Ve,n)}var Et=null,rl=!1,Mo=!1;function xs(e){Et===null?Et=[e]:Et.push(e)}function Gf(e){rl=!0,xs(e)}function Bt(){if(!Mo&&Et!==null){Mo=!0;var e=0,t=ie;try{var n=Et;for(ie=1;e>=i,l-=i,Ct=1<<32-at(t)+l|n<Y?(Re=Q,Q=null):Re=Q.sibling;var re=_(m,Q,g[Y],z);if(re===null){Q===null&&(Q=Re);break}e&&Q&&re.alternate===null&&t(m,Q),d=o(re,d,Y),H===null?V=re:H.sibling=re,H=re,Q=Re}if(Y===g.length)return n(m,Q),he&&nn(m,Y),V;if(Q===null){for(;YY?(Re=Q,Q=null):Re=Q.sibling;var Yt=_(m,Q,re.value,z);if(Yt===null){Q===null&&(Q=Re);break}e&&Q&&Yt.alternate===null&&t(m,Q),d=o(Yt,d,Y),H===null?V=Yt:H.sibling=Yt,H=Yt,Q=Re}if(re.done)return n(m,Q),he&&nn(m,Y),V;if(Q===null){for(;!re.done;Y++,re=g.next())re=R(m,re.value,z),re!==null&&(d=o(re,d,Y),H===null?V=re:H.sibling=re,H=re);return he&&nn(m,Y),V}for(Q=r(m,Q);!re.done;Y++,re=g.next())re=M(Q,m,Y,re.value,z),re!==null&&(e&&re.alternate!==null&&Q.delete(re.key===null?Y:re.key),d=o(re,d,Y),H===null?V=re:H.sibling=re,H=re);return e&&Q.forEach(function(Pd){return t(m,Pd)}),he&&nn(m,Y),V}function Se(m,d,g,z){if(typeof g=="object"&&g!==null&&g.type===Oe&&g.key===null&&(g=g.props.children),typeof g=="object"&&g!==null){switch(g.$$typeof){case ke:e:{for(var V=g.key,H=d;H!==null;){if(H.key===V){if(V=g.type,V===Oe){if(H.tag===7){n(m,H.sibling),d=l(H,g.props.children),d.return=m,m=d;break e}}else if(H.elementType===V||typeof V=="object"&&V!==null&&V.$$typeof===Ae&&_s(V)===H.type){n(m,H.sibling),d=l(H,g.props),d.ref=fr(m,H,g),d.return=m,m=d;break e}n(m,H);break}else t(m,H);H=H.sibling}g.type===Oe?(d=fn(g.props.children,m.mode,z,g.key),d.return=m,m=d):(z=zl(g.type,g.key,g.props,null,m.mode,z),z.ref=fr(m,d,g),z.return=m,m=z)}return i(m);case Le:e:{for(H=g.key;d!==null;){if(d.key===H)if(d.tag===4&&d.stateNode.containerInfo===g.containerInfo&&d.stateNode.implementation===g.implementation){n(m,d.sibling),d=l(d,g.children||[]),d.return=m,m=d;break e}else{n(m,d);break}else t(m,d);d=d.sibling}d=Ii(g,m.mode,z),d.return=m,m=d}return i(m);case Ae:return H=g._init,Se(m,d,H(g._payload),z)}if($n(g))return $(m,d,g,z);if(G(g))return A(m,d,g,z);ul(m,g)}return typeof g=="string"&&g!==""||typeof g=="number"?(g=""+g,d!==null&&d.tag===6?(n(m,d.sibling),d=l(d,g),d.return=m,m=d):(n(m,d),d=Oi(g,m.mode,z),d.return=m,m=d),i(m)):n(m,d)}return Se}var Ln=Ns(!0),Ps=Ns(!1),sl=Mt(null),al=null,zn=null,Wo=null;function Ho(){Wo=zn=al=null}function Qo(e){var t=sl.current;de(sl),e._currentValue=t}function Ko(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function Tn(e,t){al=e,Wo=zn=null,e=e.dependencies,e!==null&&e.firstContext!==null&&((e.lanes&t)!==0&&(He=!0),e.firstContext=null)}function nt(e){var t=e._currentValue;if(Wo!==e)if(e={context:e,memoizedValue:t,next:null},zn===null){if(al===null)throw Error(s(308));zn=e,al.dependencies={lanes:0,firstContext:e}}else zn=zn.next=e;return t}var rn=null;function Go(e){rn===null?rn=[e]:rn.push(e)}function js(e,t,n,r){var l=t.interleaved;return l===null?(n.next=n,Go(t)):(n.next=l.next,l.next=n),t.interleaved=n,Nt(e,r)}function Nt(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var $t=!1;function Yo(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Rs(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Pt(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function At(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,(ee&2)!==0){var l=r.pending;return l===null?t.next=t:(t.next=l.next,l.next=t),r.pending=t,Nt(e,n)}return l=r.interleaved,l===null?(t.next=t,Go(r)):(t.next=l.next,l.next=t),r.interleaved=t,Nt(e,n)}function cl(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,uo(e,n)}}function Ls(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var l=null,o=null;if(n=n.firstBaseUpdate,n!==null){do{var i={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};o===null?l=o=i:o=o.next=i,n=n.next}while(n!==null);o===null?l=o=t:o=o.next=t}else l=o=t;n={baseState:r.baseState,firstBaseUpdate:l,lastBaseUpdate:o,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function fl(e,t,n,r){var l=e.updateQueue;$t=!1;var o=l.firstBaseUpdate,i=l.lastBaseUpdate,c=l.shared.pending;if(c!==null){l.shared.pending=null;var f=c,y=f.next;f.next=null,i===null?o=y:i.next=y,i=f;var j=e.alternate;j!==null&&(j=j.updateQueue,c=j.lastBaseUpdate,c!==i&&(c===null?j.firstBaseUpdate=y:c.next=y,j.lastBaseUpdate=f))}if(o!==null){var R=l.baseState;i=0,j=y=f=null,c=o;do{var _=c.lane,M=c.eventTime;if((r&_)===_){j!==null&&(j=j.next={eventTime:M,lane:0,tag:c.tag,payload:c.payload,callback:c.callback,next:null});e:{var $=e,A=c;switch(_=t,M=n,A.tag){case 1:if($=A.payload,typeof $=="function"){R=$.call(M,R,_);break e}R=$;break e;case 3:$.flags=$.flags&-65537|128;case 0:if($=A.payload,_=typeof $=="function"?$.call(M,R,_):$,_==null)break e;R=B({},R,_);break e;case 2:$t=!0}}c.callback!==null&&c.lane!==0&&(e.flags|=64,_=l.effects,_===null?l.effects=[c]:_.push(c))}else M={eventTime:M,lane:_,tag:c.tag,payload:c.payload,callback:c.callback,next:null},j===null?(y=j=M,f=R):j=j.next=M,i|=_;if(c=c.next,c===null){if(c=l.shared.pending,c===null)break;_=c,c=_.next,_.next=null,l.lastBaseUpdate=_,l.shared.pending=null}}while(!0);if(j===null&&(f=R),l.baseState=f,l.firstBaseUpdate=y,l.lastBaseUpdate=j,t=l.shared.interleaved,t!==null){l=t;do i|=l.lane,l=l.next;while(l!==t)}else o===null&&(l.shared.lanes=0);un|=i,e.lanes=i,e.memoizedState=R}}function zs(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=bo.transition;bo.transition={};try{e(!1),t()}finally{ie=n,bo.transition=r}}function Zs(){return rt().memoizedState}function Jf(e,t,n){var r=Qt(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},Js(e))qs(t,n);else if(n=js(e,t,n,r),n!==null){var l=Be();mt(n,e,r,l),bs(n,t,r)}}function qf(e,t,n){var r=Qt(e),l={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(Js(e))qs(t,l);else{var o=e.alternate;if(e.lanes===0&&(o===null||o.lanes===0)&&(o=t.lastRenderedReducer,o!==null))try{var i=t.lastRenderedState,c=o(i,n);if(l.hasEagerState=!0,l.eagerState=c,ct(c,i)){var f=t.interleaved;f===null?(l.next=l,Go(t)):(l.next=f.next,f.next=l),t.interleaved=l;return}}catch{}finally{}n=js(e,t,l,r),n!==null&&(l=Be(),mt(n,e,r,l),bs(n,t,r))}}function Js(e){var t=e.alternate;return e===ve||t!==null&&t===ve}function qs(e,t){mr=hl=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function bs(e,t,n){if((n&4194240)!==0){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,uo(e,n)}}var gl={readContext:nt,useCallback:De,useContext:De,useEffect:De,useImperativeHandle:De,useInsertionEffect:De,useLayoutEffect:De,useMemo:De,useReducer:De,useRef:De,useState:De,useDebugValue:De,useDeferredValue:De,useTransition:De,useMutableSource:De,useSyncExternalStore:De,useId:De,unstable_isNewReconciler:!1},bf={readContext:nt,useCallback:function(e,t){return wt().memoizedState=[e,t===void 0?null:t],e},useContext:nt,useEffect:Vs,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,ml(4194308,4,Qs.bind(null,t,e),n)},useLayoutEffect:function(e,t){return ml(4194308,4,e,t)},useInsertionEffect:function(e,t){return ml(4,2,e,t)},useMemo:function(e,t){var n=wt();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=wt();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=Jf.bind(null,ve,e),[r.memoizedState,e]},useRef:function(e){var t=wt();return e={current:e},t.memoizedState=e},useState:$s,useDebugValue:ii,useDeferredValue:function(e){return wt().memoizedState=e},useTransition:function(){var e=$s(!1),t=e[0];return e=Zf.bind(null,e[1]),wt().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=ve,l=wt();if(he){if(n===void 0)throw Error(s(407));n=n()}else{if(n=t(),je===null)throw Error(s(349));(on&30)!==0||Ds(r,t,n)}l.memoizedState=n;var o={value:n,getSnapshot:t};return l.queue=o,Vs(Ms.bind(null,r,o,e),[e]),r.flags|=2048,yr(9,Fs.bind(null,r,o,n,t),void 0,null),n},useId:function(){var e=wt(),t=je.identifierPrefix;if(he){var n=_t,r=Ct;n=(r&~(1<<32-at(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=vr++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=i.createElement(n,{is:r.is}):(e=i.createElement(n),n==="select"&&(i=e,r.multiple?i.multiple=!0:r.size&&(i.size=r.size))):e=i.createElementNS(e,n),e[yt]=t,e[ar]=r,xa(e,t,!1,!1),t.stateNode=e;e:{switch(i=Jl(n,r),n){case"dialog":fe("cancel",e),fe("close",e),l=r;break;case"iframe":case"object":case"embed":fe("load",e),l=r;break;case"video":case"audio":for(l=0;lMn&&(t.flags|=128,r=!0,xr(o,!1),t.lanes=4194304)}else{if(!r)if(e=dl(i),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),xr(o,!0),o.tail===null&&o.tailMode==="hidden"&&!i.alternate&&!he)return Fe(t),null}else 2*we()-o.renderingStartTime>Mn&&n!==1073741824&&(t.flags|=128,r=!0,xr(o,!1),t.lanes=4194304);o.isBackwards?(i.sibling=t.child,t.child=i):(n=o.last,n!==null?n.sibling=i:t.child=i,o.last=i)}return o.tail!==null?(t=o.tail,o.rendering=t,o.tail=t.sibling,o.renderingStartTime=we(),t.sibling=null,n=me.current,ce(me,r?n&1|2:n&1),t):(Fe(t),null);case 22:case 23:return Li(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&(t.mode&1)!==0?(be&1073741824)!==0&&(Fe(t),t.subtreeFlags&6&&(t.flags|=8192)):Fe(t),null;case 24:return null;case 25:return null}throw Error(s(156,t.tag))}function ud(e,t){switch(Bo(t),t.tag){case 1:return We(t.type)&&tl(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return On(),de(Ve),de(Ie),qo(),e=t.flags,(e&65536)!==0&&(e&128)===0?(t.flags=e&-65537|128,t):null;case 5:return Zo(t),null;case 13:if(de(me),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(s(340));Rn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return de(me),null;case 4:return On(),null;case 10:return Qo(t.type._context),null;case 22:case 23:return Li(),null;case 24:return null;default:return null}}var Sl=!1,Me=!1,sd=typeof WeakSet=="function"?WeakSet:Set,U=null;function Dn(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){xe(e,t,r)}else n.current=null}function yi(e,t,n){try{n()}catch(r){xe(e,t,r)}}var ka=!1;function ad(e,t){if(Lo=Ar,e=es(),ko(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var l=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break e}var i=0,c=-1,f=-1,y=0,j=0,R=e,_=null;t:for(;;){for(var M;R!==n||l!==0&&R.nodeType!==3||(c=i+l),R!==o||r!==0&&R.nodeType!==3||(f=i+r),R.nodeType===3&&(i+=R.nodeValue.length),(M=R.firstChild)!==null;)_=R,R=M;for(;;){if(R===e)break t;if(_===n&&++y===l&&(c=i),_===o&&++j===r&&(f=i),(M=R.nextSibling)!==null)break;R=_,_=R.parentNode}R=M}n=c===-1||f===-1?null:{start:c,end:f}}else n=null}n=n||{start:0,end:0}}else n=null;for(zo={focusedElem:e,selectionRange:n},Ar=!1,U=t;U!==null;)if(t=U,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,U=e;else for(;U!==null;){t=U;try{var $=t.alternate;if((t.flags&1024)!==0)switch(t.tag){case 0:case 11:case 15:break;case 1:if($!==null){var A=$.memoizedProps,Se=$.memoizedState,m=t.stateNode,d=m.getSnapshotBeforeUpdate(t.elementType===t.type?A:dt(t.type,A),Se);m.__reactInternalSnapshotBeforeUpdate=d}break;case 3:var g=t.stateNode.containerInfo;g.nodeType===1?g.textContent="":g.nodeType===9&&g.documentElement&&g.removeChild(g.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(s(163))}}catch(z){xe(t,t.return,z)}if(e=t.sibling,e!==null){e.return=t.return,U=e;break}U=t.return}return $=ka,ka=!1,$}function wr(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var l=r=r.next;do{if((l.tag&e)===e){var o=l.destroy;l.destroy=void 0,o!==void 0&&yi(t,n,o)}l=l.next}while(l!==r)}}function kl(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function xi(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function Ea(e){var t=e.alternate;t!==null&&(e.alternate=null,Ea(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[yt],delete t[ar],delete t[Do],delete t[Qf],delete t[Kf])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function Ca(e){return e.tag===5||e.tag===3||e.tag===4}function _a(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||Ca(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function wi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=br));else if(r!==4&&(e=e.child,e!==null))for(wi(e,t,n),e=e.sibling;e!==null;)wi(e,t,n),e=e.sibling}function Si(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Si(e,t,n),e=e.sibling;e!==null;)Si(e,t,n),e=e.sibling}var ze=null,pt=!1;function Vt(e,t,n){for(n=n.child;n!==null;)Na(e,t,n),n=n.sibling}function Na(e,t,n){if(gt&&typeof gt.onCommitFiberUnmount=="function")try{gt.onCommitFiberUnmount(Dr,n)}catch{}switch(n.tag){case 5:Me||Dn(n,t);case 6:var r=ze,l=pt;ze=null,Vt(e,t,n),ze=r,pt=l,ze!==null&&(pt?(e=ze,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):ze.removeChild(n.stateNode));break;case 18:ze!==null&&(pt?(e=ze,n=n.stateNode,e.nodeType===8?Io(e.parentNode,n):e.nodeType===1&&Io(e,n),qn(e)):Io(ze,n.stateNode));break;case 4:r=ze,l=pt,ze=n.stateNode.containerInfo,pt=!0,Vt(e,t,n),ze=r,pt=l;break;case 0:case 11:case 14:case 15:if(!Me&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){l=r=r.next;do{var o=l,i=o.destroy;o=o.tag,i!==void 0&&((o&2)!==0||(o&4)!==0)&&yi(n,t,i),l=l.next}while(l!==r)}Vt(e,t,n);break;case 1:if(!Me&&(Dn(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(c){xe(n,t,c)}Vt(e,t,n);break;case 21:Vt(e,t,n);break;case 22:n.mode&1?(Me=(r=Me)||n.memoizedState!==null,Vt(e,t,n),Me=r):Vt(e,t,n);break;default:Vt(e,t,n)}}function Pa(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new sd),t.forEach(function(r){var l=yd.bind(null,e,r);n.has(r)||(n.add(r),r.then(l,l))})}}function ht(e,t){var n=t.deletions;if(n!==null)for(var r=0;rl&&(l=i),r&=~o}if(r=l,r=we()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*fd(r/1960))-r,10e?16:e,Ht===null)var r=!1;else{if(e=Ht,Ht=null,Pl=0,(ee&6)!==0)throw Error(s(331));var l=ee;for(ee|=4,U=e.current;U!==null;){var o=U,i=o.child;if((U.flags&16)!==0){var c=o.deletions;if(c!==null){for(var f=0;fwe()-Ci?an(e,0):Ei|=n),Ke(e,t)}function $a(e,t){t===0&&((e.mode&1)===0?t=1:(t=Mr,Mr<<=1,(Mr&130023424)===0&&(Mr=4194304)));var n=Be();e=Nt(e,t),e!==null&&(Gn(e,t,n),Ke(e,n))}function gd(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),$a(e,n)}function yd(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,l=e.memoizedState;l!==null&&(n=l.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(s(314))}r!==null&&r.delete(t),$a(e,n)}var Aa;Aa=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||Ve.current)He=!0;else{if((e.lanes&n)===0&&(t.flags&128)===0)return He=!1,od(e,t,n);He=(e.flags&131072)!==0}else He=!1,he&&(t.flags&1048576)!==0&&ws(t,ol,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;wl(e,t),e=t.pendingProps;var l=Nn(t,Ie.current);Tn(t,n),l=ti(null,t,r,e,l,n);var o=ni();return t.flags|=1,typeof l=="object"&&l!==null&&typeof l.render=="function"&&l.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,We(r)?(o=!0,nl(t)):o=!1,t.memoizedState=l.state!==null&&l.state!==void 0?l.state:null,Yo(t),l.updater=yl,t.stateNode=l,l._reactInternals=t,si(t,r,e,n),t=di(null,t,r,!0,o,n)):(t.tag=0,he&&o&&Uo(t),Ue(null,t,l,n),t=t.child),t;case 16:r=t.elementType;e:{switch(wl(e,t),e=t.pendingProps,l=r._init,r=l(r._payload),t.type=r,l=t.tag=wd(r),e=dt(r,e),l){case 0:t=fi(null,t,r,e,n);break e;case 1:t=pa(null,t,r,e,n);break e;case 11:t=sa(null,t,r,e,n);break e;case 14:t=aa(null,t,r,dt(r.type,e),n);break e}throw Error(s(306,r,""))}return t;case 0:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:dt(r,l),fi(e,t,r,l,n);case 1:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:dt(r,l),pa(e,t,r,l,n);case 3:e:{if(ha(t),e===null)throw Error(s(387));r=t.pendingProps,o=t.memoizedState,l=o.element,Rs(e,t),fl(t,r,null,n);var i=t.memoizedState;if(r=i.element,o.isDehydrated)if(o={element:r,isDehydrated:!1,cache:i.cache,pendingSuspenseBoundaries:i.pendingSuspenseBoundaries,transitions:i.transitions},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){l=In(Error(s(423)),t),t=ma(e,t,r,n,l);break e}else if(r!==l){l=In(Error(s(424)),t),t=ma(e,t,r,n,l);break e}else for(qe=Ft(t.stateNode.containerInfo.firstChild),Je=t,he=!0,ft=null,n=Ps(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Rn(),r===l){t=jt(e,t,n);break e}Ue(e,t,r,n)}t=t.child}return t;case 5:return Ts(t),e===null&&Ao(t),r=t.type,l=t.pendingProps,o=e!==null?e.memoizedProps:null,i=l.children,To(r,l)?i=null:o!==null&&To(r,o)&&(t.flags|=32),da(e,t),Ue(e,t,i,n),t.child;case 6:return e===null&&Ao(t),null;case 13:return va(e,t,n);case 4:return Xo(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=Ln(t,null,r,n):Ue(e,t,r,n),t.child;case 11:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:dt(r,l),sa(e,t,r,l,n);case 7:return Ue(e,t,t.pendingProps,n),t.child;case 8:return Ue(e,t,t.pendingProps.children,n),t.child;case 12:return Ue(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,l=t.pendingProps,o=t.memoizedProps,i=l.value,ce(sl,r._currentValue),r._currentValue=i,o!==null)if(ct(o.value,i)){if(o.children===l.children&&!Ve.current){t=jt(e,t,n);break e}}else for(o=t.child,o!==null&&(o.return=t);o!==null;){var c=o.dependencies;if(c!==null){i=o.child;for(var f=c.firstContext;f!==null;){if(f.context===r){if(o.tag===1){f=Pt(-1,n&-n),f.tag=2;var y=o.updateQueue;if(y!==null){y=y.shared;var j=y.pending;j===null?f.next=f:(f.next=j.next,j.next=f),y.pending=f}}o.lanes|=n,f=o.alternate,f!==null&&(f.lanes|=n),Ko(o.return,n,t),c.lanes|=n;break}f=f.next}}else if(o.tag===10)i=o.type===t.type?null:o.child;else if(o.tag===18){if(i=o.return,i===null)throw Error(s(341));i.lanes|=n,c=i.alternate,c!==null&&(c.lanes|=n),Ko(i,n,t),i=o.sibling}else i=o.child;if(i!==null)i.return=o;else for(i=o;i!==null;){if(i===t){i=null;break}if(o=i.sibling,o!==null){o.return=i.return,i=o;break}i=i.return}o=i}Ue(e,t,l.children,n),t=t.child}return t;case 9:return l=t.type,r=t.pendingProps.children,Tn(t,n),l=nt(l),r=r(l),t.flags|=1,Ue(e,t,r,n),t.child;case 14:return r=t.type,l=dt(r,t.pendingProps),l=dt(r.type,l),aa(e,t,r,l,n);case 15:return ca(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,l=t.pendingProps,l=t.elementType===r?l:dt(r,l),wl(e,t),t.tag=1,We(r)?(e=!0,nl(t)):e=!1,Tn(t,n),ta(t,r,l),si(t,r,l,n),di(null,t,r,!0,e,n);case 19:return ya(e,t,n);case 22:return fa(e,t,n)}throw Error(s(156,t.tag))};function Va(e,t){return Su(e,t)}function xd(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function ot(e,t,n,r){return new xd(e,t,n,r)}function Ti(e){return e=e.prototype,!(!e||!e.isReactComponent)}function wd(e){if(typeof e=="function")return Ti(e)?1:0;if(e!=null){if(e=e.$$typeof,e===ge)return 11;if(e===vt)return 14}return 2}function Gt(e,t){var n=e.alternate;return n===null?(n=ot(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function zl(e,t,n,r,l,o){var i=2;if(r=e,typeof e=="function")Ti(e)&&(i=1);else if(typeof e=="string")i=5;else e:switch(e){case Oe:return fn(n.children,l,o,t);case $e:i=8,l|=8;break;case it:return e=ot(12,n,t,l|2),e.elementType=it,e.lanes=o,e;case Ye:return e=ot(13,n,t,l),e.elementType=Ye,e.lanes=o,e;case st:return e=ot(19,n,t,l),e.elementType=st,e.lanes=o,e;case ye:return Tl(n,l,o,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case ut:i=10;break e;case se:i=9;break e;case ge:i=11;break e;case vt:i=14;break e;case Ae:i=16,r=null;break e}throw Error(s(130,e==null?e:typeof e,""))}return t=ot(i,n,t,l),t.elementType=e,t.type=r,t.lanes=o,t}function fn(e,t,n,r){return e=ot(7,e,r,t),e.lanes=n,e}function Tl(e,t,n,r){return e=ot(22,e,r,t),e.elementType=ye,e.lanes=n,e.stateNode={isHidden:!1},e}function Oi(e,t,n){return e=ot(6,e,null,t),e.lanes=n,e}function Ii(e,t,n){return t=ot(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Sd(e,t,n,r,l){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=io(0),this.expirationTimes=io(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=io(0),this.identifierPrefix=r,this.onRecoverableError=l,this.mutableSourceEagerHydrationData=null}function Di(e,t,n,r,l,o,i,c,f){return e=new Sd(e,t,n,c,f),t===1?(t=1,o===!0&&(t|=8)):t=0,o=ot(3,null,null,t),e.current=o,o.stateNode=e,o.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},Yo(o),e}function kd(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(u)}catch(a){console.error(a)}}return u(),Ai.exports=Dd(),Ai.exports}var nc;function Fd(){if(nc)return Bl;nc=1;var u=vc();return Bl.createRoot=u.createRoot,Bl.hydrateRoot=u.hydrateRoot,Bl}var Md=Fd();const Ud=hc(Md);vc();/** + * @remix-run/router v1.23.4 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */function Nr(){return Nr=Object.assign?Object.assign.bind():function(u){for(var a=1;a"u")throw new Error(a)}function gc(u,a){if(!u){typeof console<"u"&&console.warn(a);try{throw new Error(a)}catch{}}}function $d(){return Math.random().toString(36).substr(2,8)}function lc(u,a){return{usr:u.state,key:u.key,idx:a}}function Qi(u,a,s,p){return s===void 0&&(s=null),Nr({pathname:typeof u=="string"?u:u.pathname,search:"",hash:""},typeof a=="string"?Bn(a):a,{state:s,key:a&&a.key||p||$d()})}function Al(u){let{pathname:a="/",search:s="",hash:p=""}=u;return s&&s!=="?"&&(a+=s.charAt(0)==="?"?s:"?"+s),p&&p!=="#"&&(a+=p.charAt(0)==="#"?p:"#"+p),a}function Bn(u){let a={};if(u){let s=u.indexOf("#");s>=0&&(a.hash=u.substr(s),u=u.substr(0,s));let p=u.indexOf("?");p>=0&&(a.search=u.substr(p),u=u.substr(0,p)),u&&(a.pathname=u)}return a}function Ad(u,a,s,p){p===void 0&&(p={});let{window:v=document.defaultView,v5Compat:x=!1}=p,w=v.history,L=Xt.Pop,E=null,C=N();C==null&&(C=0,w.replaceState(Nr({},w.state,{idx:C}),""));function N(){return(w.state||{idx:null}).idx}function k(){L=Xt.Pop;let O=N(),K=O==null?null:O-C;C=O,E&&E({action:L,location:W.location,delta:K})}function I(O,K){L=Xt.Push;let le=Qi(W.location,O,K);C=N()+1;let b=lc(le,C),ue=W.createHref(le);try{w.pushState(b,"",ue)}catch(ke){if(ke instanceof DOMException&&ke.name==="DataCloneError")throw ke;v.location.assign(ue)}x&&E&&E({action:L,location:W.location,delta:1})}function D(O,K){L=Xt.Replace;let le=Qi(W.location,O,K);C=N();let b=lc(le,C),ue=W.createHref(le);w.replaceState(b,"",ue),x&&E&&E({action:L,location:W.location,delta:0})}function X(O){let K=v.location.origin!=="null"?v.location.origin:v.location.href,le=typeof O=="string"?O:Al(O);return le=le.replace(/ $/,"%20"),Ce(K,"No window.location.(origin|href) available to create URL for href: "+le),new URL(le,K)}let W={get action(){return L},get location(){return u(v,w)},listen(O){if(E)throw new Error("A history only accepts one active listener");return v.addEventListener(rc,k),E=O,()=>{v.removeEventListener(rc,k),E=null}},createHref(O){return a(v,O)},createURL:X,encodeLocation(O){let K=X(O);return{pathname:K.pathname,search:K.search,hash:K.hash}},push:I,replace:D,go(O){return w.go(O)}};return W}var oc;(function(u){u.data="data",u.deferred="deferred",u.redirect="redirect",u.error="error"})(oc||(oc={}));function Vd(u,a,s){return s===void 0&&(s="/"),Wd(u,a,s)}function Wd(u,a,s,p){let v=typeof a=="string"?Bn(a):a,x=Ji(v.pathname||"/",s);if(x==null)return null;let w=yc(u);Hd(w);let L=null,E=np(x);for(let C=0;L==null&&C{let E={relativePath:L===void 0?x.path||"":L,caseSensitive:x.caseSensitive===!0,childrenIndex:w,route:x};E.relativePath.startsWith("/")&&(Ce(E.relativePath.startsWith(p),'Absolute route path "'+E.relativePath+'" nested under path '+('"'+p+'" is not valid. An absolute child route path ')+"must start with the combined path of all its parent routes."),E.relativePath=E.relativePath.slice(p.length));let C=Zt([p,E.relativePath]),N=s.concat(E);x.children&&x.children.length>0&&(Ce(x.index!==!0,"Index routes must not have child routes. Please remove "+('all child routes from route path "'+C+'".')),yc(x.children,a,N,C)),!(x.path==null&&!x.index)&&a.push({path:C,score:Jd(C,x.index),routesMeta:N})};return u.forEach((x,w)=>{var L;if(x.path===""||!((L=x.path)!=null&&L.includes("?")))v(x,w);else for(let E of xc(x.path))v(x,w,E)}),a}function xc(u){let a=u.split("/");if(a.length===0)return[];let[s,...p]=a,v=s.endsWith("?"),x=s.replace(/\?$/,"");if(p.length===0)return v?[x,""]:[x];let w=xc(p.join("/")),L=[];return L.push(...w.map(E=>E===""?x:[x,E].join("/"))),v&&L.push(...w),L.map(E=>u.startsWith("/")&&E===""?"/":E)}function Hd(u){u.sort((a,s)=>a.score!==s.score?s.score-a.score:qd(a.routesMeta.map(p=>p.childrenIndex),s.routesMeta.map(p=>p.childrenIndex)))}const Qd=/^:[\w-]+$/,Kd=3,Gd=2,Yd=1,Xd=10,Zd=-2,ic=u=>u==="*";function Jd(u,a){let s=u.split("/"),p=s.length;return s.some(ic)&&(p+=Zd),a&&(p+=Gd),s.filter(v=>!ic(v)).reduce((v,x)=>v+(Qd.test(x)?Kd:x===""?Yd:Xd),p)}function qd(u,a){return u.length===a.length&&u.slice(0,-1).every((p,v)=>p===a[v])?u[u.length-1]-a[a.length-1]:0}function bd(u,a,s){let{routesMeta:p}=u,v={},x="/",w=[];for(let L=0;L{let{paramName:I,isOptional:D}=N;if(I==="*"){let W=L[k]||"";w=x.slice(0,x.length-W.length).replace(/(.)\/+$/,"$1")}const X=L[k];return D&&!X?C[I]=void 0:C[I]=(X||"").replace(/%2F/g,"/"),C},{}),pathname:x,pathnameBase:w,pattern:u}}function tp(u,a,s){a===void 0&&(a=!1),s===void 0&&(s=!0),gc(u==="*"||!u.endsWith("*")||u.endsWith("/*"),'Route path "'+u+'" will be treated as if it were '+('"'+u.replace(/\*$/,"/*")+'" because the `*` character must ')+"always follow a `/` in the pattern. To get rid of this warning, "+('please change the route path to "'+u.replace(/\*$/,"/*")+'".'));let p=[],v="^"+u.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(w,L,E)=>(p.push({paramName:L,isOptional:E!=null}),E?"/?([^\\/]+)?":"/([^\\/]+)"));return u.endsWith("*")?(p.push({paramName:"*"}),v+=u==="*"||u==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):s?v+="\\/*$":u!==""&&u!=="/"&&(v+="(?:(?=\\/|$))"),[new RegExp(v,a?void 0:"i"),p]}function np(u){try{return u.split("/").map(a=>decodeURIComponent(a).replace(/\//g,"%2F")).join("/")}catch(a){return gc(!1,'The URL path "'+u+'" could not be decoded because it is is a malformed URL segment. This is probably due to a bad percent '+("encoding ("+a+").")),u}}function Ji(u,a){if(a==="/")return u;if(!u.toLowerCase().startsWith(a.toLowerCase()))return null;let s=a.endsWith("/")?a.length-1:a.length,p=u.charAt(s);return p&&p!=="/"?null:u.slice(s)||"/"}function rp(u,a){a===void 0&&(a="/");let{pathname:s,search:p="",hash:v=""}=typeof u=="string"?Bn(u):u,x;return s?(s=kc(s),s.startsWith("/")?x=uc(s.substring(1),"/"):x=uc(s,a)):x=a,{pathname:x,search:ip(p),hash:up(v)}}function uc(u,a){let s=a.replace(/\/+$/,"").split("/");return u.split("/").forEach(v=>{v===".."?s.length>1&&s.pop():v!=="."&&s.push(v)}),s.length>1?s.join("/"):"/"}function Hi(u,a,s,p){return"Cannot include a '"+u+"' character in a manually specified "+("`to."+a+"` field ["+JSON.stringify(p)+"]. Please separate it out to the ")+("`to."+s+"` field. Alternatively you may provide the full path as ")+'a string in and the router will parse it for you.'}function lp(u){return u.filter((a,s)=>s===0||a.route.path&&a.route.path.length>0)}function wc(u,a){let s=lp(u);return a?s.map((p,v)=>v===s.length-1?p.pathname:p.pathnameBase):s.map(p=>p.pathnameBase)}function Sc(u,a,s,p){p===void 0&&(p=!1);let v;typeof u=="string"?v=Bn(u):(v=Nr({},u),Ce(!v.pathname||!v.pathname.includes("?"),Hi("?","pathname","search",v)),Ce(!v.pathname||!v.pathname.includes("#"),Hi("#","pathname","hash",v)),Ce(!v.search||!v.search.includes("#"),Hi("#","search","hash",v)));let x=u===""||v.pathname==="",w=x?"/":v.pathname,L;if(w==null)L=s;else{let k=a.length-1;if(!p&&w.startsWith("..")){let I=w.split("/");for(;I[0]==="..";)I.shift(),k-=1;v.pathname=I.join("/")}L=k>=0?a[k]:"/"}let E=rp(v,L),C=w&&w!=="/"&&w.endsWith("/"),N=(x||w===".")&&s.endsWith("/");return!E.pathname.endsWith("/")&&(C||N)&&(E.pathname+="/"),E}const kc=u=>u.replace(/\/\/+/g,"/"),Zt=u=>kc(u.join("/")),op=u=>u.replace(/\/+$/,"").replace(/^\/*/,"/"),ip=u=>!u||u==="?"?"":u.startsWith("?")?u:"?"+u,up=u=>!u||u==="#"?"":u.startsWith("#")?u:"#"+u;function sp(u){return u!=null&&typeof u.status=="number"&&typeof u.statusText=="string"&&typeof u.internal=="boolean"&&"data"in u}const Ec=["post","put","patch","delete"];new Set(Ec);const ap=["get",...Ec];new Set(ap);/** + * React Router v6.30.6 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */function Pr(){return Pr=Object.assign?Object.assign.bind():function(u){for(var a=1;a{L.current=!0}),T.useCallback(function(C,N){if(N===void 0&&(N={}),!L.current)return;if(typeof C=="number"){p.go(C);return}let k=Sc(C,JSON.parse(w),x,N.relative==="path");u==null&&a!=="/"&&(k.pathname=k.pathname==="/"?a:Zt([a,k.pathname])),(N.replace?p.replace:p.push)(k,N.state,N)},[a,p,w,x,u])}function pp(){let{matches:u}=T.useContext(Jt),a=u[u.length-1];return a?a.params:{}}function Nc(u,a){let{relative:s}=a===void 0?{}:a,{future:p}=T.useContext(dn),{matches:v}=T.useContext(Jt),{pathname:x}=Hl(),w=JSON.stringify(wc(v,p.v7_relativeSplatPath));return T.useMemo(()=>Sc(u,JSON.parse(w),x,s==="path"),[u,w,x,s])}function hp(u,a){return mp(u,a)}function mp(u,a,s,p){jr()||Ce(!1);let{navigator:v}=T.useContext(dn),{matches:x}=T.useContext(Jt),w=x[x.length-1],L=w?w.params:{};w&&w.pathname;let E=w?w.pathnameBase:"/";w&&w.route;let C=Hl(),N;if(a){var k;let O=typeof a=="string"?Bn(a):a;E==="/"||(k=O.pathname)!=null&&k.startsWith(E)||Ce(!1),N=O}else N=C;let I=N.pathname||"/",D=I;if(E!=="/"){let O=E.replace(/^\//,"").split("/");D="/"+I.replace(/^\//,"").split("/").slice(O.length).join("/")}let X=Vd(u,{pathname:D}),W=wp(X&&X.map(O=>Object.assign({},O,{params:Object.assign({},L,O.params),pathname:Zt([E,v.encodeLocation?v.encodeLocation(O.pathname).pathname:O.pathname]),pathnameBase:O.pathnameBase==="/"?E:Zt([E,v.encodeLocation?v.encodeLocation(O.pathnameBase).pathname:O.pathnameBase])})),x,s,p);return a&&W?T.createElement(Wl.Provider,{value:{location:Pr({pathname:"/",search:"",hash:"",state:null,key:"default"},N),navigationType:Xt.Pop}},W):W}function vp(){let u=Cp(),a=sp(u)?u.status+" "+u.statusText:u instanceof Error?u.message:JSON.stringify(u),s=u instanceof Error?u.stack:null,v={padding:"0.5rem",backgroundColor:"rgba(200,200,200, 0.5)"};return T.createElement(T.Fragment,null,T.createElement("h2",null,"Unexpected Application Error!"),T.createElement("h3",{style:{fontStyle:"italic"}},a),s?T.createElement("pre",{style:v},s):null,null)}const gp=T.createElement(vp,null);class yp extends T.Component{constructor(a){super(a),this.state={location:a.location,revalidation:a.revalidation,error:a.error}}static getDerivedStateFromError(a){return{error:a}}static getDerivedStateFromProps(a,s){return s.location!==a.location||s.revalidation!=="idle"&&a.revalidation==="idle"?{error:a.error,location:a.location,revalidation:a.revalidation}:{error:a.error!==void 0?a.error:s.error,location:s.location,revalidation:a.revalidation||s.revalidation}}componentDidCatch(a,s){console.error("React Router caught the following error during render",a,s)}render(){return this.state.error!==void 0?T.createElement(Jt.Provider,{value:this.props.routeContext},T.createElement(Cc.Provider,{value:this.state.error,children:this.props.component})):this.props.children}}function xp(u){let{routeContext:a,match:s,children:p}=u,v=T.useContext(qi);return v&&v.static&&v.staticContext&&(s.route.errorElement||s.route.ErrorBoundary)&&(v.staticContext._deepestRenderedBoundaryId=s.route.id),T.createElement(Jt.Provider,{value:a},p)}function wp(u,a,s,p){var v;if(a===void 0&&(a=[]),s===void 0&&(s=null),p===void 0&&(p=null),u==null){var x;if(!s)return null;if(s.errors)u=s.matches;else if((x=p)!=null&&x.v7_partialHydration&&a.length===0&&!s.initialized&&s.matches.length>0)u=s.matches;else return null}let w=u,L=(v=s)==null?void 0:v.errors;if(L!=null){let N=w.findIndex(k=>k.route.id&&(L==null?void 0:L[k.route.id])!==void 0);N>=0||Ce(!1),w=w.slice(0,Math.min(w.length,N+1))}let E=!1,C=-1;if(s&&p&&p.v7_partialHydration)for(let N=0;N=0?w=w.slice(0,C+1):w=[w[0]];break}}}return w.reduceRight((N,k,I)=>{let D,X=!1,W=null,O=null;s&&(D=L&&k.route.id?L[k.route.id]:void 0,W=k.route.errorElement||gp,E&&(C<0&&I===0?(Np("route-fallback"),X=!0,O=null):C===I&&(X=!0,O=k.route.hydrateFallbackElement||null)));let K=a.concat(w.slice(0,I+1)),le=()=>{let b;return D?b=W:X?b=O:k.route.Component?b=T.createElement(k.route.Component,null):k.route.element?b=k.route.element:b=N,T.createElement(xp,{match:k,routeContext:{outlet:N,matches:K,isDataRoute:s!=null},children:b})};return s&&(k.route.ErrorBoundary||k.route.errorElement||I===0)?T.createElement(yp,{location:s.location,revalidation:s.revalidation,component:W,error:D,children:le(),routeContext:{outlet:null,matches:K,isDataRoute:!0}}):le()},null)}var Pc=(function(u){return u.UseBlocker="useBlocker",u.UseRevalidator="useRevalidator",u.UseNavigateStable="useNavigate",u})(Pc||{}),jc=(function(u){return u.UseBlocker="useBlocker",u.UseLoaderData="useLoaderData",u.UseActionData="useActionData",u.UseRouteError="useRouteError",u.UseNavigation="useNavigation",u.UseRouteLoaderData="useRouteLoaderData",u.UseMatches="useMatches",u.UseRevalidator="useRevalidator",u.UseNavigateStable="useNavigate",u.UseRouteId="useRouteId",u})(jc||{});function Sp(u){let a=T.useContext(qi);return a||Ce(!1),a}function kp(u){let a=T.useContext(cp);return a||Ce(!1),a}function Ep(u){let a=T.useContext(Jt);return a||Ce(!1),a}function Rc(u){let a=Ep(),s=a.matches[a.matches.length-1];return s.route.id||Ce(!1),s.route.id}function Cp(){var u;let a=T.useContext(Cc),s=kp(),p=Rc();return a!==void 0?a:(u=s.errors)==null?void 0:u[p]}function _p(){let{router:u}=Sp(Pc.UseNavigateStable),a=Rc(jc.UseNavigateStable),s=T.useRef(!1);return _c(()=>{s.current=!0}),T.useCallback(function(v,x){x===void 0&&(x={}),s.current&&(typeof v=="number"?u.navigate(v):u.navigate(v,Pr({fromRouteId:a},x)))},[u,a])}const sc={};function Np(u,a,s){sc[u]||(sc[u]=!0)}function Pp(u,a){u==null||u.v7_startTransition,u==null||u.v7_relativeSplatPath}function $l(u){Ce(!1)}function jp(u){let{basename:a="/",children:s=null,location:p,navigationType:v=Xt.Pop,navigator:x,static:w=!1,future:L}=u;jr()&&Ce(!1);let E=a.replace(/^\/*/,"/"),C=T.useMemo(()=>({basename:E,navigator:x,static:w,future:Pr({v7_relativeSplatPath:!1},L)}),[E,L,x,w]);typeof p=="string"&&(p=Bn(p));let{pathname:N="/",search:k="",hash:I="",state:D=null,key:X="default"}=p,W=T.useMemo(()=>{let O=Ji(N,E);return O==null?null:{location:{pathname:O,search:k,hash:I,state:D,key:X},navigationType:v}},[E,N,k,I,D,X,v]);return W==null?null:T.createElement(dn.Provider,{value:C},T.createElement(Wl.Provider,{children:s,value:W}))}function Rp(u){let{children:a,location:s}=u;return hp(Ki(a),s)}new Promise(()=>{});function Ki(u,a){a===void 0&&(a=[]);let s=[];return T.Children.forEach(u,(p,v)=>{if(!T.isValidElement(p))return;let x=[...a,v];if(p.type===T.Fragment){s.push.apply(s,Ki(p.props.children,x));return}p.type!==$l&&Ce(!1),!p.props.index||!p.props.children||Ce(!1);let w={id:p.props.id||x.join("-"),caseSensitive:p.props.caseSensitive,element:p.props.element,Component:p.props.Component,index:p.props.index,path:p.props.path,loader:p.props.loader,action:p.props.action,errorElement:p.props.errorElement,ErrorBoundary:p.props.ErrorBoundary,hasErrorBoundary:p.props.ErrorBoundary!=null||p.props.errorElement!=null,shouldRevalidate:p.props.shouldRevalidate,handle:p.props.handle,lazy:p.props.lazy};p.props.children&&(w.children=Ki(p.props.children,x)),s.push(w)}),s}/** + * React Router DOM v6.30.6 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */function Gi(){return Gi=Object.assign?Object.assign.bind():function(u){for(var a=1;a{C&&ac?ac(()=>E(k)):E(k)},[E,C]);return T.useLayoutEffect(()=>w.listen(N),[w,N]),T.useEffect(()=>Pp(p),[p]),T.createElement(jp,{basename:a,children:s,location:L.location,navigationType:L.action,navigator:w,future:p})}const Mp=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u",Up=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,Vl=T.forwardRef(function(a,s){let{onClick:p,relative:v,reloadDocument:x,replace:w,state:L,target:E,to:C,preventScrollReset:N,viewTransition:k}=a,I=Lp(a,Op),{basename:D}=T.useContext(dn),X,W=!1;if(typeof C=="string"&&Up.test(C)&&(X=C,Mp))try{let b=new URL(window.location.href),ue=C.startsWith("//")?new URL(b.protocol+C):new URL(C),ke=Ji(ue.pathname,D);ue.origin===b.origin&&ke!=null?C=ke+ue.search+ue.hash:W=!0}catch{}let O=fp(C,{relative:v}),K=Bp(C,{replace:w,state:L,target:E,preventScrollReset:N,relative:v,viewTransition:k});function le(b){p&&p(b),b.defaultPrevented||K(b)}return T.createElement("a",Gi({},I,{href:X||O,onClick:W||x?p:le,ref:s,target:E}))});var cc;(function(u){u.UseScrollRestoration="useScrollRestoration",u.UseSubmit="useSubmit",u.UseSubmitFetcher="useSubmitFetcher",u.UseFetcher="useFetcher",u.useViewTransitionState="useViewTransitionState"})(cc||(cc={}));var fc;(function(u){u.UseFetcher="useFetcher",u.UseFetchers="useFetchers",u.UseScrollRestoration="useScrollRestoration"})(fc||(fc={}));function Bp(u,a){let{target:s,replace:p,state:v,preventScrollReset:x,relative:w,viewTransition:L}=a===void 0?{}:a,E=bi(),C=Hl(),N=Nc(u,{relative:w});return T.useCallback(k=>{if(Tp(k,s)){k.preventDefault();let I=p!==void 0?p:Al(C)===Al(N);E(u,{replace:I,state:v,preventScrollReset:x,relative:w,viewTransition:L})}},[C,E,N,p,v,s,u,x,w,L])}const pn="/api/recipes";async function $p(u,a){const s=new URLSearchParams;return u&&s.set("search",u),a&&a!=="Alle"&&s.set("category",a),(await fetch(`${pn}?${s}`)).json()}async function Ap(u){const a=await fetch(`${pn}/${u}`);if(!a.ok)throw new Error("Rezept nicht gefunden");return a.json()}async function Vp(u){return(await fetch(pn,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(u)})).json()}async function Wp(u,a){return(await fetch(`${pn}/${u}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(a)})).json()}async function Hp(u){return(await fetch(`${pn}/${u}`,{method:"DELETE"})).json()}async function Qp(u){const a=await fetch(`${pn}/from-tiktok`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url:u})});if(!a.ok)throw new Error("Fehler beim Abrufen der TikTok-Daten");return a.json()}async function Kp(){return(await fetch(`${pn}/categories`)).json()}const dc={Frühstück:"bg-yellow-100 text-yellow-800",Hauptgericht:"bg-red-100 text-red-800",Dessert:"bg-pink-100 text-pink-800",Snack:"bg-green-100 text-green-800",Getränk:"bg-blue-100 text-blue-800",Sonstiges:"bg-gray-100 text-gray-800"},pc={Frühstück:"🍳",Hauptgericht:"🍽️",Dessert:"🍰",Snack:"🍿",Getränk:"🥤",Sonstiges:"📋"};function Lc({category:u}){const a=dc[u]||dc.Sonstiges,s=pc[u]||pc.Sonstiges;return S.jsxs("span",{className:`text-xs font-medium px-2 py-1 rounded-full whitespace-nowrap ${a}`,children:[s," ",u]})}function Gp({recipe:u}){var a,s;return S.jsxs(Vl,{to:`/recipe/${u.id}`,className:"bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow",children:[u.thumbnail_url&&S.jsx("img",{src:u.thumbnail_url,alt:u.name,className:"w-full h-48 object-cover"}),S.jsxs("div",{className:"p-4",children:[S.jsxs("div",{className:"flex items-start justify-between gap-2 mb-2",children:[S.jsx("h3",{className:"font-semibold text-lg leading-tight",children:u.name}),S.jsx(Lc,{category:u.category})]}),u.description&&S.jsx("p",{className:"text-gray-500 text-sm line-clamp-2 mb-2",children:u.description}),S.jsxs("div",{className:"flex items-center gap-4 text-xs text-gray-400",children:[u.tiktok_author&&S.jsxs("span",{children:["@",u.tiktok_author]}),S.jsxs("span",{children:[((a=u.ingredients)==null?void 0:a.length)||0," Zutaten"]}),S.jsxs("span",{children:[((s=u.steps)==null?void 0:s.length)||0," Schritte"]})]})]})]})}const Yp=["Alle","Frühstück","Hauptgericht","Dessert","Snack","Getränk","Sonstiges"];function Xp(){const[u,a]=T.useState([]),[s,p]=T.useState([]),[v,x]=T.useState(""),[w,L]=T.useState("Alle"),[E,C]=T.useState(!0);T.useEffect(()=>{N(),k()},[v,w]);const N=async()=>{C(!0);try{const D=await $p(v,w);a(D)}catch(D){console.error("Fehler beim Laden:",D)}C(!1)},k=async()=>{try{const D=await Kp();p(D)}catch(D){console.error("Fehler beim Laden der Kategorien:",D)}},I=D=>{if(D==="Alle")return s.reduce((W,O)=>W+O.count,0);const X=s.find(W=>W.category===D);return X?X.count:0};return S.jsxs("div",{children:[S.jsx("div",{className:"mb-6",children:S.jsx("input",{type:"text",placeholder:"Rezepte suchen...",value:v,onChange:D=>x(D.target.value),className:"w-full px-4 py-3 border border-gray-300 rounded-xl text-lg focus:outline-none focus:ring-2 focus:ring-orange-400 focus:border-transparent"})}),S.jsx("div",{className:"flex flex-wrap gap-2 mb-6",children:Yp.map(D=>S.jsxs("button",{onClick:()=>L(D),className:`px-3 py-1.5 rounded-full text-sm font-medium transition-colors ${w===D?"bg-orange-500 text-white":"bg-white border border-gray-200 text-gray-600 hover:border-orange-300"}`,children:[D," (",I(D),")"]},D))}),E?S.jsx("div",{className:"text-center py-12 text-gray-400",children:"Laden..."}):u.length===0?S.jsxs("div",{className:"text-center py-12",children:[S.jsx("p",{className:"text-gray-400 text-lg mb-2",children:"Keine Rezepte gefunden"}),S.jsx("p",{className:"text-gray-400 text-sm",children:"Füge dein erstes Rezept über TikTok-Links hinzu!"})]}):S.jsx("div",{className:"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4",children:u.map(D=>S.jsx(Gp,{recipe:D},D.id))})]})}function Yi({ingredients:u,editable:a,onChange:s}){const[p,v]=T.useState({}),x=C=>v(N=>({...N,[C]:!N[C]})),w=(C,N,k)=>{const I=u.map((D,X)=>X===C?{...D,[N]:k}:D);s(I)},L=()=>{s([...u,{name:"",amount:"",unit:""}])},E=C=>{s(u.filter((N,k)=>k!==C))};return S.jsxs("div",{children:[S.jsx("h3",{className:"font-semibold text-lg mb-3",children:"Zutaten"}),S.jsx("ul",{className:"space-y-2",children:u.map((C,N)=>S.jsxs("li",{className:"flex items-center gap-2",children:[!a&&S.jsx("input",{type:"checkbox",checked:!!p[N],onChange:()=>x(N),className:"w-4 h-4 accent-orange-500"}),a?S.jsxs(S.Fragment,{children:[S.jsx("input",{type:"text",value:C.amount,onChange:k=>w(N,"amount",k.target.value),placeholder:"Menge",className:"w-16 px-2 py-1 border border-gray-300 rounded text-sm"}),S.jsx("input",{type:"text",value:C.unit,onChange:k=>w(N,"unit",k.target.value),placeholder:"Einheit",className:"w-14 px-2 py-1 border border-gray-300 rounded text-sm"}),S.jsx("input",{type:"text",value:C.name,onChange:k=>w(N,"name",k.target.value),placeholder:"Zutat",className:"flex-1 px-2 py-1 border border-gray-300 rounded text-sm"}),S.jsx("button",{onClick:()=>E(N),className:"text-red-400 hover:text-red-600 text-lg",children:"×"})]}):S.jsxs("span",{className:`${p[N]?"line-through text-gray-400":""}`,children:[C.amount&&`${C.amount} `,C.unit&&`${C.unit} `,C.name]})]},N))}),a&&S.jsx("button",{onClick:L,className:"mt-2 text-sm text-orange-500 hover:text-orange-600 font-medium",children:"+ Zutat hinzufügen"})]})}function Xi({steps:u,editable:a,onChange:s}){const[p,v]=T.useState({}),x=N=>v(k=>({...k,[N]:!k[N]})),w=(N,k)=>{const I=u.map((D,X)=>X===N?k:D);s(I)},L=()=>s([...u,""]),E=N=>s(u.filter((k,I)=>I!==N)),C=(N,k)=>{const I=[...u],D=N+k;D<0||D>=I.length||([I[N],I[D]]=[I[D],I[N]],s(I))};return S.jsxs("div",{children:[S.jsx("h3",{className:"font-semibold text-lg mb-3",children:"Zubereitung"}),S.jsx("ol",{className:"space-y-3",children:u.map((N,k)=>S.jsxs("li",{className:"flex items-start gap-3",children:[!a&&S.jsx("button",{onClick:()=>x(k),className:`mt-1 w-6 h-6 rounded-full border-2 flex-shrink-0 flex items-center justify-center text-xs font-bold transition-colors ${p[k]?"bg-orange-500 border-orange-500 text-white":"border-gray-300 text-gray-400"}`,children:p[k]?"✓":k+1}),a?S.jsxs("div",{className:"flex-1 flex gap-2",children:[S.jsxs("span",{className:"mt-2 text-sm font-bold text-gray-400 w-5",children:[k+1,"."]}),S.jsx("textarea",{value:N,onChange:I=>w(k,I.target.value),rows:2,className:"flex-1 px-2 py-1 border border-gray-300 rounded text-sm resize-none"}),S.jsxs("div",{className:"flex flex-col gap-1",children:[S.jsx("button",{onClick:()=>C(k,-1),className:"text-gray-400 hover:text-gray-600 text-xs",children:"↑"}),S.jsx("button",{onClick:()=>C(k,1),className:"text-gray-400 hover:text-gray-600 text-xs",children:"↓"}),S.jsx("button",{onClick:()=>E(k),className:"text-red-400 hover:text-red-600 text-xs",children:"×"})]})]}):S.jsx("p",{className:`${p[k]?"line-through text-gray-400":""}`,children:N})]},k))}),a&&S.jsx("button",{onClick:L,className:"mt-2 text-sm text-orange-500 hover:text-orange-600 font-medium",children:"+ Schritt hinzufügen"})]})}const Zp=["Frühstück","Hauptgericht","Dessert","Snack","Getränk","Sonstiges"];function Jp(){const u=bi(),[a,s]=T.useState(""),[p,v]=T.useState(!1),[x,w]=T.useState(""),[L,E]=T.useState(!1),[C,N]=T.useState(""),[k,I]=T.useState(""),[D,X]=T.useState("Sonstiges"),[W,O]=T.useState([]),[K,le]=T.useState([]),[b,ue]=T.useState(""),[ke,Le]=T.useState(""),[Oe,$e]=T.useState(!1),it=async()=>{var se;if(a.trim()){v(!0),w("");try{const ge=await Qp(a);ge.tiktok&&(ue(ge.tiktok.thumbnail),Le(ge.tiktok.author)),ge.extracted?(N(ge.extracted.name||""),I(ge.extracted.description||""),X(ge.extracted.category||"Sonstiges"),O(ge.extracted.ingredients||[]),le(ge.extracted.steps||[])):N(((se=ge.tiktok)==null?void 0:se.title)||""),$e(!0)}catch{w("Fehler beim Abrufen der TikTok-Daten. Überprüfe die URL.")}v(!1)}},ut=async()=>{if(C.trim()){E(!0);try{const se=await Vp({name:C,description:k,category:D,ingredients:W,steps:K,tiktok_url:a,tiktok_author:ke,thumbnail_url:b});u(`/recipe/${se.id}`)}catch{w("Fehler beim Speichern.")}E(!1)}};return S.jsxs("div",{className:"max-w-2xl mx-auto",children:[S.jsx("h1",{className:"text-2xl font-bold mb-6",children:"Rezept hinzufügen"}),S.jsxs("div",{className:"bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6",children:[S.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"TikTok Video-URL"}),S.jsxs("div",{className:"flex gap-2",children:[S.jsx("input",{type:"url",value:a,onChange:se=>s(se.target.value),placeholder:"https://www.tiktok.com/@user/video/...",className:"flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-400",onKeyDown:se=>se.key==="Enter"&&it()}),S.jsx("button",{onClick:it,disabled:p,className:"bg-orange-500 hover:bg-orange-600 disabled:bg-orange-300 text-white px-6 py-2 rounded-lg font-medium transition-colors",children:p?"Laden...":"Extrahieren"})]}),x&&S.jsx("p",{className:"text-red-500 text-sm mt-2",children:x})]}),Oe&&S.jsxs("div",{className:"bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-6",children:[b&&S.jsx("img",{src:b,alt:"",className:"w-full h-48 object-cover rounded-lg"}),S.jsxs("div",{children:[S.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-1",children:"Name"}),S.jsx("input",{type:"text",value:C,onChange:se=>N(se.target.value),className:"w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-400"})]}),S.jsxs("div",{children:[S.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-1",children:"Beschreibung"}),S.jsx("textarea",{value:k,onChange:se=>I(se.target.value),rows:2,className:"w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-400 resize-none"})]}),S.jsxs("div",{children:[S.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-1",children:"Kategorie"}),S.jsx("select",{value:D,onChange:se=>X(se.target.value),className:"w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-400",children:Zp.map(se=>S.jsx("option",{value:se,children:se},se))})]}),S.jsx(Yi,{ingredients:W,editable:!0,onChange:O}),S.jsx(Xi,{steps:K,editable:!0,onChange:le}),S.jsxs("div",{className:"flex gap-3 pt-2",children:[S.jsx("button",{onClick:ut,disabled:L||!C.trim(),className:"flex-1 bg-orange-500 hover:bg-orange-600 disabled:bg-orange-300 text-white py-3 rounded-lg font-semibold transition-colors",children:L?"Speichern...":"Rezept speichern"}),S.jsx("button",{onClick:()=>u("/"),className:"px-6 py-3 border border-gray-300 rounded-lg font-medium hover:bg-gray-50 transition-colors",children:"Abbrechen"})]})]})]})}const qp=["Frühstück","Hauptgericht","Dessert","Snack","Getränk","Sonstiges"];function bp(){var W,O;const{id:u}=pp(),a=bi(),[s,p]=T.useState(null),[v,x]=T.useState(!0),[w,L]=T.useState(!1),[E,C]=T.useState({});T.useEffect(()=>{N()},[u]);const N=async()=>{try{const K=await Ap(u);p(K),C(K)}catch{a("/")}x(!1)},k=async()=>{await Wp(u,E),p(E),L(!1)},I=async()=>{confirm("Rezept wirklich löschen?")&&(await Hp(u),a("/"))},D=(K,le)=>C(b=>({...b,[K]:le}));if(v)return S.jsx("div",{className:"text-center py-12 text-gray-400",children:"Laden..."});if(!s)return null;const X=(O=(W=s.tiktok_url)==null?void 0:W.match(/video\/(\d+)/))==null?void 0:O[1];return S.jsxs("div",{className:"max-w-2xl mx-auto",children:[S.jsx(Vl,{to:"/",className:"text-orange-500 hover:text-orange-600 text-sm font-medium mb-4 inline-block",children:"← Zurück"}),S.jsxs("div",{className:"bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden",children:[X&&S.jsx("div",{className:"w-full",style:{paddingBottom:"177.78%",position:"relative"},children:S.jsx("iframe",{src:`https://www.tiktok.com/embed/v2/${X}`,className:"absolute inset-0 w-full h-full",allowFullScreen:!0})}),S.jsxs("div",{className:"p-6 space-y-6",children:[S.jsxs("div",{className:"flex items-start justify-between gap-3",children:[w?S.jsx("input",{type:"text",value:E.name,onChange:K=>D("name",K.target.value),className:"text-2xl font-bold flex-1 px-2 py-1 border border-gray-300 rounded"}):S.jsx("h1",{className:"text-2xl font-bold",children:s.name}),S.jsx(Lc,{category:w?E.category:s.category})]}),w&&S.jsxs("div",{children:[S.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-1",children:"Kategorie"}),S.jsx("select",{value:E.category,onChange:K=>D("category",K.target.value),className:"w-full px-3 py-2 border border-gray-300 rounded-lg text-sm",children:qp.map(K=>S.jsx("option",{value:K,children:K},K))})]}),!w&&s.description&&S.jsx("p",{className:"text-gray-500",children:s.description}),w&&S.jsxs("div",{children:[S.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-1",children:"Beschreibung"}),S.jsx("textarea",{value:E.description,onChange:K=>D("description",K.target.value),rows:2,className:"w-full px-3 py-2 border border-gray-300 rounded-lg text-sm resize-none"})]}),w?S.jsx(Yi,{ingredients:E.ingredients,editable:!0,onChange:K=>D("ingredients",K)}):S.jsx(Yi,{ingredients:s.ingredients}),w?S.jsx(Xi,{steps:E.steps,editable:!0,onChange:K=>D("steps",K)}):S.jsx(Xi,{steps:s.steps}),s.tiktok_author&&S.jsxs("p",{className:"text-xs text-gray-400",children:["Von: @",s.tiktok_author]}),S.jsx("div",{className:"flex gap-3 pt-2 border-t border-gray-100",children:w?S.jsxs(S.Fragment,{children:[S.jsx("button",{onClick:k,className:"flex-1 bg-orange-500 hover:bg-orange-600 text-white py-2 rounded-lg font-medium transition-colors",children:"Speichern"}),S.jsx("button",{onClick:()=>{L(!1),C(s)},className:"px-4 py-2 border border-gray-300 rounded-lg font-medium hover:bg-gray-50 transition-colors",children:"Abbrechen"})]}):S.jsxs(S.Fragment,{children:[S.jsx("button",{onClick:()=>L(!0),className:"flex-1 border border-orange-300 text-orange-600 hover:bg-orange-50 py-2 rounded-lg font-medium transition-colors",children:"Bearbeiten"}),S.jsx("button",{onClick:I,className:"px-4 py-2 border border-red-300 text-red-600 hover:bg-red-50 rounded-lg font-medium transition-colors",children:"Löschen"})]})})]})]})]})}function eh(){return S.jsxs("div",{className:"min-h-screen",children:[S.jsx("nav",{className:"bg-white shadow-sm border-b border-gray-200",children:S.jsxs("div",{className:"max-w-6xl mx-auto px-4 py-3 flex items-center justify-between",children:[S.jsx(Vl,{to:"/",className:"text-xl font-bold text-orange-600",children:"TikTok Rezepte"}),S.jsx(Vl,{to:"/add",className:"bg-orange-500 hover:bg-orange-600 text-white px-4 py-2 rounded-lg font-medium transition-colors",children:"+ Rezept hinzufügen"})]})}),S.jsx("main",{className:"max-w-6xl mx-auto px-4 py-6",children:S.jsxs(Rp,{children:[S.jsx($l,{path:"/",element:S.jsx(Xp,{})}),S.jsx($l,{path:"/add",element:S.jsx(Jp,{})}),S.jsx($l,{path:"/recipe/:id",element:S.jsx(bp,{})})]})})]})}Ud.createRoot(document.getElementById("root")).render(S.jsx(mc.StrictMode,{children:S.jsx(Fp,{children:S.jsx(eh,{})})})); diff --git a/frontend/dist/index.html b/frontend/dist/index.html new file mode 100644 index 0000000..8b8b5b4 --- /dev/null +++ b/frontend/dist/index.html @@ -0,0 +1,13 @@ + + + + + + TikTok Rezepte + + + + +
+ + diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..ce8ec8b --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + TikTok Rezepte + + +
+ + + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..10eb869 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,23 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location /api/ { + proxy_pass http://backend:3001; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + try_files $uri $uri/ /index.html; + } + + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml; +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..d76527d --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,104 @@ +import { Routes, Route, Link, Navigate } from 'react-router-dom'; +import { AuthProvider, useAuth } from './context/AuthContext'; +import Home from './pages/Home'; +import AddRecipe from './pages/AddRecipe'; +import RecipeDetail from './pages/RecipeDetail'; +import Login from './pages/Login'; +import Register from './pages/Register'; +import Admin from './pages/Admin'; +import Profile from './pages/Profile'; + +function ProtectedRoute({ children }) { + const { user, loading } = useAuth(); + if (loading) return
Laden...
; + if (!user) return ; + return children; +} + +function AdminRoute({ children }) { + const { user, loading } = useAuth(); + if (loading) return
Laden...
; + if (!user) return ; + if (!user.is_admin) return ; + return children; +} + +function AppContent() { + const { user, logout } = useAuth(); + + if (!user) { + return ( + + } /> + } /> + } /> + + ); + } + + return ( +
+ + +
+ + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + +
+
+ ); +} + +export default function App() { + return ( + + + + ); +} diff --git a/frontend/src/api.js b/frontend/src/api.js new file mode 100644 index 0000000..2805362 --- /dev/null +++ b/frontend/src/api.js @@ -0,0 +1,60 @@ +const API_BASE = '/api/recipes'; + +async function apiFetch(url, options = {}) { + const res = await fetch(url, options); + const text = await res.text(); + if (!res.ok) { + let msg = `Server-Fehler (${res.status})`; + try { msg = JSON.parse(text).error || msg; } catch {} + throw new Error(msg); + } + if (!text) throw new Error('Leere Antwort vom Server – Backend läuft nicht?'); + return JSON.parse(text); +} + +export async function fetchRecipes(search, category) { + const params = new URLSearchParams(); + if (search) params.set('search', search); + if (category && category !== 'Alle') params.set('category', category); + return apiFetch(`${API_BASE}?${params}`); +} + +export async function fetchRecipe(id) { + return apiFetch(`${API_BASE}/${id}`); +} + +export async function saveRecipe(recipe) { + return apiFetch(API_BASE, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(recipe), + }); +} + +export async function updateRecipe(id, recipe) { + return apiFetch(`${API_BASE}/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(recipe), + }); +} + +export async function deleteRecipe(id) { + return apiFetch(`${API_BASE}/${id}`, { method: 'DELETE' }); +} + +export async function extractFromTikTok(url) { + return apiFetch(`${API_BASE}/from-tiktok`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url }), + }); +} + +export async function fetchCategories() { + return apiFetch(`${API_BASE}/categories`); +} + +export async function checkAiStatus() { + return apiFetch(`${API_BASE}/ai-status`); +} diff --git a/frontend/src/auth.js b/frontend/src/auth.js new file mode 100644 index 0000000..97df902 --- /dev/null +++ b/frontend/src/auth.js @@ -0,0 +1,70 @@ +const AUTH_BASE = '/api/auth'; + +async function authFetch(url, options = {}) { + const res = await fetch(url, options); + const text = await res.text(); + if (!text) throw new Error('Leere Antwort vom Server – Backend läuft nicht?'); + const data = JSON.parse(text); + if (!res.ok) throw new Error(data.error || `Server-Fehler (${res.status})`); + return data; +} + +export async function loginUser(username, password) { + return authFetch(`${AUTH_BASE}/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }), + }); +} + +export async function registerUser(username, password, inviteToken) { + return authFetch(`${AUTH_BASE}/register`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password, inviteToken }), + }); +} + +export async function fetchCurrentUser(token) { + const res = await fetch(`${AUTH_BASE}/me`, { + headers: { Authorization: `Bearer ${token}` }, + }); + const text = await res.text(); + if (!res.ok || !text) throw new Error('Nicht authentifiziert'); + return JSON.parse(text); +} + +export async function createInvites(token, count = 1) { + return authFetch(`${AUTH_BASE}/invite`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, + body: JSON.stringify({ count }), + }); +} + +export async function fetchInvites(token) { + return authFetch(`${AUTH_BASE}/invites`, { + headers: { Authorization: `Bearer ${token}` }, + }); +} + +export async function fetchUsers(token) { + return authFetch(`${AUTH_BASE}/users`, { + headers: { Authorization: `Bearer ${token}` }, + }); +} + +export async function updateCredentials(token, { currentPassword, newUsername, newPassword }) { + return authFetch(`${AUTH_BASE}/credentials`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, + body: JSON.stringify({ currentPassword, newUsername, newPassword }), + }); +} + +export async function deleteUser(token, userId) { + return authFetch(`${AUTH_BASE}/users/${userId}`, { + method: 'DELETE', + headers: { Authorization: `Bearer ${token}` }, + }); +} diff --git a/frontend/src/components/CategoryBadge.jsx b/frontend/src/components/CategoryBadge.jsx new file mode 100644 index 0000000..4f58984 --- /dev/null +++ b/frontend/src/components/CategoryBadge.jsx @@ -0,0 +1,28 @@ +const CATEGORY_COLORS = { + 'Frühstück': 'bg-yellow-100 text-yellow-800', + 'Hauptgericht': 'bg-red-100 text-red-800', + 'Dessert': 'bg-pink-100 text-pink-800', + 'Snack': 'bg-green-100 text-green-800', + 'Getränk': 'bg-blue-100 text-blue-800', + 'Sonstiges': 'bg-gray-100 text-gray-800', +}; + +const CATEGORY_ICONS = { + 'Frühstück': '🍳', + 'Hauptgericht': '🍽️', + 'Dessert': '🍰', + 'Snack': '🍿', + 'Getränk': '🥤', + 'Sonstiges': '📋', +}; + +export default function CategoryBadge({ category }) { + const colorClass = CATEGORY_COLORS[category] || CATEGORY_COLORS['Sonstiges']; + const icon = CATEGORY_ICONS[category] || CATEGORY_ICONS['Sonstiges']; + + return ( + + {icon} {category} + + ); +} diff --git a/frontend/src/components/IngredientList.jsx b/frontend/src/components/IngredientList.jsx new file mode 100644 index 0000000..5e0b850 --- /dev/null +++ b/frontend/src/components/IngredientList.jsx @@ -0,0 +1,82 @@ +import { useState } from 'react'; + +export default function IngredientList({ ingredients, editable, onChange }) { + const [checked, setChecked] = useState({}); + + const toggle = (i) => setChecked(prev => ({ ...prev, [i]: !prev[i] })); + + const updateIngredient = (i, field, value) => { + const updated = ingredients.map((ing, idx) => + idx === i ? { ...ing, [field]: value } : ing + ); + onChange(updated); + }; + + const addIngredient = () => { + onChange([...ingredients, { name: '', amount: '', unit: '' }]); + }; + + const removeIngredient = (i) => { + onChange(ingredients.filter((_, idx) => idx !== i)); + }; + + return ( +
+

Zutaten

+
    + {ingredients.map((ing, i) => ( +
  • + {!editable && ( + toggle(i)} + className="w-4 h-4 accent-blue-600" + /> + )} + {editable ? ( + <> + updateIngredient(i, 'amount', e.target.value)} + placeholder="Menge" + className="w-16 px-2 py-1 border border-gray-300 rounded text-sm" + /> + updateIngredient(i, 'unit', e.target.value)} + placeholder="Einheit" + className="w-14 px-2 py-1 border border-gray-300 rounded text-sm" + /> + updateIngredient(i, 'name', e.target.value)} + placeholder="Zutat" + className="flex-1 px-2 py-1 border border-gray-300 rounded text-sm" + /> + + + ) : ( + + {ing.amount && `${ing.amount} `} + {ing.unit && `${ing.unit} `} + {ing.name} + + )} +
  • + ))} +
+ {editable && ( + + )} +
+ ); +} diff --git a/frontend/src/components/RecipeCard.jsx b/frontend/src/components/RecipeCard.jsx new file mode 100644 index 0000000..6c65184 --- /dev/null +++ b/frontend/src/components/RecipeCard.jsx @@ -0,0 +1,35 @@ +import { Link } from 'react-router-dom'; +import CategoryBadge from './CategoryBadge'; + +export default function RecipeCard({ recipe }) { + return ( + + {recipe.thumbnail_url && ( + {recipe.name} + )} +
+
+

{recipe.name}

+ +
+ {recipe.description && ( +

+ {recipe.description} +

+ )} +
+ {recipe.tiktok_author && @{recipe.tiktok_author}} + {recipe.ingredients?.length || 0} Zutaten + {recipe.steps?.length || 0} Schritte +
+
+ + ); +} diff --git a/frontend/src/components/StepList.jsx b/frontend/src/components/StepList.jsx new file mode 100644 index 0000000..5fa5142 --- /dev/null +++ b/frontend/src/components/StepList.jsx @@ -0,0 +1,76 @@ +import { useState } from 'react'; + +export default function StepList({ steps, editable, onChange }) { + const [completed, setCompleted] = useState({}); + + const toggle = (i) => setCompleted(prev => ({ ...prev, [i]: !prev[i] })); + + const updateStep = (i, value) => { + const updated = steps.map((s, idx) => (idx === i ? value : s)); + onChange(updated); + }; + + const addStep = () => onChange([...steps, '']); + + const removeStep = (i) => onChange(steps.filter((_, idx) => idx !== i)); + + const moveStep = (i, direction) => { + const arr = [...steps]; + const j = i + direction; + if (j < 0 || j >= arr.length) return; + [arr[i], arr[j]] = [arr[j], arr[i]]; + onChange(arr); + }; + + return ( +
+

Zubereitung

+
    + {steps.map((step, i) => ( +
  1. + {!editable && ( + + )} + {editable ? ( +
    + {i + 1}. +