diff --git a/APPWRITE_SETUP.md b/APPWRITE_SETUP.md new file mode 100644 index 0000000..a8c4fc1 --- /dev/null +++ b/APPWRITE_SETUP.md @@ -0,0 +1,97 @@ +# Appwrite Neu-Einrichtung - Schritt für Schritt + +## Schritt 1: Neues Projekt in Appwrite erstellen + +1. **Gehe zu Appwrite Dashboard:** + - Falls du cloud.appwrite.io nutzt: https://cloud.appwrite.io + - Falls du webklar.com nutzt: https://appwrite.webklar.com + +2. **Erstelle ein neues Projekt:** + - Klicke auf "Create Project" + - Name: `EmailSorter` (oder ein anderer Name) + - Kopiere die **Project ID** (wird angezeigt) + +## Schritt 2: API Key erstellen + +1. **Gehe zu Settings → API Credentials** +2. **Klicke auf "Create API Key"** +3. **Konfiguration:** + - Name: `EmailSorter Backend` + - Scopes: Wähle **alle Berechtigungen** (Full Access) + - Expiration: Optional (oder leer lassen für kein Ablaufdatum) +4. **Kopiere den API Key** (wird nur einmal angezeigt!) + +## Schritt 3: Datenbank erstellen + +1. **Gehe zu Databases** +2. **Klicke auf "Create Database"** +3. **Konfiguration:** + - Database ID: `email_sorter_db` (oder ein anderer Name) + - Name: `EmailSorter Database` +4. **Kopiere die Database ID** + +## Schritt 4: .env Dateien aktualisieren + +### server/.env aktualisieren: + +```env +APPWRITE_ENDPOINT=https://appwrite.webklar.com/v1 +# ODER falls cloud.appwrite.io: +# APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1 + +APPWRITE_PROJECT_ID=DEINE_NEW_PROJECT_ID_HIER +APPWRITE_API_KEY=DEIN_NEW_API_KEY_HIER +APPWRITE_DATABASE_ID=email_sorter_db +``` + +### client/.env aktualisieren: + +```env +VITE_APPWRITE_ENDPOINT=https://appwrite.webklar.com/v1 +# ODER falls cloud.appwrite.io: +# VITE_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1 + +VITE_APPWRITE_PROJECT_ID=DEINE_NEW_PROJECT_ID_HIER +``` + +## Schritt 5: Bootstrap ausführen + +Nachdem du die .env Dateien aktualisiert hast: + +```powershell +cd server +npm run bootstrap:v2 +``` + +Dies erstellt automatisch alle benötigten Collections: +- `products` +- `questions` +- `submissions` +- `answers` +- `orders` +- `email_accounts` +- `email_stats` +- `subscriptions` +- `user_preferences` +- `email_digests` + +## Schritt 6: Verifizierung + +Nach erfolgreichem Bootstrap solltest du sehen: +- ✓ Database created/exists +- ✓ Alle Collections wurden erstellt +- ✓ Alle Attribute wurden hinzugefügt + +## Troubleshooting + +**Fehler: "Project not found"** +- Prüfe, ob die PROJECT_ID korrekt ist +- Prüfe, ob du den richtigen Endpoint verwendest + +**Fehler: "Unauthorized"** +- Prüfe, ob der API_KEY korrekt ist +- Stelle sicher, dass der API Key alle Berechtigungen hat + +**Fehler: "Database not found"** +- Stelle sicher, dass die DATABASE_ID korrekt ist +- Das Bootstrap-Skript erstellt die Datenbank automatisch, wenn sie nicht existiert diff --git a/GOOGLE_OAUTH_SETUP.md b/GOOGLE_OAUTH_SETUP.md new file mode 100644 index 0000000..2d7100f --- /dev/null +++ b/GOOGLE_OAUTH_SETUP.md @@ -0,0 +1,75 @@ +# Google OAuth Setup - Test Users hinzufügen + +## Problem +Wenn du Google OAuth für Gmail verwendest, musst du während der Entwicklung **Test Users** hinzufügen, damit diese die App verwenden können. + +## Lösung: Test Users hinzufügen + +### Schritt 1: Google Cloud Console öffnen +1. Gehe zu [console.cloud.google.com](https://console.cloud.google.com) +2. Wähle dein Projekt aus + +### Schritt 2: OAuth Consent Screen öffnen +1. Gehe zu **APIs & Services** → **OAuth consent screen** +2. Scroll nach unten zu **Test users** + +### Schritt 3: Test Users hinzufügen +1. Klicke auf **+ ADD USERS** +2. Füge die E-Mail-Adressen hinzu, die die App verwenden sollen: + - Deine eigene E-Mail-Adresse + - E-Mail-Adressen von anderen Entwicklern/Test-Usern +3. Klicke auf **ADD** + +### Schritt 4: Wichtig! +- **Jede E-Mail-Adresse**, die Gmail verbinden möchte, **muss** als Test User hinzugefügt sein +- Ohne Test User bekommst du den Fehler: `access_denied` oder `invalid_grant` + +## App veröffentlichen (für Produktion) ✅ + +**Sobald die App veröffentlicht ist, müssen KEINE User mehr manuell hinzugefügt werden!** + +### Veröffentlichungsschritte: + +1. **Gehe zu OAuth consent screen** + - APIs & Services → OAuth consent screen + +2. **Klicke auf "PUBLISH APP"** + - Die App wechselt von "Testing" zu "In production" + +3. **Verifizierung (falls erforderlich)** + - Google kann zusätzliche Informationen anfordern + - Meist nur bei bestimmten Scopes oder vielen Nutzern nötig + +4. **Fertig!** + - ✅ Alle Google-Nutzer können die App jetzt verwenden + - ✅ Keine Test Users mehr nötig + - ✅ Keine manuelle E-Mail-Eingabe mehr erforderlich + +### Wichtig: + +- **Während der Entwicklung:** Test Users müssen manuell hinzugefügt werden +- **Nach Veröffentlichung:** Alle können die App verwenden, keine manuelle Eingabe nötig! + +**⚠️ Hinweis:** Wenn du die App wieder auf "Testing" zurücksetzt, müssen wieder Test Users hinzugefügt werden. + +## Aktuelle Konfiguration prüfen + +Deine Redirect URI sollte sein: +- Entwicklung: `http://localhost:3000/api/oauth/gmail/callback` +- Produktion: `https://deine-domain.de/api/oauth/gmail/callback` + +Diese muss in **Credentials** → **OAuth 2.0 Client IDs** → **Authorized redirect URIs** eingetragen sein. + +## Troubleshooting + +**Fehler: "access_denied"** +- → Test User nicht hinzugefügt +- → Lösung: E-Mail als Test User hinzufügen + +**Fehler: "invalid_grant"** +- → Redirect URI stimmt nicht überein +- → Lösung: Redirect URI in Google Cloud Console prüfen + +**Fehler: "redirect_uri_mismatch"** +- → Redirect URI in .env stimmt nicht mit Google Cloud Console überein +- → Lösung: Beide prüfen und angleichen diff --git a/PROJECT_RENAME_GUIDE.md b/PROJECT_RENAME_GUIDE.md new file mode 100644 index 0000000..ab1c50d --- /dev/null +++ b/PROJECT_RENAME_GUIDE.md @@ -0,0 +1,115 @@ +# Projekt Umbenennung: ANDJJJJJJ → EmailSorter + +## ✅ Automatisch erledigt + +Die folgenden Dateien wurden bereits aktualisiert: + +1. ✅ **Git Remote URL** - Aktualisiert in `.git/config` + - Alt: `https://git.webklar.com/knso/ANDJJJJJJ` + - Neu: `https://git.webklar.com/knso/EmailSorter` + +2. ✅ **Client package.json** - Name aktualisiert + - Alt: `"name": "client"` + - Neu: `"name": "emailsorter-client"` + +3. ✅ **README.md** - Bereits korrekt (verwendet "EmailSorter") + +## 📁 Manuelle Schritte (mit GitHub Desktop) + +### Schritt 1: Repository auf Server umbenennen (falls noch nicht geschehen) + +1. Gehe zu `https://git.webklar.com/knso/ANDJJJJJJ` +2. Benenne das Repository in "EmailSorter" um +3. Oder erstelle ein neues Repository "EmailSorter" und pushe den Code dorthin + +### Schritt 2: Lokalen Ordner umbenennen + +**Option A: Mit Windows Explorer** +1. Schließe alle Terminals/Editoren, die auf den Ordner zugreifen +2. Gehe zu `C:\Users\User\Documents\GitHub\` +3. Rechtsklick auf `ANDJJJJJJ` → Umbenennen +4. Benenne um zu `EmailSorter` + +**Option B: Mit PowerShell** +```powershell +# Schließe alle Prozesse, die auf den Ordner zugreifen +# Dann: +cd C:\Users\User\Documents\GitHub +Rename-Item -Path "ANDJJJJJJ" -NewName "EmailSorter" +``` + +### Schritt 3: GitHub Desktop aktualisieren + +1. Öffne GitHub Desktop +2. Klicke auf **File** → **Add Local Repository** +3. Wähle den umbenannten Ordner `C:\Users\User\Documents\GitHub\EmailSorter` +4. Oder: Wenn das Repository bereits in GitHub Desktop ist: + - Rechtsklick auf das Repository → **Repository Settings** + - Aktualisiere den **Local Path** auf den neuen Pfad + +### Schritt 4: Git Remote URL verifizieren + +In GitHub Desktop: +1. Öffne **Repository** → **Repository Settings** → **Remote** +2. Stelle sicher, dass die URL `https://git.webklar.com/knso/EmailSorter` ist +3. Falls nicht, aktualisiere sie manuell + +Oder im Terminal: +```bash +cd C:\Users\User\Documents\GitHub\EmailSorter +git remote -v +``` + +Sollte zeigen: +``` +origin https://git.webklar.com/knso/EmailSorter (fetch) +origin https://git.webklar.com/knso/EmailSorter (push) +``` + +### Schritt 5: Testen + +1. Öffne ein neues Terminal im umbenannten Ordner +2. Teste Git: + ```bash + git status + git remote -v + ``` +3. Teste die App: + ```bash + cd client + npm run dev + ``` + +## ⚠️ Wichtig + +- **Schließe alle Terminals/Editoren** bevor du den Ordner umbenennst +- **Backup erstellen** (optional, aber empfohlen) +- **Git History bleibt erhalten** - keine Sorge, die Commits gehen nicht verloren + +## ✅ Checkliste + +- [ ] Repository auf Server umbenannt (oder neues Repository erstellt) +- [ ] Lokaler Ordner umbenannt +- [ ] GitHub Desktop aktualisiert +- [ ] Git Remote URL verifiziert +- [ ] App getestet (client und server starten) + +## 🆘 Falls etwas schief geht + +1. **Git Remote URL zurücksetzen:** + ```bash + git remote set-url origin https://git.webklar.com/knso/EmailSorter + ``` + +2. **GitHub Desktop neu einrichten:** + - Entferne das alte Repository + - Füge den umbenannten Ordner neu hinzu + +3. **Falls der Ordner nicht umbenannt werden kann:** + - Stelle sicher, dass alle Prozesse geschlossen sind + - Prüfe, ob Dateien geöffnet sind + - Versuche es als Administrator + +--- + +**Fertig!** Dein Projekt heißt jetzt "EmailSorter" 🎉 diff --git a/PROJECT_REVIEW_SUMMARY.md b/PROJECT_REVIEW_SUMMARY.md new file mode 100644 index 0000000..42d2e0c --- /dev/null +++ b/PROJECT_REVIEW_SUMMARY.md @@ -0,0 +1,151 @@ +# Projekt-Überprüfung: EmailSorter + +**Datum:** 2026-01-20 +**Status:** ⚠️ Mehrere Probleme gefunden + +--- + +## ✅ Was gut ist + +1. **Git Konfiguration** - Remote URL korrekt auf EmailSorter aktualisiert +2. **Package.json Dateien** - Namen korrekt (emailsorter-client, email-sorter-server) +3. **README.md** - Verwendet bereits "EmailSorter" +4. **Environment Files** - Keine hardcoded Secrets in .env.example +5. **Haupt-Bootstrap** - bootstrap-v2.mjs ist aktuell und korrekt + +--- + +## 🔴 KRITISCHE Probleme + +### 1. Hardcoded API Keys (Sicherheitsrisiko!) + +**Gefunden in:** +- `setup-appwrite.ps1` (Zeilen 5-6) +- `server/cleanup.mjs` (Zeilen 5-6) + +**Problem:** +- API Keys sind direkt im Code hardcoded +- Werden ins Git Repository committed +- Können von jedem eingesehen werden + +**Lösung:** +- API Keys aus Code entfernen +- Stattdessen aus `.env` Datei oder Umgebungsvariablen lesen +- Falls bereits committed: API Keys im Appwrite Dashboard rotieren! + +--- + +## ⚠️ WICHTIGE Probleme + +### 2. Veraltete setup-appwrite.ps1 + +**Problem:** +- Verwendet `bootstrap-appwrite.mjs` (alt) statt `bootstrap-v2.mjs` (neu) +- Verwendet veraltete Umgebungsvariablen (DB_ID, TABLE_*) +- Nachricht spricht von "13 questions seeded" (nicht mehr relevant) +- Enthält hardcoded API Keys + +**Lösung:** +- Datei aktualisieren oder als veraltet markieren +- Sollte `bootstrap-v2.mjs` verwenden +- API Keys aus `.env` lesen + +### 3. Veraltete Referenzen zu bootstrap-appwrite.mjs + +**Gefunden in:** +- `server/package.json` - Script "bootstrap" verweist noch auf alte Datei +- `server/verify-setup.mjs` - Prüft alte Datei +- `server/MANUAL_TEST_CHECKLIST.md` - Erwähnt alte Datei +- `server/TASK_4_COMPLETION_SUMMARY.md` - Erwähnt alte Datei +- `TASK_5_COMPLETION.md` - Erwähnt alte Datei + +**Lösung:** +- Dokumentation aktualisieren +- package.json Script kann bleiben (für Rückwärtskompatibilität), aber sollte auf v2 verweisen + +### 4. server/cleanup.mjs - Hardcoded Credentials + +**Problem:** +- Enthält hardcoded Appwrite Project ID und API Key +- Sollte aus Umgebungsvariablen lesen + +**Lösung:** +- Umstellen auf `dotenv` und Umgebungsvariablen + +--- + +## 📝 KLEINERE Probleme / Verbesserungen + +### 5. starter-for-react Ordner + +**Frage:** +- Ist dieser Ordner noch benötigt? +- Scheint ein altes Template zu sein +- Kann möglicherweise entfernt werden + +### 6. Konsistenz in Dokumentation + +**Problem:** +- Einige MD-Dateien erwähnen noch `bootstrap-appwrite.mjs` +- Sollten auf `bootstrap-v2.mjs` verweisen + +--- + +## ✅ Empfohlene Aktionen + +### Sofort (Sicherheit): + +1. **API Keys rotieren** in Appwrite Dashboard + - Alte Keys sind bereits im Git Repository sichtbar + - Neue Keys erstellen und alte deaktivieren + +2. **Hardcoded Keys entfernen** aus: + - `setup-appwrite.ps1` + - `server/cleanup.mjs` + +3. **.gitignore prüfen** - Sicherstellen, dass `.env` Dateien nicht committed werden + +### Kurzfristig: + +4. **setup-appwrite.ps1 aktualisieren** oder entfernen +5. **cleanup.mjs** auf Umgebungsvariablen umstellen +6. **Dokumentation aktualisieren** (bootstrap-appwrite → bootstrap-v2) + +### Optional: + +7. **starter-for-react** Ordner prüfen (entfernen falls nicht benötigt) +8. **Veraltete Dokumentation** aufräumen + +--- + +## 📋 Checkliste + +- [ ] API Keys in Appwrite rotiert +- [ ] Hardcoded Keys aus setup-appwrite.ps1 entfernt +- [ ] Hardcoded Keys aus cleanup.mjs entfernt +- [ ] setup-appwrite.ps1 aktualisiert oder entfernt +- [ ] cleanup.mjs auf .env umgestellt +- [ ] Dokumentation aktualisiert +- [ ] .gitignore geprüft +- [ ] starter-for-react geprüft/entfernt + +--- + +## 🔍 Weitere Prüfungen + +### Konfigurationsdateien: +- ✅ `client/package.json` - Korrekt +- ✅ `server/package.json` - Korrekt +- ✅ `.git/config` - Korrekt (EmailSorter) +- ✅ `README.md` - Korrekt +- ✅ `server/env.example` - Keine Secrets +- ✅ `client/env.example` - Keine Secrets + +### Projektstruktur: +- ✅ Alle wichtigen Ordner vorhanden +- ✅ Bootstrap-Skripte vorhanden +- ✅ Dokumentation vorhanden + +--- + +**Nächste Schritte:** Siehe "Empfohlene Aktionen" oben. diff --git a/README.md b/README.md index f6aad4d..703eeff 100644 --- a/README.md +++ b/README.md @@ -1,229 +1,241 @@ -# Email Sortierer Setup +# EmailSorter -Ein Multi-Step-Formular zur Konfiguration von Email-Präferenzen mit Appwrite-Datenspeicherung und Stripe-Bezahlung. +KI-gestützte E-Mail-Sortierung für mehr Produktivität und weniger Stress. + +## Überblick + +EmailSorter ist eine SaaS-Anwendung, die automatisch E-Mails kategorisiert und sortiert. Die Anwendung nutzt: + +- **React + Vite** Frontend mit Tailwind CSS +- **Node.js + Express** Backend +- **Appwrite** für Datenbank und Authentifizierung +- **Stripe** für Zahlungen und Subscriptions +- **Mistral AI** für KI-basierte E-Mail-Kategorisierung +- **Gmail/Outlook API** für E-Mail-Integration +- **n8n** (optional) für Automatisierungsworkflows + +## Projektstruktur + +``` +/ +├── client/ # React Frontend +│ ├── src/ +│ │ ├── components/ # UI Komponenten +│ │ ├── pages/ # Seiten +│ │ ├── context/ # React Context +│ │ └── lib/ # Utilities +│ └── package.json +├── server/ # Node.js Backend +│ ├── routes/ # API Routen +│ ├── services/ # Business Logic +│ └── package.json +├── n8n/ # n8n Workflows +│ └── workflows/ +└── public/ # Legacy Frontend +``` ## Quick Start +### 1. Repository klonen + ```bash -# 1. Dependencies installieren -cd server -npm install - -# 2. Setup überprüfen -npm run verify - -# 3. Umgebungsvariablen konfigurieren -cp ../.env.example .env -# Bearbeiten Sie .env und fügen Sie Ihre Credentials ein - -# 4. Datenbank initialisieren -npm run bootstrap -# Kopieren Sie die Database-ID und fügen Sie sie in .env ein - -# 5. Tests ausführen -npm test - -# 6. Server starten -npm start - -# 7. Browser öffnen -# http://localhost:3000 +git clone +cd emailsorter ``` -## Voraussetzungen - -- Node.js (v18 oder höher) -- Appwrite Account (https://cloud.appwrite.io) -- Stripe Account (https://stripe.com) - -## Installation - -1. **Repository klonen und Dependencies installieren:** +### 2. Dependencies installieren ```bash -cd server +# Frontend +cd client +npm install + +# Backend +cd ../server npm install ``` -2. **Umgebungsvariablen konfigurieren:** - -Kopieren Sie `.env.example` zu `.env` und füllen Sie alle Werte aus: +### 3. Umgebungsvariablen konfigurieren ```bash -cp .env.example .env +# Frontend +cd client +cp env.example .env +# Bearbeite .env mit deinen Appwrite Credentials + +# Backend +cd ../server +cp env.example .env +# Bearbeite .env mit allen erforderlichen Credentials ``` -Erforderliche Werte: -- `APPWRITE_ENDPOINT`: Ihre Appwrite API Endpoint (z.B. https://cloud.appwrite.io/v1) -- `APPWRITE_PROJECT_ID`: Ihre Appwrite Projekt-ID -- `APPWRITE_API_KEY`: Ihr Appwrite API Key (mit allen Berechtigungen) -- `APPWRITE_DATABASE_ID`: Wird nach Bootstrap-Script automatisch gesetzt -- `STRIPE_SECRET_KEY`: Ihr Stripe Secret Key (sk_test_...) -- `STRIPE_WEBHOOK_SECRET`: Ihr Stripe Webhook Secret (whsec_...) - -3. **Appwrite Datenbank initialisieren:** +### 4. Appwrite Datenbank einrichten ```bash -npm run bootstrap +cd server +npm run bootstrap:v2 ``` -Dieses Script erstellt: -- Eine neue Datenbank "EmailSorter" -- 5 Collections: products, questions, submissions, answers, orders -- Ein Produkt "Email Sorter Setup" -- 13 Fragen für den Fragebogen - -**Wichtig:** Nach dem Bootstrap-Script wird die Database-ID in der Konsole ausgegeben. Kopieren Sie diese ID und fügen Sie sie in Ihre `.env` Datei als `APPWRITE_DATABASE_ID` ein. - -4. **Stripe Webhook konfigurieren:** - -Für lokale Entwicklung mit Stripe CLI: +### 5. Development Server starten ```bash -stripe listen --forward-to localhost:3000/stripe/webhook +# Terminal 1: Backend +cd server +npm run dev + +# Terminal 2: Frontend +cd client +npm run dev ``` -Kopieren Sie das angezeigte Webhook-Secret und fügen Sie es als `STRIPE_WEBHOOK_SECRET` in Ihre `.env` Datei ein. +Die App ist nun erreichbar unter: +- Frontend: http://localhost:5173 +- Backend: http://localhost:3000 -Für Produktion: Erstellen Sie einen Webhook in Ihrem Stripe Dashboard mit der URL `https://ihre-domain.com/stripe/webhook` und dem Event `checkout.session.completed`. +## Konfiguration -## Server starten +### Appwrite Setup + +1. Erstelle ein Projekt auf [cloud.appwrite.io](https://cloud.appwrite.io) +2. Erstelle einen API Key mit allen Berechtigungen +3. Führe `npm run bootstrap:v2` aus, um die Datenbank zu erstellen + +### Stripe Setup + +1. Erstelle einen Account auf [stripe.com](https://stripe.com) +2. Erstelle Produkte und Preise für Basic, Pro, Business Pläne +3. Konfiguriere den Webhook für `/api/subscription/webhook` + +### Google OAuth (Gmail) + +1. Erstelle ein Projekt in der [Google Cloud Console](https://console.cloud.google.com) +2. Aktiviere die Gmail API +3. Erstelle OAuth 2.0 Credentials +4. Füge `http://localhost:3000/api/oauth/gmail/callback` als Redirect URI hinzu + +### Microsoft OAuth (Outlook) + +1. Registriere eine App in [Azure AD](https://portal.azure.com) +2. Füge Microsoft Graph Berechtigungen hinzu (Mail.Read, Mail.ReadWrite) +3. Füge `http://localhost:3000/api/oauth/outlook/callback` als Redirect URI hinzu + +### Mistral AI API + +1. Erstelle einen API Key auf [console.mistral.ai](https://console.mistral.ai) +2. Füge den Key als `MISTRAL_API_KEY` hinzu + +## Features + +### Landing Page +- Hero Section mit CTA +- Feature-Übersicht +- Pricing-Tabelle mit 3 Plänen +- Testimonials +- FAQ Sektion + +### Authentifizierung +- E-Mail/Passwort Registration +- Login mit Session-Management +- Passwort-Reset (konfigurierbar) + +### Dashboard +- E-Mail-Statistiken (sortiert heute/Woche/Monat) +- Kategorien-Verteilung +- Verbundene E-Mail-Konten +- Schnellzugriff-Aktionen + +### E-Mail-Sortierung +- Automatische Kategorisierung mit KI +- Unterstützte Kategorien: + - VIP / Wichtig + - Kunden / Projekte + - Rechnungen / Belege + - Newsletter + - Werbung / Promotions + - Social Media + - Security / 2FA + - Versand / Bestellungen + +### Subscription +- 14 Tage kostenlose Testphase +- 3 Pläne: Basic (9€), Pro (19€), Business (49€) +- Stripe Customer Portal +- Automatische Verlängerung + +## API Dokumentation + +### Authentifizierung +Die API nutzt Appwrite Sessions für Authentifizierung. + +### Endpoints + +#### E-Mail +- `GET /api/email/accounts` - Verbundene E-Mail-Konten abrufen +- `POST /api/email/connect` - Neues E-Mail-Konto verbinden +- `DELETE /api/email/accounts/:id` - E-Mail-Konto trennen +- `GET /api/email/stats` - Sortierstatistiken abrufen +- `POST /api/email/sort` - Manuelle Sortierung auslösen + +#### OAuth +- `GET /api/oauth/gmail` - Gmail OAuth starten +- `GET /api/oauth/gmail/callback` - Gmail OAuth Callback +- `GET /api/oauth/outlook` - Outlook OAuth starten +- `GET /api/oauth/outlook/callback` - Outlook OAuth Callback + +#### Subscription +- `POST /api/subscription/checkout` - Checkout Session erstellen +- `GET /api/subscription/status` - Subscription Status abrufen +- `POST /api/subscription/portal` - Customer Portal Session + +## n8n Integration (Optional) + +Für visuelle Automatisierung kann n8n verwendet werden: + +1. Importiere den Workflow aus `n8n/workflows/email-sorter-workflow.json` +2. Konfiguriere Gmail OAuth und OpenAI Credentials +3. Aktiviere den Webhook-Trigger + +Siehe `n8n/README.md` für Details. + +## Deployment + +### Frontend (Vercel/Netlify) ```bash -npm start +cd client +npm run build +# Deploye dist/ Ordner ``` -Der Server läuft auf http://localhost:3000 +### Backend (Railway/Render/Heroku) -## Verwendung +1. Setze alle Umgebungsvariablen +2. Deploy mit `npm start` als Start-Befehl -1. Öffnen Sie http://localhost:3000 in Ihrem Browser -2. Füllen Sie den mehrstufigen Fragebogen aus -3. Überprüfen Sie die Zusammenfassung -4. Klicken Sie auf "Jetzt kaufen" um zur Stripe-Bezahlung weitergeleitet zu werden -5. Verwenden Sie Stripe Test-Kreditkarte: `4242 4242 4242 4242` +### Stripe Webhook -## API Endpunkte - -### GET /api/questions -Lädt alle aktiven Fragen für ein Produkt. - -**Query Parameter:** -- `productSlug`: Produkt-Slug (z.B. "email-sorter") - -**Response:** -```json -[ - { - "$id": "...", - "key": "email", - "label": "Ihre E-Mail-Adresse", - "type": "email", - "required": true, - "step": 1, - "order": 1 - } -] +Aktualisiere die Webhook-URL im Stripe Dashboard auf deine Produktions-URL: ``` - -### POST /api/submissions -Erstellt eine neue Submission mit Kundenantworten. - -**Request Body:** -```json -{ - "productSlug": "email-sorter", - "answers": { - "email": "kunde@example.com", - "name": "Max Mustermann" - } -} +https://your-domain.com/api/subscription/webhook ``` -**Response:** -```json -{ - "submissionId": "..." -} -``` - -### POST /api/checkout -Erstellt eine Stripe Checkout Session. - -**Request Body:** -```json -{ - "submissionId": "..." -} -``` - -**Response:** -```json -{ - "url": "https://checkout.stripe.com/..." -} -``` - -### POST /stripe/webhook -Empfängt Stripe Webhook Events (nur für Stripe). - -## Datenmodell - -### Products Collection -- `slug`: Eindeutiger Produkt-Identifier -- `title`: Produktname -- `priceCents`: Preis in Cent -- `currency`: Währung (z.B. "eur") -- `isActive`: Produkt aktiv/inaktiv - -### Questions Collection -- `productId`: Referenz zum Produkt -- `key`: Eindeutiger Schlüssel für die Antwort -- `label`: Anzeigetext -- `type`: Feldtyp (text, email, select, multiselect, textarea) -- `required`: Pflichtfeld ja/nein -- `step`: Schritt-Nummer im Formular -- `order`: Reihenfolge innerhalb des Schritts -- `optionsJson`: JSON-Array mit Auswahloptionen (für select/multiselect) -- `isActive`: Frage aktiv/inaktiv - -### Submissions Collection -- `productId`: Referenz zum Produkt -- `status`: Status (draft, paid) -- `customerEmail`: Kunden-Email -- `customerName`: Kundenname -- `finalSummaryJson`: JSON mit allen Antworten -- `priceCents`: Preis in Cent -- `currency`: Währung - -### Answers Collection -- `submissionId`: Referenz zur Submission -- `answersJson`: JSON mit allen Antworten - -### Orders Collection -- `submissionId`: Referenz zur Submission -- `orderDataJson`: JSON mit Stripe Session Daten - ## Troubleshooting -### Server startet nicht -- Überprüfen Sie, dass alle Umgebungsvariablen in `.env` gesetzt sind -- Stellen Sie sicher, dass Port 3000 nicht bereits verwendet wird +### Frontend startet nicht +- Prüfe, ob alle npm packages installiert sind +- Prüfe `.env` Datei im client Ordner -### Fragen werden nicht geladen -- Überprüfen Sie die Appwrite-Verbindung und API-Key -- Stellen Sie sicher, dass das Bootstrap-Script erfolgreich durchgelaufen ist -- Überprüfen Sie die Browser-Konsole auf Fehler +### Backend-Fehler +- Prüfe alle Umgebungsvariablen in `.env` +- Prüfe Appwrite Verbindung und API Key -### Stripe Checkout funktioniert nicht -- Überprüfen Sie, dass `STRIPE_SECRET_KEY` korrekt gesetzt ist -- Für lokale Tests: Stellen Sie sicher, dass Stripe CLI läuft -- Überprüfen Sie die Server-Logs auf Fehler +### OAuth funktioniert nicht +- Prüfe Redirect URIs in Google/Microsoft Console +- Prüfe Client ID und Secret -### Webhook wird nicht empfangen -- Für lokale Tests: Stellen Sie sicher, dass `stripe listen` läuft -- Überprüfen Sie, dass `STRIPE_WEBHOOK_SECRET` korrekt gesetzt ist -- Überprüfen Sie die Stripe Dashboard Webhook-Logs +### KI-Kategorisierung fehlerhaft +- Prüfe Mistral API Key +- Prüfe Rate Limits auf console.mistral.ai ## Lizenz diff --git a/SETUP_GUIDE.md b/SETUP_GUIDE.md new file mode 100644 index 0000000..a34bbdd --- /dev/null +++ b/SETUP_GUIDE.md @@ -0,0 +1,373 @@ +# EmailSorter - Einrichtungsanleitung + +Diese Anleitung führt dich durch die komplette Einrichtung von EmailSorter. + +--- + +## Inhaltsverzeichnis + +1. [Voraussetzungen](#voraussetzungen) +2. [Appwrite einrichten](#1-appwrite-einrichten) +3. [Stripe einrichten](#2-stripe-einrichten) +4. [Google OAuth einrichten](#3-google-oauth-einrichten-gmail) +5. [Microsoft OAuth einrichten](#4-microsoft-oauth-einrichten-outlook) +6. [Mistral AI einrichten](#5-mistral-ai-einrichten) +7. [Projekt starten](#6-projekt-starten) +8. [Fehlerbehebung](#fehlerbehebung) + +--- + +## Voraussetzungen + +- Node.js 18+ installiert +- npm oder yarn +- Git + +--- + +## 1. Appwrite einrichten + +### 1.1 Account erstellen + +1. Gehe zu [cloud.appwrite.io](https://cloud.appwrite.io) +2. Erstelle einen kostenlosen Account +3. Erstelle ein neues Projekt (z.B. "EmailSorter") + +### 1.2 API Key erstellen + +1. Gehe zu **Settings** → **API Credentials** +2. Klicke auf **Create API Key** +3. Name: `EmailSorter Backend` +4. Wähle **alle Berechtigungen** aus (Full Access) +5. Kopiere den API Key + +### 1.3 Datenbank erstellen + +1. Gehe zu **Databases** +2. Klicke auf **Create Database** +3. Name: `email_sorter_db` +4. Kopiere die **Database ID** + +### 1.4 Bootstrap ausführen + +```bash +cd server +npm run bootstrap:v2 +``` + +Dies erstellt automatisch alle benötigten Collections: +- `products` +- `questions` +- `submissions` +- `answers` +- `orders` +- `email_accounts` +- `email_stats` +- `subscriptions` +- `user_preferences` + +### 1.5 .env konfigurieren + +```env +# server/.env +APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1 +APPWRITE_PROJECT_ID=deine_projekt_id +APPWRITE_API_KEY=dein_api_key +APPWRITE_DATABASE_ID=email_sorter_db +``` + +```env +# client/.env +VITE_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1 +VITE_APPWRITE_PROJECT_ID=deine_projekt_id +``` + +--- + +## 2. Stripe einrichten + +### 2.1 Account erstellen + +1. Gehe zu [dashboard.stripe.com](https://dashboard.stripe.com) +2. Erstelle einen Account +3. Wechsle in den **Test Mode** (Toggle oben rechts) + +### 2.2 API Keys kopieren + +1. Gehe zu **Developers** → **API keys** +2. Kopiere den **Secret key** (beginnt mit `sk_test_`) + +### 2.3 Produkte erstellen + +Gehe zu **Products** → **Add product**: + +#### Basic Plan +- Name: `EmailSorter Basic` +- Preis: `9.00 EUR` / Monat +- Kopiere die **Price ID** (beginnt mit `price_`) + +#### Pro Plan +- Name: `EmailSorter Pro` +- Preis: `19.00 EUR` / Monat +- Kopiere die **Price ID** + +#### Business Plan +- Name: `EmailSorter Business` +- Preis: `49.00 EUR` / Monat +- Kopiere die **Price ID** + +### 2.4 Webhook einrichten + +1. Gehe zu **Developers** → **Webhooks** +2. Klicke auf **Add endpoint** +3. URL: `https://deine-domain.de/api/subscription/webhook` + - Für lokale Tests: Nutze [Stripe CLI](https://stripe.com/docs/stripe-cli) +4. Events auswählen: + - `checkout.session.completed` + - `customer.subscription.updated` + - `customer.subscription.deleted` + - `invoice.payment_failed` + - `invoice.payment_succeeded` +5. Kopiere das **Signing Secret** (beginnt mit `whsec_`) + +### 2.5 Lokaler Webhook-Test mit Stripe CLI + +```bash +# Installieren +# Windows: scoop install stripe +# Mac: brew install stripe/stripe-cli/stripe + +# Login +stripe login + +# Webhook forwarden +stripe listen --forward-to localhost:3000/api/subscription/webhook +``` + +### 2.6 .env konfigurieren + +```env +# server/.env +STRIPE_SECRET_KEY=sk_test_... +STRIPE_WEBHOOK_SECRET=whsec_... +STRIPE_PRICE_BASIC=price_... +STRIPE_PRICE_PRO=price_... +STRIPE_PRICE_BUSINESS=price_... +``` + +--- + +## 3. Google OAuth einrichten (Gmail) + +### 3.1 Google Cloud Projekt erstellen + +1. Gehe zu [console.cloud.google.com](https://console.cloud.google.com) +2. Erstelle ein neues Projekt (oder wähle ein bestehendes) + +### 3.2 Gmail API aktivieren + +1. Gehe zu **APIs & Services** → **Library** +2. Suche nach "Gmail API" +3. Klicke auf **Enable** + +### 3.3 OAuth Consent Screen + +1. Gehe zu **APIs & Services** → **OAuth consent screen** +2. Wähle **External** +3. Fülle aus: + - App name: `EmailSorter` + - User support email: Deine E-Mail + - Developer contact: Deine E-Mail +4. **Scopes** hinzufügen: + - `https://www.googleapis.com/auth/gmail.modify` + - `https://www.googleapis.com/auth/gmail.labels` + - `https://www.googleapis.com/auth/userinfo.email` +5. **Test users** hinzufügen (während der Entwicklung) + +### 3.4 OAuth Credentials erstellen + +1. Gehe zu **APIs & Services** → **Credentials** +2. Klicke auf **Create Credentials** → **OAuth client ID** +3. Typ: **Web application** +4. Name: `EmailSorter Web` +5. **Authorized redirect URIs**: + - `http://localhost:3000/api/oauth/gmail/callback` (Entwicklung) + - `https://deine-domain.de/api/oauth/gmail/callback` (Produktion) +6. Kopiere **Client ID** und **Client Secret** + +### 3.5 .env konfigurieren + +```env +# server/.env +GOOGLE_CLIENT_ID=xxx.apps.googleusercontent.com +GOOGLE_CLIENT_SECRET=GOCSPX-xxx +GOOGLE_REDIRECT_URI=http://localhost:3000/api/oauth/gmail/callback +``` + +--- + +## 4. Microsoft OAuth einrichten (Outlook) + +### 4.1 Azure App Registration + +1. Gehe zu [portal.azure.com](https://portal.azure.com) +2. Suche nach "App registrations" +3. Klicke auf **New registration** +4. Name: `EmailSorter` +5. Supported account types: **Accounts in any organizational directory and personal Microsoft accounts** +6. Redirect URI: `Web` → `http://localhost:3000/api/oauth/outlook/callback` + +### 4.2 Client Secret erstellen + +1. Gehe zu **Certificates & secrets** +2. Klicke auf **New client secret** +3. Description: `EmailSorter Backend` +4. Expires: `24 months` +5. Kopiere den **Value** (wird nur einmal angezeigt!) + +### 4.3 API Permissions + +1. Gehe zu **API permissions** +2. Klicke auf **Add a permission** → **Microsoft Graph** → **Delegated permissions** +3. Füge hinzu: + - `Mail.ReadWrite` + - `User.Read` + - `offline_access` +4. Klicke auf **Grant admin consent** (falls möglich) + +### 4.4 .env konfigurieren + +```env +# server/.env +MICROSOFT_CLIENT_ID=xxx-xxx-xxx +MICROSOFT_CLIENT_SECRET=xxx +MICROSOFT_REDIRECT_URI=http://localhost:3000/api/oauth/outlook/callback +``` + +--- + +## 5. Mistral AI einrichten + +### 5.1 Account erstellen + +1. Gehe zu [console.mistral.ai](https://console.mistral.ai) +2. Erstelle einen Account +3. Gehe zu **API Keys** +4. Klicke auf **Create new key** +5. Kopiere den API Key + +### 5.2 .env konfigurieren + +```env +# server/.env +MISTRAL_API_KEY=dein_mistral_api_key +``` + +--- + +## 6. Projekt starten + +### 6.1 Dependencies installieren + +```bash +# Backend +cd server +npm install + +# Frontend +cd ../client +npm install +``` + +### 6.2 Environment Files erstellen + +```bash +# Server +cp server/env.example server/.env +# Fülle die Werte aus! + +# Client +cp client/env.example client/.env +# Fülle die Werte aus! +``` + +### 6.3 Datenbank initialisieren + +```bash +cd server +npm run bootstrap:v2 +``` + +### 6.4 Server starten + +```bash +# Terminal 1 - Backend +cd server +npm run dev + +# Terminal 2 - Frontend +cd client +npm run dev +``` + +### 6.5 Öffnen + +- **Frontend**: http://localhost:5173 +- **Backend API**: http://localhost:3000 +- **API Docs**: http://localhost:3000 + +--- + +## Fehlerbehebung + +### "APPWRITE_PROJECT_ID is not defined" + +Stelle sicher, dass die `.env` Datei existiert und die Variablen gesetzt sind. + +### "Invalid OAuth redirect URI" + +Die Redirect URI in der OAuth-Konfiguration muss **exakt** mit der in der `.env` übereinstimmen. + +### "Rate limit exceeded" + +- Gmail: Max 10.000 Requests/Tag +- Outlook: Max 10.000 Requests/App/Tag + +### "Mistral AI: Model not found" + +Prüfe, ob der API Key gültig ist und das Guthaben ausreicht. + +### "Stripe webhook signature invalid" + +- Nutze das korrekte Webhook Secret (`whsec_...`) +- Bei lokalem Test: Nutze die Stripe CLI + +### Microsoft OAuth: "Client credential must not be empty" + +Stelle sicher, dass `MICROSOFT_CLIENT_ID` und `MICROSOFT_CLIENT_SECRET` gesetzt sind. Falls du Outlook nicht nutzen möchtest, kannst du diese leer lassen - der Server startet trotzdem. + +--- + +## Checkliste + +- [ ] Appwrite Projekt erstellt +- [ ] Appwrite API Key erstellt +- [ ] Appwrite Database erstellt +- [ ] Bootstrap ausgeführt (`npm run bootstrap:v2`) +- [ ] Stripe Account erstellt +- [ ] Stripe Produkte erstellt (Basic, Pro, Business) +- [ ] Stripe Webhook eingerichtet +- [ ] Google OAuth Credentials erstellt (optional) +- [ ] Microsoft App Registration erstellt (optional) +- [ ] Mistral AI API Key erstellt +- [ ] Alle `.env` Dateien konfiguriert +- [ ] Server startet ohne Fehler +- [ ] Frontend startet ohne Fehler + +--- + +## Support + +Bei Fragen oder Problemen: +- GitHub Issues +- support@emailsorter.de diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/client/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..d2e7761 --- /dev/null +++ b/client/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/client/env.example b/client/env.example new file mode 100644 index 0000000..d811053 --- /dev/null +++ b/client/env.example @@ -0,0 +1,10 @@ +# EmailSorter Frontend Configuration +# Copy this file to .env and fill in your values + +# Appwrite Configuration +VITE_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1 +VITE_APPWRITE_PROJECT_ID=your-project-id + +# OAuth URLs (generated by your backend) +VITE_GMAIL_OAUTH_URL=http://localhost:3000/api/oauth/gmail +VITE_OUTLOOK_OAUTH_URL=http://localhost:3000/api/oauth/outlook diff --git a/client/eslint.config.js b/client/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/client/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..3143338 --- /dev/null +++ b/client/index.html @@ -0,0 +1,17 @@ + + + + + + + + + + + EmailSorter - Your inbox, finally organized + + +
+ + + diff --git a/client/package-lock.json b/client/package-lock.json new file mode 100644 index 0000000..f4db025 --- /dev/null +++ b/client/package-lock.json @@ -0,0 +1,4954 @@ +{ + "name": "client", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "client", + "version": "0.0.0", + "dependencies": { + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", + "appwrite": "^21.5.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.562.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.12.0", + "tailwind-merge": "^3.4.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@tailwindcss/vite": "^4.1.18", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "tailwindcss": "^4.1.18", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz", + "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "tailwindcss": "4.1.18" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.8.tgz", + "integrity": "sha512-r0bBaXu5Swb05doFYO2kTWHMovJnNVbCsII0fhesM8bNRlLhXIuckley4a2DaD+vOdmm5G+zGkQZAPZsF80+YQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", + "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.0.tgz", + "integrity": "sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.53.0", + "@typescript-eslint/type-utils": "8.53.0", + "@typescript-eslint/utils": "8.53.0", + "@typescript-eslint/visitor-keys": "8.53.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.53.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.0.tgz", + "integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.53.0", + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/typescript-estree": "8.53.0", + "@typescript-eslint/visitor-keys": "8.53.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.0.tgz", + "integrity": "sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.53.0", + "@typescript-eslint/types": "^8.53.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.0.tgz", + "integrity": "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/visitor-keys": "8.53.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.0.tgz", + "integrity": "sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.53.0.tgz", + "integrity": "sha512-BBAUhlx7g4SmcLhn8cnbxoxtmS7hcq39xKCgiutL3oNx1TaIp+cny51s8ewnKMpVUKQUGb41RAUWZ9kxYdovuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/typescript-estree": "8.53.0", + "@typescript-eslint/utils": "8.53.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.0.tgz", + "integrity": "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.0.tgz", + "integrity": "sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.53.0", + "@typescript-eslint/tsconfig-utils": "8.53.0", + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/visitor-keys": "8.53.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.0.tgz", + "integrity": "sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.53.0", + "@typescript-eslint/types": "8.53.0", + "@typescript-eslint/typescript-estree": "8.53.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.0.tgz", + "integrity": "sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", + "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.53", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/appwrite": { + "version": "21.5.0", + "resolved": "https://registry.npmjs.org/appwrite/-/appwrite-21.5.0.tgz", + "integrity": "sha512-643bMRZVYXMluXvSXbdaLAi9qqTJLWbVGguKH4vH6IdKHur6gGIirhCOqAEt33pV4TOFJ55VBu8c/+Ft1ke2SA==", + "license": "BSD-3-Clause" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", + "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001764", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", + "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.562.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz", + "integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz", + "integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.12.0.tgz", + "integrity": "sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==", + "license": "MIT", + "dependencies": { + "react-router": "7.12.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwind-merge": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", + "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.53.0.tgz", + "integrity": "sha512-xHURCQNxZ1dsWn0sdOaOfCSQG0HKeqSj9OexIxrz6ypU6wHYOdX2I3D2b8s8wFSsSOYJb+6q283cLiLlkEsBYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.53.0", + "@typescript-eslint/parser": "8.53.0", + "@typescript-eslint/typescript-estree": "8.53.0", + "@typescript-eslint/utils": "8.53.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..dcee6ea --- /dev/null +++ b/client/package.json @@ -0,0 +1,44 @@ +{ + "name": "emailsorter-client", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", + "appwrite": "^21.5.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.562.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.12.0", + "tailwind-merge": "^3.4.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@tailwindcss/vite": "^4.1.18", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "tailwindcss": "^4.1.18", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } +} diff --git a/client/public/vite.svg b/client/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/client/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx new file mode 100644 index 0000000..8ccdb94 --- /dev/null +++ b/client/src/App.tsx @@ -0,0 +1,142 @@ +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' +import { AuthProvider, useAuth } from '@/context/AuthContext' +import { usePageTracking } from '@/hooks/useAnalytics' +import { initAnalytics } from '@/lib/analytics' +import { Home } from '@/pages/Home' +import { Login } from '@/pages/Login' +import { Register } from '@/pages/Register' +import { Dashboard } from '@/pages/Dashboard' +import { Setup } from '@/pages/Setup' +import { Settings } from '@/pages/Settings' +import { ForgotPassword } from '@/pages/ForgotPassword' +import { ResetPassword } from '@/pages/ResetPassword' +import { VerifyEmail } from '@/pages/VerifyEmail' +import { Privacy } from '@/pages/Privacy' +import { Imprint } from '@/pages/Imprint' + +// Initialize analytics on app startup +initAnalytics() + +// Loading spinner component +function LoadingSpinner() { + return ( +
+
+
+

Loading...

+
+
+ ) +} + +// Protected route wrapper - requires authentication +function ProtectedRoute({ children }: { children: React.ReactNode }) { + const { user, loading } = useAuth() + + if (loading) { + return + } + + if (!user) { + return + } + + return <>{children} +} + +// Public route that redirects to dashboard if logged in +function PublicRoute({ children }: { children: React.ReactNode }) { + const { user, loading } = useAuth() + + if (loading) { + return + } + + if (user) { + return + } + + return <>{children} +} + +function AppRoutes() { + // Track page views on route changes + usePageTracking() + + return ( + + {/* Public pages */} + } /> + + {/* Auth pages - redirect to dashboard if logged in */} + + + + } + /> + + + + } + /> + + {/* Password recovery - always accessible */} + } /> + } /> + + {/* Email verification - always accessible */} + } /> + + {/* Legal pages - always accessible */} + } /> + } /> + + {/* Protected pages - require authentication */} + + + + } + /> + + + + } + /> + + + + } + /> + + {/* Catch all - redirect to home */} + } /> + + ) +} + +function App() { + return ( + + + + + + ) +} + +export default App diff --git a/client/src/assets/react.svg b/client/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/client/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/components/landing/FAQ.tsx b/client/src/components/landing/FAQ.tsx new file mode 100644 index 0000000..8c2db48 --- /dev/null +++ b/client/src/components/landing/FAQ.tsx @@ -0,0 +1,119 @@ +import { useState } from 'react' +import { ChevronDown, HelpCircle } from 'lucide-react' +import { cn } from '@/lib/utils' + +const faqs = [ + { + question: "Are my emails secure?", + answer: "Yes! We use OAuth – we never see your password. Content is only analyzed briefly, never stored." + }, + { + question: "Which email providers work?", + answer: "Gmail and Outlook. More coming soon." + }, + { + question: "Can I create custom rules?", + answer: "Absolutely! You can set VIP contacts and define custom categories." + }, + { + question: "What about old emails?", + answer: "The last 30 days are analyzed. You decide if they should be sorted too." + }, + { + question: "Can I cancel anytime?", + answer: "Yes, with one click. No tricks, no long commitments." + }, + { + question: "Do I need a credit card?", + answer: "No, the 14-day trial is completely free." + }, + { + question: "Does it work on mobile?", + answer: "Yes! Sorting runs on our servers – works in any email app." + }, + { + question: "What if the AI sorts wrong?", + answer: "Just correct it. The AI learns and gets better over time." + }, +] + +export function FAQ() { + const [openIndex, setOpenIndex] = useState(0) + + return ( +
+
+ {/* Section header */} +
+
+ +
+

