From 7e7ec1013b94af5a76598f6c40f46cb2793cd7a1 Mon Sep 17 00:00:00 2001 From: ANDJ Date: Sat, 31 Jan 2026 12:05:47 +0100 Subject: [PATCH] eerrerer erdfsfsfsdf --- client/package-lock.json | 4 +- client/src/components/OnboardingProgress.tsx | 42 ++--- client/src/components/UpgradePrompt.tsx | 4 +- client/src/components/landing/FAQ.tsx | 123 +++++---------- client/src/components/landing/Features.tsx | 32 ++-- client/src/components/landing/Footer.tsx | 52 +------ client/src/components/landing/Hero.tsx | 144 ++++++++---------- client/src/components/landing/HowItWorks.tsx | 10 +- client/src/components/landing/Navbar.tsx | 4 +- .../src/components/landing/Testimonials.tsx | 72 ++++----- .../src/components/landing/TrustSection.tsx | 48 ++++++ client/src/components/ui/badge.tsx | 1 + client/src/components/ui/button.tsx | 1 + client/src/context/AuthContext.tsx | 1 + client/src/lib/analytics.ts | 2 +- client/src/lib/api.ts | 17 ++- client/src/lib/appwrite.ts | 10 +- client/src/pages/Dashboard.tsx | 79 +++++----- client/src/pages/ForgotPassword.tsx | 4 +- client/src/pages/Home.tsx | 4 +- client/src/pages/Login.tsx | 4 +- client/src/pages/Register.tsx | 4 +- client/src/pages/ResetPassword.tsx | 4 +- client/src/pages/Settings.tsx | 60 ++++---- client/src/pages/Setup.tsx | 42 +++-- client/src/pages/VerifyEmail.tsx | 8 +- docs/PRODUCT_STRATEGY_2WEEK.md | 133 ++++++++++++++++ 27 files changed, 480 insertions(+), 429 deletions(-) create mode 100644 client/src/components/landing/TrustSection.tsx create mode 100644 docs/PRODUCT_STRATEGY_2WEEK.md diff --git a/client/package-lock.json b/client/package-lock.json index f4db025..148a508 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -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", diff --git a/client/src/components/OnboardingProgress.tsx b/client/src/components/OnboardingProgress.tsx index 223ffa9..33fd66f 100644 --- a/client/src/components/OnboardingProgress.tsx +++ b/client/src/components/OnboardingProgress.tsx @@ -17,39 +17,41 @@ const stepLabels: Record = { '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 ( -
+
-

Getting started

-

Step {currentStepNumber} of {totalSteps}

+

Getting started

+

Step {currentStepNumber} of {totalSteps}

