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", "version": "0.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "client", "name": "emailsorter-client",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-accordion": "^1.2.12",

View File

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

View File

@@ -1,5 +1,5 @@
import { Button } from '@/components/ui/button' 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 { useNavigate } from 'react-router-dom'
import { trackUpgradeClicked } from '@/lib/analytics' import { trackUpgradeClicked } from '@/lib/analytics'
import { useAuth } from '@/context/AuthContext' import { useAuth } from '@/context/AuthContext'
@@ -97,7 +97,7 @@ export function UpgradePrompt({
onClick={handleUpgrade} onClick={handleUpgrade}
className="flex-1 bg-primary-600 hover:bg-primary-700" 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 Upgrade
</Button> </Button>
<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 = [ const faqs = [
{ {
question: "Are my emails secure?", question: "Why not just use Gmail filters?",
answer: "Yes! We use OAuth we never see your password. Content is only analyzed briefly, never stored." 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?", question: "What happens to my emails?",
answer: "Gmail and Outlook. More coming soon." 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?", question: "Can this mess up my inbox?",
answer: "Absolutely! You can set VIP contacts and define custom categories." answer: "We only add labels or move to folders. We don't delete. Disconnect and nothing stays changed."
}, },
{ {
question: "What about old emails?", question: "Do you need my password?",
answer: "The last 30 days are analyzed. You decide if they should be sorted too." 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?", question: "What if I don't like it?",
answer: "Yes, with one click. No tricks, no long commitments." answer: "Cancel anytime. No contract. Your data is yours; disconnect and we stop. Free trial, no card."
},
{
question: "Do I need a credit card?",
answer: "No, the 14-day trial is completely free."
},
{
question: "Does it work on mobile?",
answer: "Yes! Sorting runs on our servers works in any email app."
},
{
question: "What if the AI sorts wrong?",
answer: "Just correct it. The AI learns and gets better over time."
}, },
] ]
export function FAQ() { export function FAQ() {
const [openIndex, setOpenIndex] = useState<number | null>(0)
return ( return (
<section id="faq" className="py-24 bg-slate-50 dark:bg-slate-900"> <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"> <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Section header */} <div className="text-center mb-12">
<div className="text-center mb-16"> <h2 className="text-3xl sm:text-4xl font-bold text-slate-900 dark:text-slate-100 mb-2">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-100 dark:bg-primary-900/30 mb-6"> Questions we get a lot
<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
</h2> </h2>
<p className="text-lg text-slate-600 dark:text-slate-400"> <p className="text-slate-600 dark:text-slate-400">
Quick answers to common questions. Straight answers. No fluff.
</p> </p>
</div> </div>
{/* FAQ items */} <div className="space-y-12">
<div className="space-y-3">
{faqs.map((faq, index) => ( {faqs.map((faq, index) => (
<FAQItem <div
key={index} key={index}
question={faq.question} className="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-10 items-baseline"
answer={faq.answer} >
isOpen={openIndex === index} <p className="text-lg md:text-xl font-semibold text-slate-900 dark:text-slate-100">
onClick={() => setOpenIndex(openIndex === index ? null : index)} {faq.question}
/> </p>
<p className="text-sm text-slate-600 dark:text-slate-400 leading-relaxed">
{faq.answer}
</p>
</div>
))} ))}
</div> </div>
{/* Contact CTA */} <p className="mt-10 text-center text-sm text-slate-600 dark:text-slate-400">
<div className="mt-12 text-center p-6 bg-white dark:bg-slate-800 rounded-2xl border border-slate-200 dark:border-slate-700"> Still unsure?{' '}
<p className="text-slate-600 dark:text-slate-400 mb-2">Still have questions?</p>
<a <a
href="mailto:support@emailsorter.com" href="mailto:support@emailsorter.webklar.com"
className="text-primary-600 dark:text-primary-400 font-semibold hover:text-primary-700 dark:hover:text-primary-300" className="text-slate-700 dark:text-slate-300 hover:underline"
> >
Contact us Email us we reply fast
</a> </a>
</div> .
</p>
</div> </div>
</section> </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 = [ const features = [
{ {
icon: Inbox, icon: Inbox,
title: "Stop drowning in emails", title: "Categories, not chaos",
description: "Clear inbox, less stress. Automatically sort newsletters, promotions, and social updates away from what matters.", description: "Leads, clients, invoices, newsletters — sorted into folders. Your inbox shows what pays first.",
color: "from-violet-500 to-purple-600", color: "from-violet-500 to-purple-600",
highlight: true, highlight: true,
}, },
{ {
icon: Zap, icon: Zap,
title: "One-click smart rules", title: "One click to sort",
description: "AI suggests, you approve. Create smart rules in seconds and apply them with one click.", description: "Connect your inbox, click Sort Now. No rules to write. We read and categorize; you review.",
color: "from-amber-500 to-orange-600", color: "from-amber-500 to-orange-600",
highlight: true, highlight: true,
}, },
{ {
icon: Settings, icon: Settings,
title: "Automation that keeps working", title: "Runs when you want",
description: "Set it and forget it. Your inbox stays organized automatically, day after day.", description: "Sort on demand or set a schedule. Your inbox stays organized without you touching it.",
color: "from-blue-500 to-cyan-600", color: "from-blue-500 to-cyan-600",
highlight: true, highlight: true,
}, },
{ {
icon: Brain, icon: Brain,
title: "AI-powered smart sorting", title: "Content-aware sorting",
description: "Our AI automatically recognizes whether an email is an invoice, newsletter, or important message.", description: "We look at sender, subject, and a short snippet to decide the category. No keyword lists.",
color: "from-green-500 to-emerald-600" color: "from-green-500 to-emerald-600"
}, },
{ {
icon: Shield, icon: Shield,
title: "GDPR compliant", title: "Minimal data",
description: "Your data stays secure. We only read email headers and metadata for sorting.", description: "We only read what we need to categorize. No storing email body or attachments. GDPR compliant.",
color: "from-pink-500 to-rose-600" color: "from-pink-500 to-rose-600"
}, },
{ {
icon: Clock, icon: Clock,
title: "Save time", title: "Less time on triage",
description: "Average 2 hours per week less on email organization. More time for what matters.", description: "Spend less time deciding what's important. Inbox shows clients and leads first.",
color: "from-indigo-500 to-blue-600" color: "from-indigo-500 to-blue-600"
}, },
] ]
@@ -57,14 +57,10 @@ export function Features() {
{/* Section header */} {/* Section header */}
<div className="text-center mb-16"> <div className="text-center mb-16">
<h2 className="text-3xl sm:text-4xl font-bold text-slate-900 dark:text-slate-100 mb-4"> <h2 className="text-3xl sm:text-4xl font-bold text-slate-900 dark:text-slate-100 mb-4">
Everything you need for{' '} What it does
<span className="text-transparent bg-clip-text bg-gradient-to-r from-primary-600 to-accent-500">
Inbox Zero
</span>
</h2> </h2>
<p className="text-lg text-slate-600 dark:text-slate-400 max-w-2xl mx-auto"> <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 Sort incoming mail into categories so your inbox shows what matters first. No rules to write.
for maximum productivity.
</p> </p>
</div> </div>

View File

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

View File

@@ -1,8 +1,9 @@
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { captureUTMParams } from '@/lib/analytics' import { captureUTMParams } from '@/lib/analytics'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge' 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() { export function Hero() {
const navigate = useNavigate() const navigate = useNavigate()
@@ -33,47 +34,46 @@ export function Hero() {
<div className="text-center lg:text-left"> <div className="text-center lg:text-left">
<Badge className="mb-6 bg-primary-500/20 text-primary-200 border-primary-400/30"> <Badge className="mb-6 bg-primary-500/20 text-primary-200 border-primary-400/30">
<Sparkles className="w-3 h-3 mr-1" /> <Sparkles className="w-3 h-3 mr-1" />
AI-powered email sorting For freelancers & small teams
</Badge> </Badge>
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-extrabold text-white leading-tight mb-6"> <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 /> <br />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-accent-400 to-primary-300"> <span className="text-transparent bg-clip-text bg-gradient-to-r from-accent-400 to-primary-300">
in minutes. sorted automatically.
</span> </span>
</h1> </h1>
<p className="text-lg sm:text-xl text-slate-300 mb-8 max-w-xl mx-auto lg:mx-0"> <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. Connect Gmail or Outlook. We put newsletters, promos, and noise in folders so your inbox stays for what pays.
Stop drowning in emails and start focusing on what matters.
</p> </p>
<div className="flex flex-col sm:flex-row gap-4 justify-center lg:justify-start mb-8"> <div className="flex flex-col sm:flex-row gap-4 justify-center lg:justify-start mb-4">
<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>
<Button <Button
size="xl" size="xl"
onClick={handleCTAClick} onClick={handleCTAClick}
variant="outline" className="group bg-accent-500 hover:bg-accent-600"
className="bg-white/10 border-white/20 text-white hover:bg-white/20 group"
> >
Connect inbox Try it free
<ArrowRight className="w-5 h-5 ml-2 group-hover:translate-x-1 transition-transform" /> <ArrowRight className="w-5 h-5 ml-2 group-hover:translate-x-1 transition-transform" />
</Button> </Button>
</div> </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 */} {/* Trust badges */}
<div className="flex flex-wrap gap-6 justify-center lg:justify-start text-slate-400 text-sm"> <div className="flex flex-wrap gap-6 justify-center lg:justify-start text-slate-400 text-sm">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Check className="w-4 h-4 text-accent-400" /> <Check className="w-4 h-4 text-accent-400" />
No credit card required No credit card
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Check className="w-4 h-4 text-accent-400" /> <Check className="w-4 h-4 text-accent-400" />
@@ -86,59 +86,22 @@ export function Hero() {
</div> </div>
</div> </div>
{/* Right side - Visual */} {/* Right side - Inbox visual (product screenshot feel) */}
<div className="relative hidden lg:block"> <div className="relative hidden lg:block">
<div className="relative"> <div className="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 max-w-md overflow-hidden">
{/* Main card */} <div className="border-b border-slate-200 dark:border-slate-700 px-4 py-2.5">
<div className="bg-white/10 backdrop-blur-xl rounded-3xl border border-white/20 p-6 shadow-2xl animate-float"> <span className="text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">Inbox</span>
<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> </div>
<div className="divide-y divide-slate-100 dark:divide-slate-800">
{/* Floating badge */} <InboxRow sender="Sarah Chen" subject="Re: Project quote" label="Lead" isFocal />
<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"> <InboxRow sender="Mike, Acme Inc" subject="Invoice #8821" label="Client" />
<Zap className="w-4 h-4 inline mr-1" /> <InboxRow sender="Newsletter" subject="Your weekly digest" label="Noise" />
AI sorting <InboxRow sender="Support" subject="Your ticket #443" label="Client" />
<InboxRow sender="Promo" subject="20% off this week" label="Noise" />
</div> </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> </div>
</div> </div>
@@ -154,26 +117,39 @@ export function Hero() {
) )
} }
interface EmailPreviewProps { type InboxLabel = 'Lead' | 'Client' | 'Noise'
category: string
color: string const labelClass: Record<InboxLabel, string> = {
sender: string Lead: 'text-primary-600 dark:text-primary-500',
subject: string Client: 'text-slate-600 dark:text-slate-600',
delay: string 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 ( 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
<div className={`w-2 h-10 rounded-full ${color}`} /> 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-1 min-w-0">
<div className="flex items-center gap-2 mb-0.5"> <p className={cn(
<span className="text-sm font-medium text-white truncate">{sender}</span> "truncate",
<span className={`text-xs px-2 py-0.5 rounded ${color} text-white`}>{category}</span> isFocal ? "text-sm font-semibold text-slate-900 dark:text-slate-100" : "text-sm font-medium text-slate-600 dark:text-slate-400"
</div> )}>
<p className="text-sm text-slate-400 truncate">{subject}</p> {sender}
</p>
<p className="text-xs text-slate-500 dark:text-slate-500 truncate mt-0.5">{subject}</p>
</div> </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> </div>
) )
} }

View File

@@ -17,19 +17,19 @@ const steps = [
icon: Link2, icon: Link2,
step: "02", step: "02",
title: "Connect email", 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, icon: Sparkles,
step: "03", step: "03",
title: "AI analyzes", title: "We categorize",
description: "Our AI learns your email patterns and creates personalized sorting rules.", description: "We read sender and subject, put each email in a category. No rules to write.",
}, },
{ {
icon: PartyPopper, icon: PartyPopper,
step: "04", step: "04",
title: "Enjoy Inbox Zero", title: "Inbox stays clean",
description: "Sit back and enjoy a clean inbox automatically.", description: "Newsletters and promos go to folders. Your inbox shows what matters first.",
}, },
] ]

View File

@@ -82,7 +82,7 @@ export function Navbar() {
Sign in Sign in
</Button> </Button>
<Button onClick={() => navigate('/register')}> <Button onClick={() => navigate('/register')}>
Get started free Try it free
</Button> </Button>
</> </>
)} )}
@@ -146,7 +146,7 @@ export function Navbar() {
Sign in Sign in
</Button> </Button>
<Button className="w-full h-11" onClick={() => navigate('/register')}> <Button className="w-full h-11" onClick={() => navigate('/register')}>
Get started free Try it free
</Button> </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, icon: Code2,
title: "Save 2+ hours/week", title: "Built in public",
description: "Less time sorting emails, more time for important tasks.", description: "We ship updates and share progress openly. No hype, no fake traction.",
}, },
{ {
icon: Brain, icon: Users,
title: "AI does it automatically", title: "Early users",
description: "Set up once, then everything runs by itself.", description: "We're in beta. Feedback from freelancers and small teams shapes the product.",
}, },
{ {
icon: Shield, icon: Zap,
title: "Privacy first", title: "Simple setup",
description: "Your emails stay private. We don't store any content.", description: "Connect Gmail or Outlook, click Sort. No long onboarding or sales call.",
},
{
icon: CheckCircle2,
title: "Easy to use",
description: "No learning curve. Ready to go in 2 minutes.",
}, },
] ]
export function Testimonials() { export function Testimonials() {
return ( return (
<section className="py-20 bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900"> <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"> <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Section header */}
<div className="text-center mb-12"> <div className="text-center mb-12">
<h2 className="text-3xl sm:text-4xl font-bold text-white mb-4"> <h2 className="text-2xl sm:text-3xl font-bold text-white mb-2">
Why EmailSorter? Honest context
</h2> </h2>
<p className="text-lg text-slate-300 max-w-2xl mx-auto"> <p className="text-slate-400 text-sm sm:text-base">
No more email chaos. Focus on what matters. We're a small product. Here's how we work.
</p> </p>
</div> </div>
{/* Benefits grid */} <div className="grid md:grid-cols-3 gap-6">
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6"> {items.map((item, index) => (
{benefits.map((benefit, index) => ( <div
<BenefitCard key={index} {...benefit} /> 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>
</div> </div>
</section> </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 * as React from "react"
import { cva, type VariantProps } from "class-variance-authority" import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-refresh/only-export-components */
import * as React from "react" import * as React from "react"
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority" 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 React, { createContext, useContext, useEffect, useState } from 'react'
import { auth } from '@/lib/appwrite' import { auth } from '@/lib/appwrite'
import type { Models } from 'appwrite' import type { Models } from 'appwrite'

View File

@@ -17,7 +17,7 @@ export interface TrackingParams {
export interface ConversionEvent { 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' 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 userId?: string
metadata?: Record<string, any> metadata?: Record<string, unknown>
sessionId?: string sessionId?: string
} }

View File

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

View File

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

View File

@@ -17,9 +17,7 @@ import {
Zap, Zap,
BarChart3, BarChart3,
Users, Users,
Bell,
Shield, Shield,
HelpCircle,
Loader2, Loader2,
Check, Check,
AlertCircle, AlertCircle,
@@ -65,7 +63,7 @@ interface SortResult {
name: string name: string
description: string description: string
confidence: number 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) if (referralRes.data) setReferralCode(referralRes.data.referralCode)
} catch (err) { } catch (err) {
console.error('Error loading dashboard data:', err) console.error('Error loading dashboard data:', err)
setError('Failed to load data') setError('Couldnt load your data. Check your connection and refresh.')
} finally { } finally {
setLoading(false) setLoading(false)
} }
@@ -139,7 +137,7 @@ export function Dashboard() {
const handleSortNow = async () => { const handleSortNow = async () => {
if (!user?.$id || accounts.length === 0) { 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 return
} }
@@ -178,7 +176,7 @@ export function Dashboard() {
} }
} catch (err) { } catch (err) {
console.error('Error sorting emails:', 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 { } finally {
setSorting(false) setSorting(false)
} }
@@ -248,13 +246,6 @@ export function Dashboard() {
</Link> </Link>
<div className="flex items-center gap-1.5 sm:gap-2 lg:gap-4"> <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 <Button
variant="ghost" variant="ghost"
onClick={() => navigate('/settings')} onClick={() => navigate('/settings')}
@@ -304,7 +295,7 @@ export function Dashboard() {
<p className="text-sm text-slate-600 dark:text-slate-400"> <p className="text-sm text-slate-600 dark:text-slate-400">
{accounts.length > 0 {accounts.length > 0
? `${accounts.length} account${accounts.length > 1 ? 's' : ''} connected` ? `${accounts.length} account${accounts.length > 1 ? 's' : ''} connected`
: 'Connect an account to get started'} : 'Connect Gmail or Outlook to sort your first emails.'}
</p> </p>
</div> </div>
<div className="flex items-center gap-2 sm:gap-3"> <div className="flex items-center gap-2 sm:gap-3">
@@ -315,7 +306,7 @@ export function Dashboard() {
aria-label="Connect email account" aria-label="Connect email account"
> >
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Connect Account Connect inbox
</Button> </Button>
) : ( ) : (
<Button <Button
@@ -359,6 +350,15 @@ export function Dashboard() {
</div> </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 message */}
{error && ( {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"> <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="flex items-center justify-center py-20">
<div className="text-center"> <div className="text-center">
<Loader2 className="w-10 h-10 animate-spin text-primary-500 dark:text-primary-400 mx-auto mb-4" /> <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>
</div> </div>
) : ( ) : (
@@ -394,9 +395,9 @@ export function Dashboard() {
<Sparkles className="w-5 h-5 text-white" /> <Sparkles className="w-5 h-5 text-white" />
</div> </div>
<div> <div>
<CardTitle className="text-lg">AI-Assistent</CardTitle> <CardTitle className="text-lg">Email sorting</CardTitle>
<CardDescription className="text-sm"> <CardDescription className="text-sm">
Automatische E-Mail-Sortierung mit KI Categorize inbox leads, clients, newsletters
</CardDescription> </CardDescription>
</div> </div>
</div> </div>
@@ -413,10 +414,10 @@ export function Dashboard() {
</div> </div>
<div> <div>
<p className={`${sortResult.isFirstRun ? 'text-lg' : 'text-sm'} font-bold text-green-800 dark:text-green-200`}> <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> </p>
{sortResult.isFirstRun && ( {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>
</div> </div>
@@ -444,10 +445,10 @@ export function Dashboard() {
<div className="bg-white/80 dark:bg-slate-800/80 p-3 rounded-lg"> <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-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"> <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>
<p className="text-xs text-green-500 dark:text-green-400 mt-1"> <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> </p>
</div> </div>
)} )}
@@ -457,7 +458,7 @@ export function Dashboard() {
{sortResult.isFirstRun && sortResult.suggestedRules && sortResult.suggestedRules.length > 0 && ( {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="mt-4 pt-4 border-t border-green-200 dark:border-green-800">
<div className="mb-3"> <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> <p className="text-xs text-green-600 dark:text-green-400 mt-0.5">Based on your email patterns</p>
</div> </div>
<div className="space-y-2 mb-3"> <div className="space-y-2 mb-3">
@@ -479,19 +480,19 @@ export function Dashboard() {
setSorting(true) setSorting(true)
try { try {
const vipSenders = sortResult.suggestedRules const vipSenders = sortResult.suggestedRules
.filter(r => r.type === 'vip_sender' && r.action?.email) .filter((r): r is typeof r & { action: { email: string } } => r.type === 'vip_sender' && !!r.action?.email)
.map(r => r.action.email) .map(r => ({ email: r.action.email }))
const companyLabels = sortResult.suggestedRules 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 => ({ .map(r => ({
name: r.action.name, name: r.action.name,
condition: r.action.condition, condition: r.action.condition ?? '',
category: r.action.category || 'promotions', category: r.action.category || 'promotions',
enabled: true, 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) { if (vipSenders.length > 0) {
updates.vipSenders = vipSenders updates.vipSenders = vipSenders
} }
@@ -502,10 +503,10 @@ export function Dashboard() {
if (Object.keys(updates).length > 0) { if (Object.keys(updates).length > 0) {
await api.saveUserPreferences(user.$id, updates) await api.saveUserPreferences(user.$id, updates)
trackRulesApplied(user.$id, sortResult.suggestedRules.length) 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: [] }) setSortResult({ ...sortResult, suggestedRules: [] })
} }
} catch (err) { } catch {
showMessage('error', 'Unable to apply rules right now. Please try again.') showMessage('error', 'Unable to apply rules right now. Please try again.')
} finally { } finally {
setSorting(false) setSorting(false)
@@ -756,7 +757,7 @@ export function Dashboard() {
<CardTitle className="text-base">Control Panel</CardTitle> <CardTitle className="text-base">Control Panel</CardTitle>
</div> </div>
<CardDescription className="text-xs"> <CardDescription className="text-xs">
KI-Einstellungen und Kategorien verwalten Categories and rules
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -767,7 +768,7 @@ export function Dashboard() {
aria-label="Open Control Panel settings" aria-label="Open Control Panel settings"
> >
<Settings className="w-4 h-4 mr-2" /> <Settings className="w-4 h-4 mr-2" />
Control Panel öffnen Open Control Panel
<ExternalLink className="w-3 h-3 ml-2" /> <ExternalLink className="w-3 h-3 ml-2" />
</Button> </Button>
</CardContent> </CardContent>
@@ -778,10 +779,10 @@ export function Dashboard() {
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Settings className="w-5 h-5 text-primary-500 dark:text-primary-400" /> <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> </div>
<CardDescription className="text-xs"> <CardDescription className="text-xs">
Schnellzugriff auf wichtige Einstellungen Quick access
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-2"> <CardContent className="space-y-2">
@@ -810,7 +811,7 @@ export function Dashboard() {
aria-label="Open all settings" aria-label="Open all settings"
> >
<Settings className="w-4 h-4 mr-2" /> <Settings className="w-4 h-4 mr-2" />
Alle Einstellungen All settings
<ExternalLink className="w-3 h-3 ml-auto" /> <ExternalLink className="w-3 h-3 ml-auto" />
</Button> </Button>
</CardContent> </CardContent>
@@ -819,9 +820,9 @@ export function Dashboard() {
{/* Account/System Karte */} {/* Account/System Karte */}
<Card className="border border-slate-200 dark:border-slate-700 shadow-sm"> <Card className="border border-slate-200 dark:border-slate-700 shadow-sm">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardTitle className="text-base">Account & System</CardTitle> <CardTitle className="text-base">Account</CardTitle>
<CardDescription className="text-xs"> <CardDescription className="text-xs">
Subscription und Konten-Status Subscription and accounts
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
@@ -932,7 +933,7 @@ export function Dashboard() {
) : ( ) : (
<div className="text-center py-4"> <div className="text-center py-4">
<Mail className="w-8 h-8 text-slate-300 dark:text-slate-600 mx-auto mb-2" /> <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 <Button
onClick={() => navigate('/setup')} onClick={() => navigate('/setup')}
variant="outline" variant="outline"
@@ -941,7 +942,7 @@ export function Dashboard() {
aria-label="Connect email account" aria-label="Connect email account"
> >
<Plus className="w-3 h-3 mr-1.5" /> <Plus className="w-3 h-3 mr-1.5" />
Connect Account Connect inbox
</Button> </Button>
</div> </div>
)} )}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -54,7 +54,7 @@ import {
Save, Save,
Edit2, Edit2,
} from 'lucide-react' } 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' import { PrivacySecurity } from '@/components/PrivacySecurity'
type TabType = 'profile' | 'accounts' | 'vip' | 'ai-control' | 'subscription' | 'privacy' | 'referrals' 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 (prefsRes.data?.vipSenders) setVipSenders(prefsRes.data.vipSenders)
if (aiControlRes.data) { if (aiControlRes.data) {
// Merge cleanup defaults if not present // Merge cleanup defaults if not present
const settings: AIControlSettings = { const raw = aiControlRes.data
...aiControlRes.data, const defaultCleanup: CleanupSettings = {
cleanup: aiControlRes.data.cleanup || { enabled: false,
readItems: {
enabled: false, enabled: false,
readItems: { action: 'archive_read',
enabled: false, gracePeriodDays: 7,
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,
},
}, },
categoryAdvanced: aiControlRes.data.categoryAdvanced || {}, promotions: {
version: aiControlRes.data.version || 1, 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) setAiControlSettings(settings)
savedSettingsRef.current = JSON.parse(JSON.stringify(settings)) // Deep copy savedSettingsRef.current = JSON.parse(JSON.stringify(settings)) // Deep copy
@@ -312,7 +314,7 @@ export function Settings() {
try { try {
const res = await api.getCleanupStatus(user.$id) const res = await api.getCleanupStatus(user.$id)
if (res.data) setCleanupStatus(res.data) if (res.data) setCleanupStatus(res.data)
} catch (err) { } catch {
// Silently fail if endpoint doesn't exist yet // Silently fail if endpoint doesn't exist yet
console.debug('Cleanup status endpoint not available') console.debug('Cleanup status endpoint not available')
} }
@@ -324,7 +326,7 @@ export function Settings() {
try { try {
const res = await api.getCleanupPreview(user.$id) const res = await api.getCleanupPreview(user.$id)
if (res.data?.preview) setCleanupPreview(res.data.preview) if (res.data?.preview) setCleanupPreview(res.data.preview)
} catch (err) { } catch {
// Silently fail if endpoint doesn't exist yet // Silently fail if endpoint doesn't exist yet
console.debug('Cleanup preview endpoint not available') console.debug('Cleanup preview endpoint not available')
} }
@@ -1058,7 +1060,7 @@ export function Settings() {
</div> </div>
<select <select
value={filterEnabled} 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" 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> <option value="all">All</option>
@@ -1070,7 +1072,7 @@ export function Settings() {
</Card> </Card>
{/* Tabs */} {/* 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"> <TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="rules" className="text-sm sm:text-base">Rules</TabsTrigger> <TabsTrigger value="rules" className="text-sm sm:text-base">Rules</TabsTrigger>
<TabsTrigger value="cleanup" className="text-sm sm:text-base">Cleanup</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"> <div className="flex flex-wrap items-center gap-2">
<select <select
value={labelSort} 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" 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> <option value="name">Sort: Name</option>
@@ -1834,7 +1836,7 @@ export function Settings() {
id="category-action" id="category-action"
value={aiControlSettings.categoryActions[selectedCategory.key] || selectedCategory.defaultAction} value={aiControlSettings.categoryActions[selectedCategory.key] || selectedCategory.defaultAction}
onChange={(e) => { 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 }) 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" 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) => { onChange={(e) => {
const newAdvanced = { ...aiControlSettings.categoryAdvanced } const newAdvanced = { ...aiControlSettings.categoryAdvanced }
if (!newAdvanced[selectedCategory.key]) newAdvanced[selectedCategory.key] = {} 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 }) 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" 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 }[] = [ const steps: { id: Step; title: string; description: string }[] = [
{ id: 'connect', title: 'Connect email', description: 'Link your mailbox' }, { id: 'connect', title: 'Connect email', description: 'Link your mailbox' },
{ id: 'preferences', title: 'Settings', description: 'Sorting preferences' }, { id: 'complete', title: 'Done', description: 'Go to dashboard' },
{ id: 'categories', title: 'Categories', description: 'Choose categories' },
{ id: 'complete', title: 'Done', description: 'Get started!' },
] ]
const stepIndex = steps.findIndex(s => s.id === currentStep) const stepIndex = steps.findIndex(s => s.id === currentStep)
@@ -128,13 +126,12 @@ export function Setup() {
} else { } else {
setConnectedProvider('gmail') setConnectedProvider('gmail')
setConnectedEmail(user.email) setConnectedEmail(user.email)
setCurrentStep('preferences') setCurrentStep('complete')
// Track onboarding step await api.updateOnboardingStep(user.$id, 'see_results', ['connect'])
await api.updateOnboardingStep(user.$id, 'first_rule', ['connect'])
trackOnboardingStep(user.$id, 'first_rule') trackOnboardingStep(user.$id, 'first_rule')
trackProviderConnected(user.$id, 'gmail') trackProviderConnected(user.$id, 'gmail')
} }
} catch (err) { } catch {
setError('Gmail connection failed. Please try again.') setError('Gmail connection failed. Please try again.')
} finally { } finally {
setConnecting(null) setConnecting(null)
@@ -155,11 +152,10 @@ export function Setup() {
} else { } else {
setConnectedProvider('outlook') setConnectedProvider('outlook')
setConnectedEmail(user.email) setConnectedEmail(user.email)
setCurrentStep('preferences') setCurrentStep('complete')
// Track onboarding step await api.updateOnboardingStep(user.$id, 'see_results', ['connect'])
await api.updateOnboardingStep(user.$id, 'first_rule', ['connect'])
} }
} catch (err) { } catch {
setError('Outlook connection failed. Please try again.') setError('Outlook connection failed. Please try again.')
} finally { } finally {
setConnecting(null) setConnecting(null)
@@ -176,13 +172,12 @@ export function Setup() {
if (response.data) { if (response.data) {
setConnectedProvider('demo') setConnectedProvider('demo')
setConnectedEmail(response.data.email) setConnectedEmail(response.data.email)
setCurrentStep('preferences') setCurrentStep('complete')
// Track onboarding step await api.updateOnboardingStep(user.$id, 'see_results', ['connect'])
await api.updateOnboardingStep(user.$id, 'first_rule', ['connect'])
trackOnboardingStep(user.$id, 'first_rule') trackOnboardingStep(user.$id, 'first_rule')
trackDemoUsed(user.$id) trackDemoUsed(user.$id)
} }
} catch (err) { } catch {
setError('Demo connection failed. Please try again.') setError('Demo connection failed. Please try again.')
} finally { } finally {
setConnecting(null) setConnecting(null)
@@ -240,7 +235,7 @@ export function Setup() {
}) })
// Mark onboarding as completed // 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) { } catch (err) {
console.error('Failed to save preferences:', err) console.error('Failed to save preferences:', err)
} finally { } finally {
@@ -336,7 +331,7 @@ export function Setup() {
<OnboardingProgress <OnboardingProgress
currentStep={onboardingState.onboarding_step} currentStep={onboardingState.onboarding_step}
completedSteps={onboardingState.completedSteps} completedSteps={onboardingState.completedSteps}
totalSteps={4} totalSteps={2}
onSkip={handleSkipOnboarding} onSkip={handleSkipOnboarding}
/> />
</div> </div>
@@ -588,12 +583,15 @@ export function Setup() {
{currentStep === 'complete' && ( {currentStep === 'complete' && (
<div className="text-center"> <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"> <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" /> <Sparkles className="w-14 h-14 text-green-600 dark:text-green-400" />
</div> </div>
<h1 className="text-4xl font-bold text-slate-900 dark:text-slate-100 mb-4">All set! 🎉</h1> <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-10 max-w-md mx-auto"> <p className="text-xl text-slate-600 dark:text-slate-400 mb-6 max-w-md mx-auto">
Your email account is connected. The AI will now start intelligent sorting. 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> </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"> <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 { try {
await auth.verifyEmail(userId, secret) await auth.verifyEmail(userId, secret)
setStatus('success') setStatus('success')
} catch (err: any) { } catch (err: unknown) {
setStatus('error') 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() await auth.sendVerification()
setError('') setError('')
alert('Neue Verifizierungs-E-Mail wurde gesendet!') alert('Neue Verifizierungs-E-Mail wurde gesendet!')
} catch (err: any) { } catch (err: unknown) {
setError(err.message || 'Fehler beim Senden') setError(err instanceof Error ? err.message : 'Fehler beim Senden')
} finally { } finally {
setStatus('error') 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.