From abf761db07be18cd877eb8b2061261b880ec301b Mon Sep 17 00:00:00 2001 From: ANDJ Date: Thu, 22 Jan 2026 19:32:12 +0100 Subject: [PATCH] Email Sorter Beta Ich habe soweit automatisiert the Emails sortieren aber ich muss noch schauen was es fur bugs es gibt wenn die app online ist deswegen wurde ich mit diesen Commit die website veroffentlichen obwohjl es sein konnte das es noch nicht fertig ist und verkaufs bereit --- APPWRITE_SETUP.md | 97 + GOOGLE_OAUTH_SETUP.md | 75 + PROJECT_RENAME_GUIDE.md | 115 + PROJECT_REVIEW_SUMMARY.md | 151 + README.md | 382 +- SETUP_GUIDE.md | 373 ++ client/.gitignore | 24 + client/README.md | 73 + client/env.example | 10 + client/eslint.config.js | 23 + client/index.html | 17 + client/package-lock.json | 4954 ++++++++++++++++ client/package.json | 44 + client/public/vite.svg | 1 + client/src/App.tsx | 142 + client/src/assets/react.svg | 1 + client/src/components/landing/FAQ.tsx | 119 + client/src/components/landing/Features.tsx | 137 + client/src/components/landing/Footer.tsx | 185 + client/src/components/landing/Hero.tsx | 179 + client/src/components/landing/HowItWorks.tsx | 111 + client/src/components/landing/Navbar.tsx | 159 + client/src/components/landing/Pricing.tsx | 193 + .../src/components/landing/Testimonials.tsx | 67 + client/src/components/ui/badge.tsx | 39 + client/src/components/ui/button.tsx | 59 + client/src/components/ui/card.tsx | 78 + client/src/components/ui/input.tsx | 32 + client/src/components/ui/label.tsx | 23 + client/src/context/AuthContext.tsx | 74 + client/src/hooks/useAnalytics.ts | 53 + client/src/index.css | 144 + client/src/lib/analytics.ts | 314 + client/src/lib/api.ts | 329 ++ client/src/lib/appwrite.ts | 81 + client/src/lib/utils.ts | 6 + client/src/main.tsx | 10 + client/src/pages/Dashboard.tsx | 707 +++ client/src/pages/ForgotPassword.tsx | 131 + client/src/pages/Home.tsx | 23 + client/src/pages/Imprint.tsx | 156 + client/src/pages/Login.tsx | 142 + client/src/pages/Privacy.tsx | 168 + client/src/pages/Register.tsx | 226 + client/src/pages/ResetPassword.tsx | 225 + client/src/pages/Settings.tsx | 593 ++ client/src/pages/Setup.tsx | 492 ++ client/src/pages/VerifyEmail.tsx | 154 + client/tsconfig.app.json | 34 + client/tsconfig.json | 7 + client/tsconfig.node.json | 26 + client/vite.config.ts | 27 + marketing/INFLUENCER_OUTREACH_TEMPLATES.md | 286 + marketing/LOGO_ANLEITUNG.md | 88 + marketing/PRODUCT_HUNT_LAUNCH_GUIDE.md | 428 ++ marketing/README.md | 216 + marketing/TIKTOK_CONTENT_SCRIPTS.md | 527 ++ marketing/TIKTOK_LOGO_ANLEITUNG.md | 64 + marketing/TIKTOK_SETUP_GUIDE.md | 318 ++ marketing/USPs_AND_MESSAGING.md | 263 + marketing/YOUTUBE_STRATEGY.md | 358 ++ marketing/logo-emailsorter-icon-only.svg | 35 + marketing/logo-emailsorter-simple.svg | 32 + marketing/logo-emailsorter.svg | 25 + marketing/logo-to-png.html | 181 + n8n/README.md | 99 + n8n/workflows/email-sorter-workflow.json | 225 + public/cancel.html | 98 +- public/index.html | 911 ++- public/success.html | 129 +- server/.env | 51 +- server/bootstrap-v2.mjs | 225 + server/cleanup.mjs | 45 +- server/config/index.mjs | 137 + server/env.example | 72 + server/index.mjs | 359 +- server/middleware/errorHandler.mjs | 106 + server/middleware/logger.mjs | 134 + server/middleware/rateLimit.mjs | 96 + server/middleware/validate.mjs | 131 + server/node_modules/.package-lock.json | 455 +- server/node_modules/dotenv/CHANGELOG.md | 80 +- server/node_modules/dotenv/README-es.md | 18 +- server/node_modules/dotenv/README.md | 65 +- server/node_modules/dotenv/lib/main.d.ts | 23 +- server/node_modules/dotenv/lib/main.js | 72 +- server/node_modules/dotenv/package.json | 6 +- server/node_modules/node-appwrite/LICENSE | 2 +- server/node_modules/node-appwrite/README.md | 69 +- .../node-appwrite/dist/client.d.mts | 10 - .../node-appwrite/dist/client.d.ts | 10 - .../node_modules/node-appwrite/dist/client.js | 27 +- .../node-appwrite/dist/client.js.map | 2 +- .../node-appwrite/dist/client.mjs | 27 +- .../node-appwrite/dist/client.mjs.map | 2 +- .../node-appwrite/dist/enums/adapter.d.mts | 6 - .../node-appwrite/dist/enums/adapter.d.ts | 6 - .../node-appwrite/dist/enums/adapter.js | 11 - .../node-appwrite/dist/enums/adapter.js.map | 1 - .../node-appwrite/dist/enums/adapter.mjs | 10 - .../node-appwrite/dist/enums/adapter.mjs.map | 1 - .../dist/enums/attribute-status.d.mts | 9 - .../dist/enums/attribute-status.d.ts | 9 - .../dist/enums/attribute-status.js | 14 - .../dist/enums/attribute-status.js.map | 1 - .../dist/enums/attribute-status.mjs | 13 - .../dist/enums/attribute-status.mjs.map | 1 - .../dist/enums/build-runtime.d.mts | 71 - .../dist/enums/build-runtime.d.ts | 71 - .../node-appwrite/dist/enums/build-runtime.js | 76 - .../dist/enums/build-runtime.js.map | 1 - .../dist/enums/build-runtime.mjs | 75 - .../dist/enums/build-runtime.mjs.map | 1 - .../dist/enums/column-status.d.mts | 9 - .../dist/enums/column-status.d.ts | 9 - .../node-appwrite/dist/enums/column-status.js | 14 - .../dist/enums/column-status.js.map | 1 - .../dist/enums/column-status.mjs | 13 - .../dist/enums/column-status.mjs.map | 1 - .../dist/enums/credit-card.d.mts | 5 +- .../node-appwrite/dist/enums/credit-card.d.ts | 5 +- .../node-appwrite/dist/enums/credit-card.js | 3 +- .../dist/enums/credit-card.js.map | 2 +- .../node-appwrite/dist/enums/credit-card.mjs | 3 +- .../dist/enums/credit-card.mjs.map | 2 +- .../dist/enums/database-type.d.mts | 6 - .../dist/enums/database-type.d.ts | 6 - .../node-appwrite/dist/enums/database-type.js | 11 - .../dist/enums/database-type.js.map | 1 - .../dist/enums/database-type.mjs | 10 - .../dist/enums/database-type.mjs.map | 1 - .../dist/enums/deployment-download-type.d.mts | 6 - .../dist/enums/deployment-download-type.d.ts | 6 - .../dist/enums/deployment-download-type.js | 11 - .../enums/deployment-download-type.js.map | 1 - .../dist/enums/deployment-download-type.mjs | 10 - .../enums/deployment-download-type.mjs.map | 1 - .../dist/enums/deployment-status.d.mts | 9 - .../dist/enums/deployment-status.d.ts | 9 - .../dist/enums/deployment-status.js | 14 - .../dist/enums/deployment-status.js.map | 1 - .../dist/enums/deployment-status.mjs | 13 - .../dist/enums/deployment-status.mjs.map | 1 - .../dist/enums/execution-method.d.mts | 3 +- .../dist/enums/execution-method.d.ts | 3 +- .../dist/enums/execution-method.js | 1 - .../dist/enums/execution-method.js.map | 2 +- .../dist/enums/execution-method.mjs | 1 - .../dist/enums/execution-method.mjs.map | 2 +- .../dist/enums/execution-status.d.mts | 9 - .../dist/enums/execution-status.d.ts | 9 - .../dist/enums/execution-status.js | 14 - .../dist/enums/execution-status.js.map | 1 - .../dist/enums/execution-status.mjs | 13 - .../dist/enums/execution-status.mjs.map | 1 - .../dist/enums/execution-trigger.d.mts | 7 - .../dist/enums/execution-trigger.d.ts | 7 - .../dist/enums/execution-trigger.js | 12 - .../dist/enums/execution-trigger.js.map | 1 - .../dist/enums/execution-trigger.mjs | 11 - .../dist/enums/execution-trigger.mjs.map | 1 - .../node-appwrite/dist/enums/framework.d.mts | 19 - .../node-appwrite/dist/enums/framework.d.ts | 19 - .../node-appwrite/dist/enums/framework.js | 24 - .../node-appwrite/dist/enums/framework.js.map | 1 - .../node-appwrite/dist/enums/framework.mjs | 23 - .../dist/enums/framework.mjs.map | 1 - .../dist/enums/health-antivirus-status.d.mts | 7 - .../dist/enums/health-antivirus-status.d.ts | 7 - .../dist/enums/health-antivirus-status.js | 12 - .../dist/enums/health-antivirus-status.js.map | 1 - .../dist/enums/health-antivirus-status.mjs | 11 - .../enums/health-antivirus-status.mjs.map | 1 - .../dist/enums/health-check-status.d.mts | 6 - .../dist/enums/health-check-status.d.ts | 6 - .../dist/enums/health-check-status.js | 11 - .../dist/enums/health-check-status.js.map | 1 - .../dist/enums/health-check-status.mjs | 10 - .../dist/enums/health-check-status.mjs.map | 1 - .../dist/enums/image-format.d.mts | 5 +- .../dist/enums/image-format.d.ts | 5 +- .../node-appwrite/dist/enums/image-format.js | 3 +- .../dist/enums/image-format.js.map | 2 +- .../node-appwrite/dist/enums/image-format.mjs | 3 +- .../dist/enums/image-format.mjs.map | 2 +- .../dist/enums/index-status.d.mts | 9 - .../dist/enums/index-status.d.ts | 9 - .../node-appwrite/dist/enums/index-status.js | 14 - .../dist/enums/index-status.js.map | 1 - .../node-appwrite/dist/enums/index-status.mjs | 13 - .../dist/enums/index-status.mjs.map | 1 - .../node-appwrite/dist/enums/index-type.d.mts | 3 +- .../node-appwrite/dist/enums/index-type.d.ts | 3 +- .../node-appwrite/dist/enums/index-type.js | 1 - .../dist/enums/index-type.js.map | 2 +- .../node-appwrite/dist/enums/index-type.mjs | 1 - .../dist/enums/index-type.mjs.map | 2 +- .../dist/enums/message-status.d.mts | 9 - .../dist/enums/message-status.d.ts | 9 - .../dist/enums/message-status.js | 14 - .../dist/enums/message-status.js.map | 1 - .../dist/enums/message-status.mjs | 13 - .../dist/enums/message-status.mjs.map | 1 - .../node-appwrite/dist/enums/name.d.mts | 4 +- .../node-appwrite/dist/enums/name.d.ts | 4 +- .../node-appwrite/dist/enums/name.js | 4 +- .../node-appwrite/dist/enums/name.js.map | 2 +- .../node-appwrite/dist/enums/name.mjs | 4 +- .../node-appwrite/dist/enums/name.mjs.map | 2 +- .../dist/enums/o-auth-provider.d.mts | 1 - .../dist/enums/o-auth-provider.d.ts | 1 - .../dist/enums/o-auth-provider.js | 1 - .../dist/enums/o-auth-provider.js.map | 2 +- .../dist/enums/o-auth-provider.mjs | 1 - .../dist/enums/o-auth-provider.mjs.map | 2 +- .../node-appwrite/dist/enums/output.d.mts | 11 - .../node-appwrite/dist/enums/output.d.ts | 11 - .../node-appwrite/dist/enums/output.js | 16 - .../node-appwrite/dist/enums/output.js.map | 1 - .../node-appwrite/dist/enums/output.mjs | 15 - .../node-appwrite/dist/enums/output.mjs.map | 1 - .../node-appwrite/dist/enums/runtime.d.mts | 10 +- .../node-appwrite/dist/enums/runtime.d.ts | 10 +- .../node-appwrite/dist/enums/runtime.js | 8 - .../node-appwrite/dist/enums/runtime.js.map | 2 +- .../node-appwrite/dist/enums/runtime.mjs | 8 - .../node-appwrite/dist/enums/runtime.mjs.map | 2 +- .../dist/enums/template-reference-type.d.mts | 7 - .../dist/enums/template-reference-type.d.ts | 7 - .../dist/enums/template-reference-type.js | 12 - .../dist/enums/template-reference-type.js.map | 1 - .../dist/enums/template-reference-type.mjs | 11 - .../enums/template-reference-type.mjs.map | 1 - .../node-appwrite/dist/enums/theme.d.mts | 6 - .../node-appwrite/dist/enums/theme.d.ts | 6 - .../node-appwrite/dist/enums/theme.js | 11 - .../node-appwrite/dist/enums/theme.js.map | 1 - .../node-appwrite/dist/enums/theme.mjs | 10 - .../node-appwrite/dist/enums/theme.mjs.map | 1 - .../node-appwrite/dist/enums/timezone.d.mts | 423 -- .../node-appwrite/dist/enums/timezone.d.ts | 423 -- .../node-appwrite/dist/enums/timezone.js | 428 -- .../node-appwrite/dist/enums/timezone.js.map | 1 - .../node-appwrite/dist/enums/timezone.mjs | 427 -- .../node-appwrite/dist/enums/timezone.mjs.map | 1 - .../dist/enums/vcs-reference-type.d.mts | 7 - .../dist/enums/vcs-reference-type.d.ts | 7 - .../dist/enums/vcs-reference-type.js | 12 - .../dist/enums/vcs-reference-type.js.map | 1 - .../dist/enums/vcs-reference-type.mjs | 11 - .../dist/enums/vcs-reference-type.mjs.map | 1 - .../node-appwrite/dist/index.d.mts | 23 - .../node-appwrite/dist/index.d.ts | 23 - .../node_modules/node-appwrite/dist/index.js | 119 - .../node-appwrite/dist/index.js.map | 2 +- .../node_modules/node-appwrite/dist/index.mjs | 23 - .../node-appwrite/dist/index.mjs.map | 2 +- .../node-appwrite/dist/models.d.mts | 1635 +----- .../node-appwrite/dist/models.d.ts | 1635 +----- .../node-appwrite/dist/operator.d.mts | 181 - .../node-appwrite/dist/operator.d.ts | 181 - .../node-appwrite/dist/operator.js | 268 - .../node-appwrite/dist/operator.js.map | 1 - .../node-appwrite/dist/operator.mjs | 266 - .../node-appwrite/dist/operator.mjs.map | 1 - .../node-appwrite/dist/query.d.mts | 200 +- .../node-appwrite/dist/query.d.ts | 200 +- .../node_modules/node-appwrite/dist/query.js | 196 - .../node-appwrite/dist/query.js.map | 2 +- .../node_modules/node-appwrite/dist/query.mjs | 196 - .../node-appwrite/dist/query.mjs.map | 2 +- .../node-appwrite/dist/services/account.d.mts | 872 +-- .../node-appwrite/dist/services/account.d.ts | 872 +-- .../node-appwrite/dist/services/account.js | 1226 ++-- .../dist/services/account.js.map | 2 +- .../node-appwrite/dist/services/account.mjs | 1226 ++-- .../dist/services/account.mjs.map | 2 +- .../node-appwrite/dist/services/avatars.d.mts | 330 +- .../node-appwrite/dist/services/avatars.d.ts | 330 +- .../node-appwrite/dist/services/avatars.js | 376 +- .../dist/services/avatars.js.map | 2 +- .../node-appwrite/dist/services/avatars.mjs | 376 +- .../dist/services/avatars.mjs.map | 2 +- .../dist/services/databases.d.mts | 1996 +------ .../dist/services/databases.d.ts | 1996 +------ .../node-appwrite/dist/services/databases.js | 2453 +++----- .../dist/services/databases.js.map | 2 +- .../node-appwrite/dist/services/databases.mjs | 2453 +++----- .../dist/services/databases.mjs.map | 2 +- .../dist/services/functions.d.mts | 756 +-- .../dist/services/functions.d.ts | 756 +-- .../node-appwrite/dist/services/functions.js | 1080 ++-- .../dist/services/functions.js.map | 2 +- .../node-appwrite/dist/services/functions.mjs | 1080 ++-- .../dist/services/functions.mjs.map | 2 +- .../node-appwrite/dist/services/graphql.d.mts | 40 +- .../node-appwrite/dist/services/graphql.d.ts | 40 +- .../node-appwrite/dist/services/graphql.js | 44 +- .../dist/services/graphql.js.map | 2 +- .../node-appwrite/dist/services/graphql.mjs | 44 +- .../dist/services/graphql.mjs.map | 2 +- .../node-appwrite/dist/services/health.d.mts | 246 +- .../node-appwrite/dist/services/health.d.ts | 246 +- .../node-appwrite/dist/services/health.js | 477 +- .../node-appwrite/dist/services/health.js.map | 2 +- .../node-appwrite/dist/services/health.mjs | 477 +- .../dist/services/health.mjs.map | 2 +- .../node-appwrite/dist/services/locale.d.mts | 30 +- .../node-appwrite/dist/services/locale.d.ts | 30 +- .../node-appwrite/dist/services/locale.js | 94 +- .../node-appwrite/dist/services/locale.js.map | 2 +- .../node-appwrite/dist/services/locale.mjs | 94 +- .../dist/services/locale.mjs.map | 2 +- .../dist/services/messaging.d.mts | 2017 ++----- .../dist/services/messaging.d.ts | 2017 ++----- .../node-appwrite/dist/services/messaging.js | 2472 +++----- .../dist/services/messaging.js.map | 2 +- .../node-appwrite/dist/services/messaging.mjs | 2472 +++----- .../dist/services/messaging.mjs.map | 2 +- .../node-appwrite/dist/services/sites.d.mts | 705 --- .../node-appwrite/dist/services/sites.d.ts | 705 --- .../node-appwrite/dist/services/sites.js | 1059 ---- .../node-appwrite/dist/services/sites.js.map | 1 - .../node-appwrite/dist/services/sites.mjs | 1058 ---- .../node-appwrite/dist/services/sites.mjs.map | 1 - .../node-appwrite/dist/services/storage.d.mts | 426 +- .../node-appwrite/dist/services/storage.d.ts | 426 +- .../node-appwrite/dist/services/storage.js | 489 +- .../dist/services/storage.js.map | 2 +- .../node-appwrite/dist/services/storage.mjs | 489 +- .../dist/services/storage.mjs.map | 2 +- .../dist/services/tables-db.d.mts | 1934 ------- .../dist/services/tables-db.d.ts | 1934 ------- .../node-appwrite/dist/services/tables-db.js | 2954 ---------- .../dist/services/tables-db.js.map | 1 - .../node-appwrite/dist/services/tables-db.mjs | 2953 ---------- .../dist/services/tables-db.mjs.map | 1 - .../node-appwrite/dist/services/teams.d.mts | 336 +- .../node-appwrite/dist/services/teams.d.ts | 336 +- .../node-appwrite/dist/services/teams.js | 387 +- .../node-appwrite/dist/services/teams.js.map | 2 +- .../node-appwrite/dist/services/teams.mjs | 387 +- .../node-appwrite/dist/services/teams.mjs.map | 2 +- .../node-appwrite/dist/services/tokens.d.mts | 133 - .../node-appwrite/dist/services/tokens.d.ts | 133 - .../node-appwrite/dist/services/tokens.js | 169 - .../node-appwrite/dist/services/tokens.js.map | 1 - .../node-appwrite/dist/services/tokens.mjs | 168 - .../dist/services/tokens.mjs.map | 1 - .../node-appwrite/dist/services/users.d.mts | 1073 +--- .../node-appwrite/dist/services/users.d.ts | 1073 +--- .../node-appwrite/dist/services/users.js | 1421 ++--- .../node-appwrite/dist/services/users.js.map | 2 +- .../node-appwrite/dist/services/users.mjs | 1421 ++--- .../node-appwrite/dist/services/users.mjs.map | 2 +- .../node_modules/node-appwrite/package.json | 2 +- server/node_modules/stripe/CHANGELOG.md | 533 +- server/node_modules/stripe/README.md | 114 +- server/node_modules/stripe/VERSION | 2 +- server/node_modules/stripe/cjs/Error.js | 35 +- .../node_modules/stripe/cjs/RequestSender.js | 227 +- .../stripe/cjs/ResourceNamespace.js | 3 + .../node_modules/stripe/cjs/StripeResource.js | 13 +- server/node_modules/stripe/cjs/Webhooks.js | 48 +- server/node_modules/stripe/cjs/apiVersion.js | 2 +- .../node_modules/stripe/cjs/autoPagination.js | 60 +- .../stripe/cjs/crypto/CryptoProvider.js | 6 + .../stripe/cjs/crypto/NodeCryptoProvider.js | 7 + .../stripe/cjs/crypto/SubtleCryptoProvider.js | 4 + server/node_modules/stripe/cjs/multipart.js | 5 +- server/node_modules/stripe/cjs/resources.js | 36 +- .../stripe/cjs/resources/Checkout/Sessions.js | 4 + .../stripe/cjs/resources/Invoices.js | 16 + .../stripe/cjs/resources/OAuth.js | 2 +- .../stripe/cjs/resources/Tax/Calculations.js | 4 + .../TestHelpers/Issuing/Authorizations.js | 8 + .../resources/TestHelpers/Issuing/Cards.js | 4 + .../TestHelpers/Treasury/OutboundPayments.js | 4 + .../TestHelpers/Treasury/OutboundTransfers.js | 4 + .../resources/Treasury/FinancialAccounts.js | 4 + server/node_modules/stripe/cjs/stripe.core.js | 49 +- server/node_modules/stripe/cjs/utils.js | 74 +- server/node_modules/stripe/esm/Error.js | 29 +- .../node_modules/stripe/esm/RequestSender.js | 229 +- .../stripe/esm/ResourceNamespace.js | 3 + .../node_modules/stripe/esm/StripeResource.js | 15 +- server/node_modules/stripe/esm/Webhooks.js | 48 +- server/node_modules/stripe/esm/apiVersion.js | 2 +- .../node_modules/stripe/esm/autoPagination.js | 62 +- .../stripe/esm/crypto/CryptoProvider.js | 6 + .../stripe/esm/crypto/NodeCryptoProvider.js | 7 + .../stripe/esm/crypto/SubtleCryptoProvider.js | 4 + server/node_modules/stripe/esm/multipart.js | 7 +- server/node_modules/stripe/esm/resources.js | 27 + .../stripe/esm/resources/Checkout/Sessions.js | 4 + .../stripe/esm/resources/Invoices.js | 16 + .../stripe/esm/resources/OAuth.js | 4 +- .../stripe/esm/resources/Tax/Calculations.js | 4 + .../TestHelpers/Issuing/Authorizations.js | 8 + .../resources/TestHelpers/Issuing/Cards.js | 4 + .../TestHelpers/Treasury/OutboundPayments.js | 4 + .../TestHelpers/Treasury/OutboundTransfers.js | 4 + .../resources/Treasury/FinancialAccounts.js | 4 + server/node_modules/stripe/esm/stripe.core.js | 49 +- server/node_modules/stripe/esm/utils.js | 67 +- server/node_modules/stripe/package.json | 26 +- .../stripe/types/AccountLinksResource.d.ts | 2 +- .../stripe/types/AccountSessions.d.ts | 291 +- .../stripe/types/AccountSessionsResource.d.ts | 347 +- .../node_modules/stripe/types/Accounts.d.ts | 248 +- .../stripe/types/AccountsResource.d.ts | 634 +- .../stripe/types/ApplicationFees.d.ts | 28 + .../stripe/types/Apps/Secrets.d.ts | 2 +- server/node_modules/stripe/types/Balance.d.ts | 70 +- .../types/BalanceTransactionSources.d.ts | 1 - .../stripe/types/BalanceTransactions.d.ts | 10 +- .../types/BalanceTransactionsResource.d.ts | 2 +- .../stripe/types/BankAccounts.d.ts | 18 +- .../types/Billing/MeterEventAdjustments.d.ts | 11 +- .../MeterEventAdjustmentsResource.d.ts | 24 +- .../types/Billing/MeterEventSummaries.d.ts | 10 +- .../stripe/types/Billing/MeterEvents.d.ts | 5 +- .../types/Billing/MeterEventsResource.d.ts | 16 +- .../stripe/types/Billing/Meters.d.ts | 12 +- .../stripe/types/Billing/MetersResource.d.ts | 36 +- .../types/BillingPortal/Configurations.d.ts | 33 +- .../BillingPortal/ConfigurationsResource.d.ts | 94 +- .../stripe/types/BillingPortal/Sessions.d.ts | 4 +- .../types/BillingPortal/SessionsResource.d.ts | 2 +- .../stripe/types/Capabilities.d.ts | 58 +- server/node_modules/stripe/types/Cards.d.ts | 22 +- server/node_modules/stripe/types/Charges.d.ts | 343 +- .../stripe/types/ChargesResource.d.ts | 18 +- .../stripe/types/Checkout/Sessions.d.ts | 585 +- .../types/Checkout/SessionsResource.d.ts | 644 ++- .../stripe/types/Climate/OrdersResource.d.ts | 2 +- .../stripe/types/Climate/Suppliers.d.ts | 3 +- .../stripe/types/ConfirmationTokens.d.ts | 447 +- .../stripe/types/CreditNoteLineItems.d.ts | 33 + .../stripe/types/CreditNotes.d.ts | 37 +- .../stripe/types/CreditNotesResource.d.ts | 29 +- .../stripe/types/CustomerSessions.d.ts | 92 +- .../types/CustomerSessionsResource.d.ts | 80 +- .../node_modules/stripe/types/Customers.d.ts | 12 +- .../stripe/types/CustomersResource.d.ts | 105 +- .../node_modules/stripe/types/Disputes.d.ts | 230 +- .../stripe/types/DisputesResource.d.ts | 135 + .../Entitlements/ActiveEntitlements.d.ts | 4 +- .../types/Entitlements/FeaturesResource.d.ts | 12 +- server/node_modules/stripe/types/Errors.d.ts | 34 +- .../node_modules/stripe/types/EventTypes.d.ts | 383 +- server/node_modules/stripe/types/Events.d.ts | 27 +- .../stripe/types/EventsResource.d.ts | 4 +- .../stripe/types/FileLinksResource.d.ts | 2 +- server/node_modules/stripe/types/Files.d.ts | 2 + .../stripe/types/FilesResource.d.ts | 5 +- .../types/FinancialConnections/Accounts.d.ts | 9 +- .../types/FinancialConnections/Sessions.d.ts | 14 + .../SessionsResource.d.ts | 16 +- .../TransactionsResource.d.ts | 2 +- .../stripe/types/Forwarding/Requests.d.ts | 19 +- .../types/Forwarding/RequestsResource.d.ts | 13 +- .../stripe/types/FundingInstructions.d.ts | 49 + .../types/Identity/VerificationReports.d.ts | 14 +- .../types/Identity/VerificationSessions.d.ts | 17 +- .../VerificationSessionsResource.d.ts | 59 +- .../stripe/types/InvoiceItemsResource.d.ts | 10 +- .../stripe/types/InvoiceLineItems.d.ts | 44 +- .../node_modules/stripe/types/Invoices.d.ts | 177 +- .../stripe/types/InvoicesResource.d.ts | 3512 +++++++++++- .../stripe/types/Issuing/Authorizations.d.ts | 220 +- .../types/Issuing/AuthorizationsResource.d.ts | 2 + .../stripe/types/Issuing/Cardholders.d.ts | 2 +- .../stripe/types/Issuing/Cards.d.ts | 43 +- .../stripe/types/Issuing/CardsResource.d.ts | 127 +- .../stripe/types/Issuing/Disputes.d.ts | 44 +- .../types/Issuing/DisputesResource.d.ts | 40 + .../stripe/types/Issuing/PhysicalBundles.d.ts | 2 +- .../stripe/types/Issuing/Transactions.d.ts | 125 +- .../node_modules/stripe/types/LineItems.d.ts | 6 +- .../node_modules/stripe/types/LoginLinks.d.ts | 2 +- .../node_modules/stripe/types/Mandates.d.ts | 16 + .../stripe/types/PaymentIntents.d.ts | 565 +- .../stripe/types/PaymentIntentsResource.d.ts | 2263 ++++++-- .../stripe/types/PaymentLinks.d.ts | 21 +- .../stripe/types/PaymentLinksResource.d.ts | 102 +- .../types/PaymentMethodConfigurations.d.ts | 168 +- .../PaymentMethodConfigurationsResource.d.ts | 356 +- .../stripe/types/PaymentMethodDomains.d.ts | 30 +- .../stripe/types/PaymentMethods.d.ts | 419 +- .../stripe/types/PaymentMethodsResource.d.ts | 164 +- server/node_modules/stripe/types/Payouts.d.ts | 27 + server/node_modules/stripe/types/Persons.d.ts | 33 +- server/node_modules/stripe/types/Plans.d.ts | 2 +- .../stripe/types/PlatformTaxFees.d.ts | 35 - server/node_modules/stripe/types/Prices.d.ts | 2 +- .../stripe/types/PricesResource.d.ts | 4 +- .../node_modules/stripe/types/Products.d.ts | 14 +- .../stripe/types/ProductsResource.d.ts | 66 +- .../stripe/types/PromotionCodes.d.ts | 2 +- .../stripe/types/PromotionCodesResource.d.ts | 4 +- server/node_modules/stripe/types/Quotes.d.ts | 12 +- .../stripe/types/QuotesResource.d.ts | 6 +- server/node_modules/stripe/types/Refunds.d.ts | 37 +- .../stripe/types/RefundsResource.d.ts | 2 +- .../types/Reporting/ReportRunsResource.d.ts | 3 +- .../stripe/types/SetupAttempts.d.ts | 53 +- .../stripe/types/SetupIntents.d.ts | 90 +- .../stripe/types/SetupIntentsResource.d.ts | 524 +- .../stripe/types/ShippingRates.d.ts | 2 +- .../stripe/types/ShippingRatesResource.d.ts | 2 +- server/node_modules/stripe/types/Sources.d.ts | 7 + .../types/SubscriptionItemsResource.d.ts | 6 +- .../stripe/types/SubscriptionSchedules.d.ts | 10 + .../types/SubscriptionSchedulesResource.d.ts | 12 +- .../stripe/types/Subscriptions.d.ts | 76 +- .../stripe/types/SubscriptionsResource.d.ts | 122 +- .../types/Tax/CalculationLineItems.d.ts | 6 +- .../stripe/types/Tax/Calculations.d.ts | 80 +- .../types/Tax/CalculationsResource.d.ts | 113 +- .../stripe/types/Tax/Registrations.d.ts | 364 ++ .../types/Tax/RegistrationsResource.d.ts | 478 ++ .../stripe/types/Tax/Settings.d.ts | 2 +- .../stripe/types/Tax/Transactions.d.ts | 57 +- .../types/Tax/TransactionsResource.d.ts | 7 +- server/node_modules/stripe/types/TaxIds.d.ts | 37 +- .../stripe/types/TaxIdsResource.d.ts | 37 +- .../node_modules/stripe/types/TaxRates.d.ts | 35 +- .../stripe/types/TaxRatesResource.d.ts | 14 +- .../stripe/types/Terminal/Configurations.d.ts | 61 + .../Terminal/ConfigurationsResource.d.ts | 148 + .../types/Terminal/ConnectionTokens.d.ts | 2 +- .../Terminal/ConnectionTokensResource.d.ts | 2 +- .../types/Terminal/LocationsResource.d.ts | 4 +- .../stripe/types/Terminal/Readers.d.ts | 5 +- .../types/Terminal/ReadersResource.d.ts | 14 +- .../ConfirmationTokensResource.d.ts | 98 +- .../Issuing/AuthorizationsResource.d.ts | 530 +- .../TestHelpers/Issuing/CardsResource.d.ts | 22 + .../Issuing/TransactionsResource.d.ts | 276 +- .../stripe/types/TestHelpers/TestClocks.d.ts | 15 + .../Treasury/OutboundPaymentsResource.d.ts | 72 +- .../Treasury/OutboundTransfersResource.d.ts | 70 + server/node_modules/stripe/types/Tokens.d.ts | 8 +- .../stripe/types/TokensResource.d.ts | 63 +- .../stripe/types/TransferReversals.d.ts | 2 +- .../node_modules/stripe/types/Transfers.d.ts | 2 +- .../stripe/types/TransfersResource.d.ts | 4 +- .../Treasury/FinancialAccountFeatures.d.ts | 6 +- .../types/Treasury/FinancialAccounts.d.ts | 14 +- .../Treasury/FinancialAccountsResource.d.ts | 84 + .../types/Treasury/InboundTransfers.d.ts | 6 +- .../types/Treasury/OutboundPayments.d.ts | 48 +- .../types/Treasury/OutboundTransfers.d.ts | 66 +- .../Treasury/OutboundTransfersResource.d.ts | 17 + .../types/Treasury/ReceivedCredits.d.ts | 22 +- .../Treasury/ReceivedCreditsResource.d.ts | 1 + .../stripe/types/Treasury/ReceivedDebits.d.ts | 1 + .../types/Treasury/TransactionEntries.d.ts | 14 +- .../stripe/types/Treasury/Transactions.d.ts | 16 +- .../stripe/types/UsageRecordSummaries.d.ts | 2 +- .../stripe/types/UsageRecords.d.ts | 2 + .../stripe/types/WebhookEndpoints.d.ts | 6 +- .../types/WebhookEndpointsResource.d.ts | 56 +- .../node_modules/stripe/types/Webhooks.d.ts | 71 +- server/node_modules/stripe/types/index.d.ts | 105 +- server/node_modules/stripe/types/lib.d.ts | 9 +- .../stripe/types/test/typescriptTest.ts | 6 +- server/package-lock.json | 477 +- server/package.json | 41 +- server/routes/analytics.mjs | 69 + server/routes/api.mjs | 174 + server/routes/email.mjs | 948 +++ server/routes/oauth.mjs | 388 ++ server/routes/stripe.mjs | 351 ++ server/services/ai-sorter.mjs | 360 ++ server/services/database.mjs | 428 ++ server/services/gmail.mjs | 357 ++ server/services/outlook.mjs | 335 ++ server/utils/response.mjs | 78 + setup-appwrite.ps1 | 78 +- starter-for-react/.env.example | 3 + starter-for-react/.gitignore | 26 + starter-for-react/LICENSE | 21 + starter-for-react/README.md | 26 + starter-for-react/eslint.config.js | 38 + starter-for-react/index.html | 19 + starter-for-react/package-lock.json | 5087 +++++++++++++++++ starter-for-react/package.json | 33 + starter-for-react/public/appwrite.svg | 8 + starter-for-react/public/react.svg | 6 + starter-for-react/src/App.css | 20 + starter-for-react/src/App.jsx | 311 + starter-for-react/src/lib/appwrite.js | 10 + starter-for-react/src/main.jsx | 10 + starter-for-react/vite.config.js | 8 + 596 files changed, 56405 insertions(+), 51231 deletions(-) create mode 100644 APPWRITE_SETUP.md create mode 100644 GOOGLE_OAUTH_SETUP.md create mode 100644 PROJECT_RENAME_GUIDE.md create mode 100644 PROJECT_REVIEW_SUMMARY.md create mode 100644 SETUP_GUIDE.md create mode 100644 client/.gitignore create mode 100644 client/README.md create mode 100644 client/env.example create mode 100644 client/eslint.config.js create mode 100644 client/index.html create mode 100644 client/package-lock.json create mode 100644 client/package.json create mode 100644 client/public/vite.svg create mode 100644 client/src/App.tsx create mode 100644 client/src/assets/react.svg create mode 100644 client/src/components/landing/FAQ.tsx create mode 100644 client/src/components/landing/Features.tsx create mode 100644 client/src/components/landing/Footer.tsx create mode 100644 client/src/components/landing/Hero.tsx create mode 100644 client/src/components/landing/HowItWorks.tsx create mode 100644 client/src/components/landing/Navbar.tsx create mode 100644 client/src/components/landing/Pricing.tsx create mode 100644 client/src/components/landing/Testimonials.tsx create mode 100644 client/src/components/ui/badge.tsx create mode 100644 client/src/components/ui/button.tsx create mode 100644 client/src/components/ui/card.tsx create mode 100644 client/src/components/ui/input.tsx create mode 100644 client/src/components/ui/label.tsx create mode 100644 client/src/context/AuthContext.tsx create mode 100644 client/src/hooks/useAnalytics.ts create mode 100644 client/src/index.css create mode 100644 client/src/lib/analytics.ts create mode 100644 client/src/lib/api.ts create mode 100644 client/src/lib/appwrite.ts create mode 100644 client/src/lib/utils.ts create mode 100644 client/src/main.tsx create mode 100644 client/src/pages/Dashboard.tsx create mode 100644 client/src/pages/ForgotPassword.tsx create mode 100644 client/src/pages/Home.tsx create mode 100644 client/src/pages/Imprint.tsx create mode 100644 client/src/pages/Login.tsx create mode 100644 client/src/pages/Privacy.tsx create mode 100644 client/src/pages/Register.tsx create mode 100644 client/src/pages/ResetPassword.tsx create mode 100644 client/src/pages/Settings.tsx create mode 100644 client/src/pages/Setup.tsx create mode 100644 client/src/pages/VerifyEmail.tsx create mode 100644 client/tsconfig.app.json create mode 100644 client/tsconfig.json create mode 100644 client/tsconfig.node.json create mode 100644 client/vite.config.ts create mode 100644 marketing/INFLUENCER_OUTREACH_TEMPLATES.md create mode 100644 marketing/LOGO_ANLEITUNG.md create mode 100644 marketing/PRODUCT_HUNT_LAUNCH_GUIDE.md create mode 100644 marketing/README.md create mode 100644 marketing/TIKTOK_CONTENT_SCRIPTS.md create mode 100644 marketing/TIKTOK_LOGO_ANLEITUNG.md create mode 100644 marketing/TIKTOK_SETUP_GUIDE.md create mode 100644 marketing/USPs_AND_MESSAGING.md create mode 100644 marketing/YOUTUBE_STRATEGY.md create mode 100644 marketing/logo-emailsorter-icon-only.svg create mode 100644 marketing/logo-emailsorter-simple.svg create mode 100644 marketing/logo-emailsorter.svg create mode 100644 marketing/logo-to-png.html create mode 100644 n8n/README.md create mode 100644 n8n/workflows/email-sorter-workflow.json create mode 100644 server/bootstrap-v2.mjs create mode 100644 server/config/index.mjs create mode 100644 server/env.example create mode 100644 server/middleware/errorHandler.mjs create mode 100644 server/middleware/logger.mjs create mode 100644 server/middleware/rateLimit.mjs create mode 100644 server/middleware/validate.mjs delete mode 100644 server/node_modules/node-appwrite/dist/enums/adapter.d.mts delete mode 100644 server/node_modules/node-appwrite/dist/enums/adapter.d.ts delete mode 100644 server/node_modules/node-appwrite/dist/enums/adapter.js delete mode 100644 server/node_modules/node-appwrite/dist/enums/adapter.js.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/adapter.mjs delete mode 100644 server/node_modules/node-appwrite/dist/enums/adapter.mjs.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/attribute-status.d.mts delete mode 100644 server/node_modules/node-appwrite/dist/enums/attribute-status.d.ts delete mode 100644 server/node_modules/node-appwrite/dist/enums/attribute-status.js delete mode 100644 server/node_modules/node-appwrite/dist/enums/attribute-status.js.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/attribute-status.mjs delete mode 100644 server/node_modules/node-appwrite/dist/enums/attribute-status.mjs.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/build-runtime.d.mts delete mode 100644 server/node_modules/node-appwrite/dist/enums/build-runtime.d.ts delete mode 100644 server/node_modules/node-appwrite/dist/enums/build-runtime.js delete mode 100644 server/node_modules/node-appwrite/dist/enums/build-runtime.js.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/build-runtime.mjs delete mode 100644 server/node_modules/node-appwrite/dist/enums/build-runtime.mjs.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/column-status.d.mts delete mode 100644 server/node_modules/node-appwrite/dist/enums/column-status.d.ts delete mode 100644 server/node_modules/node-appwrite/dist/enums/column-status.js delete mode 100644 server/node_modules/node-appwrite/dist/enums/column-status.js.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/column-status.mjs delete mode 100644 server/node_modules/node-appwrite/dist/enums/column-status.mjs.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/database-type.d.mts delete mode 100644 server/node_modules/node-appwrite/dist/enums/database-type.d.ts delete mode 100644 server/node_modules/node-appwrite/dist/enums/database-type.js delete mode 100644 server/node_modules/node-appwrite/dist/enums/database-type.js.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/database-type.mjs delete mode 100644 server/node_modules/node-appwrite/dist/enums/database-type.mjs.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/deployment-download-type.d.mts delete mode 100644 server/node_modules/node-appwrite/dist/enums/deployment-download-type.d.ts delete mode 100644 server/node_modules/node-appwrite/dist/enums/deployment-download-type.js delete mode 100644 server/node_modules/node-appwrite/dist/enums/deployment-download-type.js.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/deployment-download-type.mjs delete mode 100644 server/node_modules/node-appwrite/dist/enums/deployment-download-type.mjs.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/deployment-status.d.mts delete mode 100644 server/node_modules/node-appwrite/dist/enums/deployment-status.d.ts delete mode 100644 server/node_modules/node-appwrite/dist/enums/deployment-status.js delete mode 100644 server/node_modules/node-appwrite/dist/enums/deployment-status.js.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/deployment-status.mjs delete mode 100644 server/node_modules/node-appwrite/dist/enums/deployment-status.mjs.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/execution-status.d.mts delete mode 100644 server/node_modules/node-appwrite/dist/enums/execution-status.d.ts delete mode 100644 server/node_modules/node-appwrite/dist/enums/execution-status.js delete mode 100644 server/node_modules/node-appwrite/dist/enums/execution-status.js.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/execution-status.mjs delete mode 100644 server/node_modules/node-appwrite/dist/enums/execution-status.mjs.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/execution-trigger.d.mts delete mode 100644 server/node_modules/node-appwrite/dist/enums/execution-trigger.d.ts delete mode 100644 server/node_modules/node-appwrite/dist/enums/execution-trigger.js delete mode 100644 server/node_modules/node-appwrite/dist/enums/execution-trigger.js.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/execution-trigger.mjs delete mode 100644 server/node_modules/node-appwrite/dist/enums/execution-trigger.mjs.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/framework.d.mts delete mode 100644 server/node_modules/node-appwrite/dist/enums/framework.d.ts delete mode 100644 server/node_modules/node-appwrite/dist/enums/framework.js delete mode 100644 server/node_modules/node-appwrite/dist/enums/framework.js.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/framework.mjs delete mode 100644 server/node_modules/node-appwrite/dist/enums/framework.mjs.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/health-antivirus-status.d.mts delete mode 100644 server/node_modules/node-appwrite/dist/enums/health-antivirus-status.d.ts delete mode 100644 server/node_modules/node-appwrite/dist/enums/health-antivirus-status.js delete mode 100644 server/node_modules/node-appwrite/dist/enums/health-antivirus-status.js.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/health-antivirus-status.mjs delete mode 100644 server/node_modules/node-appwrite/dist/enums/health-antivirus-status.mjs.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/health-check-status.d.mts delete mode 100644 server/node_modules/node-appwrite/dist/enums/health-check-status.d.ts delete mode 100644 server/node_modules/node-appwrite/dist/enums/health-check-status.js delete mode 100644 server/node_modules/node-appwrite/dist/enums/health-check-status.js.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/health-check-status.mjs delete mode 100644 server/node_modules/node-appwrite/dist/enums/health-check-status.mjs.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/index-status.d.mts delete mode 100644 server/node_modules/node-appwrite/dist/enums/index-status.d.ts delete mode 100644 server/node_modules/node-appwrite/dist/enums/index-status.js delete mode 100644 server/node_modules/node-appwrite/dist/enums/index-status.js.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/index-status.mjs delete mode 100644 server/node_modules/node-appwrite/dist/enums/index-status.mjs.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/message-status.d.mts delete mode 100644 server/node_modules/node-appwrite/dist/enums/message-status.d.ts delete mode 100644 server/node_modules/node-appwrite/dist/enums/message-status.js delete mode 100644 server/node_modules/node-appwrite/dist/enums/message-status.js.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/message-status.mjs delete mode 100644 server/node_modules/node-appwrite/dist/enums/message-status.mjs.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/output.d.mts delete mode 100644 server/node_modules/node-appwrite/dist/enums/output.d.ts delete mode 100644 server/node_modules/node-appwrite/dist/enums/output.js delete mode 100644 server/node_modules/node-appwrite/dist/enums/output.js.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/output.mjs delete mode 100644 server/node_modules/node-appwrite/dist/enums/output.mjs.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/template-reference-type.d.mts delete mode 100644 server/node_modules/node-appwrite/dist/enums/template-reference-type.d.ts delete mode 100644 server/node_modules/node-appwrite/dist/enums/template-reference-type.js delete mode 100644 server/node_modules/node-appwrite/dist/enums/template-reference-type.js.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/template-reference-type.mjs delete mode 100644 server/node_modules/node-appwrite/dist/enums/template-reference-type.mjs.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/theme.d.mts delete mode 100644 server/node_modules/node-appwrite/dist/enums/theme.d.ts delete mode 100644 server/node_modules/node-appwrite/dist/enums/theme.js delete mode 100644 server/node_modules/node-appwrite/dist/enums/theme.js.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/theme.mjs delete mode 100644 server/node_modules/node-appwrite/dist/enums/theme.mjs.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/timezone.d.mts delete mode 100644 server/node_modules/node-appwrite/dist/enums/timezone.d.ts delete mode 100644 server/node_modules/node-appwrite/dist/enums/timezone.js delete mode 100644 server/node_modules/node-appwrite/dist/enums/timezone.js.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/timezone.mjs delete mode 100644 server/node_modules/node-appwrite/dist/enums/timezone.mjs.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/vcs-reference-type.d.mts delete mode 100644 server/node_modules/node-appwrite/dist/enums/vcs-reference-type.d.ts delete mode 100644 server/node_modules/node-appwrite/dist/enums/vcs-reference-type.js delete mode 100644 server/node_modules/node-appwrite/dist/enums/vcs-reference-type.js.map delete mode 100644 server/node_modules/node-appwrite/dist/enums/vcs-reference-type.mjs delete mode 100644 server/node_modules/node-appwrite/dist/enums/vcs-reference-type.mjs.map delete mode 100644 server/node_modules/node-appwrite/dist/operator.d.mts delete mode 100644 server/node_modules/node-appwrite/dist/operator.d.ts delete mode 100644 server/node_modules/node-appwrite/dist/operator.js delete mode 100644 server/node_modules/node-appwrite/dist/operator.js.map delete mode 100644 server/node_modules/node-appwrite/dist/operator.mjs delete mode 100644 server/node_modules/node-appwrite/dist/operator.mjs.map delete mode 100644 server/node_modules/node-appwrite/dist/services/sites.d.mts delete mode 100644 server/node_modules/node-appwrite/dist/services/sites.d.ts delete mode 100644 server/node_modules/node-appwrite/dist/services/sites.js delete mode 100644 server/node_modules/node-appwrite/dist/services/sites.js.map delete mode 100644 server/node_modules/node-appwrite/dist/services/sites.mjs delete mode 100644 server/node_modules/node-appwrite/dist/services/sites.mjs.map delete mode 100644 server/node_modules/node-appwrite/dist/services/tables-db.d.mts delete mode 100644 server/node_modules/node-appwrite/dist/services/tables-db.d.ts delete mode 100644 server/node_modules/node-appwrite/dist/services/tables-db.js delete mode 100644 server/node_modules/node-appwrite/dist/services/tables-db.js.map delete mode 100644 server/node_modules/node-appwrite/dist/services/tables-db.mjs delete mode 100644 server/node_modules/node-appwrite/dist/services/tables-db.mjs.map delete mode 100644 server/node_modules/node-appwrite/dist/services/tokens.d.mts delete mode 100644 server/node_modules/node-appwrite/dist/services/tokens.d.ts delete mode 100644 server/node_modules/node-appwrite/dist/services/tokens.js delete mode 100644 server/node_modules/node-appwrite/dist/services/tokens.js.map delete mode 100644 server/node_modules/node-appwrite/dist/services/tokens.mjs delete mode 100644 server/node_modules/node-appwrite/dist/services/tokens.mjs.map delete mode 100644 server/node_modules/stripe/types/PlatformTaxFees.d.ts create mode 100644 server/routes/analytics.mjs create mode 100644 server/routes/api.mjs create mode 100644 server/routes/email.mjs create mode 100644 server/routes/oauth.mjs create mode 100644 server/routes/stripe.mjs create mode 100644 server/services/ai-sorter.mjs create mode 100644 server/services/database.mjs create mode 100644 server/services/gmail.mjs create mode 100644 server/services/outlook.mjs create mode 100644 server/utils/response.mjs create mode 100644 starter-for-react/.env.example create mode 100644 starter-for-react/.gitignore create mode 100644 starter-for-react/LICENSE create mode 100644 starter-for-react/README.md create mode 100644 starter-for-react/eslint.config.js create mode 100644 starter-for-react/index.html create mode 100644 starter-for-react/package-lock.json create mode 100644 starter-for-react/package.json create mode 100644 starter-for-react/public/appwrite.svg create mode 100644 starter-for-react/public/react.svg create mode 100644 starter-for-react/src/App.css create mode 100644 starter-for-react/src/App.jsx create mode 100644 starter-for-react/src/lib/appwrite.js create mode 100644 starter-for-react/src/main.jsx create mode 100644 starter-for-react/vite.config.js 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

-
-