+ FAQ +

+

+ Quick answers to common questions. +

+
+ + {/* FAQ items */} +
+ {faqs.map((faq, index) => ( + setOpenIndex(openIndex === index ? null : index)} + /> + ))} +
+ + {/* Contact CTA */} +
+

Still have questions?

+ + Contact us → + +
+
+
+ ) +} + +interface FAQItemProps { + question: string + answer: string + isOpen: boolean + onClick: () => void +} + +function FAQItem({ question, answer, isOpen, onClick }: FAQItemProps) { + return ( +
+ +
+

{answer}

+
+
+ ) +} diff --git a/client/src/components/landing/Features.tsx b/client/src/components/landing/Features.tsx new file mode 100644 index 0000000..16d3162 --- /dev/null +++ b/client/src/components/landing/Features.tsx @@ -0,0 +1,137 @@ +import { + Brain, + Zap, + Shield, + Clock, + Tags, + Settings, + Inbox, + Filter +} from 'lucide-react' + +const features = [ + { + icon: Brain, + title: "AI-powered categorization", + description: "Our AI automatically recognizes whether an email is an invoice, newsletter, or important message.", + color: "from-violet-500 to-purple-600" + }, + { + icon: Zap, + title: "Real-time sorting", + description: "New emails are categorized instantly. Your inbox arrives already sorted.", + color: "from-amber-500 to-orange-600" + }, + { + icon: Tags, + title: "Smart labels", + description: "Automatic labels for VIP, clients, invoices, newsletters, social media and more.", + color: "from-blue-500 to-cyan-600" + }, + { + icon: Shield, + title: "GDPR compliant", + description: "Your data stays secure. We only read email headers and metadata for sorting.", + color: "from-green-500 to-emerald-600" + }, + { + icon: Clock, + title: "Save time", + description: "Average 2 hours per week less on email organization. More time for what matters.", + color: "from-pink-500 to-rose-600" + }, + { + icon: Settings, + title: "Fully customizable", + description: "Define your own rules, VIP contacts, and categories based on your needs.", + color: "from-indigo-500 to-blue-600" + }, +] + +export function Features() { + return ( +
+
+ {/* Section header */} +
+

+ Everything you need for{' '} + + Inbox Zero + +

+

+ EmailSorter combines AI technology with proven email management methods + for maximum productivity. +

+
+ + {/* Features grid */} +
+ {features.map((feature, index) => ( + + ))} +
+ + {/* Bottom illustration */} +
+
+
+ {/* Before */} +
+
+ +
+

Before

+

Inbox chaos

+
847
+

unread emails

+
+ + {/* Arrow */} +
+
+ +
+
+ + {/* After */} +
+
+ +
+

After

+

All sorted

+
12
+

important emails

+
+
+
+
+
+
+ ) +} + +interface FeatureCardProps { + icon: React.ElementType + title: string + description: string + color: string + index: number +} + +function FeatureCard({ icon: Icon, title, description, color, index }: FeatureCardProps) { + return ( +
+
+ +
+

{title}

+

{description}

+
+ ) +} diff --git a/client/src/components/landing/Footer.tsx b/client/src/components/landing/Footer.tsx new file mode 100644 index 0000000..fdab605 --- /dev/null +++ b/client/src/components/landing/Footer.tsx @@ -0,0 +1,185 @@ +import { Link } from 'react-router-dom' +import { Mail, Twitter, Linkedin, Github } from 'lucide-react' + +export function Footer() { + return ( +
+
+
+ {/* Brand */} +
+ +
+ +
+ + EmailSorter + + +

+ AI-powered email sorting for more productivity and less stress. +

+ {/* Social links */} + +
+ + {/* Product */} +
+

Product

+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + + Roadmap + +
  • +
+
+ + {/* Company */} +
+

Company

+ +
+ + {/* Legal */} +
+

Legal

+ +
+
+ + {/* Bottom bar */} +
+
+

+ © {new Date().getFullYear()} EmailSorter. All rights reserved. +

+

+ Made with ❤️ +

+
+ {/* webklar.com Verweis */} +
+

+ Need a website? +

+ + Visit webklar.com + + + + +
+
+
+
+ ) +} diff --git a/client/src/components/landing/Hero.tsx b/client/src/components/landing/Hero.tsx new file mode 100644 index 0000000..221f033 --- /dev/null +++ b/client/src/components/landing/Hero.tsx @@ -0,0 +1,179 @@ +import { useNavigate } from 'react-router-dom' +import { captureUTMParams } from '@/lib/analytics' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { ArrowRight, Mail, Inbox, Sparkles, Check, Zap } from 'lucide-react' + +export function Hero() { + const navigate = useNavigate() + + const handleCTAClick = () => { + // Capture UTM parameters before navigation + captureUTMParams() + navigate('/register') + } + + return ( +
+ {/* Background */} +
+
+ + {/* Grid pattern overlay */} +
+ +
+
+ {/* Left side - Text content */} +
+ + + AI-powered email sorting + + +

+ Your inbox. +
+ + Finally organized. + +

+ +

+ EmailSorter uses AI to automatically categorize your emails. + Newsletters, invoices, important contacts – everything lands + exactly where it belongs. +

+ +
+ + +
+ + {/* Trust badges */} +
+
+ + No credit card required +
+
+ + Gmail & Outlook +
+
+ + GDPR compliant +
+
+
+ + {/* Right side - Visual */} +
+
+ {/* Main card */} +
+
+
+ +
+
+

Your Inbox

+

Auto-sorted

+
+
+ + {/* Email categories preview */} +
+ + + + +
+
+ + {/* Floating badge */} +
+ + AI sorting +
+
+
+
+
+ + {/* Scroll indicator */} +
+
+
+
+
+
+ ) +} + +interface EmailPreviewProps { + category: string + color: string + sender: string + subject: string + delay: string +} + +function EmailPreview({ category, color, sender, subject, delay }: EmailPreviewProps) { + return ( +
+
+
+
+ {sender} + {category} +
+

{subject}

+
+ +
+ ) +} diff --git a/client/src/components/landing/HowItWorks.tsx b/client/src/components/landing/HowItWorks.tsx new file mode 100644 index 0000000..a2ef567 --- /dev/null +++ b/client/src/components/landing/HowItWorks.tsx @@ -0,0 +1,111 @@ +import { + UserPlus, + Link2, + Sparkles, + PartyPopper, + ArrowDown +} from 'lucide-react' + +const steps = [ + { + icon: UserPlus, + step: "01", + title: "Create account", + description: "Sign up for free in less than 60 seconds. No credit card required.", + }, + { + icon: Link2, + step: "02", + title: "Connect email", + description: "Connect Gmail or Outlook with one click. Secure OAuth authentication.", + }, + { + icon: Sparkles, + step: "03", + title: "AI analyzes", + description: "Our AI learns your email patterns and creates personalized sorting rules.", + }, + { + icon: PartyPopper, + step: "04", + title: "Enjoy Inbox Zero", + description: "Sit back and enjoy a clean inbox – automatically.", + }, +] + +export function HowItWorks() { + return ( +
+
+ {/* Section header */} +
+

+ 4 steps to a{' '} + + clean inbox + +

+

+ Get started in minutes – no technical knowledge required. +

+
+ + {/* Steps */} +
+ {/* Connection line */} +
+ +
+ {steps.map((item, index) => ( + + ))} +
+
+ + {/* CTA */} +
+
+ +

Ready to get started?

+ + Try it free now → + +
+
+
+
+ ) +} + +interface StepCardProps { + icon: React.ElementType + step: string + title: string + description: string +} + +function StepCard({ icon: Icon, step, title, description }: StepCardProps) { + return ( +
+ {/* Card */} +
+ {/* Step number */} +
+ {step} +
+ + {/* Icon */} +
+ +
+ + {/* Content */} +

{title}

+

{description}

+
+
+ ) +} diff --git a/client/src/components/landing/Navbar.tsx b/client/src/components/landing/Navbar.tsx new file mode 100644 index 0000000..c5dce71 --- /dev/null +++ b/client/src/components/landing/Navbar.tsx @@ -0,0 +1,159 @@ +import { useState, useCallback } from 'react' +import { Link, useNavigate, useLocation } from 'react-router-dom' +import { Button } from '@/components/ui/button' +import { useAuth } from '@/context/AuthContext' +import { Menu, X, Mail, Sparkles } from 'lucide-react' + +export function Navbar() { + const [isMenuOpen, setIsMenuOpen] = useState(false) + const { user } = useAuth() + const navigate = useNavigate() + const location = useLocation() + + // Smooth scroll to section + const scrollToSection = useCallback((sectionId: string) => { + setIsMenuOpen(false) + + // If not on home page, navigate first + if (location.pathname !== '/') { + navigate('/') + setTimeout(() => { + const element = document.getElementById(sectionId) + element?.scrollIntoView({ behavior: 'smooth', block: 'start' }) + }, 100) + } else { + const element = document.getElementById(sectionId) + element?.scrollIntoView({ behavior: 'smooth', block: 'start' }) + } + }, [location.pathname, navigate]) + + return ( + + ) +} diff --git a/client/src/components/landing/Pricing.tsx b/client/src/components/landing/Pricing.tsx new file mode 100644 index 0000000..c43f802 --- /dev/null +++ b/client/src/components/landing/Pricing.tsx @@ -0,0 +1,193 @@ +import { useNavigate } from 'react-router-dom' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { Check, X, Sparkles } from 'lucide-react' + +const plans = [ + { + name: "Basic", + price: "9", + period: "/ month", + description: "Perfect for getting started", + features: [ + { text: "1 email account", included: true }, + { text: "500 emails / day", included: true }, + { text: "Basic categories", included: true }, + { text: "Email support", included: true }, + { text: "Historical email analysis", included: false }, + { text: "Custom rules", included: false }, + { text: "Priority support", included: false }, + ], + cta: "Start Basic", + popular: false, + priceId: "price_basic_monthly" + }, + { + name: "Pro", + price: "19", + period: "/ month", + description: "For power users", + features: [ + { text: "3 email accounts", included: true }, + { text: "Unlimited emails", included: true }, + { text: "All categories", included: true }, + { text: "Email support", included: true }, + { text: "Historical email analysis", included: true }, + { text: "Custom rules", included: true }, + { text: "Priority support", included: false }, + ], + cta: "Start Pro", + popular: true, + priceId: "price_pro_monthly" + }, + { + name: "Business", + price: "49", + period: "/ month", + description: "For teams & companies", + features: [ + { text: "10 email accounts", included: true }, + { text: "Unlimited emails", included: true }, + { text: "All categories", included: true }, + { text: "Email + chat support", included: true }, + { text: "Historical email analysis", included: true }, + { text: "Custom rules", included: true }, + { text: "Priority support", included: true }, + ], + cta: "Start Business", + popular: false, + priceId: "price_business_monthly" + }, +] + +export function Pricing() { + const navigate = useNavigate() + + return ( +
+
+ {/* Section header */} +
+ + + 14-day free trial + +

+ Simple, transparent pricing +

+

+ Choose the plan that fits you. Cancel anytime, no hidden costs. +

+
+ + {/* Pricing cards */} +
+ {plans.map((plan, index) => ( + navigate(`/register?plan=${plan.name.toLowerCase()}`)} + /> + ))} +
+ + {/* FAQ teaser */} +
+

