Webklarintegrierung #1

Merged
ANDJ merged 2 commits from Webklarintegrierung into main 2026-01-31 14:02:34 +00:00
27 changed files with 480 additions and 429 deletions
Showing only changes of commit 7e7ec1013b - Show all commits

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')
}

View File

@@ -0,0 +1,133 @@
# Email Sorter — Product Strategy (2-Week / Reddit Launch)
**Role:** Product owner. **Goal:** First paying users from Reddit (r/buildinpublic, r/SaaS, r/freelance). **Constraint:** Understandable in under 10 seconds.
---
## 1. Homepage & Messaging
### Problems today
- **Hero:** "Clean inbox automatically in minutes" is vague. "Minutes" undersells; "clean" is generic.
- **Subhead:** "Create smart rules, apply in one click" — sounds like manual work, not automatic.
- **Badge:** "AI-powered email sorting" — buzzword; doesnt say who its for or what outcome.
- **CTAs:** "Try Demo" vs "Connect inbox" — two choices slow decision; primary action unclear.
### Proposed copy (exact)
| Element | Current | Proposed |
|--------|---------|----------|
| **Badge** | AI-powered email sorting | For freelancers & small teams |
| **Headline** | Clean inbox automatically in minutes. | **Leads, clients, spam — sorted automatically.** |
| **Subhead** | Create smart rules… | Connect Gmail or Outlook. We put newsletters, promos, and noise in folders so your inbox stays for what pays. |
| **Primary CTA** | Try Demo (first) | **Try it free** (one button; goes to register or demo) |
| **Secondary** | Connect inbox | See how it works (scroll or short demo) |
### Implementation
- One primary CTA above the fold: **Try it free**`/register`. Remove or demote "Try Demo" to a small link under the button: "Or try a 30-second demo first."
- Remove "in minutes" and "smart rules" from hero. No "Inbox Zero" in hero (use only in Features if at all).
- Trust line: keep "No credit card · Gmail & Outlook · GDPR compliant" but shorten to one line.
---
## 2. Activation & Onboarding (60-Second Flow)
### Minimum steps before value
1. **Sign up** (email + password or Google; no long form).
2. **Connect inbox** OR **Try Demo** (pick one as default; Demo gets you to "Sort complete" in one click).
3. **Done** → Dashboard with "Sort Now" or auto-result.
### What to remove or defer
- **Remove:** Step "Settings" (Sorting Intensity: Light/Medium/Strict). Use a single default: Medium. Expose in Settings later.
- **Remove:** Step "Choose your categories". Default: all 6 core categories (VIP, Clients, Invoices, Newsletter, Social, Security). No picker during onboarding.
- **Remove:** "Historical emails" toggle. Default: off for first run (faster). Optional in Settings.
- **Keep:** Connect email (Gmail/Outlook) + Demo. One click to "Done" then Dashboard.
- **Skip button:** Keep "Skip" but rename to "Ill do this later" and only show after theyve seen the connect step (so they can still land on dashboard with empty state).
### 60-second flow (concrete)
1. **015s:** Land on `/register` or home → click "Try it free" → sign up (email or Google).
2. **1545s:** One screen: "Connect Gmail or Outlook" + prominent "Try with sample inbox" (demo). No steps 23.
3. **4560s:** After connect or demo → "Youre in. Click Sort Now." → Dashboard. If demo: one "Sort Now" click → instant result.
### Implementation
- Collapse Setup into **one step**: Connect (with Demo as primary option for first-time). After connect or demo → go straight to Dashboard.
- Move "Sorting intensity" and "Categories" to Settings (and optional "tune later" link from dashboard empty state).
- Default for new users: Demo first (so they see a result in 30s), then "Connect your real inbox to sort it."
---
## 3. Core Feature Focus
### One main selling point
**"Automatic email categories: Leads, clients, invoices, newsletters, spam — without rules."**
- The moment of value: user sees **their** emails (or demo emails) sorted into clear categories and inbox count dropping.
- Everything in the app should point to: connect → sort once → see result. No "AI suggests, you approve" as hero message.
### Features to hide or delay (for 2-week launch)
- **Hide:** "Control Panel", "Smart suggestions" / "Apply suggested rules" as primary path. Keep in dashboard for power users but dont push in onboarding.
- **Hide:** Daily digest / "Todays Digest" for new users (show after 2nd sort or after 7 days).
- **Hide:** Referral / Share results until after first successful sort and upgrade prompt.
- **De-emphasize:** Multiple email accounts (show "1 account" in pricing; multi-account in Settings, not hero).
- **Remove from landing:** "Inbox Zero" as headline (overused). Use "sorted inbox" or "inbox that stays clean."
### Features to keep prominent
- Connect one inbox (Gmail/Outlook).
- **Sort Now** + result: "X emails categorized, inbox reduced by Y, time saved Z."
- Single clear upgrade moment: when they hit limit or after first sort ("Unlimited sorts from $X/month").
---
## 4. UX/UI Improvements
### Trust & clarity
- **Navbar:** Add one line under logo: "B2B email sorting" or keep minimal. CTA: "Try it free" (not "Get started free").
- **Pricing section:** One price for Reddit launch: e.g. **$9/month** or **$7/month** (single plan). "Most Popular" on the only paid plan. Remove Business tier for now.
- **Empty state (Dashboard, no account):** One sentence: "Connect Gmail or Outlook to sort your first emails." One button: "Connect inbox." No extra cards (Control Panel, Einstellungen) until one account is connected.
- **Empty state (Dashboard, account connected, no sort yet):** "Click Sort Now to categorize your inbox. Takes about 30 seconds." Big "Sort Now" button.
- **First-time sort result:** Keep current "First sort complete!" + numbers. Add one line: "Weve put newsletters and promos in folders. Check your inbox — only important mail is left."
### Defaults
- **Onboarding:** Default = Demo (so they see value without OAuth). Then "Connect your real inbox."
- **Categories:** All 6 selected by default; no picker during onboarding.
- **Strictness:** Medium; no selector in flow.
### Skeptical / impatient users
- **Above the fold:** No carousel, no "4 steps". One headline, one subhead, one CTA.
- **FAQ:** Move "Do I need a credit card?" and "Can I cancel anytime?" to top. Add: "What do you do with my email?" → "We only read headers and labels to assign categories. We dont store email content."
- **Footer:** Short. Imprint, Privacy, Contact. No long feature list.
---
## 5. Monetization (Early Stage)
### Pricing that feels "no-brainer" for freelancers
- **Free:** 1 account, 500 emails/month, basic categories. Enough to feel the product.
- **Single paid plan:** **$9/month** (or **$7/month** for first 100 customers). "Unlimited emails, 1 account, all categories, cancel anytime."
- **Remove for now:** $19 Pro, $49 Business. One plan = no choice paralysis.
- **Trial:** 14-day free trial, no card. After trial, card required or account stays free-tier (500/mo).
### Early-adopter experiment
- **Reddit launch offer:** "First 50 from r/SaaS or r/freelance: $5/month for 6 months." Use a coupon or a separate plan ID. Mention in Reddit post and a small banner on pricing: "Reddit launch: $5/mo for 6 months — use code REDDIT50."
- **Churn:** Focus on "Sort Now" success in first 7 days. If theyve done 2+ sorts and connected a real inbox, send one email: "Youve sorted X emails. Upgrade to unlimited for $9/mo." No aggressive upsells.
---
## 6. Retention & Defensibility
### One integration that increases switching cost
- **Gmail labels (or Outlook folders) as the integration.** Product already sorts into categories; make the output visible where they live:
- **Sync categories to Gmail labels** (e.g. "EmailSorter/Clients", "EmailSorter/Newsletter"). User sees labels in Gmail; moving away means losing those labels or redoing work.
- Implementation: After sort, apply Gmail API `users.labels` + `messages.modify` to add the label to each message. One-way: Email Sorter → Gmail. No need for bi-directional sync in v1.
- **Alternative (simpler):** **Weekly digest email.** "You sorted 47 emails this week. Top category: Newsletter (20)." Builds habit and touchpoint; unsubscribing = losing a small benefit.
- **Recommendation:** Gmail (and later Outlook) label sync. Real defensibility; realistic for a solo dev (Gmail API is well documented). Ship "Sync to Gmail labels" as a Pro feature or postfree-trial hook.
---
## Implementation Checklist (Priority Order)
- [ ] **Hero:** New headline, subhead, single CTA "Try it free", demo as secondary link.
- [ ] **Onboarding:** Single step (Connect or Demo) → Dashboard. Move Settings + Categories to Settings page.
- [ ] **Pricing:** One paid plan $9/mo; optional Reddit code REDDIT50 ($5/mo for 6 months).
- [ ] **Dashboard empty states:** Copy and single primary action per state.
- [ ] **FAQ:** Reorder; add "What do you do with my email?"; keep short.
- [ ] **Defensibility:** Design/spec "Sync categories to Gmail labels" for postlaunch.