erdfsfsfsdf
This commit is contained in:
2026-01-31 12:05:47 +01:00
parent a28ca580d2
commit 7e7ec1013b
27 changed files with 480 additions and 429 deletions

View File

@@ -1,11 +1,11 @@
{
"name": "client",
"name": "emailsorter-client",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "client",
"name": "emailsorter-client",
"version": "0.0.0",
"dependencies": {
"@radix-ui/react-accordion": "^1.2.12",

View File

@@ -17,39 +17,41 @@ const stepLabels: Record<string, string> = {
'completed': 'Completed',
}
const stepOrder = ['connect', 'first_rule', 'see_results', 'auto_schedule']
const stepOrderShort = ['connect', 'see_results']
export function OnboardingProgress({ currentStep, completedSteps, totalSteps, onSkip }: OnboardingProgressProps) {
const stepIndex = ['connect', 'first_rule', 'see_results', 'auto_schedule'].indexOf(currentStep)
const currentStepNumber = stepIndex >= 0 ? stepIndex + 1 : 0
const progress = totalSteps > 0 ? (completedSteps.length / totalSteps) * 100 : 0
const steps = totalSteps === 2 ? stepOrderShort : stepOrder
const stepIndex = steps.indexOf(currentStep)
const currentStepNumber = stepIndex >= 0 ? stepIndex + 1 : 1
const progress = totalSteps > 0 ? (completedSteps.filter(s => steps.includes(s)).length / totalSteps) * 100 : 0
if (currentStep === 'completed' || currentStep === 'not_started') {
return null
}
return (
<div className="bg-white border border-slate-200 rounded-lg p-4 shadow-sm">
<div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg p-4 shadow-sm">
<div className="flex items-center justify-between mb-3">
<div>
<p className="text-sm font-semibold text-slate-900">Getting started</p>
<p className="text-xs text-slate-500">Step {currentStepNumber} of {totalSteps}</p>
<p className="text-sm font-semibold text-slate-900 dark:text-slate-100">Getting started</p>
<p className="text-xs text-slate-500 dark:text-slate-400">Step {currentStepNumber} of {totalSteps}</p>
</div>
<Button variant="ghost" size="sm" onClick={onSkip} className="text-slate-500 hover:text-slate-700">
<Button variant="ghost" size="sm" onClick={onSkip} className="text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-300">
<X className="w-4 h-4 mr-1" />
Skip
I&apos;ll do this later
</Button>
</div>
{/* Progress bar */}
<div className="w-full h-2 bg-slate-100 rounded-full overflow-hidden mb-2">
<div className="w-full h-2 bg-slate-100 dark:bg-slate-700 rounded-full overflow-hidden mb-2">
<div
className="h-full bg-primary-500 transition-all duration-300"
style={{ width: `${progress}%` }}
style={{ width: `${Math.min(100, progress)}%` }}
/>
</div>
{/* Step indicators */}
<div className="flex items-center gap-2 text-xs text-slate-500">
{['connect', 'first_rule', 'see_results', 'auto_schedule'].map((step, idx) => {
<div className="flex items-center gap-2 text-xs text-slate-500 dark:text-slate-400">
{steps.map((step, idx) => {
const isCompleted = completedSteps.includes(step)
const isCurrent = currentStep === step
@@ -59,8 +61,8 @@ export function OnboardingProgress({ currentStep, completedSteps, totalSteps, on
isCompleted
? 'bg-green-500 text-white'
: isCurrent
? 'bg-primary-500 text-white ring-2 ring-primary-200'
: 'bg-slate-200 text-slate-400'
? 'bg-primary-500 text-white ring-2 ring-primary-200 dark:ring-primary-800'
: 'bg-slate-200 dark:bg-slate-600 text-slate-400'
}`}>
{isCompleted ? (
<Check className="w-3 h-3" />
@@ -69,13 +71,13 @@ export function OnboardingProgress({ currentStep, completedSteps, totalSteps, on
)}
</div>
<span className={`truncate hidden sm:inline ${
isCurrent ? 'text-slate-900 font-medium' : ''
isCurrent ? 'text-slate-900 dark:text-slate-100 font-medium' : ''
}`}>
{stepLabels[step] || step}
{stepLabels[step] || (step === 'see_results' ? 'Done' : step)}
</span>
{idx < 3 && (
{idx < steps.length - 1 && (
<div className={`flex-1 h-0.5 mx-1 ${
isCompleted ? 'bg-green-500' : 'bg-slate-200'
isCompleted ? 'bg-green-500' : 'bg-slate-200 dark:bg-slate-600'
}`} />
)}
</div>

View File

@@ -1,5 +1,5 @@
import { Button } from '@/components/ui/button'
import { X, Sparkles, Zap, Infinity } from 'lucide-react'
import { X, Sparkles, Zap, Infinity as InfinityIcon } from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { trackUpgradeClicked } from '@/lib/analytics'
import { useAuth } from '@/context/AuthContext'
@@ -97,7 +97,7 @@ export function UpgradePrompt({
onClick={handleUpgrade}
className="flex-1 bg-primary-600 hover:bg-primary-700"
>
<Infinity className="w-4 h-4 mr-2" />
<InfinityIcon className="w-4 h-4 mr-2" />
Upgrade
</Button>
<Button

View File

@@ -1,119 +1,66 @@
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: "Why not just use Gmail filters?",
answer: "Gmail filters need rules you write (sender, keywords). We read the email and put it in Lead, Client, or Noise — no rules."
},
{
question: "Which email providers work?",
answer: "Gmail and Outlook. More coming soon."
question: "What happens to my emails?",
answer: "We only read headers and a short snippet to choose the label. We don't store your mail or use it for ads. Disconnect and we stop."
},
{
question: "Can I create custom rules?",
answer: "Absolutely! You can set VIP contacts and define custom categories."
question: "Can this mess up my inbox?",
answer: "We only add labels or move to folders. We don't delete. Disconnect and nothing stays changed."
},
{
question: "What about old emails?",
answer: "The last 30 days are analyzed. You decide if they should be sorted too."
question: "Do you need my password?",
answer: "No. You sign in with Google or Microsoft. We never see or store your password. You can revoke access anytime."
},
{
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."
question: "What if I don't like it?",
answer: "Cancel anytime. No contract. Your data is yours; disconnect and we stop. Free trial, no card."
},
]
export function FAQ() {
const [openIndex, setOpenIndex] = useState<number | null>(0)
return (
<section id="faq" className="py-24 bg-slate-50 dark:bg-slate-900">
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Section header */}
<div className="text-center mb-16">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-100 dark:bg-primary-900/30 mb-6">
<HelpCircle className="w-8 h-8 text-primary-600 dark:text-primary-400" />
</div>
<h2 className="text-3xl sm:text-4xl font-bold text-slate-900 dark:text-slate-100 mb-4">
FAQ
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl sm:text-4xl font-bold text-slate-900 dark:text-slate-100 mb-2">
Questions we get a lot
</h2>
<p className="text-lg text-slate-600 dark:text-slate-400">
Quick answers to common questions.
<p className="text-slate-600 dark:text-slate-400">
Straight answers. No fluff.
</p>
</div>
{/* FAQ items */}
<div className="space-y-3">
<div className="space-y-12">
{faqs.map((faq, index) => (
<FAQItem
<div
key={index}
question={faq.question}
answer={faq.answer}
isOpen={openIndex === index}
onClick={() => setOpenIndex(openIndex === index ? null : index)}
/>
className="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-10 items-baseline"
>
<p className="text-lg md:text-xl font-semibold text-slate-900 dark:text-slate-100">
{faq.question}
</p>
<p className="text-sm text-slate-600 dark:text-slate-400 leading-relaxed">
{faq.answer}
</p>
</div>
))}
</div>
{/* Contact CTA */}
<div className="mt-12 text-center p-6 bg-white dark:bg-slate-800 rounded-2xl border border-slate-200 dark:border-slate-700">
<p className="text-slate-600 dark:text-slate-400 mb-2">Still have questions?</p>
<a
href="mailto:support@emailsorter.com"
className="text-primary-600 dark:text-primary-400 font-semibold hover:text-primary-700 dark:hover:text-primary-300"
<p className="mt-10 text-center text-sm text-slate-600 dark:text-slate-400">
Still unsure?{' '}
<a
href="mailto:support@emailsorter.webklar.com"
className="text-slate-700 dark:text-slate-300 hover:underline"
>
Contact us
Email us we reply fast
</a>
</div>
.
</p>
</div>
</section>
)
}
interface FAQItemProps {
question: string
answer: string
isOpen: boolean
onClick: () => void
}
function FAQItem({ question, answer, isOpen, onClick }: FAQItemProps) {
return (
<div className="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden">
<button
className="w-full px-6 py-4 text-left flex items-center justify-between hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors"
onClick={onClick}
>
<span className="font-semibold text-slate-900 dark:text-slate-100 pr-4">{question}</span>
<ChevronDown
className={cn(
"w-5 h-5 text-slate-400 dark:text-slate-500 transition-transform duration-200 flex-shrink-0",
isOpen && "rotate-180"
)}
/>
</button>
<div
className={cn(
"overflow-hidden transition-all duration-200",
isOpen ? "max-h-40" : "max-h-0"
)}
>
<p className="px-6 pb-4 text-slate-600 dark:text-slate-400">{answer}</p>
</div>
</div>
)
}

View File

@@ -11,41 +11,41 @@ import {
const features = [
{
icon: Inbox,
title: "Stop drowning in emails",
description: "Clear inbox, less stress. Automatically sort newsletters, promotions, and social updates away from what matters.",
title: "Categories, not chaos",
description: "Leads, clients, invoices, newsletters — sorted into folders. Your inbox shows what pays first.",
color: "from-violet-500 to-purple-600",
highlight: true,
},
{
icon: Zap,
title: "One-click smart rules",
description: "AI suggests, you approve. Create smart rules in seconds and apply them with one click.",
title: "One click to sort",
description: "Connect your inbox, click Sort Now. No rules to write. We read and categorize; you review.",
color: "from-amber-500 to-orange-600",
highlight: true,
},
{
icon: Settings,
title: "Automation that keeps working",
description: "Set it and forget it. Your inbox stays organized automatically, day after day.",
title: "Runs when you want",
description: "Sort on demand or set a schedule. Your inbox stays organized without you touching it.",
color: "from-blue-500 to-cyan-600",
highlight: true,
},
{
icon: Brain,
title: "AI-powered smart sorting",
description: "Our AI automatically recognizes whether an email is an invoice, newsletter, or important message.",
title: "Content-aware sorting",
description: "We look at sender, subject, and a short snippet to decide the category. No keyword lists.",
color: "from-green-500 to-emerald-600"
},
{
icon: Shield,
title: "GDPR compliant",
description: "Your data stays secure. We only read email headers and metadata for sorting.",
title: "Minimal data",
description: "We only read what we need to categorize. No storing email body or attachments. GDPR compliant.",
color: "from-pink-500 to-rose-600"
},
{
icon: Clock,
title: "Save time",
description: "Average 2 hours per week less on email organization. More time for what matters.",
title: "Less time on triage",
description: "Spend less time deciding what's important. Inbox shows clients and leads first.",
color: "from-indigo-500 to-blue-600"
},
]
@@ -57,14 +57,10 @@ export function Features() {
{/* Section header */}
<div className="text-center mb-16">
<h2 className="text-3xl sm:text-4xl font-bold text-slate-900 dark:text-slate-100 mb-4">
Everything you need for{' '}
<span className="text-transparent bg-clip-text bg-gradient-to-r from-primary-600 to-accent-500">
Inbox Zero
</span>
What it does
</h2>
<p className="text-lg text-slate-600 dark:text-slate-400 max-w-2xl mx-auto">
EmailSorter combines AI technology with proven email management methods
for maximum productivity.
Sort incoming mail into categories so your inbox shows what matters first. No rules to write.
</p>
</div>

View File

@@ -17,7 +17,7 @@ export function Footer() {
</span>
</Link>
<p className="text-sm text-slate-400 mb-6">
AI-powered email sorting for more productivity and less stress.
Email sorting for freelancers and small teams. Gmail & Outlook.
</p>
{/* Social links */}
<div className="flex gap-4">
@@ -70,26 +70,19 @@ export function Footer() {
FAQ
</button>
</li>
<li>
<a href="#" className="hover:text-white transition-colors">
Roadmap
</a>
</li>
</ul>
</div>
{/* Company */}
{/* Contact */}
<div>
<h4 className="font-semibold text-white mb-4">Company</h4>
<h4 className="font-semibold text-white mb-4">Contact</h4>
<ul className="space-y-3">
<li>
<a
href="https://webklar.com"
target="_blank"
rel="noopener noreferrer"
href="mailto:support@emailsorter.webklar.com"
className="hover:text-white transition-colors"
>
About us
support@emailsorter.webklar.com
</a>
</li>
<li>
@@ -99,25 +92,7 @@ export function Footer() {
rel="noopener noreferrer"
className="hover:text-white transition-colors"
>
Blog
</a>
</li>
<li>
<a
href="https://webklar.com"
target="_blank"
rel="noopener noreferrer"
className="hover:text-white transition-colors"
>
Careers
</a>
</li>
<li>
<a
href="mailto:support@webklar.com"
className="hover:text-white transition-colors"
>
Contact
webklar.com
</a>
</li>
</ul>
@@ -142,16 +117,6 @@ export function Footer() {
Impressum
</Link>
</li>
<li>
<a
href="https://webklar.com"
target="_blank"
rel="noopener noreferrer"
className="hover:text-white transition-colors"
>
webklar.com
</a>
</li>
</ul>
</div>
</div>
@@ -160,10 +125,7 @@ export function Footer() {
<div className="mt-12 pt-8 border-t border-slate-800">
<div className="flex flex-col md:flex-row justify-between items-center gap-4 mb-4">
<p className="text-sm text-slate-500">
© {new Date().getFullYear()} EmailSorter. All rights reserved.
</p>
<p className="text-sm text-slate-500">
Made with
© {new Date().getFullYear()} EmailSorter
</p>
</div>
{/* webklar.com Verweis */}

View File

@@ -1,8 +1,9 @@
import { useNavigate } from 'react-router-dom'
import { captureUTMParams } from '@/lib/analytics'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { ArrowRight, Mail, Inbox, Sparkles, Check, Zap } from 'lucide-react'
import { ArrowRight, Sparkles, Check } from 'lucide-react'
export function Hero() {
const navigate = useNavigate()
@@ -33,47 +34,46 @@ export function Hero() {
<div className="text-center lg:text-left">
<Badge className="mb-6 bg-primary-500/20 text-primary-200 border-primary-400/30">
<Sparkles className="w-3 h-3 mr-1" />
AI-powered email sorting
For freelancers & small teams
</Badge>
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-extrabold text-white leading-tight mb-6">
Clean inbox automatically
Leads, clients, spam
<br />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-accent-400 to-primary-300">
in minutes.
sorted automatically.
</span>
</h1>
<p className="text-lg sm:text-xl text-slate-300 mb-8 max-w-xl mx-auto lg:mx-0">
Create smart rules, apply in one click, keep it clean with automation.
Stop drowning in emails and start focusing on what matters.
Connect Gmail or Outlook. We put newsletters, promos, and noise in folders so your inbox stays for what pays.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center lg:justify-start mb-8">
<Button
size="xl"
onClick={() => navigate('/setup?demo=true')}
className="group bg-accent-500 hover:bg-accent-600"
>
<Sparkles className="w-5 h-5 mr-2" />
Try Demo
</Button>
<div className="flex flex-col sm:flex-row gap-4 justify-center lg:justify-start mb-4">
<Button
size="xl"
onClick={handleCTAClick}
variant="outline"
className="bg-white/10 border-white/20 text-white hover:bg-white/20 group"
className="group bg-accent-500 hover:bg-accent-600"
>
Connect inbox
Try it free
<ArrowRight className="w-5 h-5 ml-2 group-hover:translate-x-1 transition-transform" />
</Button>
</div>
<p className="text-sm text-slate-400 mb-8">
<button
type="button"
onClick={() => navigate('/setup?demo=true')}
className="underline hover:text-slate-300 transition-colors"
>
Or try a 30-second demo first
</button>
</p>
{/* Trust badges */}
<div className="flex flex-wrap gap-6 justify-center lg:justify-start text-slate-400 text-sm">
<div className="flex items-center gap-2">
<Check className="w-4 h-4 text-accent-400" />
No credit card required
No credit card
</div>
<div className="flex items-center gap-2">
<Check className="w-4 h-4 text-accent-400" />
@@ -86,59 +86,22 @@ export function Hero() {
</div>
</div>
{/* Right side - Visual */}
{/* Right side - Inbox visual (product screenshot feel) */}
<div className="relative hidden lg:block">
<div className="relative">
{/* Main card */}
<div className="bg-white/10 backdrop-blur-xl rounded-3xl border border-white/20 p-6 shadow-2xl animate-float">
<div className="flex items-center gap-3 mb-6">
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center">
<Inbox className="w-6 h-6 text-white" />
</div>
<div>
<h3 className="font-semibold text-white">Your Inbox</h3>
<p className="text-sm text-slate-400">Auto-sorted</p>
</div>
</div>
{/* Email categories preview */}
<div className="space-y-3">
<EmailPreview
category="Important"
color="bg-red-500"
sender="John Smith"
subject="Meeting tomorrow at 10"
delay="stagger-1"
/>
<EmailPreview
category="Invoice"
color="bg-green-500"
sender="Amazon"
subject="Invoice for order #12345"
delay="stagger-2"
/>
<EmailPreview
category="Newsletter"
color="bg-purple-500"
sender="Tech Daily"
subject="Latest AI trends"
delay="stagger-3"
/>
<EmailPreview
category="Social"
color="bg-cyan-500"
sender="LinkedIn"
subject="3 new connection requests"
delay="stagger-4"
/>
</div>
<div className="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 max-w-md overflow-hidden">
<div className="border-b border-slate-200 dark:border-slate-700 px-4 py-2.5">
<span className="text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">Inbox</span>
</div>
{/* Floating badge */}
<div className="absolute -right-4 top-1/4 bg-accent-500 text-white px-4 py-2 rounded-full text-sm font-semibold shadow-lg animate-pulse-glow">
<Zap className="w-4 h-4 inline mr-1" />
AI sorting
<div className="divide-y divide-slate-100 dark:divide-slate-800">
<InboxRow sender="Sarah Chen" subject="Re: Project quote" label="Lead" isFocal />
<InboxRow sender="Mike, Acme Inc" subject="Invoice #8821" label="Client" />
<InboxRow sender="Newsletter" subject="Your weekly digest" label="Noise" />
<InboxRow sender="Support" subject="Your ticket #443" label="Client" />
<InboxRow sender="Promo" subject="20% off this week" label="Noise" />
</div>
<p className="text-xs text-slate-400 dark:text-slate-500 px-4 py-3 border-t border-slate-100 dark:border-slate-800">
This happens automatically on new emails.
</p>
</div>
</div>
</div>
@@ -154,26 +117,39 @@ export function Hero() {
)
}
interface EmailPreviewProps {
category: string
color: string
sender: string
subject: string
delay: string
type InboxLabel = 'Lead' | 'Client' | 'Noise'
const labelClass: Record<InboxLabel, string> = {
Lead: 'text-primary-600 dark:text-primary-500',
Client: 'text-slate-600 dark:text-slate-600',
Noise: 'text-slate-400 dark:text-slate-500',
}
function EmailPreview({ category, color, sender, subject, delay }: EmailPreviewProps) {
interface InboxRowProps {
sender: string
subject: string
label: InboxLabel
isFocal?: boolean
}
function InboxRow({ sender, subject, label, isFocal = false }: InboxRowProps) {
return (
<div className={`flex items-center gap-3 bg-white/5 rounded-xl p-3 border border-white/10 opacity-0 animate-[fadeIn_0.5s_ease-out_forwards] ${delay}`}>
<div className={`w-2 h-10 rounded-full ${color}`} />
<div
className={cn(
"flex items-center gap-4 px-4 py-2.5",
isFocal && "border-l-2 border-l-primary-500 bg-slate-50/80 dark:bg-slate-800/50"
)}
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-0.5">
<span className="text-sm font-medium text-white truncate">{sender}</span>
<span className={`text-xs px-2 py-0.5 rounded ${color} text-white`}>{category}</span>
</div>
<p className="text-sm text-slate-400 truncate">{subject}</p>
<p className={cn(
"truncate",
isFocal ? "text-sm font-semibold text-slate-900 dark:text-slate-100" : "text-sm font-medium text-slate-600 dark:text-slate-400"
)}>
{sender}
</p>
<p className="text-xs text-slate-500 dark:text-slate-500 truncate mt-0.5">{subject}</p>
</div>
<Mail className="w-4 h-4 text-slate-500 flex-shrink-0" />
<span className={cn("text-xs flex-shrink-0", labelClass[label])}>{label}</span>
</div>
)
}

View File

@@ -17,19 +17,19 @@ const steps = [
icon: Link2,
step: "02",
title: "Connect email",
description: "Connect Gmail or Outlook with one click. Secure OAuth authentication.",
description: "Sign in with Google or Microsoft. We never see your password.",
},
{
icon: Sparkles,
step: "03",
title: "AI analyzes",
description: "Our AI learns your email patterns and creates personalized sorting rules.",
title: "We categorize",
description: "We read sender and subject, put each email in a category. No rules to write.",
},
{
icon: PartyPopper,
step: "04",
title: "Enjoy Inbox Zero",
description: "Sit back and enjoy a clean inbox automatically.",
title: "Inbox stays clean",
description: "Newsletters and promos go to folders. Your inbox shows what matters first.",
},
]

View File

@@ -82,7 +82,7 @@ export function Navbar() {
Sign in
</Button>
<Button onClick={() => navigate('/register')}>
Get started free
Try it free
</Button>
</>
)}
@@ -146,7 +146,7 @@ export function Navbar() {
Sign in
</Button>
<Button className="w-full h-11" onClick={() => navigate('/register')}>
Get started free
Try it free
</Button>
</>
)}

View File

@@ -1,67 +1,51 @@
import { CheckCircle2, Clock, Brain, Shield } from 'lucide-react'
import { Code2, Users, Zap } from 'lucide-react'
const benefits = [
const items = [
{
icon: Clock,
title: "Save 2+ hours/week",
description: "Less time sorting emails, more time for important tasks.",
icon: Code2,
title: "Built in public",
description: "We ship updates and share progress openly. No hype, no fake traction.",
},
{
icon: Brain,
title: "AI does it automatically",
description: "Set up once, then everything runs by itself.",
icon: Users,
title: "Early users",
description: "We're in beta. Feedback from freelancers and small teams shapes the product.",
},
{
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.",
icon: Zap,
title: "Simple setup",
description: "Connect Gmail or Outlook, click Sort. No long onboarding or sales call.",
},
]
export function Testimonials() {
return (
<section className="py-20 bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Section header */}
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl sm:text-4xl font-bold text-white mb-4">
Why EmailSorter?
<h2 className="text-2xl sm:text-3xl font-bold text-white mb-2">
Honest context
</h2>
<p className="text-lg text-slate-300 max-w-2xl mx-auto">
No more email chaos. Focus on what matters.
<p className="text-slate-400 text-sm sm:text-base">
We're a small product. Here's how we work.
</p>
</div>
{/* Benefits grid */}
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6">
{benefits.map((benefit, index) => (
<BenefitCard key={index} {...benefit} />
<div className="grid md:grid-cols-3 gap-6">
{items.map((item, index) => (
<div
key={index}
className="bg-white/5 backdrop-blur-sm rounded-xl p-6 border border-white/10"
>
<div className="w-10 h-10 rounded-lg bg-primary-500/20 flex items-center justify-center mb-4">
<item.icon className="w-5 h-5 text-primary-400" />
</div>
<h3 className="text-base font-semibold text-white mb-1">{item.title}</h3>
<p className="text-slate-400 text-sm">{item.description}</p>
</div>
))}
</div>
</div>
</section>
)
}
interface BenefitCardProps {
icon: React.ElementType
title: string
description: string
}
function BenefitCard({ icon: Icon, title, description }: BenefitCardProps) {
return (
<div className="bg-white/5 backdrop-blur-sm rounded-xl p-6 border border-white/10 hover:bg-white/10 transition-colors">
<div className="w-12 h-12 rounded-lg bg-primary-500/20 flex items-center justify-center mb-4">
<Icon className="w-6 h-6 text-primary-400" />
</div>
<h3 className="text-lg font-semibold text-white mb-2">{title}</h3>
<p className="text-slate-400 text-sm">{description}</p>
</div>
)
}

View File

@@ -0,0 +1,48 @@
import { Shield, Mail, Trash2 } from 'lucide-react'
export function TrustSection() {
return (
<section id="trust" className="py-16 sm:py-20 bg-white dark:bg-slate-900 border-y border-slate-200 dark:border-slate-800">
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl sm:text-3xl font-bold text-slate-900 dark:text-slate-100 text-center mb-10">
Your data, in plain language
</h2>
<ul className="space-y-6">
<li className="flex gap-4 items-start">
<div className="w-10 h-10 rounded-lg bg-slate-100 dark:bg-slate-800 flex items-center justify-center flex-shrink-0">
<Mail className="w-5 h-5 text-slate-600 dark:text-slate-400" />
</div>
<div>
<p className="font-semibold text-slate-900 dark:text-slate-100 mb-0.5">We only read what we need</p>
<p className="text-slate-600 dark:text-slate-400 text-sm">
We use sender, subject, and a short snippet to decide the category (e.g. newsletter vs client). We don&apos;t store your email body or attachments.
</p>
</div>
</li>
<li className="flex gap-4 items-start">
<div className="w-10 h-10 rounded-lg bg-slate-100 dark:bg-slate-800 flex items-center justify-center flex-shrink-0">
<Shield className="w-5 h-5 text-slate-600 dark:text-slate-400" />
</div>
<div>
<p className="font-semibold text-slate-900 dark:text-slate-100 mb-0.5">No selling, no ads</p>
<p className="text-slate-600 dark:text-slate-400 text-sm">
Your email data is not used for advertising or sold to anyone. We run a paid product; our revenue comes from subscriptions, not your inbox.
</p>
</div>
</li>
<li className="flex gap-4 items-start">
<div className="w-10 h-10 rounded-lg bg-slate-100 dark:bg-slate-800 flex items-center justify-center flex-shrink-0">
<Trash2 className="w-5 h-5 text-slate-600 dark:text-slate-400" />
</div>
<div>
<p className="font-semibold text-slate-900 dark:text-slate-100 mb-0.5">You can leave anytime</p>
<p className="text-slate-600 dark:text-slate-400 text-sm">
Disconnect your account and we stop. Cancel your subscription with one click. No lock-in, no &quot;contact sales&quot; to leave.
</p>
</div>
</li>
</ul>
</div>
</section>
)
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-refresh/only-export-components */
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-refresh/only-export-components */
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-refresh/only-export-components */
import React, { createContext, useContext, useEffect, useState } from 'react'
import { auth } from '@/lib/appwrite'
import type { Models } from 'appwrite'

View File

@@ -17,7 +17,7 @@ export interface TrackingParams {
export interface ConversionEvent {
type: 'page_view' | 'signup' | 'trial_start' | 'purchase' | 'email_connected' | 'onboarding_step' | 'provider_connected' | 'demo_used' | 'suggested_rules_generated' | 'rule_created' | 'rules_applied' | 'limit_reached' | 'upgrade_clicked' | 'referral_shared' | 'sort_completed' | 'account_deleted'
userId?: string
metadata?: Record<string, any>
metadata?: Record<string, unknown>
sessionId?: string
}

View File

@@ -105,7 +105,7 @@ export const api = {
name: string
description: string
confidence: number
action: any
action?: { name?: string }
}>
}>('/email/sort', {
method: 'POST',
@@ -276,6 +276,7 @@ export const api = {
blockedSenders?: string[]
customRules?: Array<{ condition: string; category: string }>
priorityTopics?: string[]
companyLabels?: Array<{ name: string; condition?: string; category: string; enabled: boolean }>
}) {
return fetchApi<{ success: boolean }>('/preferences', {
method: 'POST',
@@ -292,8 +293,8 @@ export const api = {
enabledCategories: string[]
categoryActions: Record<string, 'inbox' | 'archive_read' | 'star'>
autoDetectCompanies: boolean
cleanup?: any
categoryAdvanced?: Record<string, any>
cleanup?: unknown
categoryAdvanced?: Record<string, unknown>
version?: number
}>(`/preferences/ai-control?userId=${userId}`)
},
@@ -302,8 +303,8 @@ export const api = {
enabledCategories?: string[]
categoryActions?: Record<string, 'inbox' | 'archive_read' | 'star'>
autoDetectCompanies?: boolean
cleanup?: any
categoryAdvanced?: Record<string, any>
cleanup?: unknown
categoryAdvanced?: Record<string, unknown>
version?: number
}) {
return fetchApi<{ success: boolean }>('/preferences/ai-control', {
@@ -407,14 +408,14 @@ export const api = {
// ═══════════════════════════════════════════════════════════════════════════
async getProducts() {
return fetchApi<any[]>('/products')
return fetchApi<unknown[]>('/products')
},
async getQuestions(productSlug: string) {
return fetchApi<any[]>(`/questions?productSlug=${productSlug}`)
return fetchApi<unknown[]>(`/questions?productSlug=${productSlug}`)
},
async createSubmission(productSlug: string, answers: Record<string, any>) {
async createSubmission(productSlug: string, answers: Record<string, unknown>) {
return fetchApi<{ submissionId: string }>('/submissions', {
method: 'POST',
body: JSON.stringify({ productSlug, answers }),

View File

@@ -18,13 +18,9 @@ export { ID }
export const auth = {
// Create a new account
async register(email: string, password: string, name?: string) {
try {
const user = await account.create(ID.unique(), email, password, name)
await this.login(email, password)
return user
} catch (error) {
throw error
}
const user = await account.create(ID.unique(), email, password, name)
await this.login(email, password)
return user
},
// Login with email and password

View File

@@ -17,9 +17,7 @@ import {
Zap,
BarChart3,
Users,
Bell,
Shield,
HelpCircle,
Loader2,
Check,
AlertCircle,
@@ -65,7 +63,7 @@ interface SortResult {
name: string
description: string
confidence: number
action: any
action?: { name?: string; email?: string; condition?: string; category?: string }
}>
}
@@ -131,7 +129,7 @@ export function Dashboard() {
if (referralRes.data) setReferralCode(referralRes.data.referralCode)
} catch (err) {
console.error('Error loading dashboard data:', err)
setError('Failed to load data')
setError('Couldnt load your data. Check your connection and refresh.')
} finally {
setLoading(false)
}
@@ -139,7 +137,7 @@ export function Dashboard() {
const handleSortNow = async () => {
if (!user?.$id || accounts.length === 0) {
setError('Please connect an email account first to start sorting.')
setError('Connect your inbox first, then click Sort Now.')
return
}
@@ -178,7 +176,7 @@ export function Dashboard() {
}
} catch (err) {
console.error('Error sorting emails:', err)
setError('Unable to sort emails right now. Please check your connection and try again.')
setError('Something went wrong. Check your connection and try again.')
} finally {
setSorting(false)
}
@@ -248,13 +246,6 @@ export function Dashboard() {
</Link>
<div className="flex items-center gap-1.5 sm:gap-2 lg:gap-4">
<Button variant="ghost" size="icon" className="hidden lg:flex h-9 w-9">
<Bell className="w-5 h-5 text-slate-500 dark:text-slate-400" />
</Button>
<Button variant="ghost" size="icon" className="hidden lg:flex h-9 w-9">
<HelpCircle className="w-5 h-5 text-slate-500 dark:text-slate-400" />
</Button>
<div className="hidden lg:block h-6 w-px bg-slate-200 dark:bg-slate-700" />
<Button
variant="ghost"
onClick={() => navigate('/settings')}
@@ -304,7 +295,7 @@ export function Dashboard() {
<p className="text-sm text-slate-600 dark:text-slate-400">
{accounts.length > 0
? `${accounts.length} account${accounts.length > 1 ? 's' : ''} connected`
: 'Connect an account to get started'}
: 'Connect Gmail or Outlook to sort your first emails.'}
</p>
</div>
<div className="flex items-center gap-2 sm:gap-3">
@@ -315,7 +306,7 @@ export function Dashboard() {
aria-label="Connect email account"
>
<Plus className="w-4 h-4 mr-2" />
Connect Account
Connect inbox
</Button>
) : (
<Button
@@ -359,6 +350,15 @@ export function Dashboard() {
</div>
)}
{/* First-time hint: account connected, no sort yet */}
{!loading && accounts.length > 0 && !sortResult && !error && (
<div className="mb-4 p-4 bg-slate-100 dark:bg-slate-800/60 rounded-xl border border-slate-200 dark:border-slate-700">
<p className="text-sm text-slate-700 dark:text-slate-300">
Click <strong>Sort Now</strong> to categorize your inbox. Takes about 30 seconds. Nothing is deleted we only add labels or move mail to folders.
</p>
</div>
)}
{/* Error message */}
{error && (
<div className="mb-3 sm:mb-4 p-2.5 sm:p-3 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-lg flex items-start sm:items-center gap-2 text-red-700 dark:text-red-300">
@@ -378,7 +378,8 @@ export function Dashboard() {
<div className="flex items-center justify-center py-20">
<div className="text-center">
<Loader2 className="w-10 h-10 animate-spin text-primary-500 dark:text-primary-400 mx-auto mb-4" />
<p className="text-slate-500 dark:text-slate-400">Loading dashboard...</p>
<p className="text-slate-500 dark:text-slate-400">Loading your data...</p>
<p className="text-slate-400 dark:text-slate-500 text-sm mt-1">One moment.</p>
</div>
</div>
) : (
@@ -394,9 +395,9 @@ export function Dashboard() {
<Sparkles className="w-5 h-5 text-white" />
</div>
<div>
<CardTitle className="text-lg">AI-Assistent</CardTitle>
<CardTitle className="text-lg">Email sorting</CardTitle>
<CardDescription className="text-sm">
Automatische E-Mail-Sortierung mit KI
Categorize inbox leads, clients, newsletters
</CardDescription>
</div>
</div>
@@ -413,10 +414,10 @@ export function Dashboard() {
</div>
<div>
<p className={`${sortResult.isFirstRun ? 'text-lg' : 'text-sm'} font-bold text-green-800 dark:text-green-200`}>
{sortResult.isFirstRun ? '🎉 First sort complete!' : 'Sorting complete!'}
{sortResult.isFirstRun ? 'Done. Your inbox is sorted.' : 'Sort complete.'}
</p>
{sortResult.isFirstRun && (
<p className="text-sm text-green-700 dark:text-green-300 mt-0.5">Your inbox is getting organized</p>
<p className="text-sm text-green-700 dark:text-green-300 mt-0.5">Newsletters and promos are in folders. Check your inbox only important mail is left.</p>
)}
</div>
</div>
@@ -444,10 +445,10 @@ export function Dashboard() {
<div className="bg-white/80 dark:bg-slate-800/80 p-3 rounded-lg">
<p className="text-xs text-green-600 dark:text-green-400 mb-1">Top category</p>
<p className="text-xl font-bold text-green-800 dark:text-green-200">
{Object.entries(sortResult.categories).sort(([_, a], [__, b]) => b - a)[0]?.[1] || 0}
{Object.entries(sortResult.categories).sort(([, a], [, b]) => b - a)[0]?.[1] || 0}
</p>
<p className="text-xs text-green-500 dark:text-green-400 mt-1">
{formatCategoryName(Object.entries(sortResult.categories).sort(([_, a], [__, b]) => b - a)[0]?.[0] || '')}
{formatCategoryName(Object.entries(sortResult.categories).sort(([, a], [, b]) => b - a)[0]?.[0] || '')}
</p>
</div>
)}
@@ -457,7 +458,7 @@ export function Dashboard() {
{sortResult.isFirstRun && sortResult.suggestedRules && sortResult.suggestedRules.length > 0 && (
<div className="mt-4 pt-4 border-t border-green-200 dark:border-green-800">
<div className="mb-3">
<p className="text-sm font-semibold text-green-900 dark:text-green-200">Smart suggestions for you</p>
<p className="text-sm font-semibold text-green-900 dark:text-green-200">Suggestions for you</p>
<p className="text-xs text-green-600 dark:text-green-400 mt-0.5">Based on your email patterns</p>
</div>
<div className="space-y-2 mb-3">
@@ -479,19 +480,19 @@ export function Dashboard() {
setSorting(true)
try {
const vipSenders = sortResult.suggestedRules
.filter(r => r.type === 'vip_sender' && r.action?.email)
.map(r => r.action.email)
.filter((r): r is typeof r & { action: { email: string } } => r.type === 'vip_sender' && !!r.action?.email)
.map(r => ({ email: r.action.email }))
const companyLabels = sortResult.suggestedRules
.filter(r => r.type === 'company_label' && r.action?.name)
.filter((r): r is typeof r & { action: { name: string; condition?: string; category?: string } } => r.type === 'company_label' && !!r.action?.name)
.map(r => ({
name: r.action.name,
condition: r.action.condition,
condition: r.action.condition ?? '',
category: r.action.category || 'promotions',
enabled: true,
}))
const updates: any = {}
const updates: { vipSenders?: Array<{ email: string; name?: string }>; companyLabels?: Array<{ name: string; condition?: string; category: string; enabled: boolean }> } = {}
if (vipSenders.length > 0) {
updates.vipSenders = vipSenders
}
@@ -502,10 +503,10 @@ export function Dashboard() {
if (Object.keys(updates).length > 0) {
await api.saveUserPreferences(user.$id, updates)
trackRulesApplied(user.$id, sortResult.suggestedRules.length)
showMessage('success', `Applied ${sortResult.suggestedRules.length} smart rules! Your inbox will stay organized.`)
showMessage('success', `${sortResult.suggestedRules.length} rules applied. Your inbox will stay organized.`)
setSortResult({ ...sortResult, suggestedRules: [] })
}
} catch (err) {
} catch {
showMessage('error', 'Unable to apply rules right now. Please try again.')
} finally {
setSorting(false)
@@ -756,7 +757,7 @@ export function Dashboard() {
<CardTitle className="text-base">Control Panel</CardTitle>
</div>
<CardDescription className="text-xs">
KI-Einstellungen und Kategorien verwalten
Categories and rules
</CardDescription>
</CardHeader>
<CardContent>
@@ -767,7 +768,7 @@ export function Dashboard() {
aria-label="Open Control Panel settings"
>
<Settings className="w-4 h-4 mr-2" />
Control Panel öffnen
Open Control Panel
<ExternalLink className="w-3 h-3 ml-2" />
</Button>
</CardContent>
@@ -778,10 +779,10 @@ export function Dashboard() {
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<Settings className="w-5 h-5 text-primary-500 dark:text-primary-400" />
<CardTitle className="text-base">Einstellungen</CardTitle>
<CardTitle className="text-base">Settings</CardTitle>
</div>
<CardDescription className="text-xs">
Schnellzugriff auf wichtige Einstellungen
Quick access
</CardDescription>
</CardHeader>
<CardContent className="space-y-2">
@@ -810,7 +811,7 @@ export function Dashboard() {
aria-label="Open all settings"
>
<Settings className="w-4 h-4 mr-2" />
Alle Einstellungen
All settings
<ExternalLink className="w-3 h-3 ml-auto" />
</Button>
</CardContent>
@@ -819,9 +820,9 @@ export function Dashboard() {
{/* Account/System Karte */}
<Card className="border border-slate-200 dark:border-slate-700 shadow-sm">
<CardHeader className="pb-3">
<CardTitle className="text-base">Account & System</CardTitle>
<CardTitle className="text-base">Account</CardTitle>
<CardDescription className="text-xs">
Subscription und Konten-Status
Subscription and accounts
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
@@ -932,7 +933,7 @@ export function Dashboard() {
) : (
<div className="text-center py-4">
<Mail className="w-8 h-8 text-slate-300 dark:text-slate-600 mx-auto mb-2" />
<p className="text-xs text-slate-500 dark:text-slate-400 mb-2">No accounts connected</p>
<p className="text-xs text-slate-500 dark:text-slate-400 mb-2">No inbox connected yet</p>
<Button
onClick={() => navigate('/setup')}
variant="outline"
@@ -941,7 +942,7 @@ export function Dashboard() {
aria-label="Connect email account"
>
<Plus className="w-3 h-3 mr-1.5" />
Connect Account
Connect inbox
</Button>
</div>
)}

View File

@@ -21,8 +21,8 @@ export function ForgotPassword() {
try {
await auth.forgotPassword(email)
setSent(true)
} catch (err: any) {
setError(err.message || 'Fehler beim Senden der E-Mail')
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Fehler beim Senden der E-Mail')
} finally {
setLoading(false)
}

View File

@@ -2,8 +2,9 @@ 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 { TrustSection } from '@/components/landing/TrustSection'
import { Pricing } from '@/components/landing/Pricing'
import { FAQ } from '@/components/landing/FAQ'
import { Footer } from '@/components/landing/Footer'
@@ -15,6 +16,7 @@ export function Home() {
<Features />
<HowItWorks />
<Testimonials />
<TrustSection />
<Pricing />
<FAQ />
<Footer />

View File

@@ -23,8 +23,8 @@ export function Login() {
try {
await login(email, password)
navigate('/dashboard')
} catch (err: any) {
setError(err.message || 'Login failed. Please check your credentials.')
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Login failed. Please check your credentials.')
} finally {
setLoading(false)
}

View File

@@ -65,8 +65,8 @@ export function Register() {
try {
await register(email, password, name)
navigate('/setup')
} catch (err: any) {
setError(err.message || 'Registration failed. Please try again.')
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Registration failed. Please try again.')
} finally {
setLoading(false)
}

View File

@@ -51,8 +51,8 @@ export function ResetPassword() {
try {
await auth.resetPassword(userId, secret, password)
setSuccess(true)
} catch (err: any) {
setError(err.message || 'Fehler beim Zurücksetzen des Passworts')
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Fehler beim Zurücksetzen des Passworts')
} finally {
setLoading(false)
}

View File

@@ -54,7 +54,7 @@ import {
Save,
Edit2,
} from 'lucide-react'
import type { AIControlSettings, CompanyLabel, CategoryInfo, CleanupStatus } from '@/types/settings'
import type { AIControlSettings, CompanyLabel, CategoryInfo, CleanupSettings, CleanupStatus, CategoryAdvanced } from '@/types/settings'
import { PrivacySecurity } from '@/components/PrivacySecurity'
type TabType = 'profile' | 'accounts' | 'vip' | 'ai-control' | 'subscription' | 'privacy' | 'referrals'
@@ -198,29 +198,31 @@ export function Settings() {
if (prefsRes.data?.vipSenders) setVipSenders(prefsRes.data.vipSenders)
if (aiControlRes.data) {
// Merge cleanup defaults if not present
const settings: AIControlSettings = {
...aiControlRes.data,
cleanup: aiControlRes.data.cleanup || {
const raw = aiControlRes.data
const defaultCleanup: CleanupSettings = {
enabled: false,
readItems: {
enabled: false,
readItems: {
enabled: false,
action: 'archive_read' as const,
gracePeriodDays: 7,
},
promotions: {
enabled: false,
matchCategoriesOrLabels: ['promotions', 'newsletters'],
action: 'archive_read' as const,
deleteAfterDays: 30,
},
safety: {
requireConfirmForDelete: true,
dryRun: false,
maxDeletesPerRun: 100,
},
action: 'archive_read',
gracePeriodDays: 7,
},
categoryAdvanced: aiControlRes.data.categoryAdvanced || {},
version: aiControlRes.data.version || 1,
promotions: {
enabled: false,
matchCategoriesOrLabels: ['promotions', 'newsletters'],
action: 'archive_read',
deleteAfterDays: 30,
},
safety: {
requireConfirmForDelete: true,
dryRun: false,
maxDeletesPerRun: 100,
},
}
const settings: AIControlSettings = {
...raw,
cleanup: (raw.cleanup as CleanupSettings | undefined) || defaultCleanup,
categoryAdvanced: (raw.categoryAdvanced as Record<string, CategoryAdvanced> | undefined) || {},
version: raw.version ?? 1,
}
setAiControlSettings(settings)
savedSettingsRef.current = JSON.parse(JSON.stringify(settings)) // Deep copy
@@ -312,7 +314,7 @@ export function Settings() {
try {
const res = await api.getCleanupStatus(user.$id)
if (res.data) setCleanupStatus(res.data)
} catch (err) {
} catch {
// Silently fail if endpoint doesn't exist yet
console.debug('Cleanup status endpoint not available')
}
@@ -324,7 +326,7 @@ export function Settings() {
try {
const res = await api.getCleanupPreview(user.$id)
if (res.data?.preview) setCleanupPreview(res.data.preview)
} catch (err) {
} catch {
// Silently fail if endpoint doesn't exist yet
console.debug('Cleanup preview endpoint not available')
}
@@ -1058,7 +1060,7 @@ export function Settings() {
</div>
<select
value={filterEnabled}
onChange={(e) => setFilterEnabled(e.target.value as any)}
onChange={(e) => setFilterEnabled((e.target.value || 'all') as 'all' | 'enabled' | 'disabled')}
className="px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100"
>
<option value="all">All</option>
@@ -1070,7 +1072,7 @@ export function Settings() {
</Card>
{/* Tabs */}
<Tabs value={controlPanelTab} onValueChange={(v) => setControlPanelTab(v as any)} className="w-full">
<Tabs value={controlPanelTab} onValueChange={(v) => setControlPanelTab((v || 'rules') as 'rules' | 'cleanup' | 'labels')} className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="rules" className="text-sm sm:text-base">Rules</TabsTrigger>
<TabsTrigger value="cleanup" className="text-sm sm:text-base">Cleanup</TabsTrigger>
@@ -1554,7 +1556,7 @@ export function Settings() {
<div className="flex flex-wrap items-center gap-2">
<select
value={labelSort}
onChange={(e) => setLabelSort(e.target.value as any)}
onChange={(e) => setLabelSort((e.target.value || 'name') as 'name' | 'newest')}
className="px-3 py-2 text-sm border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100"
>
<option value="name">Sort: Name</option>
@@ -1834,7 +1836,7 @@ export function Settings() {
id="category-action"
value={aiControlSettings.categoryActions[selectedCategory.key] || selectedCategory.defaultAction}
onChange={(e) => {
const newActions = { ...aiControlSettings.categoryActions, [selectedCategory.key]: e.target.value as any }
const newActions = { ...aiControlSettings.categoryActions, [selectedCategory.key]: (e.target.value || 'inbox') as 'inbox' | 'archive_read' | 'star' }
setAiControlSettings({ ...aiControlSettings, categoryActions: newActions })
}}
className="w-full px-4 py-3 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 focus:ring-2 focus:ring-primary-500 focus:border-transparent"
@@ -1881,7 +1883,7 @@ export function Settings() {
onChange={(e) => {
const newAdvanced = { ...aiControlSettings.categoryAdvanced }
if (!newAdvanced[selectedCategory.key]) newAdvanced[selectedCategory.key] = {}
newAdvanced[selectedCategory.key] = { ...newAdvanced[selectedCategory.key], priority: e.target.value as any }
newAdvanced[selectedCategory.key] = { ...newAdvanced[selectedCategory.key], priority: (e.target.value || 'medium') as 'low' | 'medium' | 'high' }
setAiControlSettings({ ...aiControlSettings, categoryAdvanced: newAdvanced })
}}
className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 mt-1"

View File

@@ -107,9 +107,7 @@ export function Setup() {
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!' },
{ id: 'complete', title: 'Done', description: 'Go to dashboard' },
]
const stepIndex = steps.findIndex(s => s.id === currentStep)
@@ -128,13 +126,12 @@ export function Setup() {
} else {
setConnectedProvider('gmail')
setConnectedEmail(user.email)
setCurrentStep('preferences')
// Track onboarding step
await api.updateOnboardingStep(user.$id, 'first_rule', ['connect'])
setCurrentStep('complete')
await api.updateOnboardingStep(user.$id, 'see_results', ['connect'])
trackOnboardingStep(user.$id, 'first_rule')
trackProviderConnected(user.$id, 'gmail')
}
} catch (err) {
} catch {
setError('Gmail connection failed. Please try again.')
} finally {
setConnecting(null)
@@ -155,11 +152,10 @@ export function Setup() {
} else {
setConnectedProvider('outlook')
setConnectedEmail(user.email)
setCurrentStep('preferences')
// Track onboarding step
await api.updateOnboardingStep(user.$id, 'first_rule', ['connect'])
setCurrentStep('complete')
await api.updateOnboardingStep(user.$id, 'see_results', ['connect'])
}
} catch (err) {
} catch {
setError('Outlook connection failed. Please try again.')
} finally {
setConnecting(null)
@@ -176,13 +172,12 @@ export function Setup() {
if (response.data) {
setConnectedProvider('demo')
setConnectedEmail(response.data.email)
setCurrentStep('preferences')
// Track onboarding step
await api.updateOnboardingStep(user.$id, 'first_rule', ['connect'])
setCurrentStep('complete')
await api.updateOnboardingStep(user.$id, 'see_results', ['connect'])
trackOnboardingStep(user.$id, 'first_rule')
trackDemoUsed(user.$id)
}
} catch (err) {
} catch {
setError('Demo connection failed. Please try again.')
} finally {
setConnecting(null)
@@ -240,7 +235,7 @@ export function Setup() {
})
// Mark onboarding as completed
await api.updateOnboardingStep(user.$id, 'completed', ['connect', 'first_rule', 'see_results', 'auto_schedule'])
await api.updateOnboardingStep(user.$id, 'completed', ['connect', 'see_results'])
} catch (err) {
console.error('Failed to save preferences:', err)
} finally {
@@ -336,7 +331,7 @@ export function Setup() {
<OnboardingProgress
currentStep={onboardingState.onboarding_step}
completedSteps={onboardingState.completedSteps}
totalSteps={4}
totalSteps={2}
onSkip={handleSkipOnboarding}
/>
</div>
@@ -588,12 +583,15 @@ export function Setup() {
{currentStep === 'complete' && (
<div className="text-center">
<div className="w-28 h-28 mx-auto mb-8 rounded-full bg-gradient-to-br from-green-100 to-green-200 flex items-center justify-center shadow-2xl shadow-green-500/20 animate-pulse">
<Sparkles className="w-14 h-14 text-green-600" />
<div className="w-28 h-28 mx-auto mb-8 rounded-full bg-gradient-to-br from-green-100 to-green-200 dark:from-green-900/40 dark:to-green-800/40 flex items-center justify-center shadow-2xl shadow-green-500/20 animate-pulse">
<Sparkles className="w-14 h-14 text-green-600 dark:text-green-400" />
</div>
<h1 className="text-4xl font-bold text-slate-900 dark:text-slate-100 mb-4">All set! 🎉</h1>
<p className="text-xl text-slate-600 dark:text-slate-400 mb-10 max-w-md mx-auto">
Your email account is connected. The AI will now start intelligent sorting.
<h1 className="text-4xl font-bold text-slate-900 dark:text-slate-100 mb-4">You&apos;re in 🎉</h1>
<p className="text-xl text-slate-600 dark:text-slate-400 mb-6 max-w-md mx-auto">
Click Sort Now on the dashboard to categorize your inbox. Takes about 30 seconds.
</p>
<p className="text-sm text-slate-500 dark:text-slate-500 mb-10">
<Link to="/settings" className="underline hover:text-slate-700 dark:hover:text-slate-300">Tune categories later in Settings</Link>
</p>
<div className="inline-flex items-center gap-4 p-5 bg-gradient-to-r from-slate-50 to-slate-100 dark:from-slate-800 dark:to-slate-700 rounded-2xl mb-10 shadow-lg">

View File

@@ -29,9 +29,9 @@ export function VerifyEmail() {
try {
await auth.verifyEmail(userId, secret)
setStatus('success')
} catch (err: any) {
} catch (err: unknown) {
setStatus('error')
setError(err.message || 'Fehler bei der Verifizierung')
setError(err instanceof Error ? err.message : 'Fehler bei der Verifizierung')
}
}
@@ -43,8 +43,8 @@ export function VerifyEmail() {
await auth.sendVerification()
setError('')
alert('Neue Verifizierungs-E-Mail wurde gesendet!')
} catch (err: any) {
setError(err.message || 'Fehler beim Senden')
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Fehler beim Senden')
} finally {
setStatus('error')
}