+ Still have questions?{' '} + +

+
+
+
+ ) +} + +interface PricingCardProps { + name: string + price: string + period: string + description: string + features: { text: string; included: boolean }[] + cta: string + popular: boolean + onSelect: () => void +} + +function PricingCard({ + name, + price, + period, + description, + features, + cta, + popular, + onSelect +}: PricingCardProps) { + return ( +
+ {popular && ( +
+ + Most Popular + +
+ )} + + {/* Header */} +
+

{name}

+

{description}

+
+ + {/* Price */} +
+
+ ${price} + {period} +
+
+ + {/* Features */} +
    + {features.map((feature, index) => ( +
  • + {feature.included ? ( +
    + +
    + ) : ( +
    + +
    + )} + + {feature.text} + +
  • + ))} +
+ + {/* CTA */} + +
+ ) +} diff --git a/client/src/components/landing/Testimonials.tsx b/client/src/components/landing/Testimonials.tsx new file mode 100644 index 0000000..8745bb0 --- /dev/null +++ b/client/src/components/landing/Testimonials.tsx @@ -0,0 +1,67 @@ +import { CheckCircle2, Clock, Brain, Shield } from 'lucide-react' + +const benefits = [ + { + icon: Clock, + title: "Save 2+ hours/week", + description: "Less time sorting emails, more time for important tasks.", + }, + { + icon: Brain, + title: "AI does it automatically", + description: "Set up once, then everything runs by itself.", + }, + { + icon: Shield, + title: "Privacy first", + description: "Your emails stay private. We don't store any content.", + }, + { + icon: CheckCircle2, + title: "Easy to use", + description: "No learning curve. Ready to go in 2 minutes.", + }, +] + +export function Testimonials() { + return ( +
+
+ {/* Section header */} +
+

+ Why EmailSorter? +

+

+ No more email chaos. Focus on what matters. +

+
+ + {/* Benefits grid */} +
+ {benefits.map((benefit, index) => ( + + ))} +
+
+
+ ) +} + +interface BenefitCardProps { + icon: React.ElementType + title: string + description: string +} + +function BenefitCard({ icon: Icon, title, description }: BenefitCardProps) { + return ( +
+
+ +
+

{title}

+

{description}

+
+ ) +} diff --git a/client/src/components/ui/badge.tsx b/client/src/components/ui/badge.tsx new file mode 100644 index 0000000..5d5efeb --- /dev/null +++ b/client/src/components/ui/badge.tsx @@ -0,0 +1,39 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary-100 text-primary-700", + secondary: + "border-transparent bg-slate-100 text-slate-700", + success: + "border-transparent bg-green-100 text-green-700", + warning: + "border-transparent bg-amber-100 text-amber-700", + destructive: + "border-transparent bg-red-100 text-red-700", + outline: "text-slate-600 border-slate-200", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/client/src/components/ui/button.tsx b/client/src/components/ui/button.tsx new file mode 100644 index 0000000..f6a01c4 --- /dev/null +++ b/client/src/components/ui/button.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-lg text-sm font-semibold ring-offset-white transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: + "bg-primary-600 text-white hover:bg-primary-700 shadow-lg shadow-primary-600/25 hover:shadow-primary-600/40", + secondary: + "bg-slate-100 text-slate-900 hover:bg-slate-200", + outline: + "border-2 border-slate-200 bg-transparent hover:bg-slate-50 hover:border-slate-300", + ghost: + "hover:bg-slate-100 hover:text-slate-900", + link: + "text-primary-600 underline-offset-4 hover:underline", + accent: + "bg-accent-500 text-white hover:bg-accent-600 shadow-lg shadow-accent-500/25", + }, + size: { + default: "h-11 px-6 py-2", + sm: "h-9 rounded-md px-4", + lg: "h-14 rounded-xl px-8 text-base", + xl: "h-16 rounded-xl px-10 text-lg", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/client/src/components/ui/card.tsx b/client/src/components/ui/card.tsx new file mode 100644 index 0000000..59c8a8c --- /dev/null +++ b/client/src/components/ui/card.tsx @@ -0,0 +1,78 @@ +import * as React from "react" +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/client/src/components/ui/input.tsx b/client/src/components/ui/input.tsx new file mode 100644 index 0000000..7a12789 --- /dev/null +++ b/client/src/components/ui/input.tsx @@ -0,0 +1,32 @@ +import * as React from "react" +import { cn } from "@/lib/utils" + +export interface InputProps + extends React.InputHTMLAttributes { + error?: string +} + +const Input = React.forwardRef( + ({ className, type, error, ...props }, ref) => { + return ( +
+ + {error && ( +

{error}

+ )} +
+ ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/client/src/components/ui/label.tsx b/client/src/components/ui/label.tsx new file mode 100644 index 0000000..dd037c6 --- /dev/null +++ b/client/src/components/ui/label.tsx @@ -0,0 +1,23 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-slate-700" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/client/src/context/AuthContext.tsx b/client/src/context/AuthContext.tsx new file mode 100644 index 0000000..68affe0 --- /dev/null +++ b/client/src/context/AuthContext.tsx @@ -0,0 +1,74 @@ +import React, { createContext, useContext, useEffect, useState } from 'react' +import { auth } from '@/lib/appwrite' +import type { Models } from 'appwrite' + +interface AuthContextType { + user: Models.User | null + loading: boolean + login: (email: string, password: string) => Promise + register: (email: string, password: string, name?: string) => Promise + logout: () => Promise + refreshUser: () => Promise +} + +const AuthContext = createContext(undefined) + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState | null>(null) + const [loading, setLoading] = useState(true) + + const refreshUser = async () => { + try { + const currentUser = await auth.getCurrentUser() + setUser(currentUser) + } catch { + setUser(null) + } + } + + useEffect(() => { + const init = async () => { + await refreshUser() + setLoading(false) + } + init() + }, []) + + const login = async (email: string, password: string) => { + await auth.login(email, password) + await refreshUser() + } + + const register = async (email: string, password: string, name?: string) => { + await auth.register(email, password, name) + await refreshUser() + } + + const logout = async () => { + await auth.logout() + setUser(null) + } + + return ( + + {children} + + ) +} + +export function useAuth() { + const context = useContext(AuthContext) + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider') + } + return context +} diff --git a/client/src/hooks/useAnalytics.ts b/client/src/hooks/useAnalytics.ts new file mode 100644 index 0000000..a61db22 --- /dev/null +++ b/client/src/hooks/useAnalytics.ts @@ -0,0 +1,53 @@ +/** + * React Hook for Analytics + * Provides easy access to analytics functions in components + */ + +import { useEffect } from 'react' +import { useLocation } from 'react-router-dom' +import { + trackPageView, + captureUTMParams, + getAllTrackingParams, + trackSignup, + trackTrialStart, + trackPurchase, + trackEmailConnected, + setUserId, + type TrackingParams, +} from '@/lib/analytics' + +/** + * Hook to automatically track page views on route changes + */ +export function usePageTracking() { + const location = useLocation() + + useEffect(() => { + // Capture UTM parameters on every navigation + captureUTMParams() + + // Track page view + trackPageView(location.pathname) + }, [location]) +} + +/** + * Hook to get tracking parameters + */ +export function useTrackingParams(): TrackingParams { + return getAllTrackingParams() +} + +/** + * Export analytics functions for use in components + */ +export const analytics = { + trackSignup, + trackTrialStart, + trackPurchase, + trackEmailConnected, + setUserId, +} + +export default usePageTracking diff --git a/client/src/index.css b/client/src/index.css new file mode 100644 index 0000000..8d58e88 --- /dev/null +++ b/client/src/index.css @@ -0,0 +1,144 @@ +/* Custom fonts - imported before Tailwind */ +@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap'); + +@import "tailwindcss"; + +/* CSS Variables for theming */ +@theme { + --font-sans: 'Plus Jakarta Sans', system-ui, sans-serif; + + /* Primary colors - Modern Green theme (webklar.com style) */ + --color-primary-50: #f0fdf4; + --color-primary-100: #dcfce7; + --color-primary-200: #bbf7d0; + --color-primary-300: #86efac; + --color-primary-400: #4ade80; + --color-primary-500: #22c55e; + --color-primary-600: #16a34a; + --color-primary-700: #15803d; + --color-primary-800: #166534; + --color-primary-900: #14532d; + + /* Accent colors - Modern Green/Emerald */ + --color-accent-400: #34d399; + --color-accent-500: #10b981; + --color-accent-600: #059669; + + /* Neutral/Slate colors */ + --color-slate-50: #f8fafc; + --color-slate-100: #f1f5f9; + --color-slate-200: #e2e8f0; + --color-slate-300: #cbd5e1; + --color-slate-400: #94a3b8; + --color-slate-500: #64748b; + --color-slate-600: #475569; + --color-slate-700: #334155; + --color-slate-800: #1e293b; + --color-slate-900: #0f172a; + --color-slate-950: #020617; +} + +/* Base styles */ +html { + scroll-behavior: smooth; +} + +body { + font-family: var(--font-sans); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + /* Improve touch scrolling on mobile */ + -webkit-overflow-scrolling: touch; + overflow-scrolling: touch; +} + +/* Improve touch targets on mobile */ +@media (max-width: 640px) { + button, a, [role="button"] { + min-height: 44px; + min-width: 44px; + } + + /* Better tap highlighting */ + * { + -webkit-tap-highlight-color: rgba(34, 197, 94, 0.1); + } +} + +/* Touch manipulation for better performance */ +.touch-manipulation { + touch-action: manipulation; + -webkit-touch-callout: none; + -webkit-user-select: none; + user-select: none; +} + +/* Selection styling */ +::selection { + background-color: var(--color-primary-200); + color: var(--color-primary-900); +} + +/* Custom scrollbar */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--color-slate-100); +} + +::-webkit-scrollbar-thumb { + background: var(--color-slate-300); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--color-slate-400); +} + +/* Gradient backgrounds */ +.gradient-hero { + background: linear-gradient(135deg, var(--color-slate-900) 0%, var(--color-primary-900) 50%, var(--color-slate-800) 100%); +} + +.gradient-mesh { + background-image: + radial-gradient(at 40% 20%, var(--color-primary-500) 0px, transparent 50%), + radial-gradient(at 80% 0%, var(--color-accent-500) 0px, transparent 50%), + radial-gradient(at 0% 50%, var(--color-primary-700) 0px, transparent 50%), + radial-gradient(at 80% 50%, var(--color-accent-400) 0px, transparent 50%), + radial-gradient(at 0% 100%, var(--color-primary-600) 0px, transparent 50%); +} + +/* Animation classes */ +@keyframes float { + 0%, 100% { transform: translateY(0px); } + 50% { transform: translateY(-10px); } +} + +@keyframes pulse-glow { + 0%, 100% { box-shadow: 0 0 20px rgba(34, 197, 94, 0.3); } + 50% { box-shadow: 0 0 40px rgba(34, 197, 94, 0.6); } +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +.animate-float { + animation: float 6s ease-in-out infinite; +} + +.animate-pulse-glow { + animation: pulse-glow 2s ease-in-out infinite; +} + +/* Stagger animation delays */ +.stagger-1 { animation-delay: 0.1s; } +.stagger-2 { animation-delay: 0.2s; } +.stagger-3 { animation-delay: 0.3s; } +.stagger-4 { animation-delay: 0.4s; } +.stagger-5 { animation-delay: 0.5s; } diff --git a/client/src/lib/analytics.ts b/client/src/lib/analytics.ts new file mode 100644 index 0000000..0fbce0b --- /dev/null +++ b/client/src/lib/analytics.ts @@ -0,0 +1,314 @@ +/** + * Analytics & Tracking Utility + * Handles UTM parameter tracking and event analytics + */ + +export interface TrackingParams { + utm_source?: string + utm_medium?: string + utm_campaign?: string + utm_term?: string + utm_content?: string + gclid?: string // Google Ads Click ID + fbclid?: string // Facebook Click ID + ref?: string // General referrer +} + +export interface ConversionEvent { + type: 'page_view' | 'signup' | 'trial_start' | 'purchase' | 'email_connected' + userId?: string + metadata?: Record +} + +const STORAGE_KEY = 'emailsorter_utm_params' +const USER_ID_KEY = 'emailsorter_user_id' + +/** + * Parse UTM parameters from URL + */ +export function parseUTMParams(): TrackingParams { + const params = new URLSearchParams(window.location.search) + const utmParams: TrackingParams = {} + + const utmKeys: Array = [ + 'utm_source', + 'utm_medium', + 'utm_campaign', + 'utm_term', + 'utm_content', + 'gclid', + 'fbclid', + 'ref', + ] + + utmKeys.forEach((key) => { + const value = params.get(key) + if (value) { + utmParams[key] = value + } + }) + + // If no UTM params but referrer exists, capture it + if (!utmParams.utm_source && document.referrer) { + try { + const referrerUrl = new URL(document.referrer) + if (referrerUrl.hostname !== window.location.hostname) { + utmParams.ref = referrerUrl.hostname + } + } catch { + // Invalid referrer URL, ignore + } + } + + return utmParams +} + +/** + * Store UTM parameters in localStorage (persists across sessions) + */ +export function storeUTMParams(params: TrackingParams): void { + if (Object.keys(params).length === 0) return + + const existing = getStoredUTMParams() + const merged = { ...existing, ...params } + + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(merged)) + + // Set expiration (30 days) + localStorage.setItem(`${STORAGE_KEY}_expiry`, String(Date.now() + 30 * 24 * 60 * 60 * 1000)) + } catch (error) { + console.warn('Failed to store UTM parameters:', error) + } +} + +/** + * Get stored UTM parameters from localStorage + */ +export function getStoredUTMParams(): TrackingParams { + try { + // Check expiry + const expiry = localStorage.getItem(`${STORAGE_KEY}_expiry`) + if (expiry && Date.now() > parseInt(expiry, 10)) { + localStorage.removeItem(STORAGE_KEY) + localStorage.removeItem(`${STORAGE_KEY}_expiry`) + return {} + } + + const stored = localStorage.getItem(STORAGE_KEY) + return stored ? JSON.parse(stored) : {} + } catch (error) { + console.warn('Failed to get stored UTM parameters:', error) + return {} + } +} + +/** + * Clear stored UTM parameters + */ +export function clearUTMParams(): void { + try { + localStorage.removeItem(STORAGE_KEY) + localStorage.removeItem(`${STORAGE_KEY}_expiry`) + } catch (error) { + console.warn('Failed to clear UTM parameters:', error) + } +} + +/** + * Capture and store UTM parameters from current URL + * Call this on page load or navigation + */ +export function captureUTMParams(): TrackingParams { + const params = parseUTMParams() + if (Object.keys(params).length > 0) { + storeUTMParams(params) + } + return params +} + +/** + * Get all tracking parameters (from URL + stored) + */ +export function getAllTrackingParams(): TrackingParams { + const urlParams = parseUTMParams() + const storedParams = getStoredUTMParams() + + // URL params take precedence + return { ...storedParams, ...urlParams } +} + +/** + * Track conversion event + * Send tracking data to server or analytics service + */ +export async function trackEvent( + event: ConversionEvent, + trackingParams?: TrackingParams +): Promise { + const params = trackingParams || getAllTrackingParams() + const userId = localStorage.getItem(USER_ID_KEY) + + const payload = { + ...event, + userId: event.userId || userId || undefined, + tracking: params, + timestamp: new Date().toISOString(), + page: window.location.pathname, + referrer: document.referrer || undefined, + userAgent: navigator.userAgent, + } + + try { + // Send to your analytics endpoint + await fetch('/api/analytics/track', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }).catch(() => { + // Silently fail if analytics endpoint doesn't exist yet + // This allows graceful degradation + }) + + // Also log to console in development + if (import.meta.env.DEV) { + console.log('📊 Analytics Event:', event.type, payload) + } + } catch (error) { + console.warn('Failed to track event:', error) + } +} + +/** + * Track page view + */ +export function trackPageView(path?: string): void { + trackEvent({ + type: 'page_view', + metadata: { + path: path || window.location.pathname, + title: document.title, + }, + }) +} + +/** + * Track signup event + */ +export function trackSignup(userId: string, email: string): void { + const trackingParams = getAllTrackingParams() + + trackEvent({ + type: 'signup', + userId, + metadata: { + email: email, + source: trackingParams.utm_source, + medium: trackingParams.utm_medium, + campaign: trackingParams.utm_campaign, + }, + }) + + // Store user ID for future events + try { + localStorage.setItem(USER_ID_KEY, userId) + } catch (error) { + console.warn('Failed to store user ID:', error) + } +} + +/** + * Track trial start + */ +export function trackTrialStart(userId: string): void { + trackEvent({ + type: 'trial_start', + userId, + metadata: { + timestamp: new Date().toISOString(), + }, + }) +} + +/** + * Track purchase/subscription + */ +export function trackPurchase(userId: string, plan: string, amount: number): void { + const trackingParams = getAllTrackingParams() + + trackEvent({ + type: 'purchase', + userId, + metadata: { + plan, + amount, + currency: 'EUR', + source: trackingParams.utm_source, + medium: trackingParams.utm_medium, + campaign: trackingParams.utm_campaign, + }, + }) +} + +/** + * Track email account connection + */ +export function trackEmailConnected(userId: string, provider: string): void { + trackEvent({ + type: 'email_connected', + userId, + metadata: { + provider, + timestamp: new Date().toISOString(), + }, + }) +} + +/** + * Set user ID (for authenticated users) + */ +export function setUserId(userId: string): void { + try { + localStorage.setItem(USER_ID_KEY, userId) + } catch (error) { + console.warn('Failed to store user ID:', error) + } +} + +/** + * Get stored user ID + */ +export function getUserId(): string | null { + try { + return localStorage.getItem(USER_ID_KEY) + } catch { + return null + } +} + +/** + * Initialize analytics + * Call this once on app startup + */ +export function initAnalytics(): void { + // Capture UTM parameters from URL + captureUTMParams() + + // Track initial page view + trackPageView() + + // Track page views on navigation (will be handled by React Router) +} + +/** + * Get tracking parameters as query string (for API calls) + */ +export function getTrackingQueryString(): string { + const params = getAllTrackingParams() + const entries = Object.entries(params).filter(([, value]) => value) + return entries.length > 0 + ? '&' + new URLSearchParams(entries as string[][]).toString() + : '' +} diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts new file mode 100644 index 0000000..8acb3d7 --- /dev/null +++ b/client/src/lib/api.ts @@ -0,0 +1,329 @@ +const API_BASE = import.meta.env.VITE_API_URL || '/api' + +interface ApiResponse { + success?: boolean + data?: T + error?: { + code: string + message: string + fields?: Record + } +} + +async function fetchApi( + endpoint: string, + options?: RequestInit +): Promise> { + try { + const response = await fetch(`${API_BASE}${endpoint}`, { + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers, + }, + }) + + const data = await response.json() + + if (!response.ok || data.success === false) { + return { + error: data.error || { + code: 'UNKNOWN', + message: `HTTP ${response.status}` + } + } + } + + return { success: true, data: data.data || data } + } catch (error) { + return { + error: { + code: 'NETWORK_ERROR', + message: error instanceof Error ? error.message : 'Network error' + } + } + } +} + +export const api = { + // ═══════════════════════════════════════════════════════════════════════════ + // EMAIL ACCOUNTS + // ═══════════════════════════════════════════════════════════════════════════ + + async getEmailAccounts(userId: string) { + return fetchApi>(`/email/accounts?userId=${userId}`) + }, + + async connectEmailAccount(userId: string, provider: 'gmail' | 'outlook', email: string, accessToken: string, refreshToken?: string) { + return fetchApi<{ accountId: string }>('/email/connect', { + method: 'POST', + body: JSON.stringify({ userId, provider, email, accessToken, refreshToken }), + }) + }, + + async disconnectEmailAccount(accountId: string, userId: string) { + return fetchApi<{ success: boolean }>(`/email/accounts/${accountId}?userId=${userId}`, { + method: 'DELETE', + }) + }, + + // ═══════════════════════════════════════════════════════════════════════════ + // EMAIL STATS & SORTING + // ═══════════════════════════════════════════════════════════════════════════ + + async getEmailStats(userId: string) { + return fetchApi<{ + totalSorted: number + todaySorted: number + weekSorted: number + categories: Record + timeSaved: number + }>(`/email/stats?userId=${userId}`) + }, + + async sortEmails(userId: string, accountId: string, maxEmails?: number, processAll?: boolean) { + return fetchApi<{ + sorted: number + inboxCleared: number + categories: Record + timeSaved: { minutes: number; formatted: string } + highlights: Array<{ type: string; count: number; message: string }> + suggestions: Array<{ type: string; message: string }> + provider?: string + isDemo?: boolean + }>('/email/sort', { + method: 'POST', + body: JSON.stringify({ userId, accountId, maxEmails, processAll }), + }) + }, + + // Demo sorting without account (for quick tests) + async sortDemo(count: number = 10) { + return fetchApi<{ + sorted: number + emails: Array<{ + from: string + subject: string + snippet: string + category: string + categoryName: string + confidence?: number + reason?: string + }> + categories: Record + aiEnabled: boolean + }>('/email/sort-demo', { + method: 'POST', + body: JSON.stringify({ count }), + }) + }, + + // Connect demo account + async connectDemoAccount(userId: string) { + return fetchApi<{ + accountId: string + email: string + provider: string + message?: string + }>('/email/connect-demo', { + method: 'POST', + body: JSON.stringify({ userId }), + }) + }, + + // Get categories + async getCategories() { + return fetchApi>('/email/categories') + }, + + // Get today's digest + async getDigest(userId: string) { + return fetchApi<{ + date: string + totalSorted: number + inboxCleared: number + timeSavedMinutes: number + stats: Record + highlights: Array<{ type: string; count: number; message: string }> + suggestions: Array<{ type: string; message: string }> + hasData: boolean + }>(`/email/digest?userId=${userId}`) + }, + + // Get digest history + async getDigestHistory(userId: string, days: number = 7) { + return fetchApi<{ + days: number + digests: Array<{ + date: string + totalSorted: number + inboxCleared: number + timeSavedMinutes: number + stats: Record + }> + totals: { + totalSorted: number + inboxCleared: number + timeSavedMinutes: number + } + }>(`/email/digest/history?userId=${userId}&days=${days}`) + }, + + // ═══════════════════════════════════════════════════════════════════════════ + // OAUTH + // ═══════════════════════════════════════════════════════════════════════════ + + async getOAuthUrl(provider: 'gmail' | 'outlook', userId: string) { + return fetchApi<{ url: string }>(`/oauth/${provider}/connect?userId=${userId}`) + }, + + async getOAuthStatus() { + return fetchApi<{ + gmail: { enabled: boolean; scopes: string[] } + outlook: { enabled: boolean; scopes: string[] } + }>('/oauth/status') + }, + + // ═══════════════════════════════════════════════════════════════════════════ + // SUBSCRIPTION + // ═══════════════════════════════════════════════════════════════════════════ + + async getSubscriptionStatus(userId: string) { + return fetchApi<{ + status: string + plan: string + features: { + emailAccounts: number + emailsPerDay: number + historicalSync: boolean + customRules: boolean + prioritySupport: boolean + } + currentPeriodEnd?: string + cancelAtPeriodEnd?: boolean + }>(`/subscription/status?userId=${userId}`) + }, + + async createSubscriptionCheckout(plan: string, userId: string, email?: string) { + return fetchApi<{ url: string; sessionId: string }>('/subscription/checkout', { + method: 'POST', + body: JSON.stringify({ userId, plan, email }), + }) + }, + + async createPortalSession(userId: string) { + return fetchApi<{ url: string }>('/subscription/portal', { + method: 'POST', + body: JSON.stringify({ userId }), + }) + }, + + async cancelSubscription(userId: string) { + return fetchApi<{ success: boolean }>('/subscription/cancel', { + method: 'POST', + body: JSON.stringify({ userId }), + }) + }, + + async reactivateSubscription(userId: string) { + return fetchApi<{ success: boolean }>('/subscription/reactivate', { + method: 'POST', + body: JSON.stringify({ userId }), + }) + }, + + // ═══════════════════════════════════════════════════════════════════════════ + // USER PREFERENCES + // ═══════════════════════════════════════════════════════════════════════════ + + async getUserPreferences(userId: string) { + return fetchApi<{ + vipSenders: Array<{ email: string; name?: string }> + blockedSenders: string[] + customRules: Array<{ condition: string; category: string }> + priorityTopics: string[] + }>(`/preferences?userId=${userId}`) + }, + + async saveUserPreferences(userId: string, preferences: { + vipSenders?: Array<{ email: string; name?: string }> + blockedSenders?: string[] + customRules?: Array<{ condition: string; category: string }> + priorityTopics?: string[] + }) { + return fetchApi<{ success: boolean }>('/preferences', { + method: 'POST', + body: JSON.stringify({ userId, ...preferences }), + }) + }, + + // ═══════════════════════════════════════════════════════════════════════════ + // PRODUCTS & QUESTIONS (Legacy) + // ═══════════════════════════════════════════════════════════════════════════ + + async getProducts() { + return fetchApi('/products') + }, + + async getQuestions(productSlug: string) { + return fetchApi(`/questions?productSlug=${productSlug}`) + }, + + async createSubmission(productSlug: string, answers: Record) { + return fetchApi<{ submissionId: string }>('/submissions', { + method: 'POST', + body: JSON.stringify({ productSlug, answers }), + }) + }, + + async createCheckout(submissionId: string) { + return fetchApi<{ url: string; sessionId: string }>('/checkout', { + method: 'POST', + body: JSON.stringify({ submissionId }), + }) + }, + + // ═══════════════════════════════════════════════════════════════════════════ + // CONFIG + // ═══════════════════════════════════════════════════════════════════════════ + + async getConfig() { + return fetchApi<{ + features: { + gmail: boolean + outlook: boolean + ai: boolean + } + pricing: { + basic: { price: number; currency: string; accounts: number } + pro: { price: number; currency: string; accounts: number } + business: { price: number; currency: string; accounts: number } + } + }>('/config') + }, + + async healthCheck() { + return fetchApi<{ + status: string + timestamp: string + version: string + environment: string + uptime: number + }>('/health') + }, +} + +export default api diff --git a/client/src/lib/appwrite.ts b/client/src/lib/appwrite.ts new file mode 100644 index 0000000..53dae5d --- /dev/null +++ b/client/src/lib/appwrite.ts @@ -0,0 +1,81 @@ +import { Client, Account, Databases, ID } from 'appwrite' + +const client = new Client() + +// Configure these in your .env file +const APPWRITE_ENDPOINT = import.meta.env.VITE_APPWRITE_ENDPOINT || 'https://cloud.appwrite.io/v1' +const APPWRITE_PROJECT_ID = import.meta.env.VITE_APPWRITE_PROJECT_ID || '' + +client + .setEndpoint(APPWRITE_ENDPOINT) + .setProject(APPWRITE_PROJECT_ID) + +export const account = new Account(client) +export const databases = new Databases(client) +export { ID } + +// Auth helper functions +export const auth = { + // Create a new account + async register(email: string, password: string, name?: string) { + const user = await account.create(ID.unique(), email, password, name) + await this.login(email, password) + return user + }, + + // Login with email and password + async login(email: string, password: string) { + return await account.createEmailPasswordSession(email, password) + }, + + // Logout current session + async logout() { + return await account.deleteSession('current') + }, + + // Get current logged in user + async getCurrentUser() { + try { + return await account.get() + } catch { + return null + } + }, + + // Check if user is logged in + async isLoggedIn() { + try { + await account.get() + return true + } catch { + return false + } + }, + + // Send password recovery email + async forgotPassword(email: string) { + return await account.createRecovery( + email, + `${window.location.origin}/reset-password` + ) + }, + + // Complete password recovery + async resetPassword(userId: string, secret: string, newPassword: string) { + return await account.updateRecovery(userId, secret, newPassword) + }, + + // Send verification email + async sendVerification() { + return await account.createVerification( + `${window.location.origin}/verify` + ) + }, + + // Complete email verification + async verifyEmail(userId: string, secret: string) { + return await account.updateVerification(userId, secret) + }, +} + +export default client diff --git a/client/src/lib/utils.ts b/client/src/lib/utils.ts new file mode 100644 index 0000000..d084cca --- /dev/null +++ b/client/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/client/src/main.tsx b/client/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/client/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/client/src/pages/Dashboard.tsx b/client/src/pages/Dashboard.tsx new file mode 100644 index 0000000..d5896fe --- /dev/null +++ b/client/src/pages/Dashboard.tsx @@ -0,0 +1,707 @@ +import { useState, useEffect } from 'react' +import { useNavigate, Link } from 'react-router-dom' +import { useAuth } from '@/context/AuthContext' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' +import { api } from '@/lib/api' +import { + Mail, + Inbox, + Tag, + Clock, + TrendingUp, + Plus, + Settings, + LogOut, + Zap, + BarChart3, + Users, + FileText, + Bell, + Shield, + HelpCircle, + ChevronRight, + Loader2, + RefreshCw, + Check, + AlertCircle, + Sparkles, + AlertTriangle, + Lightbulb, + Archive +} from 'lucide-react' + +interface EmailStats { + totalSorted: number + todaySorted: number + weekSorted: number + categories: Record + timeSaved: number +} + +interface EmailAccount { + id: string + email: string + provider: string + connected: boolean + lastSync?: string +} + +interface SortResult { + sorted: number + inboxCleared: number + categories: Record + timeSaved: { minutes: number; formatted: string } + highlights: Array<{ type: string; count: number; message: string }> + suggestions: Array<{ type: string; message: string }> + provider?: string + isDemo?: boolean +} + +interface Digest { + date: string + totalSorted: number + inboxCleared: number + timeSavedMinutes: number + stats: Record + highlights: Array<{ type: string; count: number; message: string }> + suggestions: Array<{ type: string; message: string }> + hasData: boolean +} + +export function Dashboard() { + const { user, logout } = useAuth() + const navigate = useNavigate() + const [stats, setStats] = useState(null) + const [accounts, setAccounts] = useState([]) + const [digest, setDigest] = useState(null) + const [loading, setLoading] = useState(true) + const [sorting, setSorting] = useState(false) + const [sortResult, setSortResult] = useState(null) + const [error, setError] = useState(null) + + useEffect(() => { + if (user?.$id) { + loadData() + } + }, [user]) + + const loadData = async () => { + if (!user?.$id) return + setLoading(true) + setError(null) + + try { + const [statsRes, accountsRes, digestRes] = await Promise.all([ + api.getEmailStats(user.$id), + api.getEmailAccounts(user.$id), + api.getDigest(user.$id), + ]) + + if (statsRes.data) setStats(statsRes.data) + if (accountsRes.data) setAccounts(accountsRes.data) + if (digestRes.data) setDigest(digestRes.data) + } catch (err) { + console.error('Error loading dashboard data:', err) + setError('Failed to load data') + } finally { + setLoading(false) + } + } + + const handleSortNow = async () => { + if (!user?.$id || accounts.length === 0) { + setError('Connect an email account first to start sorting.') + return + } + + setSorting(true) + setSortResult(null) + setError(null) + + try { + const result = await api.sortEmails(user.$id, accounts[0].id) + if (result.data) { + setSortResult(result.data) + // Refresh stats and digest + const [statsRes, digestRes] = await Promise.all([ + api.getEmailStats(user.$id), + api.getDigest(user.$id), + ]) + if (statsRes.data) setStats(statsRes.data) + if (digestRes.data) setDigest(digestRes.data) + } else if (result.error) { + setError(result.error.message || 'Sorting failed') + } + } catch (err) { + console.error('Error sorting emails:', err) + setError('Error sorting emails') + } finally { + setSorting(false) + } + } + + const handleConnectDemo = async () => { + if (!user?.$id) return + + setLoading(true) + setError(null) + + try { + const result = await api.connectDemoAccount(user.$id) + if (result.data) { + const accountsRes = await api.getEmailAccounts(user.$id) + if (accountsRes.data) setAccounts(accountsRes.data) + } else if (result.error) { + setError(result.error.message || 'Could not create demo account') + } + } catch (err) { + console.error('Error connecting demo:', err) + setError('Error creating demo account') + } finally { + setLoading(false) + } + } + + const handleLogout = async () => { + await logout() + navigate('/') + } + + const displayStats: EmailStats = stats || { + totalSorted: 0, + todaySorted: 0, + weekSorted: 0, + categories: {}, + timeSaved: 0, + } + + const categoryColors: Record = { + 'vip': 'bg-amber-500', + 'Important': 'bg-amber-500', + 'customers': 'bg-blue-500', + 'Clients': 'bg-blue-500', + 'invoices': 'bg-green-500', + 'Invoices': 'bg-green-500', + 'newsletters': 'bg-purple-500', + 'Newsletter': 'bg-purple-500', + 'social': 'bg-pink-500', + 'Social': 'bg-pink-500', + 'promotions': 'bg-orange-500', + 'Promotions': 'bg-orange-500', + 'security': 'bg-red-500', + 'Security': 'bg-red-500', + 'calendar': 'bg-indigo-500', + 'Calendar': 'bg-indigo-500', + 'review': 'bg-slate-500', + 'Review': 'bg-slate-500', + } + + const categoryLabels: Record = { + 'vip': 'Important', + 'customers': 'Clients', + 'invoices': 'Invoices', + 'newsletters': 'Newsletter', + 'social': 'Social', + 'promotions': 'Promotions', + 'security': 'Security', + 'calendar': 'Calendar', + 'review': 'Review', + } + + const formatCategoryName = (key: string) => categoryLabels[key] || key + + return ( +
+ {/* Header */} +
+
+
+ +
+ +
+ + EmailSorter + + + +
+ + +
+ + + + +
+
+
+
+ +
+ {/* Welcome section */} +
+