-
- {/* Progress bar */} -
+
- {/* Step indicators */} -
- {['connect', 'first_rule', 'see_results', 'auto_schedule'].map((step, idx) => { +
+ {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 ? ( @@ -69,13 +71,13 @@ export function OnboardingProgress({ currentStep, completedSteps, totalSteps, on )}
- {idx < 3 && ( + {idx < steps.length - 1 && (
)}
diff --git a/client/src/components/UpgradePrompt.tsx b/client/src/components/UpgradePrompt.tsx index 78bafd6..cdc4672 100644 --- a/client/src/components/UpgradePrompt.tsx +++ b/client/src/components/UpgradePrompt.tsx @@ -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" > - + Upgrade -
-

{answer}

-
-
- ) -} diff --git a/client/src/components/landing/Features.tsx b/client/src/components/landing/Features.tsx index e6f118f..e150b3a 100644 --- a/client/src/components/landing/Features.tsx +++ b/client/src/components/landing/Features.tsx @@ -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 */}

- Everything you need for{' '} - - Inbox Zero - + What it does

- 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.

diff --git a/client/src/components/landing/Footer.tsx b/client/src/components/landing/Footer.tsx index d97e5dd..5beacb6 100644 --- a/client/src/components/landing/Footer.tsx +++ b/client/src/components/landing/Footer.tsx @@ -17,7 +17,7 @@ export function Footer() {

- AI-powered email sorting for more productivity and less stress. + Email sorting for freelancers and small teams. Gmail & Outlook.

{/* Social links */}
@@ -70,26 +70,19 @@ export function Footer() { FAQ -
  • - - Roadmap - -
  • - {/* Company */} + {/* Contact */}
    -

    Company

    +

    Contact

    @@ -142,16 +117,6 @@ export function Footer() { Impressum -
  • - - webklar.com - -
  • @@ -160,10 +125,7 @@ export function Footer() {

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

    -

    - Made with ❤️ + © {new Date().getFullYear()} EmailSorter

    {/* webklar.com Verweis */} diff --git a/client/src/components/landing/Hero.tsx b/client/src/components/landing/Hero.tsx index c4e0f65..1085072 100644 --- a/client/src/components/landing/Hero.tsx +++ b/client/src/components/landing/Hero.tsx @@ -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() {
    - AI-powered email sorting + For freelancers & small teams

    - Clean inbox automatically + Leads, clients, spam —
    - in minutes. + sorted automatically.

    - 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.

    -
    - +
    +

    + +

    {/* Trust badges */}
    - No credit card required + No credit card
    @@ -86,59 +86,22 @@ export function Hero() {
    - {/* Right side - Visual */} + {/* Right side - Inbox visual (product screenshot feel) */}
    -
    - {/* Main card */} -
    -
    -
    - -
    -
    -

    Your Inbox

    -

    Auto-sorted

    -
    -
    - - {/* Email categories preview */} -
    - - - - -
    +
    +
    + Inbox
    - - {/* Floating badge */} -
    - - AI sorting +
    + + + + +
    +

    + This happens automatically on new emails. +

    @@ -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 = { + 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 ( -
    -
    +
    -
    - {sender} - {category} -
    -

    {subject}

    +

    + {sender} +

    +

    {subject}

    - + {label}
    ) } diff --git a/client/src/components/landing/HowItWorks.tsx b/client/src/components/landing/HowItWorks.tsx index 0932599..7e2f897 100644 --- a/client/src/components/landing/HowItWorks.tsx +++ b/client/src/components/landing/HowItWorks.tsx @@ -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.", }, ] diff --git a/client/src/components/landing/Navbar.tsx b/client/src/components/landing/Navbar.tsx index 0447d7b..c672e5a 100644 --- a/client/src/components/landing/Navbar.tsx +++ b/client/src/components/landing/Navbar.tsx @@ -82,7 +82,7 @@ export function Navbar() { Sign in )} @@ -146,7 +146,7 @@ export function Navbar() { Sign in )} diff --git a/client/src/components/landing/Testimonials.tsx b/client/src/components/landing/Testimonials.tsx index 8745bb0..f72797e 100644 --- a/client/src/components/landing/Testimonials.tsx +++ b/client/src/components/landing/Testimonials.tsx @@ -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 header */} +
    -

    - Why EmailSorter? +

    + Honest context

    -

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

    + We're a small product. Here's how we work.

    - {/* Benefits grid */} -
    - {benefits.map((benefit, index) => ( - +
    + {items.map((item, index) => ( +
    +
    + +
    +

    {item.title}

    +

    {item.description}

    +
    ))}
    ) } - -interface BenefitCardProps { - icon: React.ElementType - title: string - description: string -} - -function BenefitCard({ icon: Icon, title, description }: BenefitCardProps) { - return ( -
    -
    - -
    -

    {title}

    -

    {description}

    -
    - ) -} diff --git a/client/src/components/landing/TrustSection.tsx b/client/src/components/landing/TrustSection.tsx new file mode 100644 index 0000000..dfffcda --- /dev/null +++ b/client/src/components/landing/TrustSection.tsx @@ -0,0 +1,48 @@ +import { Shield, Mail, Trash2 } from 'lucide-react' + +export function TrustSection() { + return ( +
    +
    +

    + Your data, in plain language +

    +
      +
    • +
      + +
      +
      +

      We only read what we need

      +

      + We use sender, subject, and a short snippet to decide the category (e.g. newsletter vs client). We don't store your email body or attachments. +

      +
      +
    • +
    • +
      + +
      +
      +

      No selling, no ads

      +

      + 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. +

      +
      +
    • +
    • +
      + +
      +
      +

      You can leave anytime

      +

      + Disconnect your account and we stop. Cancel your subscription with one click. No lock-in, no "contact sales" to leave. +

      +
      +
    • +
    +
    +
    + ) +} diff --git a/client/src/components/ui/badge.tsx b/client/src/components/ui/badge.tsx index b108408..4531df9 100644 --- a/client/src/components/ui/badge.tsx +++ b/client/src/components/ui/badge.tsx @@ -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" diff --git a/client/src/components/ui/button.tsx b/client/src/components/ui/button.tsx index 263843f..8d502ae 100644 --- a/client/src/components/ui/button.tsx +++ b/client/src/components/ui/button.tsx @@ -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" diff --git a/client/src/context/AuthContext.tsx b/client/src/context/AuthContext.tsx index 68affe0..0bd707b 100644 --- a/client/src/context/AuthContext.tsx +++ b/client/src/context/AuthContext.tsx @@ -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' diff --git a/client/src/lib/analytics.ts b/client/src/lib/analytics.ts index ae74b9c..b2a8efb 100644 --- a/client/src/lib/analytics.ts +++ b/client/src/lib/analytics.ts @@ -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 + metadata?: Record sessionId?: string } diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts index 275c11b..adee46f 100644 --- a/client/src/lib/api.ts +++ b/client/src/lib/api.ts @@ -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 autoDetectCompanies: boolean - cleanup?: any - categoryAdvanced?: Record + cleanup?: unknown + categoryAdvanced?: Record version?: number }>(`/preferences/ai-control?userId=${userId}`) }, @@ -302,8 +303,8 @@ export const api = { enabledCategories?: string[] categoryActions?: Record autoDetectCompanies?: boolean - cleanup?: any - categoryAdvanced?: Record + cleanup?: unknown + categoryAdvanced?: Record version?: number }) { return fetchApi<{ success: boolean }>('/preferences/ai-control', { @@ -407,14 +408,14 @@ export const api = { // ═══════════════════════════════════════════════════════════════════════════ async getProducts() { - return fetchApi('/products') + return fetchApi('/products') }, async getQuestions(productSlug: string) { - return fetchApi(`/questions?productSlug=${productSlug}`) + return fetchApi(`/questions?productSlug=${productSlug}`) }, - async createSubmission(productSlug: string, answers: Record) { + async createSubmission(productSlug: string, answers: Record) { return fetchApi<{ submissionId: string }>('/submissions', { method: 'POST', body: JSON.stringify({ productSlug, answers }), diff --git a/client/src/lib/appwrite.ts b/client/src/lib/appwrite.ts index fa60e1b..53dae5d 100644 --- a/client/src/lib/appwrite.ts +++ b/client/src/lib/appwrite.ts @@ -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 diff --git a/client/src/pages/Dashboard.tsx b/client/src/pages/Dashboard.tsx index b7875ae..d7d1cd0 100644 --- a/client/src/pages/Dashboard.tsx +++ b/client/src/pages/Dashboard.tsx @@ -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('Couldn’t 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() {
    - - -
    @@ -315,7 +306,7 @@ export function Dashboard() { aria-label="Connect email account" > - Connect Account + Connect inbox ) : (
    @@ -413,10 +414,10 @@ export function Dashboard() {

    - {sortResult.isFirstRun ? '🎉 First sort complete!' : 'Sorting complete!'} + {sortResult.isFirstRun ? 'Done. Your inbox is sorted.' : 'Sort complete.'}

    {sortResult.isFirstRun && ( -

    Your inbox is getting organized

    +

    Newsletters and promos are in folders. Check your inbox — only important mail is left.

    )}
    @@ -444,10 +445,10 @@ export function Dashboard() {

    Top category

    - {Object.entries(sortResult.categories).sort(([_, a], [__, b]) => b - a)[0]?.[1] || 0} + {Object.entries(sortResult.categories).sort(([, a], [, b]) => b - a)[0]?.[1] || 0}

    - {formatCategoryName(Object.entries(sortResult.categories).sort(([_, a], [__, b]) => b - a)[0]?.[0] || '')} + {formatCategoryName(Object.entries(sortResult.categories).sort(([, a], [, b]) => b - a)[0]?.[0] || '')}

    )} @@ -457,7 +458,7 @@ export function Dashboard() { {sortResult.isFirstRun && sortResult.suggestedRules && sortResult.suggestedRules.length > 0 && (
    -

    Smart suggestions for you

    +

    Suggestions for you

    Based on your email patterns

    @@ -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() { Control Panel
    - KI-Einstellungen und Kategorien verwalten + Categories and rules @@ -767,7 +768,7 @@ export function Dashboard() { aria-label="Open Control Panel settings" > - Control Panel öffnen + Open Control Panel @@ -778,10 +779,10 @@ export function Dashboard() {
    - Einstellungen + Settings
    - Schnellzugriff auf wichtige Einstellungen + Quick access
    @@ -810,7 +811,7 @@ export function Dashboard() { aria-label="Open all settings" > - Alle Einstellungen + All settings @@ -819,9 +820,9 @@ export function Dashboard() { {/* Account/System Karte */} - Account & System + Account - Subscription und Konten-Status + Subscription and accounts @@ -932,7 +933,7 @@ export function Dashboard() { ) : (
    -

    No accounts connected

    +

    No inbox connected yet

    )} diff --git a/client/src/pages/ForgotPassword.tsx b/client/src/pages/ForgotPassword.tsx index 28e99e0..2b805cb 100644 --- a/client/src/pages/ForgotPassword.tsx +++ b/client/src/pages/ForgotPassword.tsx @@ -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) } diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx index 7e62ab9..9242847 100644 --- a/client/src/pages/Home.tsx +++ b/client/src/pages/Home.tsx @@ -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() { +
    diff --git a/client/src/pages/Login.tsx b/client/src/pages/Login.tsx index e063756..e985c61 100644 --- a/client/src/pages/Login.tsx +++ b/client/src/pages/Login.tsx @@ -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) } diff --git a/client/src/pages/Register.tsx b/client/src/pages/Register.tsx index 9e95341..3009c89 100644 --- a/client/src/pages/Register.tsx +++ b/client/src/pages/Register.tsx @@ -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) } diff --git a/client/src/pages/ResetPassword.tsx b/client/src/pages/ResetPassword.tsx index 7402566..bee3c06 100644 --- a/client/src/pages/ResetPassword.tsx +++ b/client/src/pages/ResetPassword.tsx @@ -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) } diff --git a/client/src/pages/Settings.tsx b/client/src/pages/Settings.tsx index 38c3186..f7e86de 100644 --- a/client/src/pages/Settings.tsx +++ b/client/src/pages/Settings.tsx @@ -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 | 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() {
    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" > @@ -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" diff --git a/client/src/pages/Setup.tsx b/client/src/pages/Setup.tsx index 99ddf84..6ec0e75 100644 --- a/client/src/pages/Setup.tsx +++ b/client/src/pages/Setup.tsx @@ -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() {
    @@ -588,12 +583,15 @@ export function Setup() { {currentStep === 'complete' && (
    -
    - +
    +
    -

    All set! 🎉

    -

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

    You're in 🎉

    +

    + Click Sort Now on the dashboard to categorize your inbox. Takes about 30 seconds. +

    +

    + Tune categories later in Settings

    diff --git a/client/src/pages/VerifyEmail.tsx b/client/src/pages/VerifyEmail.tsx index e90571f..be56785 100644 --- a/client/src/pages/VerifyEmail.tsx +++ b/client/src/pages/VerifyEmail.tsx @@ -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') } diff --git a/docs/PRODUCT_STRATEGY_2WEEK.md b/docs/PRODUCT_STRATEGY_2WEEK.md new file mode 100644 index 0000000..bde2067 --- /dev/null +++ b/docs/PRODUCT_STRATEGY_2WEEK.md @@ -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; doesn’t say who it’s 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 "I’ll do this later" and only show after they’ve seen the connect step (so they can still land on dashboard with empty state). + +### 60-second flow (concrete) +1. **0–15s:** Land on `/register` or home → click "Try it free" → sign up (email or Google). +2. **15–45s:** One screen: "Connect Gmail or Outlook" + prominent "Try with sample inbox" (demo). No steps 2–3. +3. **45–60s:** After connect or demo → "You’re 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 don’t push in onboarding. +- **Hide:** Daily digest / "Today’s 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: "We’ve 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 don’t 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 they’ve done 2+ sorts and connected a real inbox, send one email: "You’ve 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 post–free-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 post–launch.