Email Sorter Beta

Ich habe soweit automatisiert the Emails sortieren aber ich muss noch schauen was es fur bugs es gibt wenn die app online  ist deswegen wurde ich mit diesen Commit die website veroffentlichen obwohjl es sein konnte  das es noch nicht fertig ist und verkaufs bereit
This commit is contained in:
2026-01-22 19:32:12 +01:00
parent 95349af50b
commit abf761db07
596 changed files with 56405 additions and 51231 deletions

View File

@@ -0,0 +1,593 @@
import { useState, useEffect } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { useAuth } from '@/context/AuthContext'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import { api } from '@/lib/api'
import {
Mail,
User,
CreditCard,
Shield,
Settings as SettingsIcon,
ArrowLeft,
Plus,
Trash2,
Check,
X,
ExternalLink,
Loader2,
Crown,
Star,
} from 'lucide-react'
type TabType = 'profile' | 'accounts' | 'vip' | 'rules' | 'subscription'
interface EmailAccount {
id: string
email: string
provider: 'gmail' | 'outlook'
connected: boolean
lastSync?: string
}
interface VIPSender {
email: string
name?: string
}
interface SortRule {
id: string
name: string
condition: string
category: string
enabled: boolean
}
interface Subscription {
status: string
plan: string
currentPeriodEnd?: string
cancelAtPeriodEnd?: boolean
}
export function Settings() {
const { user } = useAuth()
const navigate = useNavigate()
const [searchParams, setSearchParams] = useSearchParams()
const activeTab = (searchParams.get('tab') as TabType) || 'profile'
const [loading, setLoading] = useState(false)
const [saving, setSaving] = useState(false)
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
const [name, setName] = useState(user?.name || '')
const [email] = useState(user?.email || '')
const [accounts, setAccounts] = useState<EmailAccount[]>([])
const [connectingProvider, setConnectingProvider] = useState<string | null>(null)
const [vipSenders, setVipSenders] = useState<VIPSender[]>([])
const [newVipEmail, setNewVipEmail] = useState('')
const [rules, setRules] = useState<SortRule[]>([
{ id: '1', name: 'Boss Emails', condition: 'from:boss@company.com', category: 'Important', enabled: true },
{ id: '2', name: 'Support Tickets', condition: 'subject:Ticket #', category: 'Clients', enabled: true },
])
const [subscription, setSubscription] = useState<Subscription | null>(null)
useEffect(() => {
loadData()
}, [user])
const loadData = async () => {
if (!user?.$id) return
setLoading(true)
try {
const [accountsRes, subsRes, prefsRes] = await Promise.all([
api.getEmailAccounts(user.$id),
api.getSubscriptionStatus(user.$id),
api.getUserPreferences(user.$id),
])
if (accountsRes.data) setAccounts(accountsRes.data)
if (subsRes.data) setSubscription(subsRes.data)
if (prefsRes.data?.vipSenders) setVipSenders(prefsRes.data.vipSenders)
} catch (error) {
console.error('Failed to load settings data:', error)
} finally {
setLoading(false)
}
}
const setTab = (tab: TabType) => {
setSearchParams({ tab })
setMessage(null)
}
const showMessage = (type: 'success' | 'error', text: string) => {
setMessage({ type, text })
setTimeout(() => setMessage(null), 5000)
}
const handleSaveProfile = async () => {
setSaving(true)
try {
showMessage('success', 'Profile saved!')
} catch {
showMessage('error', 'Failed to save')
} finally {
setSaving(false)
}
}
const handleConnectAccount = async (provider: 'gmail' | 'outlook') => {
if (!user?.$id) return
setConnectingProvider(provider)
try {
const res = await api.getOAuthUrl(provider, user.$id)
if (res.data?.url) {
window.location.href = res.data.url
}
} catch {
showMessage('error', `Failed to connect ${provider}`)
setConnectingProvider(null)
}
}
const handleDisconnectAccount = async (accountId: string) => {
if (!user?.$id) return
try {
await api.disconnectEmailAccount(accountId, user.$id)
setAccounts(accounts.filter(a => a.id !== accountId))
showMessage('success', 'Account disconnected')
} catch {
showMessage('error', 'Failed to disconnect')
}
}
const handleAddVip = () => {
if (!newVipEmail.trim() || !newVipEmail.includes('@')) return
if (vipSenders.some(v => v.email === newVipEmail)) {
showMessage('error', 'This email is already in the VIP list')
return
}
setVipSenders([...vipSenders, { email: newVipEmail }])
setNewVipEmail('')
showMessage('success', 'VIP added')
}
const handleRemoveVip = (email: string) => {
setVipSenders(vipSenders.filter(v => v.email !== email))
}
const handleSaveVips = async () => {
if (!user?.$id) return
setSaving(true)
try {
await api.saveUserPreferences(user.$id, { vipSenders })
showMessage('success', 'VIP list saved!')
} catch {
showMessage('error', 'Failed to save')
} finally {
setSaving(false)
}
}
const toggleRule = (ruleId: string) => {
setRules(rules.map(r =>
r.id === ruleId ? { ...r, enabled: !r.enabled } : r
))
}
const handleManageSubscription = async () => {
if (!user?.$id) return
try {
const res = await api.createPortalSession(user.$id)
if (res.data?.url) {
window.location.href = res.data.url
}
} catch {
showMessage('error', 'Failed to open customer portal')
}
}
const handleUpgrade = async (plan: string) => {
if (!user?.$id) return
try {
const res = await api.createSubscriptionCheckout(plan, user.$id, user.email)
if (res.data?.url) {
window.location.href = res.data.url
}
} catch {
showMessage('error', 'Failed to start checkout')
}
}
const tabs = [
{ id: 'profile' as TabType, label: 'Profile', icon: User },
{ id: 'accounts' as TabType, label: 'Email Accounts', icon: Mail },
{ id: 'vip' as TabType, label: 'VIP List', icon: Star },
{ id: 'rules' as TabType, label: 'Sorting Rules', icon: SettingsIcon },
{ id: 'subscription' as TabType, label: 'Subscription', icon: CreditCard },
]
return (
<div className="min-h-screen bg-slate-50">
<header className="bg-white border-b border-slate-200 sticky top-0 z-40">
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center h-16">
<Button variant="ghost" onClick={() => navigate('/dashboard')} className="mr-4">
<ArrowLeft className="w-5 h-5 mr-2" />
Back
</Button>
<div className="flex items-center gap-2">
<SettingsIcon className="w-5 h-5 text-slate-500" />
<h1 className="text-lg font-semibold text-slate-900">Settings</h1>
</div>
</div>
</div>
</header>
<main className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{message && (
<div className={`mb-6 p-4 rounded-lg flex items-center gap-2 ${
message.type === 'success'
? 'bg-green-50 text-green-700 border border-green-200'
: 'bg-red-50 text-red-700 border border-red-200'
}`}>
{message.type === 'success' ? <Check className="w-5 h-5" /> : <X className="w-5 h-5" />}
{message.text}
</div>
)}
<div className="flex flex-col lg:flex-row gap-8">
<nav className="lg:w-64 flex-shrink-0">
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setTab(tab.id)}
className={`w-full flex items-center gap-3 px-4 py-3 text-left transition-colors ${
activeTab === tab.id
? 'bg-primary-50 text-primary-700 border-l-4 border-primary-500'
: 'text-slate-600 hover:bg-slate-50 border-l-4 border-transparent'
}`}
>
<tab.icon className="w-5 h-5" />
<span className="font-medium">{tab.label}</span>
</button>
))}
</div>
</nav>
<div className="flex-1 min-w-0">
{loading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-primary-500" />
</div>
) : (
<>
{activeTab === 'profile' && (
<Card>
<CardHeader>
<CardTitle>Profile</CardTitle>
<CardDescription>Manage your personal information</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center gap-4">
<div className="w-20 h-20 rounded-full bg-gradient-to-br from-primary-400 to-primary-600 flex items-center justify-center text-white text-2xl font-bold">
{name?.charAt(0)?.toUpperCase() || email?.charAt(0)?.toUpperCase() || 'U'}
</div>
<div>
<h3 className="font-semibold text-slate-900">{name || 'User'}</h3>
<p className="text-slate-500">{email}</p>
</div>
</div>
<div className="space-y-4">
<div>
<Label htmlFor="name">Name</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Your name"
/>
</div>
<div>
<Label htmlFor="email">Email</Label>
<Input id="email" value={email} disabled className="bg-slate-50" />
<p className="text-xs text-slate-500 mt-1">Email address cannot be changed</p>
</div>
</div>
<Button onClick={handleSaveProfile} disabled={saving}>
{saving ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : null}
Save
</Button>
</CardContent>
</Card>
)}
{activeTab === 'accounts' && (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Connected Email Accounts</CardTitle>
<CardDescription>Connect your email accounts for automatic sorting</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{accounts.length > 0 ? (
accounts.map((account) => (
<div key={account.id} className="flex items-center justify-between p-4 bg-slate-50 rounded-lg">
<div className="flex items-center gap-4">
<div className={`w-12 h-12 rounded-lg flex items-center justify-center ${
account.provider === 'gmail' ? 'bg-red-100' : 'bg-blue-100'
}`}>
<Mail className={`w-6 h-6 ${account.provider === 'gmail' ? 'text-red-600' : 'text-blue-600'}`} />
</div>
<div>
<p className="font-medium text-slate-900">{account.email}</p>
<p className="text-sm text-slate-500 capitalize">{account.provider}</p>
</div>
</div>
<div className="flex items-center gap-3">
<Badge variant={account.connected ? 'success' : 'secondary'}>
{account.connected ? 'Connected' : 'Disconnected'}
</Badge>
<Button variant="ghost" size="icon" onClick={() => handleDisconnectAccount(account.id)}>
<Trash2 className="w-4 h-4 text-red-500" />
</Button>
</div>
</div>
))
) : (
<p className="text-center text-slate-500 py-8">No email accounts connected yet</p>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Add Account</CardTitle>
<CardDescription>Connect a new email account</CardDescription>
</CardHeader>
<CardContent>
<div className="grid sm:grid-cols-2 gap-4">
<button
onClick={() => handleConnectAccount('gmail')}
disabled={connectingProvider === 'gmail'}
className="flex items-center gap-4 p-4 border-2 border-slate-200 rounded-xl hover:border-red-300 hover:bg-red-50 transition-all disabled:opacity-50"
>
{connectingProvider === 'gmail' ? (
<Loader2 className="w-8 h-8 animate-spin text-red-500" />
) : (
<div className="w-12 h-12 rounded-lg bg-red-100 flex items-center justify-center">
<svg className="w-6 h-6" viewBox="0 0 24 24">
<path fill="#EA4335" d="M12 11.3L1.5 3.5h21z"/>
<path fill="#34A853" d="M12 12.7L1.5 20.5V3.5z"/>
<path fill="#FBBC05" d="M1.5 20.5h21v-17z"/>
<path fill="#4285F4" d="M22.5 3.5v17L12 12.7z"/>
</svg>
</div>
)}
<div className="text-left">
<p className="font-semibold text-slate-900">Gmail</p>
<p className="text-sm text-slate-500">Connect Google account</p>
</div>
</button>
<button
onClick={() => handleConnectAccount('outlook')}
disabled={connectingProvider === 'outlook'}
className="flex items-center gap-4 p-4 border-2 border-slate-200 rounded-xl hover:border-blue-300 hover:bg-blue-50 transition-all disabled:opacity-50"
>
{connectingProvider === 'outlook' ? (
<Loader2 className="w-8 h-8 animate-spin text-blue-500" />
) : (
<div className="w-12 h-12 rounded-lg bg-blue-100 flex items-center justify-center">
<svg className="w-6 h-6" viewBox="0 0 24 24">
<path fill="#0078D4" d="M2 6.5v11c0 .83.67 1.5 1.5 1.5h9c.83 0 1.5-.67 1.5-1.5v-11c0-.83-.67-1.5-1.5-1.5h-9C2.67 5 2 5.67 2 6.5z"/>
<path fill="#0078D4" d="M14 6v12l8-6z"/>
</svg>
</div>
)}
<div className="text-left">
<p className="font-semibold text-slate-900">Outlook</p>
<p className="text-sm text-slate-500">Connect Microsoft account</p>
</div>
</button>
</div>
</CardContent>
</Card>
</div>
)}
{activeTab === 'vip' && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="w-5 h-5 text-amber-500" />
VIP List
</CardTitle>
<CardDescription>Emails from these senders will always be marked as important</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex gap-2">
<Input
placeholder="email@example.com"
value={newVipEmail}
onChange={(e) => setNewVipEmail(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleAddVip()}
/>
<Button onClick={handleAddVip}>
<Plus className="w-4 h-4 mr-2" />
Add
</Button>
</div>
<div className="space-y-2">
{vipSenders.length > 0 ? (
vipSenders.map((vip) => (
<div key={vip.email} className="flex items-center justify-between p-3 bg-amber-50 border border-amber-100 rounded-lg">
<div className="flex items-center gap-3">
<Star className="w-5 h-5 text-amber-500" />
<span className="text-slate-700">{vip.email}</span>
</div>
<Button variant="ghost" size="icon" onClick={() => handleRemoveVip(vip.email)}>
<X className="w-4 h-4 text-slate-400 hover:text-red-500" />
</Button>
</div>
))
) : (
<p className="text-center text-slate-500 py-8">No VIP senders added yet</p>
)}
</div>
{vipSenders.length > 0 && (
<Button onClick={handleSaveVips} disabled={saving}>
{saving ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : null}
Save changes
</Button>
)}
</CardContent>
</Card>
)}
{activeTab === 'rules' && (
<Card>
<CardHeader>
<CardTitle>Sorting Rules</CardTitle>
<CardDescription>Custom rules for email sorting</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{rules.map((rule) => (
<div key={rule.id} className={`flex items-center justify-between p-4 rounded-lg border ${
rule.enabled ? 'bg-white border-slate-200' : 'bg-slate-50 border-slate-100'
}`}>
<div className="flex items-center gap-4">
<button
onClick={() => toggleRule(rule.id)}
className={`w-10 h-6 rounded-full transition-colors ${rule.enabled ? 'bg-primary-500' : 'bg-slate-300'}`}
>
<div className={`w-4 h-4 bg-white rounded-full transform transition-transform mx-1 ${
rule.enabled ? 'translate-x-4' : 'translate-x-0'
}`} />
</button>
<div>
<p className={`font-medium ${rule.enabled ? 'text-slate-900' : 'text-slate-500'}`}>{rule.name}</p>
<p className="text-sm text-slate-500 font-mono">{rule.condition}</p>
</div>
</div>
<Badge variant={rule.enabled ? 'default' : 'secondary'}>{rule.category}</Badge>
</div>
))}
<Button variant="outline" className="w-full">
<Plus className="w-4 h-4 mr-2" />
Create new rule
</Button>
</CardContent>
</Card>
)}
{activeTab === 'subscription' && (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Current Subscription</CardTitle>
<CardDescription>Manage your EmailSorter subscription</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between p-4 bg-gradient-to-r from-primary-50 to-accent-50 rounded-xl border border-primary-100">
<div className="flex items-center gap-4">
<div className="w-14 h-14 rounded-xl bg-white shadow-sm flex items-center justify-center">
<Crown className="w-7 h-7 text-primary-500" />
</div>
<div>
<div className="flex items-center gap-2">
<h3 className="font-bold text-lg text-slate-900">{subscription?.plan || 'Trial'}</h3>
<Badge variant={subscription?.status === 'active' ? 'success' : 'warning'}>
{subscription?.status === 'active' ? 'Active' : 'Trial'}
</Badge>
</div>
{subscription?.currentPeriodEnd && (
<p className="text-sm text-slate-500">
Next billing: {new Date(subscription.currentPeriodEnd).toLocaleDateString('en-US')}
</p>
)}
</div>
</div>
<Button onClick={handleManageSubscription}>
<ExternalLink className="w-4 h-4 mr-2" />
Manage
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Available Plans</CardTitle>
<CardDescription>Choose the plan that fits you</CardDescription>
</CardHeader>
<CardContent>
<div className="grid sm:grid-cols-3 gap-4">
{[
{ id: 'basic', name: 'Basic', price: '9', features: ['1 email account', '500 emails/day', 'Standard support'] },
{ id: 'pro', name: 'Pro', price: '19', features: ['3 email accounts', 'Unlimited emails', 'Historical sorting', 'Priority support'], popular: true },
{ id: 'business', name: 'Business', price: '49', features: ['10 email accounts', 'Unlimited emails', 'Team features', 'API access', '24/7 support'] },
].map((plan) => (
<div key={plan.id} className={`relative p-6 rounded-xl border-2 ${
plan.popular ? 'border-primary-500 bg-primary-50' : 'border-slate-200 bg-white'
}`}>
{plan.popular && (
<div className="absolute -top-3 left-1/2 -translate-x-1/2">
<Badge className="bg-primary-500 text-white">Popular</Badge>
</div>
)}
<h3 className="font-bold text-lg text-slate-900">{plan.name}</h3>
<div className="mt-2 mb-4">
<span className="text-3xl font-bold text-slate-900">${plan.price}</span>
<span className="text-slate-500">/month</span>
</div>
<ul className="space-y-2 mb-6">
{plan.features.map((feature) => (
<li key={feature} className="flex items-center gap-2 text-sm text-slate-600">
<Check className="w-4 h-4 text-green-500" />
{feature}
</li>
))}
</ul>
<Button
className="w-full"
variant={plan.popular ? 'default' : 'outline'}
onClick={() => handleUpgrade(plan.id)}
disabled={subscription?.plan === plan.id}
>
{subscription?.plan === plan.id ? 'Current plan' : 'Select'}
</Button>
</div>
))}
</div>
</CardContent>
</Card>
</div>
)}
</>
)}
</div>
</div>
</main>
</div>
)
}