+ Welcome back{user?.name ? `, ${user.name}` : ''}! 👋 +

+

+ Your email overview for today. +

+
+ + {/* Error message */} + {error && ( +
+ +

{error}

+ +
+ )} + + {/* Sort Result Toast */} + {sortResult && ( +
+
+
+ +

Sorting complete!

+
+ {sortResult.isDemo && ( + + Demo + + )} +
+
+
+

Sorted

+

{sortResult.sorted}

+
+
+

Time saved

+

{sortResult.timeSaved.formatted}

+
+ {Object.entries(sortResult.categories).slice(0, 2).map(([cat, count]) => ( +
+

{formatCategoryName(cat)}

+

{count}

+
+ ))} +
+ {Object.keys(sortResult.categories).length > 2 && ( +
+

Categories:

+
+ {Object.entries(sortResult.categories).map(([cat, count]) => ( + + {formatCategoryName(cat)}: {count} + + ))} +
+
+ )} +
+ )} + + {loading ? ( +
+
+ +

Loading dashboard...

+
+
+ ) : ( + <> + {/* Daily Digest Card */} + {digest?.hasData && ( + + +
+
+
+ +
+
+

Today's Digest

+

{new Date(digest.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}

+
+
+ {digest.inboxCleared > 0 && ( + + + {digest.inboxCleared} + + )} +
+ +
+
+

Processed

+

{digest.totalSorted}

+
+
+

Cleared

+

{digest.inboxCleared}

+
+
+

Saved

+

+ {digest.timeSavedMinutes > 60 + ? `${Math.floor(digest.timeSavedMinutes / 60)}h ${digest.timeSavedMinutes % 60}m` + : `${digest.timeSavedMinutes}m`} +

+
+
+ + {/* Highlights */} + {digest.highlights.length > 0 && ( +
+

+ + Needs Attention +

+
+ {digest.highlights.map((highlight, idx) => ( +
+ {highlight.message} +
+ ))} +
+
+ )} + + {/* Suggestions */} + {digest.suggestions.length > 0 && ( +
+

+ + Suggestions +

+
+ {digest.suggestions.map((suggestion, idx) => ( +

+ {suggestion.message} +

+ ))} +
+
+ )} +
+
+ )} + + {/* Stats cards */} +
+ + + 60 + ? `${Math.floor(displayStats.timeSaved / 60)}h ${displayStats.timeSaved % 60}m` + : `${displayStats.timeSaved}m`} + subtitle="this week" + color="bg-green-500" + /> + +
+ +
+ {/* Categories breakdown */} + + + + + Categories Overview + + + Distribution this week + + + + {Object.keys(displayStats.categories).length > 0 ? ( +
+ {Object.entries(displayStats.categories).map(([category, count]) => { + const total = Object.values(displayStats.categories).reduce((a, b) => a + b, 0) + const percentage = total > 0 ? Math.round((count / total) * 100) : 0 + + return ( +
+
+
+
+ {formatCategoryName(category)} +
+
+ {count} + ({percentage}%) +
+
+
+
+
+
+ ) + })} +
+ ) : ( +
+ +

No category statistics yet

+

Start a sort to see statistics

+
+ )} + + + + {/* Connected accounts */} + + + + + Email Accounts + + + Connected mailboxes + + + + {accounts.length > 0 ? ( + accounts.map((account) => ( +
+
+
+ +
+
+

{account.email}

+

{account.provider}

+
+
+ + {account.connected ? 'Active' : 'Off'} + +
+ )) + ) : ( +
+
+ +
+

+ No email accounts connected +

+

+ Connect an account to get started +

+
+ )} + +
+ + {accounts.length === 0 && ( + + )} +
+
+
+
+ + {/* Quick actions */} +
+

Quick Actions

+
+ + navigate('/settings?tab=rules')} + /> + navigate('/settings?tab=vip')} + /> + {}} + disabled + /> +
+
+ + )} +
+
+ ) +} + +interface StatsCardProps { + icon: React.ElementType + title: string + value: string + subtitle: string + color: string +} + +function StatsCard({ icon: Icon, title, value, subtitle, color }: StatsCardProps) { + return ( + + +
+
+

{title}

+

{value}

+

{subtitle}

+
+
+ +
+
+
+
+ ) +} + +interface QuickActionProps { + icon: React.ElementType + title: string + description: string + onClick: () => void + disabled?: boolean + loading?: boolean + highlight?: boolean +} + +function QuickAction({ icon: Icon, title, description, onClick, disabled, loading, highlight }: QuickActionProps) { + return ( + + ) +} diff --git a/client/src/pages/ForgotPassword.tsx b/client/src/pages/ForgotPassword.tsx new file mode 100644 index 0000000..da2ee66 --- /dev/null +++ b/client/src/pages/ForgotPassword.tsx @@ -0,0 +1,131 @@ +import { useState } from 'react' +import { Link } from 'react-router-dom' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { auth } from '@/lib/appwrite' +import { Mail, ArrowLeft, Loader2, CheckCircle } from 'lucide-react' + +export function ForgotPassword() { + const [email, setEmail] = useState('') + const [loading, setLoading] = useState(false) + const [sent, setSent] = useState(false) + const [error, setError] = useState('') + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setError('') + setLoading(true) + + try { + await auth.forgotPassword(email) + setSent(true) + } catch (err: any) { + setError(err.message || 'Fehler beim Senden der E-Mail') + } finally { + setLoading(false) + } + } + + return ( +
+
+ {/* Logo */} + +
+ +
+ + EmailSorter + + + + + + Passwort vergessen? + + {sent + ? 'Prüfe dein E-Mail-Postfach' + : 'Gib deine E-Mail-Adresse ein und wir senden dir einen Link zum Zurücksetzen.' + } + + + + {sent ? ( +
+
+ +
+

E-Mail gesendet!

+

+ Wir haben dir eine E-Mail mit einem Link zum Zurücksetzen deines Passworts an {email} gesendet. +

+

+ Keine E-Mail erhalten? Prüfe deinen Spam-Ordner oder versuche es erneut. +

+
+ + + + +
+
+ ) : ( +
+ {error && ( +
+ {error} +
+ )} + +
+ + setEmail(e.target.value)} + required + autoFocus + /> +
+ + + +
+ + + Zurück zum Login + +
+
+ )} +
+
+
+
+ ) +} diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx new file mode 100644 index 0000000..7e62ab9 --- /dev/null +++ b/client/src/pages/Home.tsx @@ -0,0 +1,23 @@ +import { Navbar } from '@/components/landing/Navbar' +import { Hero } from '@/components/landing/Hero' +import { Features } from '@/components/landing/Features' +import { HowItWorks } from '@/components/landing/HowItWorks' +import { Pricing } from '@/components/landing/Pricing' +import { Testimonials } from '@/components/landing/Testimonials' +import { FAQ } from '@/components/landing/FAQ' +import { Footer } from '@/components/landing/Footer' + +export function Home() { + return ( +
+ + + + + + + +
+
+ ) +} diff --git a/client/src/pages/Imprint.tsx b/client/src/pages/Imprint.tsx new file mode 100644 index 0000000..028cc3a --- /dev/null +++ b/client/src/pages/Imprint.tsx @@ -0,0 +1,156 @@ +import { Link } from 'react-router-dom' +import { ArrowLeft, Building2 } from 'lucide-react' + +export function Imprint() { + return ( +
+ {/* Header */} +
+
+ + + Back to Home + +
+
+ + {/* Content */} +
+
+ {/* Title */} +
+
+ +
+
+

Impressum

+

Legal Information

+
+
+ + {/* Content - Placeholder for webklar.com content */} +
+

+ Note: This imprint is managed by webklar.com. Please refer to their imprint for detailed information. +

+ +
+

Information according to § 5 TMG

+ +
+
+

Operator

+

EmailSorter is operated by:

+

+ webklar.com
+ Kenso Grimm, Justin Klein +

+

+ For complete contact details and legal information, please visit:{' '} + + webklar.com/impressum + +

+
+ +
+

Contact

+
+

+ Email:{' '} + + support@webklar.com + +

+

+ Phone:{' '} + + +49 176 23726355 + + {' / '} + + +49 170 4969375 + +

+

+ For questions regarding EmailSorter specifically:{' '} + + support@emailsorter.com + +

+
+
+ +
+

Responsible for Content

+

+ The content of this website is the responsibility of webklar.com. + For detailed information, please refer to the official imprint at{' '} + + webklar.com/impressum + +

+
+ +
+

Liability for Links

+

+ Our website contains links to external websites. We have no influence on the content of these websites. + Therefore, we cannot assume any liability for these external contents. +

+
+ +
+

Copyright

+

+ The content and works on this website are subject to German copyright law. + Reproduction, processing, distribution, and any form of commercialization require the written consent of the respective author or creator. +

+
+
+ +
+

+ Important: This is a simplified version. For the complete and legally binding imprint, please visit{' '} + + webklar.com/impressum + +

+
+
+
+
+
+
+ ) +} diff --git a/client/src/pages/Login.tsx b/client/src/pages/Login.tsx new file mode 100644 index 0000000..d8232df --- /dev/null +++ b/client/src/pages/Login.tsx @@ -0,0 +1,142 @@ +import { useState } from 'react' +import { Link, useNavigate } from 'react-router-dom' +import { useAuth } from '@/context/AuthContext' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Mail, Lock, ArrowRight, AlertCircle } from 'lucide-react' + +export function Login() { + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState('') + const [loading, setLoading] = useState(false) + + const { login } = useAuth() + const navigate = useNavigate() + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setError('') + setLoading(true) + + try { + await login(email, password) + navigate('/dashboard') + } catch (err: any) { + setError(err.message || 'Login failed. Please check your credentials.') + } finally { + setLoading(false) + } + } + + return ( +
+ {/* Left side - Form */} +
+
+ {/* Logo */} + +
+ +
+ + EmailSorter + + + +

+ Welcome back +

+

+ Sign in to access your dashboard. +

+ + {/* Error message */} + {error && ( +
+ +

{error}

+
+ )} + + {/* Form */} +
+
+ +
+ + setEmail(e.target.value)} + className="pl-10 bg-slate-800 border-slate-700 text-white placeholder:text-slate-400 focus:border-primary-500" + required + /> +
+
+ +
+
+ + + Forgot? + +
+
+ + setPassword(e.target.value)} + className="pl-10 bg-slate-800 border-slate-700 text-white placeholder:text-slate-400 focus:border-primary-500" + required + /> +
+
+ + +
+ +

+ Don't have an account?{' '} + + Sign up free + +

+
+
+ + {/* Right side - Decorative */} +
+
+
+ +
+

+ Your inbox under control +

+

+ Thousands of users already trust EmailSorter for more productive email communication. +

+
+
+
+ ) +} diff --git a/client/src/pages/Privacy.tsx b/client/src/pages/Privacy.tsx new file mode 100644 index 0000000..497a5d6 --- /dev/null +++ b/client/src/pages/Privacy.tsx @@ -0,0 +1,168 @@ +import { Link } from 'react-router-dom' +import { ArrowLeft, Shield } from 'lucide-react' + +export function Privacy() { + return ( +
+ {/* Header */} +
+
+ + + Back to Home + +
+
+ + {/* Content */} +
+
+ {/* Title */} +
+
+ +
+
+

Privacy Policy

+

Last updated: {new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}

+
+
+ + {/* Content - Placeholder for webklar.com content */} +
+

+ Note: This privacy policy is managed by webklar.com. Please refer to their privacy policy for detailed information. +

+ +
+

Data Protection Information

+

+ EmailSorter is operated by webklar.com. The following privacy policy applies to the use of this website and our services. +

+ +

1. Responsible Party

+

+ The responsible party for data processing on this website is: +

+
+

+ webklar.com
+ Kenso Grimm, Justin Klein +

+

+ Contact:
+ Email: support@webklar.com
+ Phone: +49 176 23726355 +

+

+ For complete contact details, please refer to the Impressum. +

+
+ +

2. Data Collection and Processing

+

+ When you use EmailSorter, we collect and process the following data: +

+
    +
  • Account information (email address, name)
  • +
  • Email metadata (sender, subject, date) for sorting purposes
  • +
  • Usage statistics and preferences
  • +
  • Payment information (processed securely via Stripe)
  • +
+ +

3. Purpose of Data Processing

+

+ We process your data exclusively for the following purposes: +

+
    +
  • Providing and improving the EmailSorter service
  • +
  • Automated email sorting and categorization
  • +
  • Processing payments and subscriptions
  • +
  • Customer support and communication
  • +
+ +

4. Data Security

+

+ We implement appropriate technical and organizational measures to protect your data against unauthorized access, loss, or destruction. +

+ +

5. Your Rights

+

+ You have the right to: +

+
    +
  • Access your personal data
  • +
  • Correct inaccurate data
  • +
  • Request deletion of your data
  • +
  • Object to data processing
  • +
  • Data portability
  • +
+ +

6. Hosting and Third-Party Services

+

+ Hosting: Our website is hosted by Netlify, which acts as a data processor. +

+

+ We use the following third-party services: +

+
    +
  • Appwrite: User authentication and database
  • +
  • Stripe: Payment processing
  • +
  • Mistral AI: Email categorization
  • +
  • Gmail/Outlook API: Email access (with your explicit consent)
  • +
  • Plausible (optional): Privacy-friendly analytics tool, if enabled
  • +
+ +

6.1. Cookies and Tracking

+

+ We do not use external fonts or unnecessary cookies. If we use any tracking tools (such as Plausible), + they are privacy-friendly and do not store personal data. We only process personal data to the extent + that it is technically or organizationally necessary. +

+ +

7. Contact Form Data

+

+ Data that you send to us via contact forms will be stored and used for processing your inquiry. + This data will not be shared with third parties without your consent. +

+ +

8. Contact

+

+ For questions regarding data protection, please contact us: +

+
+

+ Email:{' '} + + support@webklar.com + +

+

+ Phone:{' '} + + +49 176 23726355 + +

+

+ For complete contact details, please refer to the Impressum. +

+
+ +
+

+ Important: This is a simplified version. For the complete and legally binding privacy policy, please visit{' '} + + webklar.com/datenschutz + +

+
+
+
+
+
+
+ ) +} diff --git a/client/src/pages/Register.tsx b/client/src/pages/Register.tsx new file mode 100644 index 0000000..445c073 --- /dev/null +++ b/client/src/pages/Register.tsx @@ -0,0 +1,226 @@ +import { useState, useEffect } from 'react' +import { Link, useNavigate, useSearchParams } from 'react-router-dom' +import { useAuth } from '@/context/AuthContext' +import { analytics } from '@/hooks/useAnalytics' +import { captureUTMParams } from '@/lib/analytics' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Badge } from '@/components/ui/badge' +import { Mail, Lock, User, ArrowRight, AlertCircle, Check, Sparkles } from 'lucide-react' + +export function Register() { + const [searchParams] = useSearchParams() + const selectedPlan = searchParams.get('plan') || 'pro' + + const [name, setName] = useState('') + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') + const [error, setError] = useState('') + const [loading, setLoading] = useState(false) + + const { register } = useAuth() + const navigate = useNavigate() + + // Capture UTM parameters on mount + useEffect(() => { + captureUTMParams() + }, []) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setError('') + + if (password !== confirmPassword) { + setError('Passwords do not match.') + return + } + + if (password.length < 8) { + setError('Password must be at least 8 characters long.') + return + } + + setLoading(true) + + try { + const user = await register(email, password, name) + + // Track signup conversion with UTM parameters + if (user?.$id) { + analytics.trackSignup(user.$id, email) + analytics.setUserId(user.$id) + } + + navigate('/setup') + } catch (err: any) { + setError(err.message || 'Registration failed. Please try again.') + } finally { + setLoading(false) + } + } + + return ( +
+ {/* Left side - Decorative */} +
+ {/* Background pattern */} +
+ +
+ + + 14-day free trial + + +

+ Start with EmailSorter today +

+ +
    + {[ + 'No credit card required', + 'Gmail & Outlook support', + 'AI-powered categorization', + 'Cancel anytime', + ].map((item, index) => ( +
  • +
    + +
    + {item} +
  • + ))} +
+ + {/* Plan indicator */} +
+

Selected plan

+

{selectedPlan}

+
+
+
+ + {/* Right side - Form */} +
+
+ {/* Logo */} + +
+ +
+ + EmailSorter + + + +

+ Create account +

+

+ Ready to go in less than a minute. +

+ + {/* Error message */} + {error && ( +
+ +

{error}

+
+ )} + + {/* Form */} +
+
+ +
+ + setName(e.target.value)} + className="pl-10" + /> +
+
+ +
+ +
+ + setEmail(e.target.value)} + className="pl-10" + required + /> +
+
+ +
+ +
+ + setPassword(e.target.value)} + className="pl-10" + required + /> +
+
+ +
+ +
+ + setConfirmPassword(e.target.value)} + className="pl-10" + required + /> +
+
+ + + +

+ By signing up, you agree to our{' '} + Terms of Service and{' '} + Privacy Policy. +

+
+ +

+ Already have an account?{' '} + + Sign in + +

+
+
+
+ ) +} diff --git a/client/src/pages/ResetPassword.tsx b/client/src/pages/ResetPassword.tsx new file mode 100644 index 0000000..8c417f6 --- /dev/null +++ b/client/src/pages/ResetPassword.tsx @@ -0,0 +1,225 @@ +import { useState, useEffect } from 'react' +import { Link, useSearchParams, useNavigate } from 'react-router-dom' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { auth } from '@/lib/appwrite' +import { Mail, Loader2, CheckCircle, XCircle, Eye, EyeOff } from 'lucide-react' + +export function ResetPassword() { + const [searchParams] = useSearchParams() + const navigate = useNavigate() + + const userId = searchParams.get('userId') + const secret = searchParams.get('secret') + + const [password, setPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') + const [showPassword, setShowPassword] = useState(false) + const [loading, setLoading] = useState(false) + const [success, setSuccess] = useState(false) + const [error, setError] = useState('') + + useEffect(() => { + if (!userId || !secret) { + setError('Ungültiger oder abgelaufener Link') + } + }, [userId, secret]) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setError('') + + if (password !== confirmPassword) { + setError('Passwörter stimmen nicht überein') + return + } + + if (password.length < 8) { + setError('Passwort muss mindestens 8 Zeichen lang sein') + return + } + + if (!userId || !secret) { + setError('Ungültiger Link') + return + } + + setLoading(true) + + try { + await auth.resetPassword(userId, secret, password) + setSuccess(true) + } catch (err: any) { + setError(err.message || 'Fehler beim Zurücksetzen des Passworts') + } finally { + setLoading(false) + } + } + + // Password strength indicator + const getPasswordStrength = () => { + if (!password) return { strength: 0, label: '', color: '' } + + let strength = 0 + if (password.length >= 8) strength++ + if (/[A-Z]/.test(password)) strength++ + if (/[a-z]/.test(password)) strength++ + if (/[0-9]/.test(password)) strength++ + if (/[^A-Za-z0-9]/.test(password)) strength++ + + const levels = [ + { strength: 1, label: 'Sehr schwach', color: 'bg-red-500' }, + { strength: 2, label: 'Schwach', color: 'bg-orange-500' }, + { strength: 3, label: 'Mittel', color: 'bg-yellow-500' }, + { strength: 4, label: 'Stark', color: 'bg-green-500' }, + { strength: 5, label: 'Sehr stark', color: 'bg-green-600' }, + ] + + return levels[strength - 1] || { strength: 0, label: '', color: '' } + } + + const passwordStrength = getPasswordStrength() + + return ( +
+
+ {/* Logo */} + +
+ +
+ + EmailSorter + + + + + + + {success ? 'Passwort geändert!' : 'Neues Passwort festlegen'} + + + {success + ? 'Dein Passwort wurde erfolgreich geändert.' + : 'Wähle ein sicheres neues Passwort für deinen Account.' + } + + + + {success ? ( +
+
+ +
+

+ Du kannst dich jetzt mit deinem neuen Passwort anmelden. +

+ +
+ ) : !userId || !secret ? ( +
+
+ +
+

Ungültiger Link

+

+ Dieser Link zum Zurücksetzen des Passworts ist ungültig oder abgelaufen. +

+ + + +
+ ) : ( +
+ {error && ( +
+ {error} +
+ )} + +
+ +
+ setPassword(e.target.value)} + required + autoFocus + /> + +
+ + {/* Password strength indicator */} + {password && ( +
+
+ {[1, 2, 3, 4, 5].map((level) => ( +
+ ))} +
+

+ {passwordStrength.label} +

+
+ )} +
+ +
+ + setConfirmPassword(e.target.value)} + required + /> + {confirmPassword && password !== confirmPassword && ( +

Passwörter stimmen nicht überein

+ )} +
+ + + + )} + + +
+
+ ) +} diff --git a/client/src/pages/Settings.tsx b/client/src/pages/Settings.tsx new file mode 100644 index 0000000..66eb28d --- /dev/null +++ b/client/src/pages/Settings.tsx @@ -0,0 +1,593 @@ +import { useState, useEffect } from 'react' +import { useNavigate, useSearchParams } from 'react-router-dom' +import { useAuth } from '@/context/AuthContext' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Badge } from '@/components/ui/badge' +import { api } from '@/lib/api' +import { + Mail, + User, + CreditCard, + Shield, + Settings as SettingsIcon, + ArrowLeft, + Plus, + Trash2, + Check, + X, + ExternalLink, + Loader2, + Crown, + Star, +} from 'lucide-react' + +type TabType = 'profile' | 'accounts' | 'vip' | 'rules' | 'subscription' + +interface EmailAccount { + id: string + email: string + provider: 'gmail' | 'outlook' + connected: boolean + lastSync?: string +} + +interface VIPSender { + email: string + name?: string +} + +interface SortRule { + id: string + name: string + condition: string + category: string + enabled: boolean +} + +interface Subscription { + status: string + plan: string + currentPeriodEnd?: string + cancelAtPeriodEnd?: boolean +} + +export function Settings() { + const { user } = useAuth() + const navigate = useNavigate() + const [searchParams, setSearchParams] = useSearchParams() + + const activeTab = (searchParams.get('tab') as TabType) || 'profile' + const [loading, setLoading] = useState(false) + const [saving, setSaving] = useState(false) + const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null) + + const [name, setName] = useState(user?.name || '') + const [email] = useState(user?.email || '') + const [accounts, setAccounts] = useState([]) + const [connectingProvider, setConnectingProvider] = useState(null) + const [vipSenders, setVipSenders] = useState([]) + const [newVipEmail, setNewVipEmail] = useState('') + const [rules, setRules] = useState([ + { id: '1', name: 'Boss Emails', condition: 'from:boss@company.com', category: 'Important', enabled: true }, + { id: '2', name: 'Support Tickets', condition: 'subject:Ticket #', category: 'Clients', enabled: true }, + ]) + const [subscription, setSubscription] = useState(null) + + useEffect(() => { + loadData() + }, [user]) + + const loadData = async () => { + if (!user?.$id) return + setLoading(true) + + try { + const [accountsRes, subsRes, prefsRes] = await Promise.all([ + api.getEmailAccounts(user.$id), + api.getSubscriptionStatus(user.$id), + api.getUserPreferences(user.$id), + ]) + + if (accountsRes.data) setAccounts(accountsRes.data) + if (subsRes.data) setSubscription(subsRes.data) + if (prefsRes.data?.vipSenders) setVipSenders(prefsRes.data.vipSenders) + } catch (error) { + console.error('Failed to load settings data:', error) + } finally { + setLoading(false) + } + } + + const setTab = (tab: TabType) => { + setSearchParams({ tab }) + setMessage(null) + } + + const showMessage = (type: 'success' | 'error', text: string) => { + setMessage({ type, text }) + setTimeout(() => setMessage(null), 5000) + } + + const handleSaveProfile = async () => { + setSaving(true) + try { + showMessage('success', 'Profile saved!') + } catch { + showMessage('error', 'Failed to save') + } finally { + setSaving(false) + } + } + + const handleConnectAccount = async (provider: 'gmail' | 'outlook') => { + if (!user?.$id) return + setConnectingProvider(provider) + + try { + const res = await api.getOAuthUrl(provider, user.$id) + if (res.data?.url) { + window.location.href = res.data.url + } + } catch { + showMessage('error', `Failed to connect ${provider}`) + setConnectingProvider(null) + } + } + + const handleDisconnectAccount = async (accountId: string) => { + if (!user?.$id) return + + try { + await api.disconnectEmailAccount(accountId, user.$id) + setAccounts(accounts.filter(a => a.id !== accountId)) + showMessage('success', 'Account disconnected') + } catch { + showMessage('error', 'Failed to disconnect') + } + } + + const handleAddVip = () => { + if (!newVipEmail.trim() || !newVipEmail.includes('@')) return + + if (vipSenders.some(v => v.email === newVipEmail)) { + showMessage('error', 'This email is already in the VIP list') + return + } + + setVipSenders([...vipSenders, { email: newVipEmail }]) + setNewVipEmail('') + showMessage('success', 'VIP added') + } + + const handleRemoveVip = (email: string) => { + setVipSenders(vipSenders.filter(v => v.email !== email)) + } + + const handleSaveVips = async () => { + if (!user?.$id) return + setSaving(true) + + try { + await api.saveUserPreferences(user.$id, { vipSenders }) + showMessage('success', 'VIP list saved!') + } catch { + showMessage('error', 'Failed to save') + } finally { + setSaving(false) + } + } + + const toggleRule = (ruleId: string) => { + setRules(rules.map(r => + r.id === ruleId ? { ...r, enabled: !r.enabled } : r + )) + } + + const handleManageSubscription = async () => { + if (!user?.$id) return + + try { + const res = await api.createPortalSession(user.$id) + if (res.data?.url) { + window.location.href = res.data.url + } + } catch { + showMessage('error', 'Failed to open customer portal') + } + } + + const handleUpgrade = async (plan: string) => { + if (!user?.$id) return + + try { + const res = await api.createSubscriptionCheckout(plan, user.$id, user.email) + if (res.data?.url) { + window.location.href = res.data.url + } + } catch { + showMessage('error', 'Failed to start checkout') + } + } + + const tabs = [ + { id: 'profile' as TabType, label: 'Profile', icon: User }, + { id: 'accounts' as TabType, label: 'Email Accounts', icon: Mail }, + { id: 'vip' as TabType, label: 'VIP List', icon: Star }, + { id: 'rules' as TabType, label: 'Sorting Rules', icon: SettingsIcon }, + { id: 'subscription' as TabType, label: 'Subscription', icon: CreditCard }, + ] + + return ( +
+
+
+
+ +
+ +

Settings

+
+
+
+
+ +
+ {message && ( +
+ {message.type === 'success' ? : } + {message.text} +
+ )} + +
+ + +
+ {loading ? ( +
+ +
+ ) : ( + <> + {activeTab === 'profile' && ( + + + Profile + Manage your personal information + + +
+
+ {name?.charAt(0)?.toUpperCase() || email?.charAt(0)?.toUpperCase() || 'U'} +
+
+

{name || 'User'}

+

{email}

+
+
+ +
+
+ + setName(e.target.value)} + placeholder="Your name" + /> +
+
+ + +

Email address cannot be changed

+
+
+ + +
+
+ )} + + {activeTab === 'accounts' && ( +
+ + + Connected Email Accounts + Connect your email accounts for automatic sorting + + + {accounts.length > 0 ? ( + accounts.map((account) => ( +
+
+
+ +
+
+

{account.email}

+

{account.provider}

+
+
+
+ + {account.connected ? 'Connected' : 'Disconnected'} + + +
+
+ )) + ) : ( +

No email accounts connected yet

+ )} +
+
+ + + + Add Account + Connect a new email account + + +
+ + + +
+
+
+
+ )} + + {activeTab === 'vip' && ( + + + + + VIP List + + Emails from these senders will always be marked as important + + +
+ setNewVipEmail(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleAddVip()} + /> + +
+ +
+ {vipSenders.length > 0 ? ( + vipSenders.map((vip) => ( +
+
+ + {vip.email} +
+ +
+ )) + ) : ( +

No VIP senders added yet

+ )} +
+ + {vipSenders.length > 0 && ( + + )} +
+
+ )} + + {activeTab === 'rules' && ( + + + Sorting Rules + Custom rules for email sorting + + + {rules.map((rule) => ( +
+
+ +
+

{rule.name}

+

{rule.condition}

+
+
+ {rule.category} +
+ ))} + + +
+
+ )} + + {activeTab === 'subscription' && ( +
+ + + Current Subscription + Manage your EmailSorter subscription + + +
+
+
+ +
+
+
+

{subscription?.plan || 'Trial'}

+ + {subscription?.status === 'active' ? 'Active' : 'Trial'} + +
+ {subscription?.currentPeriodEnd && ( +

+ Next billing: {new Date(subscription.currentPeriodEnd).toLocaleDateString('en-US')} +

+ )} +
+
+ +
+
+
+ + + + Available Plans + Choose the plan that fits you + + +
+ {[ + { id: 'basic', name: 'Basic', price: '9', features: ['1 email account', '500 emails/day', 'Standard support'] }, + { id: 'pro', name: 'Pro', price: '19', features: ['3 email accounts', 'Unlimited emails', 'Historical sorting', 'Priority support'], popular: true }, + { id: 'business', name: 'Business', price: '49', features: ['10 email accounts', 'Unlimited emails', 'Team features', 'API access', '24/7 support'] }, + ].map((plan) => ( +
+ {plan.popular && ( +
+ Popular +
+ )} +

{plan.name}

+
+ ${plan.price} + /month +
+
    + {plan.features.map((feature) => ( +
  • + + {feature} +
  • + ))} +
+ +
+ ))} +
+
+
+
+ )} + + )} +
+
+
+
+ ) +} diff --git a/client/src/pages/Setup.tsx b/client/src/pages/Setup.tsx new file mode 100644 index 0000000..acd5c21 --- /dev/null +++ b/client/src/pages/Setup.tsx @@ -0,0 +1,492 @@ +import { useState, useEffect } from 'react' +import { useNavigate, Link, useSearchParams } from 'react-router-dom' +import { useAuth } from '@/context/AuthContext' +import { Button } from '@/components/ui/button' +import { Card, CardContent } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' +import { api } from '@/lib/api' +import { + Mail, + ArrowRight, + ArrowLeft, + Check, + Sparkles, + ChevronRight, + Link2, + Settings, + Zap, + Loader2, + AlertCircle +} from 'lucide-react' + +type Step = 'connect' | 'preferences' | 'categories' | 'complete' + +export function Setup() { + const [searchParams] = useSearchParams() + const isFromCheckout = searchParams.get('subscription') === 'success' + const autoSetup = searchParams.get('setup') === 'auto' + + const [currentStep, setCurrentStep] = useState('connect') + const [connectedProvider, setConnectedProvider] = useState(null) + const [connectedEmail, setConnectedEmail] = useState(null) + const [connecting, setConnecting] = useState(null) + const [error, setError] = useState(null) + const [preferences, setPreferences] = useState({ + sortingStrictness: 'medium', + historicalSync: true, + }) + const [selectedCategories, setSelectedCategories] = useState([ + 'vip', 'customers', 'invoices', 'newsletters', 'social' + ]) + const [saving, setSaving] = useState(false) + const [checkingAccounts, setCheckingAccounts] = useState(isFromCheckout) + + const { user } = useAuth() + const navigate = useNavigate() + + // Check if user already has connected accounts after successful checkout + useEffect(() => { + if (isFromCheckout && user?.$id) { + const checkAccounts = async () => { + try { + const accountsRes = await api.getEmailAccounts(user.$id) + if (accountsRes.data && accountsRes.data.length > 0) { + // User already has accounts connected - redirect to dashboard + navigate('/dashboard?subscription=success&ready=true') + } else { + setCheckingAccounts(false) + } + } catch (err) { + console.error('Error checking accounts:', err) + setCheckingAccounts(false) + } + } + checkAccounts() + } + }, [isFromCheckout, user, navigate]) + + const steps: { id: Step; title: string; description: string }[] = [ + { id: 'connect', title: 'Connect email', description: 'Link your mailbox' }, + { id: 'preferences', title: 'Settings', description: 'Sorting preferences' }, + { id: 'categories', title: 'Categories', description: 'Choose categories' }, + { id: 'complete', title: 'Done', description: 'Get started!' }, + ] + + const stepIndex = steps.findIndex(s => s.id === currentStep) + + const handleConnectGmail = async () => { + if (!user?.$id) return + setConnecting('gmail') + setError(null) + + try { + const response = await api.getOAuthUrl('gmail', user.$id) + if (response.data?.url) { + window.location.href = response.data.url + } else { + setConnectedProvider('gmail') + setConnectedEmail(user.email) + setCurrentStep('preferences') + } + } catch (err) { + setError('Gmail connection failed. Please try again.') + } finally { + setConnecting(null) + } + } + + const handleConnectOutlook = async () => { + if (!user?.$id) return + setConnecting('outlook') + setError(null) + + try { + const response = await api.getOAuthUrl('outlook', user.$id) + if (response.data?.url) { + window.location.href = response.data.url + } else { + setConnectedProvider('outlook') + setConnectedEmail(user.email) + setCurrentStep('preferences') + } + } catch (err) { + setError('Outlook connection failed. Please try again.') + } finally { + setConnecting(null) + } + } + + const handleNext = () => { + const nextIndex = stepIndex + 1 + if (nextIndex < steps.length) { + setCurrentStep(steps[nextIndex].id) + } + } + + const handleBack = () => { + const prevIndex = stepIndex - 1 + if (prevIndex >= 0) { + setCurrentStep(steps[prevIndex].id) + } + } + + const handleComplete = async () => { + if (!user?.$id) { + navigate('/dashboard') + return + } + + setSaving(true) + try { + await api.saveUserPreferences(user.$id, { + vipSenders: [], + blockedSenders: [], + customRules: [], + priorityTopics: selectedCategories, + }) + } catch (err) { + console.error('Failed to save preferences:', err) + } finally { + setSaving(false) + navigate('/dashboard') + } + } + + const categories = [ + { id: 'vip', name: 'Important / VIP', description: 'Priority contacts', icon: '⭐', color: 'bg-amber-500' }, + { id: 'customers', name: 'Clients / Projects', description: 'Business correspondence', icon: '💼', color: 'bg-blue-500' }, + { id: 'invoices', name: 'Invoices / Receipts', description: 'Financial documents', icon: '📄', color: 'bg-green-500' }, + { id: 'newsletters', name: 'Newsletter', description: 'Subscribed newsletters', icon: '📰', color: 'bg-purple-500' }, + { id: 'social', name: 'Social / Platforms', description: 'LinkedIn, Twitter, etc.', icon: '👥', color: 'bg-pink-500' }, + { id: 'security', name: 'Security / 2FA', description: 'Security codes', icon: '🔐', color: 'bg-red-500' }, + { id: 'calendar', name: 'Calendar / Events', description: 'Appointments & invites', icon: '📅', color: 'bg-indigo-500' }, + { id: 'promotions', name: 'Promotions / Deals', description: 'Marketing emails', icon: '🏷️', color: 'bg-orange-500' }, + ] + + const toggleCategory = (id: string) => { + setSelectedCategories(prev => + prev.includes(id) + ? prev.filter(c => c !== id) + : [...prev, id] + ) + } + + // Show loading while checking accounts + if (checkingAccounts) { + return ( +
+
+ +

Setting up your account...

+
+
+ ) + } + + return ( +
+
+
+
+ +
+ +
+ + EmailSorter + + + +
+
+
+ + {/* Success message after checkout */} + {isFromCheckout && ( +
+
+
+ +
+
+

Payment successful!

+

+ Your subscription is active. Let's connect your email account to get started. +

+
+
+
+ )} + +
+ {/* Progress */} +
+
+ {steps.map((step, index) => ( +
+
+
+ {index < stepIndex ? : index + 1} +
+ +
+ {index < steps.length - 1 && ( +
+ )} +
+ ))} +
+
+ + {error && ( +
+ +

{error}

+
+ )} + +
+ {currentStep === 'connect' && ( +
+
+ +
+

Connect your email account

+

+ Choose your email provider. The connection is secure and your data stays private. +

+ +
+ + + +
+ +
+

+ 🔒 Your data is secure. We don't store email content and only have read access. +

+
+
+ )} + + {currentStep === 'preferences' && ( +
+
+
+ +
+

Sorting Settings

+

+ Customize how strictly the AI should sort your emails. +

+
+ + + +
+ +
+ {[ + { id: 'light', name: 'Light', desc: 'Only obvious distractions', emoji: '🌱' }, + { id: 'medium', name: 'Medium', desc: 'Balanced sorting', emoji: '⚖️' }, + { id: 'strict', name: 'Strict', desc: 'Inbox nearly empty', emoji: '🎯' }, + ].map((option) => ( + + ))} +
+
+ +
+
+

Historical emails

+

Analyze and sort last 30 days

+
+ +
+
+
+
+ )} + + {currentStep === 'categories' && ( +
+
+
+ +
+

Choose your categories

+

+ Which categories should your emails be sorted into? +

+
+ +
+ {categories.map((category) => ( + + ))} +
+ +

+ You can change these categories later in settings. +

+
+ )} + + {currentStep === 'complete' && ( +
+
+ +
+

All set! 🎉

+

+ Your email account is connected. The AI will now start intelligent sorting. +

+ +
+
+ +
+
+

+ {connectedProvider === 'gmail' ? 'Gmail' : connectedProvider === 'outlook' ? 'Outlook' : 'Email'} connected +

+

{connectedEmail || user?.email}

+
+ Active +
+ + +
+ )} +
+ + {currentStep !== 'connect' && currentStep !== 'complete' && ( +
+ + +
+ )} +
+
+ ) +} diff --git a/client/src/pages/VerifyEmail.tsx b/client/src/pages/VerifyEmail.tsx new file mode 100644 index 0000000..ea45d12 --- /dev/null +++ b/client/src/pages/VerifyEmail.tsx @@ -0,0 +1,154 @@ +import { useState, useEffect } from 'react' +import { Link, useSearchParams, useNavigate } from 'react-router-dom' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card' +import { auth } from '@/lib/appwrite' +import { Mail, Loader2, CheckCircle, XCircle, RefreshCw } from 'lucide-react' + +export function VerifyEmail() { + const [searchParams] = useSearchParams() + const navigate = useNavigate() + + const userId = searchParams.get('userId') + const secret = searchParams.get('secret') + + const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading') + const [error, setError] = useState('') + + useEffect(() => { + verifyEmail() + }, [userId, secret]) + + const verifyEmail = async () => { + if (!userId || !secret) { + setStatus('error') + setError('Ungültiger Verifizierungslink') + return + } + + try { + await auth.verifyEmail(userId, secret) + setStatus('success') + } catch (err: any) { + setStatus('error') + setError(err.message || 'Fehler bei der Verifizierung') + } + } + + const handleResendVerification = async () => { + setStatus('loading') + setError('') + + try { + await auth.sendVerification() + setError('') + alert('Neue Verifizierungs-E-Mail wurde gesendet!') + } catch (err: any) { + setError(err.message || 'Fehler beim Senden') + } finally { + setStatus('error') + } + } + + return ( +
+
+ {/* Logo */} + +
+ +
+ + EmailSorter + + + + + + + {status === 'loading' && 'E-Mail wird verifiziert...'} + {status === 'success' && 'E-Mail verifiziert!'} + {status === 'error' && 'Verifizierung fehlgeschlagen'} + + + {status === 'loading' && 'Bitte warte einen Moment.'} + {status === 'success' && 'Deine E-Mail-Adresse wurde erfolgreich bestätigt.'} + {status === 'error' && error} + + + + {status === 'loading' && ( +
+ +

Verifizierung läuft...

+
+ )} + + {status === 'success' && ( +
+
+ +
+ +
+
+

+ Dein Account ist jetzt vollständig aktiviert! +

+
+ +

+ Du kannst jetzt alle Features von EmailSorter nutzen. +

+ + +
+
+ )} + + {status === 'error' && ( +
+
+ +
+ +
+
+

+ {error || 'Der Verifizierungslink ist ungültig oder abgelaufen.'} +

+
+ +

+ Falls dein Link abgelaufen ist, kannst du eine neue Verifizierungs-E-Mail anfordern. +

+ +
+ + + +
+
+
+ )} +
+
+ + {/* Help text */} +

+ Probleme? Kontaktiere uns unter{' '} + + support@emailsorter.de + +

+
+
+ ) +} diff --git a/client/tsconfig.app.json b/client/tsconfig.app.json new file mode 100644 index 0000000..73feb92 --- /dev/null +++ b/client/tsconfig.app.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Path alias */ + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/client/tsconfig.json b/client/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/client/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/client/tsconfig.node.json b/client/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/client/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/client/vite.config.ts b/client/vite.config.ts new file mode 100644 index 0000000..8046901 --- /dev/null +++ b/client/vite.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' +import path from 'path' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react(), tailwindcss()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:3000', + changeOrigin: true, + }, + '/stripe': { + target: 'http://localhost:3000', + changeOrigin: true, + }, + }, + }, +}) diff --git a/marketing/INFLUENCER_OUTREACH_TEMPLATES.md b/marketing/INFLUENCER_OUTREACH_TEMPLATES.md new file mode 100644 index 0000000..1d288e8 --- /dev/null +++ b/marketing/INFLUENCER_OUTREACH_TEMPLATES.md @@ -0,0 +1,286 @@ +# Influencer Outreach Templates für EmailSorter + +## Outreach-Strategie Übersicht + +### Zielgruppen für Influencer + +1. **Productivity-Influencer** (10K-100K Follower) + - Tech-Tipps & Life Hacks + - Selbstständige/Freelancer + - Remote Work Content + +2. **Freelancer-Influencer** (5K-50K Follower) + - Business-Tipps + - Work-Life-Balance + - Productivity-Tools + +3. **Startup-Influencer** (10K-100K Follower) + - Entrepreneurship Content + - SaaS-Tools + - Business Growth + +4. **Tech-Influencer** (20K-200K Follower) + - App-Reviews + - AI-Tools + - Digital Solutions + +--- + +## Outreach Template #1: Personal & Warm (Empfohlen) + +**Betreff:** Können wir zusammenarbeiten? [Name] + EmailSorter + +``` +Hey [Name], + +ich verfolge deinen Content schon länger - besonders dein Video über [spezifisches Video/Thema] hat mir richtig geholfen. 💯 + +Ich habe EmailSorter gebaut, einen AI E-Mail-Sortierer, der genau das Problem löst, das du in deinem letzten Video angesprochen hast: [spezifisches Problem erwähnen]. + +Die Idee: +• KI sortiert E-Mails automatisch in Kategorien +• Spart 2+ Stunden pro Woche +• Perfekt für Freelancer & Unternehmer + +Ich würde es super finden, wenn du es mal testen könntest. Falls es dir gefällt, könnten wir über eine Kooperation sprechen: + +Option 1: Affiliate-Partnership (20-30% Provision pro Sale) +Option 2: Sponsored Post (Einmalzahlung) +Option 3: Kostenloser Zugang gegen ehrliche Review/Erwähnung + +Was denkst du? Ich schicke dir gerne einen kostenlosen Zugang zum Testen. + +Grüße, +[Dein Name] + +P.S.: Falls du nicht interessiert bist, ist das total okay - ich mag deinen Content trotzdem weiterhin! 😊 +``` + +--- + +## Outreach Template #2: Kürzer & Direkter + +**Betreff:** Kooperation: EmailSorter + [Kanal-Name] + +``` +Hi [Name], + +kurz und knapp: Ich habe EmailSorter gebaut - ein AI-Tool, das E-Mails automatisch sortiert. + +Ich dachte, es könnte zu deiner Audience passen, weil du Content über [Thema] machst. + +Möchtest du es kostenlos testen? Falls ja, können wir über eine Kooperation sprechen. + +Kurzfassung: +✓ AI sortiert E-Mails automatisch +✓ Spart Zeit (2+ Std/Woche) +✓ 14 Tage kostenloser Trial + +Sag einfach Bescheid, wenn Interesse besteht! + +Grüße, +[Dein Name] +``` + +--- + +## Outreach Template #3: Value-First (Keine Verkaufsaussage) + +**Betreff:** Kostenloses Tool für deine Community + +``` +Hey [Name], + +ich bin ein großer Fan deines Contents und wollte dir etwas zeigen, das deiner Community helfen könnte. + +Ich habe EmailSorter entwickelt - ein Tool, das E-Mails automatisch mit KI sortiert. Viele Freelancer/Unternehmer (wie deine Audience) haben das Problem mit unorganisierten E-Mails. + +Ich dachte, es könnte für dich und deine Follower interessant sein. Falls ja, kannst du es gerne kostenlos nutzen - keine Verpflichtungen! + +Falls du darüber berichten möchtest, wäre das natürlich super, aber kein Muss. + +Interesse? Dann melde dich einfach! + +Grüße, +[Dein Name] +``` + +--- + +## Follow-Up Template (nach 5-7 Tagen) + +**Betreff:** Quick Follow-Up: EmailSorter + +``` +Hey [Name], + +nur ein kurzes Follow-Up zu meiner letzten Nachricht - ich weiß, dass dein Postfach voll sein kann! 😅 + +Falls du EmailSorter mal testen möchtest, sag einfach Bescheid. Falls nicht, auch völlig in Ordnung. + +Grüße, +[Dein Name] +``` + +--- + +## Influencer-Liste Template (für interne Organisation) + +### Liste erstellen mit: + +1. **Influencer Name:** @username +2. **Plattform:** TikTok/YouTube/Instagram +3. **Follower:** ~XX.XXX +4. **Niche:** Productivity/Freelancer/Startup +5. **Engagement Rate:** ~X% (basierend auf letzten 10 Posts) +6. **Kontakt:** Email/DM +7. **Outreach Status:** Not Contacted / Contacted / Responded / In Discussion / Partnered / Declined +8. **Notes:** Besondere Infos, warum er/sie passt + +--- + +## Kooperations-Modelle (ausführlich) + +### Modell 1: Affiliate-Partnership +**Für dich:** 20-30% Provision pro verkauftem Abo +**Für Influencer:** Passives Einkommen, kein Risiko +**Vorteil:** Win-Win, langfristige Partnerschaft möglich + +**Vorlage für Affiliate-Agreement:** +``` +EmailSorter Affiliate-Partnership + +• Provision: 25% pro Sale (Monats- oder Jahresabo) +• Tracking: Einzigartiger Affiliate-Link +• Auszahlung: Monatlich, bei 50€+ Guthaben +• Materialien: Banner, Video-Assets, Promo-Codes +• Term: Keine Bindung, jederzeit kündbar +``` + +--- + +### Modell 2: Sponsored Post (Einmalzahlung) +**Für dich:** Einmalige Erwähnung/Review +**Für Influencer:** Festes Honorar (50-500€ je nach Reichweite) +**Vorteil:** Klare Vereinbarung, einmalig + +**Preis-Leitfaden:** +- 5K-10K Follower: 50-100€ +- 10K-50K Follower: 100-250€ +- 50K-100K Follower: 250-500€ +- 100K+ Follower: 500€+ (individuell verhandeln) + +--- + +### Modell 3: Product for Post (Kostenloser Zugang) +**Für dich:** Kostenloser Zugang (normal 9-19€/Monat) +**Für Influencer:** Ehrliche Review/Erwähnung +**Vorteil:** Niedriges Budget, authentisch + +**Erwartungen klar kommunizieren:** +- "Keine Verpflichtung, aber ehrliche Meinung wäre super" +- "Wenn es dir nicht gefällt, sag es auch" +- "Falls du es magst, wäre eine kurze Erwähnung toll" + +--- + +### Modell 4: Ambassador-Programm (Langfristig) +**Für dich:** Monatliche Vergütung + Provision +**Für Influencer:** Regelmäßige Erwähnungen, exklusiver Zugang +**Vorteil:** Langfristige Partnerschaft, höhere Conversion + +**Beispiel:** +``` +EmailSorter Ambassador + +• Monatliches Honorar: 200€ +• Plus 20% Provision pro Sale durch deinen Code +• Regelmäßige Content-Collabs (2-4x/Monat) +• Exklusiver Zugang zu neuen Features +• Term: 3-6 Monate (verlängerbar) +``` + +--- + +## Outreach-Checkliste + +### Vor dem Kontakt: +- [ ] Influencer-Profile durchgeschaut (letzte 10 Posts) +- [ ] Engagement Rate geprüft (echte Follower?) +- [ ] Kontakt-E-Mail/DM-Adresse gefunden +- [ ] Personalisierung vorbereitet (spezifisches Video/Thema erwähnen) +- [ ] Eigenes Profil professionell (falls er/sie zurückcheckt) + +### Bei Kontakt: +- [ ] Persönliche Nachricht (kein Spam-Template) +- [ ] Wert anbieten (kostenloser Zugang) +- [ ] Flexible Kooperations-Modelle +- [ ] Keine Druckausübung +- [ ] Freundlich & respektvoll + +### Nach Kontakt: +- [ ] Follow-Up nach 5-7 Tagen (falls keine Antwort) +- [ ] Geduldig sein (Influencer sind beschäftigt) +- [ ] Bei Absage höflich bleiben (vielleicht später) +- [ ] Bei Interesse schnell reagieren + +--- + +## Tracking & Organisation + +### Excel/Google Sheets Vorlage: + +| Name | Platform | Follower | Niche | Status | Contact Date | Response | Notes | +|------|----------|----------|-------|--------|--------------|----------|-------| +| @username1 | TikTok | 25K | Productivity | Contacted | 2026-01-20 | - | - | +| @username2 | YouTube | 45K | Freelancer | Partnered | 2026-01-18 | Yes | 20% affiliate | + +### UTM-Parameter für Tracking: + +Für jeden Influencer einen eigenen UTM: +``` +https://deine-domain.de/register?utm_source=influencer&utm_medium=tiktok&utm_campaign=@username&utm_content=promo-code-XYZ +``` + +--- + +## Erfolgsmetriken + +### KPIs für Influencer-Kooperationen: +- **Reach:** Wie viele Leute haben den Content gesehen? +- **Click-Through Rate:** Wie viele haben auf den Link geklickt? +- **Conversion Rate:** Wie viele haben sich registriert? +- **ROI:** Revenue durch Influencer vs. Kosten (Honorar/Provision) + +### Beispiel-Berechnung: +``` +Influencer mit 50K Follower +Post-Reach: 30.000 (60% von Followern) +CTR: 2% = 600 Clicks +Conversion: 5% = 30 Signups +Free-to-Paid: 10% = 3 Paid Users +MRR: 3 × 9€ = 27€/Monat + +Kosten: +- Sponsored Post: 250€ (einmalig) +- Oder Affiliate: 0€ vorab, ~7€/Monat Provision + +ROI nach 1 Jahr: +27€ × 12 = 324€ Revenue +- 250€ Kosten = 74€ Profit +Oder bei Affiliate: 324€ Revenue, ~81€ Provision = 243€ Profit +``` + +--- + +## Red Flags (Influencer meiden) + +❌ **Fake Follower:** Engagement Rate < 1% +❌ **Nicht passende Nische:** Komplett andere Zielgruppe +❌ **Zu teuer:** Forderung > 1000€ bei < 100K Follower +❌ **Unprofessionell:** Schlechte Kommunikation, unzuverlässig +❌ **Negative Reviews:** Bekannt für schlechte Kooperationen + +--- + +*Zuletzt aktualisiert: 2026-01-20* diff --git a/marketing/LOGO_ANLEITUNG.md b/marketing/LOGO_ANLEITUNG.md new file mode 100644 index 0000000..9a6d74b --- /dev/null +++ b/marketing/LOGO_ANLEITUNG.md @@ -0,0 +1,88 @@ +# Logo-Anleitung für TikTok + +## Verfügbare Logos + +Ich habe 3 verschiedene Logo-Varianten für dich erstellt: + +### 1. `logo-emailsorter-icon-only.svg` ⭐ **EMPFOHLEN für TikTok** +- **Nur Icon** (kein Text) +- Perfekt für Profilbilder (quadratisch, 1:1) +- Modern und clean +- Grüner Hintergrund mit weißem E-Mail-Icon + +### 2. `logo-emailsorter-simple.svg` +- Icon + Text "EmailSorter" +- Gut für Header-Bilder oder größere Formate +- Grüner Gradient-Hintergrund + +### 3. `logo-emailsorter.svg` +- Detailliertes Design mit mehr Elementen +- Gut für größere Formate + +--- + +## Verwendung für TikTok + +### Profilbild (empfohlen: `logo-emailsorter-icon-only.svg`) + +1. **SVG zu PNG konvertieren:** + - Öffne die SVG-Datei in einem Browser (Chrome, Edge, Firefox) + - Rechtsklick → "Bild speichern unter" → Als PNG speichern + - Oder nutze einen Online-Konverter: https://cloudconvert.com/svg-to-png + +2. **Größe für TikTok:** + - TikTok Profilbild: **400x400px** (Minimum) + - Optimal: **512x512px** oder **1024x1024px** + - Format: PNG oder JPG + +3. **Upload:** + - TikTok App öffnen + - Profil → Bearbeiten → Profilbild ändern + - PNG-Datei hochladen + +--- + +## Alternative: Logo online erstellen + +Falls du ein anderes Design möchtest, kannst du auch kostenlose Tools nutzen: + +### Kostenlose Logo-Ersteller: +- **Canva** (https://canva.com) - Viele Templates, einfach zu bedienen +- **LogoMaker** (https://logomaker.com) - KI-generierte Logos +- **Hatchful** (https://hatchful.shopify.com) - Shopify's kostenloser Logo-Maker + +### Design-Tipps für TikTok: +- **Einfach halten** - Weniger ist mehr +- **Hoher Kontrast** - Gut sichtbar auch als kleines Profilbild +- **Quadratisch** - 1:1 Format (512x512px optimal) +- **Grüne Farben** - Passt zu deiner Marke (EmailSorter) + +--- + +## Farben für dein Logo + +Basierend auf deinem Projekt: +- **Primär:** `#22c55e` (Grün) +- **Sekundär:** `#16a34a` (Dunkleres Grün) +- **Akzent:** `#10b981` (Emerald) +- **Hintergrund:** Weiß oder grüner Gradient + +--- + +## Schnellstart + +1. Öffne `logo-emailsorter-icon-only.svg` im Browser +2. Rechtsklick → "Bild speichern unter" → Als PNG speichern +3. Falls nötig, Größe anpassen (512x512px) +4. In TikTok hochladen + +**Fertig!** 🎉 + +--- + +## Weitere Optionen + +Falls du das Logo anpassen möchtest: +- SVG-Dateien können mit jedem Text-Editor bearbeitet werden +- Oder nutze kostenlose Tools wie Inkscape (https://inkscape.org) +- Ich kann auch Anpassungen vornehmen (andere Farben, Text, etc.) diff --git a/marketing/PRODUCT_HUNT_LAUNCH_GUIDE.md b/marketing/PRODUCT_HUNT_LAUNCH_GUIDE.md new file mode 100644 index 0000000..a1e8620 --- /dev/null +++ b/marketing/PRODUCT_HUNT_LAUNCH_GUIDE.md @@ -0,0 +1,428 @@ +# Product Hunt Launch Guide für EmailSorter + +## Vorbereitung (2-3 Wochen vor Launch) + +### Woche 1: Community Building + +**1. Account vorbereiten** +- [ ] Product Hunt Account erstellen/verifizieren +- [ ] Profilbild + Bio optimieren +- [ ] Maker-Profil vervollständigen + +**2. Hunter finden** +- [ ] Hunter mit großer Followerschaft identifizieren (10K+ Upvotes) +- [ ] Produkte in ähnlicher Nische finden (Email, Productivity, AI) +- [ ] Top Hunters kontaktieren (siehe Template unten) + +**3. Social Media vorbereiten** +- [ ] Twitter/X Account bereit für Launch +- [ ] LinkedIn Post vorbereiten +- [ ] Community-Gruppen identifizieren (Indie Hackers, etc.) + +--- + +### Woche 2: Assets erstellen + +**1. Launch-Assets** +- [ ] **Screenshots:** 3-5 hochwertige Produkt-Screenshots + - Dashboard Overview + - Before/After E-Mail-Inbox + - Kategorien-Ansicht + - Statistiken/Metrics + +- [ ] **GIFs/Videos:** Kurze Demo (30-60 Sek) + - Screen Recording der E-Mail-Sortierung + - Quick Setup Tutorial + - Before/After Transformation + +- [ ] **Thumbnail:** Eye-catching Hauptbild (1200x675px) + - Zeigt Hauptbenefit (organisierte E-Mails) + - Brand Colors + - Clear Value Proposition + +**2. Text-Content** +- [ ] **Tagline:** Ein Satz der das Problem löst + - Beispiele: + - "AI sorts your emails automatically, saving 2+ hours per week" + - "Your inbox, finally organized. AI-powered email categorization." + - "Never miss an important email again. AI auto-sorts your inbox." + +- [ ] **Description:** 2-3 Absätze + ``` + 📧 EmailSorter uses AI to automatically categorize your emails + while you sleep, saving you 2+ hours per week. + + Perfect for: + • Freelancers juggling multiple clients + • Entrepreneurs managing business communications + • Remote workers drowning in email chaos + + Features: + ✓ AI-powered categorization (Important, Invoices, Newsletters, etc.) + ✓ Gmail & Outlook support + ✓ Automatic sorting while you sleep + ✓ Privacy-first (GDPR compliant) + ✓ 14-day free trial, no credit card required + + Try EmailSorter today and reclaim your inbox! 🚀 + ``` + +- [ ] **Topics/Tags:** 5 relevant topics + - Email Management + - Productivity + - AI Tools + - SaaS + - Business Tools + +--- + +### Woche 3: Finale Vorbereitung + +**1. Landing Page optimieren** +- [ ] Product Hunt Badge/Banner hinzufügen ("Made the Front Page!") +- [ ] Launch-Day Countdown (optional) +- [ ] Social Proof vorbereiten + +**2. Email-Liste vorbereiten** +- [ ] E-Mail an bestehende Nutzer (wenn vorhanden) +- [ ] E-Mail an Newsletter-Subscribers +- [ ] E-Mail an Freunde/Family für Support + +**3. Support vorbereiten** +- [ ] FAQ-Artikel vorbereiten +- [ ] Support-Channels ready (Discord, Email, etc.) +- [ ] Onboarding optimieren für neue Nutzer + +--- + +## Launch-Day Strategie + +### Tag davor (Launch um 00:01 PST/PDT) + +**Am Vortag:** +- [ ] Posting um 00:01 PST veröffentlichen (8:01 Uhr CET / 9:01 Uhr CEST) +- [ ] Twitter Post sofort nach Launch +- [ ] LinkedIn Post veröffentlichen +- [ ] In Community-Gruppen posten (Indie Hackers, etc.) + +### Launch-Day Timeline + +**00:01 PST (Launch):** +- [ ] Posting auf Product Hunt live +- [ ] Social Media Posts veröffentlichen +- [ ] Freunde/Family benachrichtigen + +**08:00 PST (erste 8 Stunden):** +- [ ] Aktive Kommunikation mit Kommentaren +- [ ] Antworte auf alle Fragen +- [ ] Share in Communities + +**16:00 PST (Nachmittag):** +- [ ] Update-Post mit Status/Statistiken +- [ ] Weitere Communities ansprechen +- [ ] Dankes-Messages an Unterstützer + +**00:00 PST (Ende des Tages):** +- [ ] Finale Updates +- [ ] Dankes-Post für alle Unterstützer +- [ ] Ergebnis teilen + +--- + +## Launch-Day Checkliste + +### Pre-Launch (Tag davor) +- [ ] Hunter bestätigt +- [ ] Alle Assets hochgeladen +- [ ] Text-Content finalisiert +- [ ] Social Media Posts vorbereitet +- [ ] Email-Listen bereit +- [ ] Team/Freunde informiert + +### Launch-Moment (00:01 PST) +- [ ] Posting veröffentlichen +- [ ] Social Media Posts raus +- [ ] Direkte Nachrichten an Close Circle +- [ ] Post in Indie Hackers / Twitter +- [ ] Email an bestehende Nutzer (wenn vorhanden) + +### Während des Tages +- [ ] Alle Kommentare beantworten +- [ ] Upvotes anerkennen (thank you!) +- [ ] Aktive Community-Engagement +- [ ] Updates bei Meilensteinen teilen +- [ ] Probleme schnell lösen (wenn auftreten) + +### Nach dem Launch +- [ ] Ergebnis analysieren +- [ ] Top Kommentare beantworten +- [ ] Follow-up Posts in den nächsten Tagen +- [ ] Dankes-Messages an Top Supporters +- [ ] Badge auf Landing Page einbauen + +--- + +## Hunter-Outreach Template + +### Template #1: Erste Anfrage + +**Betreff:** Would you like to hunt EmailSorter on Product Hunt? + +``` +Hey [Hunter Name], + +I've been following your Product Hunt hunts and love how you support +AI and productivity tools. + +I've built EmailSorter - an AI tool that automatically sorts emails +into categories (Important, Invoices, Newsletters, etc.), saving +users 2+ hours per week. + +I'm planning to launch on [DATE] and would be honored if you'd +consider hunting it. + +I can provide: +✓ All launch assets (screenshots, GIFs, videos) +✓ Complete product description +✓ Launch strategy & timeline +✓ Your name in the maker list (of course!) + +Would you be interested? Happy to answer any questions! + +Best, +[Your Name] + +P.S.: You can test EmailSorter free for 14 days if you'd like to try it first. +``` + +--- + +### Template #2: Follow-Up (nach 5-7 Tagen) + +**Betreff:** Quick follow-up: EmailSorter Product Hunt launch + +``` +Hey [Hunter Name], + +Just a quick follow-up to my last message about EmailSorter. + +Launch is planned for [DATE]. Still interested in hunting it? + +If not, totally fine - just wanted to check in! + +Best, +[Your Name] +``` + +--- + +## Social Media Posts für Launch-Day + +### Twitter/X Post + +``` +🚀 Just launched EmailSorter on @ProductHunt! + +AI sorts your emails automatically, saving 2+ hours per week. + +📧 Never miss important emails again +⏰ Auto-sorting while you sleep +🎯 Perfect for freelancers & entrepreneurs + +Support us: [Product Hunt Link] + +#ProductHunt #EmailProductivity #AITools +``` + +--- + +### LinkedIn Post + +``` +I'm excited to share that EmailSorter is live on Product Hunt today! 🎉 + +As a freelancer, I was drowning in emails - spending hours sorting +newsletters from invoices, important messages from spam. + +So I built EmailSorter: AI that automatically categorizes your emails +while you sleep, saving 2+ hours per week. + +Perfect for: +✓ Freelancers juggling multiple clients +✓ Entrepreneurs managing business communications +✓ Anyone who wants their inbox organized + +Would love your support: [Product Hunt Link] + +If you have any questions or feedback, I'm here to chat! + +#ProductHunt #SaaS #EmailProductivity +``` + +--- + +### Indie Hackers Post + +``` +Launched EmailSorter on Product Hunt today! 🚀 + +TL;DR: AI sorts your emails automatically, saving 2+ hours per week. + +Problem: I was spending 2+ hours daily just sorting emails. +Solution: Built an AI that categorizes emails while I sleep. +Result: Reclaimed my inbox, saved 10+ hours per week. + +If you'd like to support the launch: [Product Hunt Link] + +Happy to answer questions about the build process, AI implementation, +or anything else! + +#ProductHunt #IndieHackers #SaaS +``` + +--- + +## Communities zum Teilen + +### Am Launch-Tag posten in: + +1. **Indie Hackers** + - [ ] Post in "Ship" Kategorie + - [ ] Einladung zu Feedback + +2. **Twitter/X** + - [ ] Post mit Product Hunt Link + - [ ] Hashtags: #ProductHunt #IndieHackers #SaaS + +3. **LinkedIn** + - [ ] Post mit Story + Link + - [ ] In relevanten Gruppen teilen + +4. **Reddit** (vorsichtig, Subreddit-Regeln prüfen!) + - [ ] r/SideProject (wenn erlaubt) + - [ ] r/productivity (als hilfreiches Tool, nicht direkt als Promotion) + +5. **Discord Communities** + - [ ] Indie Hackers Discord + - [ ] Product Hunt Discord + - [ ] SaaS-Communities + +--- + +## Erfolgs-Metriken + +### Ziele für Launch-Day + +**Optimistisch:** +- Top 5 Product of the Day +- 500+ Upvotes +- 50+ Kommentare +- Top 20 Product of the Week + +**Realistisch:** +- Top 10 Product of the Day +- 200-500 Upvotes +- 20-50 Kommentare +- Top 50 Product of the Week + +**Minimum:** +- Top 20 Product of the Day +- 100+ Upvotes +- 10+ Kommentare +- Top 100 Product of the Week + +### Nach-Launch Metriken + +**Woche 1:** +- Website Traffic Spike +- Signups durch Product Hunt +- Conversion Rate aus PH Traffic +- Press/Coverage (falls vorhanden) + +**Woche 2-4:** +- Langfristige Traffic-Steigerung +- SEO-Boost durch Backlinks +- Community-Wachstum +- Paid Conversions + +--- + +## FAQ für Launch-Day + +### Häufige Fragen vorbereiten: + +**Q: How does it work?** +A: Connect your Gmail or Outlook account. Our AI analyzes your emails + and sorts them into categories (Important, Invoices, Newsletters, etc.) + automatically. You can customize categories and rules to fit your needs. + +**Q: Is it safe? What about privacy?** +A: Yes, we're GDPR compliant and only access emails you explicitly + authorize. We never read email content beyond what's needed for + categorization. You can revoke access anytime. + +**Q: How much does it cost?** +A: 14-day free trial, no credit card required. After that: + - Basic: €9/month (500 emails/day) + - Pro: €19/month (Unlimited) + - Business: €49/month (Teams) + +**Q: What email providers are supported?** +A: Currently Gmail and Outlook. More providers coming soon! + +**Q: How accurate is the AI?** +A: Our AI has a 95%+ accuracy rate. You can always manually adjust + categories and train the AI on your preferences. + +--- + +## Post-Launch Strategie + +### Tag nach Launch +- [ ] Dankes-Post an Community +- [ ] Analyse der Metriken +- [ ] Follow-up mit Top Supportern +- [ ] Blog-Post über Launch-Erfahrung + +### Woche nach Launch +- [ ] Badge auf Landing Page +- [ ] Testimonials von PH-Nutzern sammeln +- [ ] Press-Kit für weitere Coverage +- [ ] Nächste Schritte planen + +### Monat nach Launch +- [ ] Retrospektive: Was hat funktioniert? +- [ ] Was kann besser werden beim nächsten Launch? +- [ ] Feedback in Produktentwicklung integrieren + +--- + +## Produkt-Hunt-Spezifische Tipps + +1. **Timing ist alles:** 00:01 PST Launch maximiert Visibility +2. **Engagement zählt:** Kommentare beantworten = mehr Upvotes +3. **Visuelle Assets:** GIFs/Videos performen besser als Screenshots +4. **Storytelling:** Persönliche Story > Feature-Liste +5. **Community:** Ehrliches Engagement > Selbstdarstellung + +--- + +## Notfall-Plan + +### Wenn Launch nicht wie geplant läuft: + +**Plan B: Re-Launch** +- Warte 6-12 Monate +- Verbessere Produkt basierend auf Feedback +- Baue Community vorher auf +- Versuche es erneut + +**Plan C: Soft Launch** +- Launch ohne Hunter +- Selbst posten +- Lerne aus dem Prozess +- Nächstes Mal mit Hunter + +--- + +*Zuletzt aktualisiert: 2026-01-20* diff --git a/marketing/README.md b/marketing/README.md new file mode 100644 index 0000000..02d4ad6 --- /dev/null +++ b/marketing/README.md @@ -0,0 +1,216 @@ +# Marketing & Promotion Strategie für EmailSorter + +## Übersicht + +Dieses Verzeichnis enthält alle Marketing-Dokumente und Strategien für die Promotion von EmailSorter. Die Strategie fokussiert sich auf organisches Wachstum durch TikTok, YouTube, Influencer-Marketing und Product Hunt. + +--- + +## Dokumentation + +### 1. **USPs_AND_MESSAGING.md** +Zielgruppen, Unique Selling Points, Messaging-Framework und Copy-Strategie für alle Kanäle. + +**Verwendung:** +- Landing Page Copy +- Social Media Posts +- Werbetexte +- Email-Kampagnen + +--- + +### 2. **TIKTOK_SETUP_GUIDE.md** +Kompletter Guide zum TikTok Account-Setup, Bio-Optimierung und ersten Schritten. + +**Verwendung:** +- Account-Erstellung +- Profil-Optimierung +- Erste Videos planen + +--- + +### 3. **TIKTOK_CONTENT_SCRIPTS.md** +10 sofort umsetzbare Video-Ideen mit Scripts, Hook-Formeln und Posting-Strategie. + +**Verwendung:** +- Video-Ideen generieren +- Scripts als Vorlage nutzen +- Content-Plan erstellen + +--- + +### 4. **YOUTUBE_STRATEGY.md** +Strategie für YouTube Shorts und Long-Form Content, SEO-Optimierung und Channel-Setup. + +**Verwendung:** +- YouTube Channel aufbauen +- Video-Formate planen +- SEO-Optimierung + +--- + +### 5. **INFLUENCER_OUTREACH_TEMPLATES.md** +Outreach-Templates, Kooperations-Modelle und Influencer-Liste-Vorlage. + +**Verwendung:** +- Influencer anschreiben +- Kooperationen verhandeln +- Outreach organisieren + +--- + +### 6. **PRODUCT_HUNT_LAUNCH_GUIDE.md** +Vollständiger Guide für Product Hunt Launch inkl. Vorbereitung, Launch-Day-Strategie und Assets. + +**Verwendung:** +- Product Hunt Launch vorbereiten +- Launch-Day planen +- Post-Launch Strategie + +--- + +## Implementierung im Code + +### Analytics & Tracking + +Das Analytics-System ist bereits implementiert: + +- **Frontend:** `client/src/lib/analytics.ts` - UTM-Parameter Tracking +- **Frontend Hook:** `client/src/hooks/useAnalytics.ts` - React Hook für Analytics +- **Backend:** `server/routes/analytics.mjs` - Analytics Endpoint + +**Verwendung:** +```typescript +import { trackSignup, trackPageView } from '@/lib/analytics' + +// Track signup conversion +trackSignup(userId, email) + +// Track page view +trackPageView() +``` + +**UTM-Parameter werden automatisch:** +- Aus URL gelesen +- In localStorage gespeichert (30 Tage) +- Bei Conversions mitgesendet + +--- + +## Quick-Start Checkliste + +### Phase 1: Vorbereitung (Woche 1-2) + +- [ ] USPs & Messaging definiert (`USPs_AND_MESSAGING.md`) +- [ ] Demo-Material erstellt (Screenshots, Videos) +- [ ] TikTok Account erstellt (`TIKTOK_SETUP_GUIDE.md`) +- [ ] YouTube Channel erstellt (`YOUTUBE_STRATEGY.md`) +- [ ] Analytics implementiert (bereits erledigt ✓) + +### Phase 2: Content-Start (Woche 2-6) + +- [ ] 5 TikTok Videos erstellt (`TIKTOK_CONTENT_SCRIPTS.md`) +- [ ] 3 YouTube Shorts erstellt +- [ ] Bio-Links mit UTM-Parametern eingerichtet +- [ ] Posting-Schedule etabliert + +### Phase 3: Wachstum (Woche 6-12) + +- [ ] Influencer-Outreach gestartet (`INFLUENCER_OUTREACH_TEMPLATES.md`) +- [ ] Product Hunt Launch vorbereitet (`PRODUCT_HUNT_LAUNCH_GUIDE.md`) +- [ ] Analytics ausgewertet und optimiert +- [ ] Content-Strategie verfeinert + +--- + +## UTM-Parameter Übersicht + +### Für Social Media Links: + +**TikTok:** +``` +https://deine-domain.de/register?utm_source=tiktok&utm_medium=social&utm_campaign=organic +``` + +**YouTube:** +``` +https://deine-domain.de/register?utm_source=youtube&utm_medium=video&utm_campaign=[video-title] +``` + +**Influencer:** +``` +https://deine-domain.de/register?utm_source=influencer&utm_medium=[platform]&utm_campaign=[influencer-name] +``` + +**Product Hunt:** +``` +https://deine-domain.de/register?utm_source=producthunt&utm_medium=referral&utm_campaign=launch +``` + +**Tracking:** Alle UTM-Parameter werden automatisch im Analytics-System erfasst. + +--- + +## Metriken & Erfolgsmessung + +### Wichtige KPIs: + +**Social Media:** +- Follower Growth +- Engagement Rate (Likes, Comments, Shares) +- Click-Through Rate (Bio-Link) + +**Website:** +- Traffic (nach Quelle) +- Conversion Rate (Visits → Signups) +- Free Trial → Paid Conversion + +**Product:** +- Signups pro Tag +- Free Trial Start Rate +- Paid Subscription Rate +- Monthly Recurring Revenue (MRR) + +### Analytics Dashboard: + +In Development: Analytics Dashboard für Marketing-Metriken (später implementieren). + +--- + +## Budget-Empfehlung + +### Minimal-Start (200-400€): +- Ring Light + Mikrofon: 50-100€ +- Canva Pro (Thumbnails): 12€/Monat +- Micro-Influencer Tests: 100-200€ +- **Gesamt Start:** ~200-400€ + +### Scaling (später): +- Ads (TikTok/YouTube): 50-100€/Monat +- Influencer-Partnerships: 200-500€/Monat +- Content-Creation Tools: 20-50€/Monat + +--- + +## Nächste Schritte + +1. **Heute:** TikTok Account erstellen, erstes Video aufnehmen +2. **Diese Woche:** 5 Videos posten, Bio optimieren +3. **Nächste Woche:** YouTube Channel starten, Analytics verfolgen +4. **In 4 Wochen:** Influencer-Outreach starten +5. **In 8-12 Wochen:** Product Hunt Launch vorbereiten + +--- + +## Support & Fragen + +Bei Fragen zur Marketing-Strategie: +1. Dokumentation in diesem Verzeichnis durchlesen +2. Best Practices aus den Guides befolgen +3. Experimentieren & Metriken verfolgen + +**Wichtigster Tipp:** Starte jetzt, nicht perfekt. Konsistenz schlägt Perfektion. + +--- + +*Zuletzt aktualisiert: 2026-01-20* diff --git a/marketing/TIKTOK_CONTENT_SCRIPTS.md b/marketing/TIKTOK_CONTENT_SCRIPTS.md new file mode 100644 index 0000000..58413d4 --- /dev/null +++ b/marketing/TIKTOK_CONTENT_SCRIPTS.md @@ -0,0 +1,527 @@ +# TikTok Content Scripts & Ideen für EmailSorter + +## Optimized Scripts (Based on User Input) + +### Script Variant 1: Direct Problem Hook (Recommended) + +**Timing: 15-25 seconds** + +**Hook (0-3 sec):** +*Show messy inbox on screen* +"Do you have a messy email inbox too?" + +**Problem (3-8 sec):** +*Point to screen showing hundreds of emails* +"Hundreds of emails, nothing organized, everything mixed together..." + +**Solution (8-18 sec):** +*Switch to EmailSorter dashboard* +"I built EmailSorter - an AI that automatically sorts your emails into categories and labels. You get fewer emails to deal with, everything organized." + +**CTA (18-25 sec):** +*Show before/after split screen* +"Try it today - link in bio, 14 days free!" + +--- + +### Script Variant 2: Relatable Hook (More Engaging) + +**Timing: 15-25 seconds** + +**Hook (0-3 sec):** +*Show messy inbox* +"Stop scrolling if your email inbox looks like this..." + +**Problem (3-9 sec):** +*Zoom in on chaotic inbox* +"Unorganized emails everywhere, can't find anything important, everything's a mess..." + +**Solution (9-18 sec):** +*Switch to EmailSorter* +"I created EmailSorter - AI automatically organizes your emails with smart labels and categories. Less clutter, more focus." + +**CTA (18-25 sec):** +"Want this too? Link in bio - try it free for 14 days!" + +--- + +### Script Variant 3: Question Hook (Best for Engagement) + +**Timing: 15-25 seconds** + +**Hook (0-3 sec):** +"Do you also have a messy email account?" + +**Problem (3-9 sec):** +*Show inbox with many unsorted emails* +"Tons of emails, nothing organized, everything mixed up..." + +**Solution (9-19 sec):** +*Show EmailSorter in action* +"Well, I built EmailSorter - an AI that sorts your emails automatically. You get organized inboxes with labels and categories, fewer emails to stress about." + +**CTA (19-25 sec):** +"Try it today - 14 days free trial, link in bio!" + +--- + +### Script Variant 4: POV Hook (Most Viral Potential) + +**Timing: 15-25 seconds** + +**Hook (0-3 sec):** +"POV: You open your email and see hundreds of unorganized messages..." + +**Problem (3-9 sec):** +*Show chaotic inbox* +"Everything's mixed together, can't tell what's important, total chaos..." + +**Solution (9-19 sec):** +*Show EmailSorter dashboard* +"That's why I built EmailSorter. AI automatically sorts your emails into categories with smart labels. Clean inbox, less stress." + +**CTA (19-25 sec):** +"Try it free for 14 days - link in bio!" + +--- + +### Visual Guide for All Scripts: + +**Screen Recording Sequence:** +1. **0-3 sec:** Show messy Gmail inbox (top right corner, many unread emails, no good labels) +2. **3-9 sec:** Zoom in on chaos, show confusion +3. **9-19 sec:** Switch to EmailSorter dashboard showing organized categories +4. **19-25 sec:** Show before/after comparison or product logo + +**Text Overlays (Optional):** +- "Before" / "After" labels +- "AI-Powered" badge +- "14 Days Free" callout +- Arrow pointing to link in bio + +**Hashtags:** +#emailhack #productivity #inboxzero #aitools #emailproductivity #freelancer #startup #techtools + +**Background Music Recommendations:** + +**Option 1: Upbeat/Productive (Recommended)** +- "Upbeat Corporate" or "Motivational Background" (TikTok Sounds) +- "Productivity Vibes" (TikTok Sounds) +- "Tech Startup" (TikTok Sounds) +- Energy level: Medium-high, keeps viewers engaged + +**Option 2: Trendy/Modern** +- Search TikTok for trending sounds in #productivity or #tech niches +- Use sounds with 100K+ uses for algorithm boost +- Examples: "That's a vibe" remixes, "Let's go" variations + +**Option 3: Calm/Professional** +- "Ambient Work Music" (TikTok Sounds) +- "Lo-Fi Productivity" (TikTok Sounds) +- "Calm Background Music" (TikTok Sounds) +- Better for tutorial/educational tone + +**Option 4: Trend-Jacking (Best for Viral)** +- Use currently trending sounds (check TikTok "Trending" tab) +- Look for sounds with rising usage in last 24-48 hours +- Match the energy to your script (upbeat for problem/solution, calm for tutorial) + +**Music Tips:** +- Keep volume at 20-30% (your voice should be clear) +- Use TikTok's built-in music library (copyright-safe) +- Test with headphones to ensure balance +- Avoid music with lyrics during your speaking parts +- Instrumental tracks work best for product demos + +**Quick Search Terms in TikTok:** +- "productivity music" +- "tech background" +- "corporate upbeat" +- "motivational instrumental" + +**Caption/Description Templates:** + +**Template 1: Direct & Simple (Recommended)** +``` +Do you also have a messy email inbox? 📧 + +I built EmailSorter - AI automatically sorts your emails into categories and labels. Less clutter, more focus! ✨ + +Try it free for 14 days - link in bio! 🔗 + +#emailhack #productivity #inboxzero #aitools #emailproductivity #freelancer #startup #techtools #smallbusiness #entrepreneur +``` + +**Template 2: Question-Based (Best for Engagement)** +``` +Stop scrolling if your email inbox looks like this... 😅 + +Hundreds of emails, nothing organized, everything mixed together. Sound familiar? + +I created EmailSorter to solve this - AI automatically organizes your emails with smart labels and categories. Game changer! 🚀 + +Try it free - link in bio! ↓ + +#emailhack #productivity #inboxzero #aitools #emailproductivity #freelancer #startup #techtools #remotework #lifehacks +``` + +**Template 3: Problem-Solution (Clear Value)** +``` +POV: You open your email and see hundreds of unorganized messages... 📬 + +That's why I built EmailSorter. AI automatically sorts your emails into categories with smart labels. Clean inbox, less stress! ✨ + +14 days free trial - no credit card required! Link in bio 🔗 + +#emailhack #productivity #inboxzero #aitools #emailproductivity #freelancer #startup #techtools #tech #smallbusiness +``` + +**Template 4: Short & Punchy (Viral Potential)** +``` +Messy inbox? I got you. 👇 + +EmailSorter = AI that organizes your emails automatically. + +Try it free - link in bio! 🔗 + +#emailhack #productivity #inboxzero #aitools #emailproductivity #freelancer #startup #techtools +``` + +**Complete Hashtag Strategy:** + +**Core Hashtags (Always Use - 3-5):** +- #emailhack (~500K posts) +- #productivity (~5M posts) +- #inboxzero (~200K posts) +- #aitools (~800K posts) +- #emailproductivity (~100K posts) + +**Niche Hashtags (Rotate - 3-5):** +- #freelancer (~2M posts) +- #startup (~3M posts) +- #techtools (~1M posts) +- #remotework (~2M posts) +- #emailmanagement (~50K posts) +- #smallbusiness (~5M posts) +- #entrepreneur (~10M posts) + +**Broad Hashtags (Use 1-2):** +- #tech (~20M posts) +- #lifehacks (~10M posts) +- #productivitytips (~500K posts) +- #businesshacks (~200K posts) + +**Hashtag Rules:** +- Use 8-12 hashtags total (optimal for TikTok algorithm) +- Mix high-volume (5M+) and niche (50K-500K) hashtags +- Don't use all trending hashtags (looks spammy) +- Test different combinations to see what works +- Update hashtags based on what's trending in your niche + +**Caption Best Practices:** +- Keep it under 150 characters for better engagement +- Use 1-2 emojis (not too many - looks unprofessional) +- Ask a question to encourage comments +- Include clear CTA (call-to-action) +- Use line breaks for readability +- Add "link in bio" or "🔗" to drive traffic + +**Engagement Boosters:** +- End with a question: "What's your biggest email problem?" +- Use "POV:", "Stop scrolling if...", "Do you also..." +- Add urgency: "Try it today", "Limited time" +- Show value: "14 days free", "No credit card required" + +--- + +## 10 Sofort umsetzbare Video-Ideen + +### 1. Problem-Hook Video + +**Hook (0-3 Sek):** +"POV: Du öffnest dein E-Mail-Postfach und siehst 500 ungelesene Nachrichten..." + +**Problem zeigen (3-10 Sek):** +*Screen Recording: Chaotischer Posteingang mit hunderten ungelesenen E-Mails* +"Ich weiß nicht mehr, was wichtig ist, was ein Newsletter ist, was eine Rechnung..." + +**Lösung (10-20 Sek):** +*Screen Recording: EmailSorter Dashboard* +"Also hab ich EmailSorter gebaut. Die KI sortiert meine E-Mails automatisch..." + +**CTA (20-30 Sek):** +*Show Before/After* +"Vorher: Chaos. Nachher: Alles sortiert. Link in Bio, 14 Tage kostenlos testen!" + +--- + +### 2. Transformation Video (Before/After) + +**Hook:** +"Vorher vs. Nachher: Meine E-Mail-Inbox nach 1 Woche EmailSorter" + +**Content:** +- Split Screen: Links "Vorher" (Chaos), Rechts "Nachher" (sortiert) +- Zeige Kategorien: Wichtig, Rechnungen, Newsletter, Social +- Statistik einblenden: "2 Stunden pro Woche gespart" + +**CTA:** +"Willst du auch? Link in Bio ↓" + +--- + +### 3. Tutorial "30 Sekunden Setup" + +**Hook:** +"So sortiere ich meine E-Mails in 30 Sekunden - ohne Aufwand" + +**Content:** +1. Öffne EmailSorter (3 Sek) +2. Klicke "E-Mail verbinden" (3 Sek) +3. Gmail/Outlook autorisieren (10 Sek) +4. Fertig! KI sortiert jetzt automatisch (5 Sek) +5. Zeige Ergebnis - sortierte E-Mails (9 Sek) + +**CTA:** +"14 Tage kostenlos testen - Link in Bio!" + +--- + +### 4. Behind the Scenes "Ich hab eine App gebaut" + +**Hook:** +"POV: Du programmierst nachts eine App, weil dich dein Posteingang nervt..." + +**Content:** +- Zeige Code-Snippets (nicht zu technisch) +- Zeige Entwicklungsprozess +- Zeige Resultat: Sortierte E-Mails +- Reaktion: "Es funktioniert tatsächlich!" + +**CTA:** +"Wenn ihr es auch nutzen wollt - Link in Bio. 14 Tage kostenlos!" + +--- + +### 5. Trend-Jacking "I Bet You Don't Know..." + +**Hook:** +"I bet you don't know that AI can sort your emails while you sleep" + +**Content:** +- Zeige EmailSorter in Aktion +- Zeige wie E-Mails über Nacht sortiert werden +- Zeige Dashboard mit Statistiken + +**CTA:** +"Try it free for 14 days - link in bio!" + +--- + +### 6. Pain Point "Relatable Moment" + +**Hook:** +"Stopp scrolling wenn du auch jeden Tag im E-Mail-Chaos versinkst" + +**Content:** +- Zeige typische Situation: Posteingang öffnen, überwältigt sein +- "Ich fühle dich" - Moment +- "Aber es gibt eine Lösung..." +- Zeige EmailSorter + +**CTA:** +"Falls du es auch brauchst - Link in Bio!" + +--- + +### 7. Statistik Video "Did You Know?" + +**Hook:** +"Did you know the average person spends 2+ hours per day on emails?" + +**Content:** +- Statistik-Grafik +- "Most of that time? Sorting and searching..." +- "What if AI could do that for you?" +- Zeige EmailSorter Demo + +**CTA:** +"Save time with EmailSorter - 14 days free trial!" + +--- + +### 8. User Testimonial (fake it till you make it) + +**Hook:** +"My inbox looked like this 1 week ago..." + +**Content:** +- Zeige "vorher" Screenshot +- "Now it's like this" - Nachher Screenshot +- "EmailSorter changed my life. No more email stress!" + +**CTA:** +"Try it yourself - link in bio!" + +--- + +### 9. Quick Tip Format + +**Hook:** +"Here's how I organize 500+ emails per day without spending time on it" + +**Content:** +- "I use EmailSorter - AI automatically sorts my emails" +- Zeige Kategorien +- Zeige wie einfach es ist +- "That's it. Game changer." + +**CTA:** +"14 days free trial - link in Bio!" + +--- + +### 10. Transformation Story + +**Hook:** +"I built this app because my inbox was driving me crazy" + +**Content:** +- Zeige Problem: Chaotischer Posteingang +- "So I coded EmailSorter..." +- Zeige Lösung: Sortierte E-Mails +- Zeige Statistiken: Zeit gespart, E-Mails sortiert + +**CTA:** +"Now you can use it too - free trial in bio!" + +--- + +## Viral-Formel Template + +``` +[HOOK 0-3 Sek] +→ [PROBLEM 3-10 Sek] +→ [LÖSUNG 10-20 Sek] +→ [CTA 20-30 Sek] +``` + +### Hook-Beispiele: +- "POV: Du..." +- "Stopp scrolling wenn..." +- "I bet you don't know..." +- "Did you know..." +- "Here's how I..." + +### Problem-Beispiele: +- Zeige chaotischen Posteingang +- Zeige verwirrte Reaktion +- Zeige Statistik über Zeitverschwendung + +### Lösungs-Beispiele: +- Screen Recording von EmailSorter +- Before/After Vergleich +- Demo der Funktionen + +### CTA-Beispiele: +- "Link in Bio - 14 Tage kostenlos!" +- "Try it free - link below!" +- "Willst du auch? Link in Bio ↓" + +--- + +## Hashtag-Strategie + +### Core Hashtags (immer verwenden): +- #productivity +- #emailhack +- #inboxzero + +### Niche Hashtags (abwechseln): +- #freelancer +- #startup +- #techtools +- #aitools +- #emailproductivity +- #remotework +- #emailmanagement + +### Trending Hashtags (wenn passend): +- #smallbusiness +- #entrepreneur +- #tech +- #lifehacks + +**Regel:** 3-5 Core Hashtags + 3-5 Niche Hashtags = 6-10 Hashtags total + +--- + +## Posting-Schedule (Erste 4 Wochen) + +### Woche 1: Aufbau +- **Montag:** Problem-Hook Video +- **Mittwoch:** Tutorial Video +- **Freitag:** Behind the Scenes + +### Woche 2: Variation +- **Montag:** Transformation Video +- **Mittwoch:** Quick Tip +- **Freitag:** Trend-Jacking + +### Woche 3: Engagement +- **Montag:** Statistik Video +- **Mittwoch:** Relatable Moment +- **Freitag:** User Testimonial + +### Woche 4: Optimierung +- Wiederhole die Videos mit den besten Engagement-Raten +- Teste neue Formate +- Antworte auf Kommentare + +--- + +## Engagement-Strategie + +### In den ersten 30 Minuten nach Posting: +1. Antworte auf jeden Kommentar +2. Like alle Kommentare +3. Stelle Rückfragen, um Diskussion anzuregen +4. Teile in anderen TikTok-Communitys (wenn erlaubt) + +### Content-Ideen für Kommentare: +- "Nutzt ihr EmailSorter schon?" +- "Wie organisiert ihr eure E-Mails?" +- "Was ist euer größtes E-Mail-Problem?" + +--- + +## TikTok Bio Template + +``` +⚡ KI sortiert deine E-Mails automatisch +📧 Spare 2+ Stunden pro Woche +🔗 14 Tage kostenlos testen +↓ Link in Bio ↓ + +#emailhack #productivity #inboxzero +``` + +--- + +## Analytics Tracking + +### Wichtige Metriken: +- **Views:** Ziel: 1000+ in Woche 1 +- **Engagement Rate:** Ziel: 5%+ +- **Click-Through Rate (Bio-Link):** Ziel: 1%+ +- **Conversion Rate (Visits → Signups):** Ziel: 2%+ + +### Tracking UTM-Parameter: +Bio-Link sollte sein: +``` +https://deine-domain.de/register?utm_source=tiktok&utm_medium=social&utm_campaign=organic +``` + +--- + +*Zuletzt aktualisiert: 2026-01-20* diff --git a/marketing/TIKTOK_LOGO_ANLEITUNG.md b/marketing/TIKTOK_LOGO_ANLEITUNG.md new file mode 100644 index 0000000..f3005bf --- /dev/null +++ b/marketing/TIKTOK_LOGO_ANLEITUNG.md @@ -0,0 +1,64 @@ +# TikTok Logo Upload - Schnellanleitung + +## 🚀 Schnellste Methode (HTML-Datei) + +1. **Öffne `logo-to-png.html`** im Browser (Chrome, Edge, Firefox) +2. **Wähle Größe:** 512x512px ist optimal für TikTok +3. **Klicke auf "Als PNG herunterladen"** +4. **Öffne TikTok App** → Profil → Bearbeiten → Profilbild ändern +5. **Lade die PNG-Datei hoch** + +**Fertig!** 🎉 + +--- + +## Alternative Methoden + +### Methode 1: Browser (Chrome/Edge) + +1. Öffne `logo-emailsorter.svg` im Browser +2. Rechtsklick auf das Logo → "Bild speichern unter" +3. Wähle Format: PNG +4. Größe: 512x512px (Browser konvertiert automatisch) + +### Methode 2: Online-Konverter + +1. Gehe zu: https://cloudconvert.com/svg-to-png +2. Lade `logo-emailsorter.svg` hoch +3. Wähle Größe: 512x512px +4. Konvertiere und lade herunter + +### Methode 3: Canva (falls du Anpassungen willst) + +1. Gehe zu: https://canva.com +2. Erstelle neues Design: 512x512px +3. Lade SVG hoch und exportiere als PNG + +--- + +## TikTok Profilbild Anforderungen + +- **Format:** PNG oder JPG +- **Größe:** Minimum 400x400px +- **Optimal:** 512x512px oder 1024x1024px +- **Format:** Quadratisch (1:1) + +--- + +## Dateien + +- `logo-emailsorter.svg` - Original SVG (skalierbar) +- `logo-to-png.html` - **Einfachste Methode** - Öffnen und Download +- `TIKTOK_LOGO_ANLEITUNG.md` - Diese Anleitung + +--- + +## Tipp + +Die **HTML-Datei** (`logo-to-png.html`) ist die einfachste Methode: +- Öffnen im Browser +- Größe wählen +- Download klicken +- Fertig! + +Keine zusätzlichen Tools nötig! ✨ diff --git a/marketing/TIKTOK_SETUP_GUIDE.md b/marketing/TIKTOK_SETUP_GUIDE.md new file mode 100644 index 0000000..99c6c61 --- /dev/null +++ b/marketing/TIKTOK_SETUP_GUIDE.md @@ -0,0 +1,318 @@ +# TikTok Setup Guide für EmailSorter + +## Account-Erstellung + +### 1. Account-Typ wählen + +**Option A: Personal Account (empfohlen für Start)** +- Einfacher zu starten +- Organisches Wachstum +- Authentischer wirkt + +**Option B: Business Account (später)** +- Analytics-Features +- Werbe-Tools +- Profile-Website-Link + +**Empfehlung:** Start mit Personal Account, upgrade später zu Business. + +--- + +### 2. Account-Setup + +**Username wählen:** +- Kurz, merkbar: `@emailsorter` oder `@inboxhack` +- Falls nicht verfügbar: `@emailsorter.app` oder `@emailsorterai` + +**Bio schreiben (150 Zeichen):** +``` +⚡ KI sortiert deine E-Mails automatisch +📧 Spare 2+ Stunden pro Woche +🔗 14 Tage kostenlos testen +↓ Link in Bio ↓ + +#emailhack #productivity #inboxzero +``` + +**Profilbild:** +- Logo oder Icon von EmailSorter +- Quadratisch (1:1), hochauflösend +- Erkennbar auch als Thumbnail + +**Website-Link:** +- Landing Page mit UTM-Parameter: + ``` + https://deine-domain.de/register?utm_source=tiktok&utm_medium=social&utm_campaign=organic + ``` + +--- + +## Erste Schritte + +### Tag 1: Account optimieren + +- [ ] Username setzen +- [ ] Bio schreiben +- [ ] Profilbild hochladen +- [ ] Website-Link einrichten +- [ ] Account auf "Public" stellen +- [ ] Erste 5 Videos vorbereiten + +### Tag 2-7: Content-Start + +- [ ] 5 Videos in Woche 1 posten +- [ ] Beste Zeiten testen (7-9 Uhr, 12-14 Uhr, 18-21 Uhr) +- [ ] Hashtags experimentieren +- [ ] Engagement verfolgen (Views, Likes, Comments) + +### Woche 2-4: Optimierung + +- [ ] Analytics auswerten (was funktioniert?) +- [ ] Mehr von erfolgreichen Formaten posten +- [ ] Community aufbauen (Kommentare beantworten) +- [ ] Cross-Promotion (YouTube, Instagram, etc.) + +--- + +## Content-Strategie + +### Video-Formate (siehe TIKTOK_CONTENT_SCRIPTS.md) + +1. **Problem-Hook Videos** (15-30 Sek) +2. **Tutorial Videos** (30-60 Sek) +3. **Transformation Videos** (Before/After) +4. **Behind the Scenes** (15-45 Sek) +5. **Trend-Jacking** (15-30 Sek) + +### Posting-Schedule + +**Erste 4 Wochen:** +- **Minimum:** 3x pro Woche +- **Optimal:** 5x pro Woche +- **Beste Zeiten:** 7-9 Uhr, 12-14 Uhr, 18-21 Uhr (CET) + +**Nach 4 Wochen:** +- Basierend auf Analytics optimieren +- Was funktioniert → mehr davon +- Was nicht funktioniert → weglassen/ändern + +--- + +## Hashtag-Strategie + +### Core Hashtags (immer verwenden): +- `#productivity` (~5M Posts) +- `#emailhack` (~500K Posts) +- `#inboxzero` (~200K Posts) + +### Niche Hashtags (abwechseln): +- `#freelancer` (~2M Posts) +- `#startup` (~3M Posts) +- `#techtools` (~1M Posts) +- `#aitools` (~800K Posts) +- `#emailproductivity` (~100K Posts) +- `#remotework` (~2M Posts) + +### Trending Hashtags (wenn passend): +- `#smallbusiness` (~5M Posts) +- `#entrepreneur` (~10M Posts) +- `#tech` (~20M Posts) +- `#lifehacks` (~10M Posts) + +**Regel:** 3-5 Core Hashtags + 3-5 Niche Hashtags = 6-10 Hashtags total + +--- + +## Posting-Best-Practices + +### Upload-Optimierung + +1. **Beste Zeiten:** + - 7-9 Uhr (Morgen-Commute) + - 12-14 Uhr (Mittagspause) + - 18-21 Uhr (Abend-Engagement) + +2. **Video-Qualität:** + - Vertikal (9:16), 1080x1920px optimal + - HD-Qualität (mind. 720p) + - Gute Beleuchtung & Audio + +3. **Hook in ersten 3 Sekunden:** + - Problem zeigen + - Frage stellen + - Interessantes Statement + +4. **Engagement fördern:** + - Frage im Video stellen + - Kommentar-Pin verwenden + - Auf Kommentare schnell antworten + +--- + +## Engagement-Strategie + +### In ersten 30 Minuten nach Posting: + +1. **Kommentare beantworten:** + - Jeden Kommentar innerhalb 1 Stunde beantworten + - Persönlich & authentisch + - Fragen stellen, Diskussion anregen + +2. **Engagement boosten:** + - Like eigene Videos (nicht übertreiben!) + - Share in Story (wenn relevant) + - In anderen relevanten Videos kommentieren + +3. **Community aufbauen:** + - Folge Accounts in ähnlicher Nische + - Engagiere dich in Kommentaren anderer Creator + - Teile wertvolle Tipps (nicht nur Promotion) + +--- + +## Analytics verstehen + +### Wichtige Metriken: + +**Views:** +- Ziel: 1000+ Views in ersten 48h (guter Start) +- Viral: 100K+ Views in 48h + +**Engagement Rate:** +- Likes: ~5-10% der Views (gut) +- Comments: ~1-2% der Views (gut) +- Shares: ~0.5-1% der Views (gut) + +**Click-Through Rate (Bio-Link):** +- Ziel: 1%+ der Views +- Gut: 2-5% der Views + +**Follower Growth:** +- Ziel: 50-100 Follower pro Woche (Start) +- Viral: 1000+ Follower pro Tag + +--- + +## Content-Plan (Monat 1) + +### Woche 1: Foundation +- **Montag:** Problem-Hook Video +- **Mittwoch:** Tutorial Video +- **Freitag:** Behind the Scenes + +### Woche 2: Variation +- **Montag:** Transformation Video +- **Mittwoch:** Quick Tip +- **Freitag:** Trend-Jacking + +### Woche 3: Engagement +- **Montag:** Statistik Video +- **Mittwoch:** Relatable Moment +- **Freitag:** User Testimonial + +### Woche 4: Optimization +- **Montag:** Best Performing Format (Repost) +- **Mittwoch:** Neue Format testen +- **Freitag:** Community Q&A + +--- + +## Equipment (Minimal) + +### Budget: 0€ +- **Kamera:** Handy-Kamera (Front-Kamera reicht) +- **Editor:** CapCut (kostenlos, sehr gut) +- **Audio:** Handy-Mikrofon (reicht für Start) + +### Budget: 50-100€ +- **Ring Light:** ~30-50€ (bessere Beleuchtung) +- **Mikrofon:** ~20-50€ (besseres Audio) +- **Dreibein:** ~10-20€ (stabilere Aufnahme) + +--- + +## Häufige Fehler vermeiden + +### ❌ Don'ts: + +1. **Zu viel auf einmal:** + - Nicht 5 Videos an einem Tag posten + - Spacing: Minimum 3-4 Stunden zwischen Posts + +2. **Zu wenig Variation:** + - Nicht immer gleiches Format + - Teste verschiedene Formate + +3. **Zu verkaufsorientiert:** + - Nicht nur "Kauft mein Produkt!" + - 80% Mehrwert, 20% Promotion + +4. **Schlechtes Timing:** + - Nicht mitten in der Nacht posten + - Beste Zeiten beachten + +5. **Zu viele Hashtags:** + - Maximal 10-12 Hashtags + - Nicht 30+ Hashtags (wirkt spammy) + +--- + +## Erfolgs-Tipps + +### ✅ Do's: + +1. **Konsistenz:** + - Regelmäßig posten (3-5x/Woche) + - Gleiche Tageszeiten (wenn möglich) + +2. **Authentizität:** + - Sei du selbst, nicht perfekt + - Zeige Persönlichkeit + +3. **Engagement:** + - Antworte auf Kommentare schnell + - Baue Community auf + +4. **Experimentieren:** + - Teste verschiedene Formate + - Was funktioniert → mehr davon + +5. **Geduld:** + - Wachstum dauert Zeit + - Erste 50 Videos sind zum Lernen + +--- + +## Quick-Start Checkliste + +### Heute (Tag 1): +- [ ] TikTok Account erstellen +- [ ] Username & Bio optimieren +- [ ] Profilbild hochladen +- [ ] Website-Link einrichten + +### Diese Woche: +- [ ] 5 Video-Ideen brainstormen +- [ ] Erstes Video aufnehmen (Handy reicht!) +- [ ] Video mit CapCut schneiden +- [ ] Erstes Video posten + +### Nächste Woche: +- [ ] 4 weitere Videos posten +- [ ] Analytics verfolgen +- [ ] Kommentare beantworten +- [ ] Community aufbauen + +--- + +## Wichtigster Tipp + +**Starte JETZT, nicht perfekt.** + +Dein erstes Video wird nicht viral gehen - und das ist okay. Die ersten 50 Videos sind zum Lernen da. Konsistenz schlägt Perfektion. + +Poste regelmäßig, sei authentisch, baue Community auf - der Rest kommt von alleine. + +--- + +*Zuletzt aktualisiert: 2026-01-20* diff --git a/marketing/USPs_AND_MESSAGING.md b/marketing/USPs_AND_MESSAGING.md new file mode 100644 index 0000000..c208eb8 --- /dev/null +++ b/marketing/USPs_AND_MESSAGING.md @@ -0,0 +1,263 @@ +# USPs & Messaging Strategie für EmailSorter + +## Zielgruppen-Definition + +### Primäre Zielgruppe 1: Freelancer & Selbstständige +**Problem:** +- Hohe E-Mail-Flut durch diverse Projekte und Kunden +- Schwierigkeit, wichtige E-Mails von Newslettern zu unterscheiden +- Zeitdruck - keine Zeit für manuelle Sortierung + +**USP:** "Spare 2+ Stunden pro Woche - KI sortiert deine E-Mails automatisch während du arbeitest" + +**Pain Points:** +- 📧 200+ E-Mails pro Tag +- ⏰ Keine Zeit für manuelle Organisation +- 😰 Angst, wichtige Kunden-Mails zu übersehen +- 💼 Work-Life-Balance durch E-Mail-Stress gestört + +--- + +### Primäre Zielgruppe 2: Kleine Unternehmer / Startup-Gründer +**Problem:** +- E-Mails von Investoren, Kunden, Partnern, Bewerbern vermischen sich +- Hoher Druck, alle wichtigen Nachrichten sofort zu sehen +- Begrenzte Ressourcen für manuelle Organisation + +**USP:** "Fokus aufs Business, nicht aufs E-Mail-Sortieren - AI macht's automatisch" + +**Pain Points:** +- 🚀 Gründungsstress + E-Mail-Chaos +- 📊 Wichtige Business-Infos gehen unter +- ⚡ Schnelle Reaktionszeiten nötig +- 💰 Kostenbewusstsein - keine teuren Tools + +--- + +### Sekundäre Zielgruppe: Remote Workers & Digitale Nomaden +**Problem:** +- Asynchrone Kommunikation über Zeitzonen +- Mehrere E-Mail-Accounts (privat + beruflich) +- Wichtige Infos auf Reise schnell finden + +**USP:** "Deine E-Mails sind organisiert, egal wo auf der Welt du bist" + +--- + +### Tertiäre Zielgruppe: Studenten mit Bewerbungsstress +**Problem:** +- Wichtige E-Mails von Unis, Praktika, Jobs gehen unter +- Viele Newsletter abonniert +- Unorganisiertes Postfach + +**USP:** "Nie wieder eine Zusage oder Deadline verpassen" + +--- + +## Core Messaging Framework + +### Haupt-Headlines (für Landing Page) + +1. **Problem-fokussiert:** + - "500 ungelesene E-Mails? Wir sortieren das." + - "Dein Posteingang. Endlich unter Kontrolle." + +2. **Lösungs-fokussiert:** + - "KI sortiert deine E-Mails, während du schläfst" + - "Automatische E-Mail-Organisation in 30 Sekunden eingerichtet" + +3. **Ergebnis-fokussiert:** + - "Spare 2+ Stunden pro Woche mit automatischer E-Mail-Sortierung" + - "Nie wieder wichtige E-Mails übersehen" + +--- + +## Unique Selling Points (USPs) + +### Top 3 USPs (Reihenfolge nach Wichtigkeit) + +1. **Zeitersparnis** + - "2+ Stunden pro Woche sparen" + - "Nie wieder manuell sortieren" + - Quantifizierbar, messbar + +2. **Automatisierung** + - "KI sortiert während du schläfst" + - "Einmal einrichten, immer organisiert" + - Fokus auf "Set & Forget" + +3. **Zuverlässigkeit** + - "Wichtige E-Mails landen immer da, wo sie sollen" + - "Nie wieder eine Deadline verpassen" + - Reduziert Stress & Angst + +--- + +## TikTok/YouTube Messaging + +### Hook-Formeln + +**Problem-Hook:** +- "POV: Du hast 500 ungelesene E-Mails..." +- "Stopp scrolling wenn du auch jeden Tag im E-Mail-Chaos versinkst" + +**Transformation-Hook:** +- "So sieht mein Posteingang nach 1 Woche EmailSorter aus" +- "Vorher vs. Nachher: Meine E-Mail-Inbox" + +**Kuriositäts-Hook:** +- "Ich hab eine App gebaut, die..." +- "Diese KI sortiert meine E-Mails automatisch" + +**Tutorial-Hook:** +- "So sortiere ich meine E-Mails in 30 Sekunden" +- "Inbox Zero ohne Aufwand - so geht's" + +--- + +## Social Media Bio-Texte + +### TikTok Bio (150 Zeichen) +``` +⚡ KI sortiert deine E-Mails automatisch +📧 Spare 2+ Stunden pro Woche +🔗 14 Tage kostenlos testen +↓ Link in Bio ↓ +``` + +### Instagram Bio +``` +⚡ Dein Posteingang. Endlich organisiert. +📧 KI-gestützte E-Mail-Sortierung +💼 Für Freelancer & Unternehmer +🔗 Kostenloser 14-Tage-Trial +``` + +### YouTube Channel Description +``` +EmailSorter hilft dir, deine E-Mails automatisch zu organisieren und Zeit zu sparen. + +Unsere KI sortiert deine E-Mails in relevante Kategorien: +• Wichtig & Dringend +• Rechnungen & Finanzen +• Newsletter & Marketing +• Social Media & Benachrichtigungen + +Perfekt für: +✓ Freelancer & Selbstständige +✓ Kleine Unternehmer & Gründer +✓ Remote Workers + +Teste EmailSorter kostenlos für 14 Tage - keine Kreditkarte erforderlich. +``` + +--- + +## Call-to-Action (CTA) Varianten + +### Primäre CTAs +- "Starte 14 Tage kostenlos" (am häufigsten) +- "Jetzt kostenlos testen" +- "Free Trial starten" + +### Sekundäre CTAs +- "Wie es funktioniert" +- "Demo ansehen" +- "Für Teams" + +### Conversion-optimierte CTAs +- "14 Tage kostenlos - keine Kreditkarte" +- "In 30 Sekunden eingerichtet" +- "Erste 50 E-Mails kostenlos" + +--- + +## Ton & Voice Guidelines + +### Brand Voice +- **Professionell, aber nahbar:** Nicht zu corporate, aber auch nicht zu lässig +- **Lösungsorientiert:** Fokus auf Benefits, nicht Features +- **Ehrlich & transparent:** Keine übertriebenen Versprechen +- **Enthusiastisch, nicht aufdringlich:** Positive Energie, aber authentisch + +### Do's ✅ +- Zeige echte Probleme der Zielgruppe +- Verwende konkrete Zahlen (2+ Stunden, 500 E-Mails) +- Erzähle Stories, nicht nur Features +- Zeige Vorher/Nachher Beispiele + +### Don'ts ❌ +- Keine technischen Jargons +- Keine übertriebenen Versprechen ("100% perfekt") +- Nicht zu viele Features auf einmal +- Kein Corporate-Sprech + +--- + +## Messaging nach Kanal + +### TikTok / Instagram Reels +- Kurz, visuell, emotional +- Problem → Lösung in 15-30 Sekunden +- Trends nutzen, authentisch wirken + +### YouTube +- Tiefer gehend, mehr Erklärung +- Tutorials & Case Studies +- Längere Formate möglich (5-15 Min) + +### Twitter/X +- Quick Tips & Insights +- Building in Public +- Engagement mit Community + +### LinkedIn +- Business-fokussiert +- B2B Messaging +- Erfolgs-Stories + +--- + +## Storytelling-Elemente + +### Founder Story (für Content) +``` +"Ich war frustriert. 300+ E-Mails jeden Tag, alles durcheinander, +wichtige Nachrichten gingen unter. Also hab ich EmailSorter gebaut +- jetzt sortiert die KI meine E-Mails automatisch, während ich +schlafe. Game Changer." +``` + +### User Success Story Template +``` +"Vorher: [Problem] +Nachher: [Lösung/Ergebnis] +Dank EmailSorter: [Quantifizierbares Ergebnis]" +``` + +--- + +## Keyword-Strategie + +### SEO Keywords +- E-Mail-Sortierung +- E-Mail-Organisation +- Automatische E-Mail-Sortierung +- Inbox Zero +- E-Mail-Produktivität +- E-Mail-Management Tool + +### Social Media Hashtags +- #emailhack +- #productivity +- #inboxzero +- #freelancer +- #startup +- #techtools +- #aitools +- #emailproductivity +- #remotework + +--- + +*Zuletzt aktualisiert: 2026-01-20* diff --git a/marketing/YOUTUBE_STRATEGY.md b/marketing/YOUTUBE_STRATEGY.md new file mode 100644 index 0000000..c9afdb2 --- /dev/null +++ b/marketing/YOUTUBE_STRATEGY.md @@ -0,0 +1,358 @@ +# YouTube Strategie für EmailSorter + +## Strategische Entscheidung: Shorts vs. Long-Form + +### Option A: YouTube Shorts (Empfohlen für Start) + +**Vorteile:** +- Schneller Wachstum möglich +- Weniger Aufwand (Content von TikTok recyclen) +- Algorithmus begünstigt Shorts +- Höhere Reichweite bei weniger Followern + +**Nachteile:** +- Weniger monétarisierbar +- Kurzlebig (trend-basiert) + +### Option B: Long-Form Content + +**Vorteile:** +- Bessere Monetarisierung +- Längerer Engagement +- Mehr SEO-Potenzial +- Authority Building + +**Nachteile:** +- Mehr Aufwand pro Video +- Längere Zeit bis Erfolg +- Braucht konsistente Produktion + +**Empfehlung:** Start mit Shorts, baue langfristig Long-Form auf. + +--- + +## YouTube Shorts Strategie + +### Content-Ideen (Recycling von TikTok) + +1. **Problem-Lösung Shorts** (15-60 Sek) + - "POV: Du öffnest dein Postfach..." + - Zeige Problem → Lösung in kurzer Zeit + +2. **Quick Tips** (30-60 Sek) + - "How to organize 500+ emails in 30 seconds" + - Step-by-Step Tutorials + +3. **Transformation Videos** (30-60 Sek) + - Before/After E-Mail-Inbox + - Split-Screen Videos + +4. **Behind the Scenes** (15-45 Sek) + - "I built an app that sorts my emails..." + - Entwickler-Story + +### Posting-Schedule + +- **Minimum:** 3x pro Woche +- **Optimal:** 5-7x pro Woche +- **Beste Zeiten:** 14-16 Uhr, 18-20 Uhr (CET) + +### Hashtags für Shorts + +- #shorts +- #emailhack +- #productivity +- #inboxzero +- #aitools +- #techtips + +--- + +## Long-Form Content Strategie + +### Video-Formate + +#### 1. Tutorials (5-10 Min) + +**Titel-Ideen:** +- "How to Organize Your Gmail Inbox in 10 Minutes" +- "Inbox Zero Method: Complete Guide" +- "Email Productivity Setup for Freelancers" + +**Struktur:** +1. Hook (0-30 Sek): Problem zeigen +2. Intro (30-60 Sek): Was wirst du lernen +3. Main Content (3-8 Min): Step-by-Step Tutorial +4. Demo EmailSorter (1-2 Min): Zeige Tool in Aktion +5. CTA (30 Sek): Free Trial Link + +--- + +#### 2. Case Studies (8-12 Min) + +**Titel-Ideen:** +- "I Automated My Email Sorting for 30 Days - Here's What Happened" +- "How I Saved 10+ Hours Per Week With Email Automation" +- "Email Chaos to Email Zen: My Transformation Story" + +**Struktur:** +1. Problem vorher (1-2 Min) +2. Lösung finden (2-3 Min) +3. Implementierung (2-3 Min) +4. Ergebnisse/Statistiken (2-3 Min) +5. Lessons Learned (1-2 Min) +6. CTA (30 Sek) + +--- + +#### 3. Product Reviews (5-8 Min) + +**Titel-Ideen:** +- "EmailSorter Review: Is AI Email Sorting Worth It?" +- "Testing EmailSorter for 7 Days - Honest Review" +- "EmailSorter vs. Manual Sorting: The Verdict" + +**Struktur:** +1. Intro (30 Sek) +2. Features & Setup (2-3 Min) +3. Pros & Cons (2-3 Min) +4. Who is it for? (1 Min) +5. Final Verdict (30 Sek) +6. CTA (30 Sek) + +--- + +#### 4. Building in Public (10-15 Min) + +**Titel-Ideen:** +- "Building EmailSorter: From Idea to Launch" +- "How I Built an AI Email Sorter in 30 Days" +- "My SaaS Journey: Building EmailSorter" + +**Struktur:** +1. The Problem (1-2 Min) +2. The Idea (1-2 Min) +3. Building Process (5-8 Min) +4. Challenges & Solutions (2-3 Min) +5. Launch & Results (1-2 Min) +6. CTA (30 Sek) + +--- + +### Long-Form Posting-Schedule + +- **Minimum:** 1x pro Woche +- **Optimal:** 2x pro Woche +- **Beste Zeiten:** Dienstag/Donnerstag, 16-18 Uhr (CET) + +--- + +## SEO-Strategie für YouTube + +### Titel-Optimierung + +**Formel:** [Keyword] + [Benefit] + [Time/Number] + [Audience] + +**Beispiele:** +- "Email Organization: Save 2 Hours Per Week (Freelancer Guide)" +- "AI Email Sorting: How to Organize 1000+ Emails Automatically" +- "Inbox Zero in 10 Minutes: Gmail Organization Tutorial" + +### Beschreibung-Template + +``` +🚀 EmailSorter: [Main Benefit] + +[2-3 Sätze über das Problem und die Lösung] + +📋 In diesem Video: +• [Point 1] +• [Point 2] +• [Point 3] +• [Point 4] + +⏱️ Timestamps: +0:00 - Intro +0:30 - [Section 1] +2:00 - [Section 2] +4:00 - [Section 3] + +🔗 EmailSorter kostenlos testen: +[Link zur Landing Page mit UTM: ?utm_source=youtube&utm_medium=video&utm_campaign=[video-title]] + +📧 Features: +✓ AI-powered email categorization +✓ Gmail & Outlook support +✓ Automatic sorting while you sleep +✓ 14-day free trial + +🔔 Abonniere für mehr Productivity-Tipps! + +#EmailProductivity #InboxZero #AITools #ProductivityTips +``` + +### Thumbnail-Gestaltung + +**Elemente:** +- Großer, klarer Text (Problem oder Benefit) +- Before/After Kontrast +- Gesicht (falls vor der Kamera) +- Bright, kontrastreiche Farben +- Brand Colors (EmailSorter) + +**Beispiele:** +- Split Screen: Chaos (links) vs. Organisiert (rechts) +- Großer Text: "500 E-Mails sortiert in 30 Sek" +- Emoji für visuellen Hook: 📧 ⚡ + +--- + +## Channel-Setup + +### Channel-Name +- "EmailSorter" oder "EmailSorter Official" + +### Channel-Beschreibung +``` +EmailSorter helps you organize your inbox automatically with AI. + +📧 Automatically categorize emails +⏰ Save 2+ hours per week +🚀 Perfect for freelancers & entrepreneurs + +Try EmailSorter free for 14 days - no credit card required! + +We share: +• Email productivity tips +• Inbox organization methods +• AI tool reviews +• Building in public (SaaS journey) + +Subscribe for weekly productivity content! +``` + +### Channel-Art/Banner +- Hero-Image mit Produkt +- Value Proposition klar sichtbar +- Link zur Website (in Banner-Link) + +### Playlists + +1. **Getting Started** + - Setup Tutorials + - Quick Tips + - Features Overview + +2. **Tutorials** + - Gmail Setup + - Outlook Setup + - Customization Guides + +3. **Case Studies** + - User Stories + - Before/After + - Results & Metrics + +4. **Building in Public** + - Development Updates + - Launch Stories + - Founder Journey + +--- + +## Cross-Promotion Strategie + +### TikTok → YouTube +- Ende jedes TikTok-Videos: "Full tutorial on YouTube! Link in bio" +- YouTube-Version ist länger, mehr Details + +### YouTube → Website +- Beschreibung: Link zur Landing Page +- Cards/End Screens: CTA zur Website +- Video-CTA: "14 Tage kostenlos testen - Link in Beschreibung" + +### YouTube → TikTok +- Shorts-Version von Long-Form Content +- "Full video on YouTube" im TikTok-Bio + +--- + +## Monetarisierung (später) + +### YouTube Monetization +- 1000 Subscriber + 4000 Watch Hours +- Affiliate Marketing +- Sponsorships (später) + +### Website Conversion +- Video-CTAs zur Landing Page +- UTM-Tracking für Attribution +- Conversion-Optimierung + +--- + +## Erfolgs-Metriken + +### Shorts Metriken +- **Views:** Ziel: 1000+ in ersten 48h +- **Subscribers:** Ziel: 50-100 pro Monat +- **Click-Through Rate (Bio-Link):** Ziel: 1-2% + +### Long-Form Metriken +- **Views:** Ziel: 500+ in ersten 7 Tagen +- **Watch Time:** Ziel: 50%+ Average View Duration +- **Subscribers:** Ziel: 100-200 pro Monat +- **Click-Through Rate:** Ziel: 2-5% + +--- + +## Content-Kalender (Monat 1) + +### Woche 1: Shorts Focus +- **Montag:** Problem-Hook Short +- **Mittwoch:** Tutorial Short +- **Freitag:** Transformation Short + +### Woche 2: Long-Form Start +- **Montag:** Tutorial Short +- **Mittwoch:** Long-Form Tutorial (5-8 Min) +- **Freitag:** Quick Tip Short + +### Woche 3: Mix +- **Montag:** Case Study Short +- **Mittwoch:** Behind the Scenes Short +- **Freitag:** Long-Form Case Study (8-10 Min) + +### Woche 4: Optimization +- **Montag:** Best Performing Short (Repost) +- **Mittwoch:** Product Review (5-8 Min) +- **Freitag:** Community Q&A Short + +--- + +## Equipment-Empfehlungen + +### Minimum Setup (Budget: 0€) +- Handy-Kamera (Front-Kamera reicht) +- KOSTNADELOSER Video-Editor (CapCut, DaVinci Resolve) +- Screen Recording (OBS Studio kostenlos) + +### Professional Setup (Budget: 200-500€) +- **Kamera:** Logitech C920/C930 (100€) +- **Mikrofon:** Blue Yeti oder Rode NT-USB (100-150€) +- **Licht:** Ring Light (50€) +- **Editor:** DaVinci Resolve (kostenlos) oder Final Cut Pro (300€) + +--- + +## Wichtigste Tipps + +1. **Konsistenz > Perfektion:** Regelmäßig posten > perfektes Video +2. **First 15 Sekunden:** Hook ist entscheidend +3. **Engagement fördern:** Fragen stellen, Kommentare anregen +4. **Thumbnail optimieren:** Teste verschiedene Designs +5. **SEO beachten:** Titel, Beschreibung, Tags optimieren + +--- + +*Zuletzt aktualisiert: 2026-01-20* diff --git a/marketing/logo-emailsorter-icon-only.svg b/marketing/logo-emailsorter-icon-only.svg new file mode 100644 index 0000000..18b2eb7 --- /dev/null +++ b/marketing/logo-emailsorter-icon-only.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/marketing/logo-emailsorter-simple.svg b/marketing/logo-emailsorter-simple.svg new file mode 100644 index 0000000..bc928b7 --- /dev/null +++ b/marketing/logo-emailsorter-simple.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + EmailSorter + diff --git a/marketing/logo-emailsorter.svg b/marketing/logo-emailsorter.svg new file mode 100644 index 0000000..76df4e0 --- /dev/null +++ b/marketing/logo-emailsorter.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + ES + diff --git a/marketing/logo-to-png.html b/marketing/logo-to-png.html new file mode 100644 index 0000000..d152699 --- /dev/null +++ b/marketing/logo-to-png.html @@ -0,0 +1,181 @@ + + + + + + EmailSorter Logo - PNG Export + + + +
+

📧 EmailSorter Logo

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

📋 So verwendest du das Logo für TikTok:

+
    +
  1. Größe wählen: Wähle oben eine Größe (512x512px ist optimal für TikTok)
  2. +
  3. Download: Klicke auf "Als PNG herunterladen"
  4. +
  5. TikTok öffnen: Öffne die TikTok App
  6. +
  7. Profil bearbeiten: Gehe zu deinem Profil → Bearbeiten
  8. +
  9. Profilbild ändern: Wähle "Profilbild ändern"
  10. +
  11. Hochladen: Wähle die heruntergeladene PNG-Datei
  12. +
  13. Fertig! 🎉
  14. +
+
+ + + + diff --git a/n8n/README.md b/n8n/README.md new file mode 100644 index 0000000..c42c83a --- /dev/null +++ b/n8n/README.md @@ -0,0 +1,99 @@ +# n8n Workflows für EmailSorter + +Dieses Verzeichnis enthält optionale n8n Workflows zur E-Mail-Automatisierung. + +## Voraussetzungen + +1. **n8n Installation** + - Cloud: [n8n.io](https://n8n.io) + - Self-hosted: `npm install -g n8n` oder Docker + +2. **Credentials einrichten** + - Gmail OAuth2 Credentials + - Mistral AI API Key (https://console.mistral.ai/) + - HTTP Header Auth für EmailSorter API + +## Workflows + +### email-sorter-workflow.json + +Haupt-Workflow für die E-Mail-Sortierung: + +1. **Webhook Trigger**: Empfängt Benachrichtigungen über neue E-Mails +2. **Gmail: E-Mail abrufen**: Holt E-Mail-Details +3. **Mistral AI: Klassifizieren**: KI kategorisiert die E-Mail +4. **Gmail: Label setzen**: Fügt entsprechendes Label hinzu +5. **Statistiken aktualisieren**: Sendet Update an EmailSorter API + +## Setup + +### 1. Workflow importieren + +```bash +# n8n CLI +n8n import:workflow --input=workflows/email-sorter-workflow.json + +# Oder über n8n UI: Settings > Import Workflow +``` + +### 2. Credentials konfigurieren + +#### Gmail OAuth2 +1. Google Cloud Console öffnen +2. OAuth 2.0 Client erstellen +3. In n8n: Credentials > Gmail OAuth2 > Authorize + +#### Mistral AI API +1. Mistral API Key erstellen auf console.mistral.ai +2. In n8n: Credentials > HTTP Header Auth +3. Name: "Authorization", Value: "Bearer YOUR_MISTRAL_API_KEY" + +### 3. Environment Variables + +```env +EMAILSORTER_API_URL=http://localhost:3000 +EMAILSORTER_API_KEY=your-api-key +``` + +### 4. Webhook URL notieren + +Nach dem Aktivieren des Workflows wird eine Webhook-URL generiert: +``` +https://your-n8n-instance.com/webhook/email-sorter-webhook +``` + +Diese URL im EmailSorter Backend konfigurieren. + +## Anpassungen + +### Eigene Kategorien hinzufügen + +Im "OpenAI: Klassifizieren" Node den System-Prompt anpassen: + +``` +Kategorisiere in: +- vip: Wichtige Kontakte +- clients: Kunden +- ... +- eigene_kategorie: Beschreibung +``` + +### Newsletter archivieren + +Nach dem Label-Node einen "Gmail: Archive" Node hinzufügen: +- Resource: Message +- Operation: Update +- Modify: Remove Label "INBOX" + +## Monitoring + +- Ausführungen in n8n UI überwachen +- Fehler-Benachrichtigungen einrichten +- Statistiken im EmailSorter Dashboard prüfen + +## Skalierung + +Für hohes E-Mail-Volumen: +- Queue Mode in n8n aktivieren +- Redis als Queue Backend nutzen +- Worker-Instanzen skalieren diff --git a/n8n/workflows/email-sorter-workflow.json b/n8n/workflows/email-sorter-workflow.json new file mode 100644 index 0000000..8e50076 --- /dev/null +++ b/n8n/workflows/email-sorter-workflow.json @@ -0,0 +1,225 @@ +{ + "name": "EmailSorter - Automatische E-Mail-Sortierung", + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "email-webhook", + "responseMode": "onReceived", + "options": {} + }, + "id": "webhook-trigger", + "name": "Email Webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 1, + "position": [250, 300], + "webhookId": "email-sorter-webhook" + }, + { + "parameters": { + "resource": "message", + "operation": "get", + "messageId": "={{ $json.emailId }}", + "options": { + "format": "metadata" + } + }, + "id": "gmail-get-email", + "name": "Gmail: E-Mail abrufen", + "type": "n8n-nodes-base.gmail", + "typeVersion": 2, + "position": [450, 200], + "credentials": { + "gmailOAuth2": { + "id": "gmail-credentials", + "name": "Gmail OAuth2" + } + } + }, + { + "parameters": { + "url": "https://api.mistral.ai/v1/chat/completions", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "{\n \"model\": \"mistral-small-latest\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": \"Du bist ein E-Mail-Klassifizierungs-Assistent. Kategorisiere E-Mails in: vip, clients, invoices, newsletters, promos, social, security, shipping, review. Antworte NUR mit dem Kategorienamen.\"\n },\n {\n \"role\": \"user\",\n \"content\": \"Von: {{ $json.from }}\\nBetreff: {{ $json.subject }}\\nVorschau: {{ $json.snippet }}\"\n }\n ],\n \"temperature\": 0.1,\n \"max_tokens\": 50\n}", + "options": {} + }, + "id": "mistral-classify", + "name": "Mistral AI: Klassifizieren", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4, + "position": [650, 200], + "credentials": { + "httpHeaderAuth": { + "id": "mistral-credentials", + "name": "Mistral API" + } + } + }, + { + "parameters": { + "conditions": { + "string": [ + { + "value1": "={{ $json.choices[0].message.content.trim() }}", + "operation": "contains", + "value2": "newsletter" + } + ] + } + }, + "id": "if-newsletter", + "name": "Ist Newsletter?", + "type": "n8n-nodes-base.if", + "typeVersion": 1, + "position": [850, 200] + }, + { + "parameters": { + "resource": "message", + "operation": "addLabels", + "messageId": "={{ $('Gmail: E-Mail abrufen').item.json.id }}", + "labelIds": ["EmailSorter/Newsletter"] + }, + "id": "gmail-label-newsletter", + "name": "Gmail: Newsletter Label", + "type": "n8n-nodes-base.gmail", + "typeVersion": 2, + "position": [1050, 100], + "credentials": { + "gmailOAuth2": { + "id": "gmail-credentials", + "name": "Gmail OAuth2" + } + } + }, + { + "parameters": { + "resource": "message", + "operation": "addLabels", + "messageId": "={{ $('Gmail: E-Mail abrufen').item.json.id }}", + "labelIds": ["={{ 'EmailSorter/' + $('Mistral AI: Klassifizieren').item.json.choices[0].message.content.trim() }}"] + }, + "id": "gmail-label-other", + "name": "Gmail: Kategorie Label", + "type": "n8n-nodes-base.gmail", + "typeVersion": 2, + "position": [1050, 300], + "credentials": { + "gmailOAuth2": { + "id": "gmail-credentials", + "name": "Gmail OAuth2" + } + } + }, + { + "parameters": { + "url": "={{ $env.EMAILSORTER_API_URL }}/api/email/stats/update", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "sendBody": true, + "bodyParameters": { + "parameters": [ + { + "name": "category", + "value": "={{ $('OpenAI: Klassifizieren').item.json.message.content.trim() }}" + }, + { + "name": "emailId", + "value": "={{ $('Gmail: E-Mail abrufen').item.json.id }}" + } + ] + }, + "options": {} + }, + "id": "http-update-stats", + "name": "Statistiken aktualisieren", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4, + "position": [1250, 200] + } + ], + "connections": { + "Email Webhook": { + "main": [ + [ + { + "node": "Gmail: E-Mail abrufen", + "type": "main", + "index": 0 + } + ] + ] + }, + "Gmail: E-Mail abrufen": { + "main": [ + [ + { + "node": "Mistral AI: Klassifizieren", + "type": "main", + "index": 0 + } + ] + ] + }, + "Mistral AI: Klassifizieren": { + "main": [ + [ + { + "node": "Ist Newsletter?", + "type": "main", + "index": 0 + } + ] + ] + }, + "Ist Newsletter?": { + "main": [ + [ + { + "node": "Gmail: Newsletter Label", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Gmail: Kategorie Label", + "type": "main", + "index": 0 + } + ] + ] + }, + "Gmail: Newsletter Label": { + "main": [ + [ + { + "node": "Statistiken aktualisieren", + "type": "main", + "index": 0 + } + ] + ] + }, + "Gmail: Kategorie Label": { + "main": [ + [ + { + "node": "Statistiken aktualisieren", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1" + }, + "staticData": null, + "tags": ["email", "automation", "ai"], + "pinData": {} +} diff --git a/public/cancel.html b/public/cancel.html index 0a24a85..7857b81 100644 --- a/public/cancel.html +++ b/public/cancel.html @@ -3,16 +3,98 @@ - Bezahlung abgebrochen - Email Sortierer + Zahlung Abgebrochen - EmailSorter + + -
-

❌ Bezahlung abgebrochen

-

Die Bezahlung wurde abgebrochen oder ist fehlgeschlagen.

-

Keine Sorge - es wurde nichts berechnet.

-

Du kannst jederzeit zurückkehren und den Vorgang erneut versuchen.

-
- Zurück zur Startseite +
+
+
+

Zahlung Abgebrochen

+

Die Zahlung wurde abgebrochen. Keine Sorge, es wurde nichts berechnet. Du kannst jederzeit erneut versuchen.

+
diff --git a/public/index.html b/public/index.html index 025ca9d..7e7c85f 100644 --- a/public/index.html +++ b/public/index.html @@ -3,258 +3,699 @@ - Email Sortierer + EmailSorter API + + + -
-

Email Sortierer

-
-