Files
Emailsorter/client/src/pages/Settings.tsx
ANDJ 89bc86b615 Try
dfssdfsfdsf
2026-04-09 21:00:04 +02:00

2901 lines
156 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useRef, useMemo } 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 { Slider } from '@/components/ui/slider'
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
import {
SidePanel,
SidePanelContent,
SidePanelHeader,
SidePanelTitle,
SidePanelDescription,
SidePanelCloseButton,
SidePanelBody,
SidePanelFooter,
} from '@/components/ui/side-panel'
import { api } from '@/lib/api'
import {
Mail,
User,
Users,
CreditCard,
Shield,
Settings as SettingsIcon,
ArrowLeft,
Plus,
Trash2,
Check,
X,
ExternalLink,
Loader2,
Crown,
Star,
Brain,
Building2,
Lock,
Copy,
AlertTriangle,
Search,
Download,
Upload,
ChevronDown,
ChevronUp,
RotateCcw,
Play,
Eye,
Camera,
Globe,
Clock,
Palette,
Save,
Edit2,
} from 'lucide-react'
import type { AIControlSettings, CompanyLabel, NameLabel, CategoryInfo, CleanupSettings, CleanupStatus, CategoryAdvanced } from '@/types/settings'
import { PrivacySecurity } from '@/components/PrivacySecurity'
type TabType = 'profile' | 'accounts' | 'vip' | 'ai-control' | 'name-labels' | 'subscription' | 'privacy' | 'referrals'
const HEX_COLOR = /^#([0-9A-Fa-f]{6})$/
function validateLabelImport(
imported: unknown,
existing: CompanyLabel[]
): { labels: CompanyLabel[]; errors: string[] } {
const errors: string[] = []
if (!Array.isArray(imported)) {
return { labels: [], errors: ['File must contain a JSON array'] }
}
const seen = new Set<string>()
const labels: CompanyLabel[] = []
imported.forEach((row, i) => {
const rowNum = i + 1
if (!row || typeof row !== 'object') {
errors.push(`Row ${rowNum}: invalid object`)
return
}
const r = row as Record<string, unknown>
const name = typeof r.name === 'string' ? r.name.trim() : ''
if (!name) {
errors.push(`Row ${rowNum}: name is required`)
return
}
if (name.length > 50) {
errors.push(`Row ${rowNum}: name must be at most 50 characters`)
return
}
if (r.color != null && r.color !== '') {
if (typeof r.color !== 'string' || !HEX_COLOR.test(r.color)) {
errors.push(`Row ${rowNum}: color must be a valid #RRGGBB hex`)
return
}
}
const key = name.toLowerCase()
if (seen.has(key)) {
errors.push(`Row ${rowNum}: duplicate name "${name}" in import`)
return
}
seen.add(key)
if (existing.some((e) => e.name.trim().toLowerCase() === key)) {
errors.push(`Row ${rowNum}: name "${name}" already exists`)
return
}
labels.push({
id: typeof r.id === 'string' && r.id ? r.id : `label_import_${Date.now()}_${i}`,
name,
condition: typeof r.condition === 'string' ? r.condition : '',
enabled: r.enabled !== false,
category: typeof r.category === 'string' ? r.category : 'promotions',
})
})
if (existing.length + labels.length > 100) {
return {
labels: [],
errors: [`Cannot exceed 100 labels total (have ${existing.length}, importing ${labels.length})`],
}
}
return { labels, errors }
}
interface EmailAccount {
id: string
email: string
provider: 'gmail' | 'outlook' | 'imap' | 'demo'
connected: boolean
lastSync?: string
isDemo?: boolean
}
interface VIPSender {
email: string
name?: string
}
interface Subscription {
status: string
plan: string
planDisplayName?: string
isFreeTier?: boolean
currentPeriodEnd?: string
cancelAtPeriodEnd?: boolean
}
function subscriptionTitle(sub: Subscription | null): string {
if (!sub) return ''
if (sub.planDisplayName) return sub.planDisplayName
if (sub.plan === 'free' || sub.isFreeTier) return 'Free plan'
if (sub.plan) return sub.plan.charAt(0).toUpperCase() + sub.plan.slice(1)
return 'Subscription'
}
function subscriptionBadge(sub: Subscription | null): {
label: string
variant: 'success' | 'warning' | 'secondary'
} {
if (!sub) return { label: '', variant: 'secondary' }
if (sub.isFreeTier) return { label: 'Free plan', variant: 'secondary' }
if (sub.status === 'active') return { label: 'Active', variant: 'success' }
const s = (sub.status || '').toLowerCase()
if (s === 'trialing' || s === 'trial') return { label: 'Trial', variant: 'warning' }
return {
label: sub.status ? sub.status.charAt(0).toUpperCase() + sub.status.slice(1) : 'Inactive',
variant: 'warning',
}
}
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 [language, setLanguage] = useState('en')
const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone)
const [hasProfileChanges, setHasProfileChanges] = useState(false)
const savedProfileRef = useRef<{ name: string; language: string; timezone: string } | null>(null)
const [accounts, setAccounts] = useState<EmailAccount[]>([])
const [connectingProvider, setConnectingProvider] = useState<string | null>(null)
const [showImapForm, setShowImapForm] = useState(false)
const [imapForm, setImapForm] = useState({ email: '', password: '', imapHost: 'imap.porkbun.com', imapPort: 993, imapSecure: true })
const [imapConnecting, setImapConnecting] = useState(false)
const [recoveringAccountId, setRecoveringAccountId] = useState<string | null>(null)
const [reSortingAccountId, setReSortingAccountId] = useState<string | null>(null)
const [vipSenders, setVipSenders] = useState<VIPSender[]>([])
const [newVipEmail, setNewVipEmail] = useState('')
const [subscription, setSubscription] = useState<Subscription | null>(null)
// AI Control state
const [aiControlSettings, setAiControlSettings] = useState<AIControlSettings>({
enabledCategories: [],
categoryActions: {},
autoDetectCompanies: true,
cleanup: {
enabled: false,
readItems: {
enabled: false,
action: 'archive_read',
gracePeriodDays: 7,
},
promotions: {
enabled: false,
matchCategoriesOrLabels: ['promotions', 'newsletters'],
action: 'archive_read',
deleteAfterDays: 30,
},
safety: {
requireConfirmForDelete: true,
},
},
})
const [categories, setCategories] = useState<CategoryInfo[]>([])
const [companyLabels, setCompanyLabels] = useState<CompanyLabel[]>([])
const [labelImportErrors, setLabelImportErrors] = useState<string[]>([])
const [isAdmin, setIsAdmin] = useState(false)
const [nameLabels, setNameLabels] = useState<NameLabel[]>([])
const [editingNameLabel, setEditingNameLabel] = useState<NameLabel | null>(null)
const [showNameLabelPanel, setShowNameLabelPanel] = useState(false)
const [referralData, setReferralData] = useState<{ referralCode: string; referralCount: number } | null>(null)
const [loadingReferral, setLoadingReferral] = useState(false)
const [resettingSort, setResettingSort] = useState(false)
// Control Panel Sub-Tabs
const [controlPanelTab, setControlPanelTab] = useState<'rules' | 'cleanup' | 'labels'>('rules')
// Unsaved changes tracking
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
const savedSettingsRef = useRef<AIControlSettings | null>(null)
// Search/Filter state
const [searchQuery, setSearchQuery] = useState('')
const [filterEnabled, setFilterEnabled] = useState<'all' | 'enabled' | 'disabled'>('all')
const [labelSort, setLabelSort] = useState<'name' | 'newest'>('name')
// Cleanup status
const [cleanupStatus, setCleanupStatus] = useState<CleanupStatus | null>(null)
const [cleanupPreview, setCleanupPreview] = useState<CleanupStatus['preview']>([])
const [runningCleanup, setRunningCleanup] = useState(false)
// Label editor
const [editingLabel, setEditingLabel] = useState<CompanyLabel | null>(null)
// Advanced options expanded state
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set())
// Side Panel state for category details
const [selectedCategory, setSelectedCategory] = useState<CategoryInfo | null>(null)
const [showCategoryPanel, setShowCategoryPanel] = useState(false)
// Side Panel state for label editor
const [showLabelPanel, setShowLabelPanel] = useState(false)
useEffect(() => {
loadData()
if (user?.$id) {
loadReferralData()
}
}, [user])
// Refetch subscription when opening this tab (fixes JWT timing vs initial loadData)
useEffect(() => {
if (activeTab !== 'subscription' || !user?.$id) return
let cancelled = false
;(async () => {
const res = await api.getSubscriptionStatus()
if (!cancelled && res.data) setSubscription(res.data)
})()
return () => {
cancelled = true
}
}, [activeTab, user?.$id])
const loadReferralData = async () => {
if (!user?.$id) return
setLoadingReferral(true)
try {
const res = await api.getReferralCode()
if (res.data) setReferralData(res.data)
} catch (err) {
console.error('Failed to load referral data:', err)
} finally {
setLoadingReferral(false)
}
}
const loadData = async () => {
if (!user?.$id) return
setLoading(true)
try {
const [accountsRes, subsRes, prefsRes, aiControlRes, companyLabelsRes, meRes] = await Promise.all([
api.getEmailAccounts(),
api.getSubscriptionStatus(),
api.getUserPreferences(),
api.getAIControlSettings(),
api.getCompanyLabels(),
user?.$id ? api.getMe() : Promise.resolve({ data: { isAdmin: false } }),
])
if (accountsRes.data) setAccounts(accountsRes.data)
if (subsRes.data) setSubscription(subsRes.data)
if (meRes.data?.isAdmin) {
setIsAdmin(true)
const nameLabelsRes = await api.getNameLabels()
if (nameLabelsRes.data) setNameLabels(nameLabelsRes.data)
} else {
setIsAdmin(false)
}
if (prefsRes.data?.vipSenders) setVipSenders(prefsRes.data.vipSenders)
const pdata = prefsRes.data as {
profile?: { displayName?: string; timezone?: string; notificationPrefs?: { language?: string } }
} | undefined
if (pdata?.profile) {
const prof = pdata.profile
if (prof.displayName != null && prof.displayName !== '') setName(prof.displayName)
if (prof.timezone) setTimezone(prof.timezone)
if (prof.notificationPrefs?.language) setLanguage(String(prof.notificationPrefs.language))
}
if (aiControlRes.data) {
// Merge cleanup defaults if not present
const raw = aiControlRes.data
const defaultCleanup: CleanupSettings = {
enabled: false,
readItems: {
enabled: false,
action: 'archive_read',
gracePeriodDays: 7,
},
promotions: {
enabled: false,
matchCategoriesOrLabels: ['promotions', 'newsletters'],
action: 'archive_read',
deleteAfterDays: 30,
},
safety: {
requireConfirmForDelete: true,
dryRun: false,
maxDeletesPerRun: 100,
},
}
const settings: AIControlSettings = {
...raw,
cleanup: (raw.cleanup as CleanupSettings | undefined) || defaultCleanup,
categoryAdvanced: (raw.categoryAdvanced as Record<string, CategoryAdvanced> | undefined) || {},
version: raw.version ?? 1,
}
setAiControlSettings(settings)
savedSettingsRef.current = JSON.parse(JSON.stringify(settings)) // Deep copy
setHasUnsavedChanges(false)
}
if (companyLabelsRes.data) setCompanyLabels(companyLabelsRes.data)
// Load categories from API or use defaults
const categoryList: CategoryInfo[] = [
{ key: 'vip', name: 'Important', description: 'Important emails from known contacts', defaultAction: 'star', color: '#ff0000', enabled: true },
{ key: 'customers', name: 'Clients', description: 'Emails from clients and projects', defaultAction: 'inbox', color: '#4285f4', enabled: true },
{ key: 'invoices', name: 'Invoices', description: 'Invoices, receipts and financial documents', defaultAction: 'inbox', color: '#0f9d58', enabled: true },
{ key: 'newsletters', name: 'Newsletter', description: 'Regular newsletters and updates', defaultAction: 'archive_read', color: '#9c27b0', enabled: true },
{ key: 'promotions', name: 'Promotions', description: 'Marketing emails and promotions', defaultAction: 'archive_read', color: '#ff9800', enabled: true },
{ key: 'social', name: 'Social', description: 'Social media and platform notifications', defaultAction: 'archive_read', color: '#00bcd4', enabled: true },
{ key: 'security', name: 'Security', description: 'Security codes and notifications', defaultAction: 'inbox', color: '#f44336', enabled: true },
{ key: 'calendar', name: 'Calendar', description: 'Calendar invites and events', defaultAction: 'inbox', color: '#673ab7', enabled: true },
{ key: 'review', name: 'Review', description: 'Emails that need manual review', defaultAction: 'inbox', color: '#607d8b', enabled: true },
]
// Update enabled status from settings
const enabledCategories = aiControlRes.data?.enabledCategories || categoryList.map(c => c.key)
const updatedCategories = categoryList.map(cat => ({
...cat,
enabled: enabledCategories.includes(cat.key),
}))
setCategories(updatedCategories)
} 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)
}
// Track changes for Control Panel
useEffect(() => {
if (activeTab === 'ai-control' && savedSettingsRef.current) {
const current = JSON.stringify(aiControlSettings)
const saved = JSON.stringify(savedSettingsRef.current)
setHasUnsavedChanges(current !== saved)
}
}, [aiControlSettings, activeTab])
// Save Control Panel settings
const handleSaveControlPanel = async () => {
if (!user?.$id) return
setSaving(true)
try {
await api.saveAIControlSettings({
enabledCategories: aiControlSettings.enabledCategories,
categoryActions: aiControlSettings.categoryActions,
autoDetectCompanies: aiControlSettings.autoDetectCompanies,
cleanup: aiControlSettings.cleanup,
categoryAdvanced: aiControlSettings.categoryAdvanced,
version: aiControlSettings.version || 1,
})
savedSettingsRef.current = JSON.parse(JSON.stringify(aiControlSettings))
setHasUnsavedChanges(false)
showMessage('success', 'Control Panel settings saved!')
} catch {
showMessage('error', 'Failed to save settings')
} finally {
setSaving(false)
}
}
// Discard changes
const handleDiscardChanges = () => {
if (savedSettingsRef.current) {
setAiControlSettings(JSON.parse(JSON.stringify(savedSettingsRef.current)))
setHasUnsavedChanges(false)
showMessage('success', 'Changes discarded')
}
}
// Load cleanup status
const loadCleanupStatus = async () => {
const aid = accounts.find((a) => a.provider !== 'demo')?.id
if (!aid) return
try {
const res = await api.getCleanupStatus(aid)
if (res.data) setCleanupStatus(res.data)
} catch {
console.debug('Cleanup status endpoint not available')
}
}
// Load cleanup preview
const loadCleanupPreview = async () => {
if (!aiControlSettings.cleanup?.enabled) return
const aid = accounts.find((a) => a.provider !== 'demo')?.id
if (!aid) return
try {
const res = await api.getCleanupPreview(aid)
if (res.data?.messages) setCleanupPreview(res.data.messages)
} catch {
console.debug('Cleanup preview endpoint not available')
}
}
// Load cleanup status when cleanup tab is active
useEffect(() => {
if (activeTab === 'ai-control' && controlPanelTab === 'cleanup' && aiControlSettings.cleanup?.enabled) {
loadCleanupStatus()
if (aiControlSettings.cleanup.safety.dryRun) {
loadCleanupPreview()
}
}
}, [activeTab, controlPanelTab, aiControlSettings.cleanup?.enabled, aiControlSettings.cleanup?.safety.dryRun, accounts])
// Run cleanup now
const handleRunCleanup = async () => {
if (!user?.$id) return
setRunningCleanup(true)
try {
const res = await api.runCleanup()
if (res.data) {
showMessage('success', `Cleanup completed: ${res.data.emailsProcessed.readItems + res.data.emailsProcessed.promotions} emails processed`)
await loadCleanupStatus()
}
} catch {
showMessage('error', 'Failed to run cleanup')
} finally {
setRunningCleanup(false)
}
}
// Filtered and sorted labels
const filteredLabels = useMemo(() => {
let filtered = companyLabels
// Search filter
if (searchQuery) {
const query = searchQuery.toLowerCase()
filtered = filtered.filter(l =>
l.name.toLowerCase().includes(query) ||
l.condition.toLowerCase().includes(query)
)
}
// Enabled/Disabled filter
if (filterEnabled !== 'all') {
filtered = filtered.filter(l =>
filterEnabled === 'enabled' ? l.enabled : !l.enabled
)
}
// Sort
if (labelSort === 'name') {
filtered = [...filtered].sort((a, b) => a.name.localeCompare(b.name))
} else if (labelSort === 'newest') {
// Reverse order (assuming newer labels are added at the end)
filtered = [...filtered].reverse()
}
return filtered
}, [companyLabels, searchQuery, filterEnabled, labelSort])
// Filtered categories
const filteredCategories = useMemo(() => {
let filtered = categories
if (searchQuery) {
const query = searchQuery.toLowerCase()
filtered = filtered.filter(c =>
c.name.toLowerCase().includes(query) ||
c.description.toLowerCase().includes(query) ||
c.key.toLowerCase().includes(query)
)
}
if (filterEnabled !== 'all') {
filtered = filtered.filter(c =>
filterEnabled === 'enabled' ? c.enabled : !c.enabled
)
}
return filtered
}, [categories, searchQuery, filterEnabled])
const handleSaveProfile = async () => {
if (!user?.$id) return
setSaving(true)
try {
const res = await api.updateProfile({
displayName: name,
timezone,
notificationPrefs: { language },
})
if (res.error) {
showMessage('error', res.error.message || 'Failed to save profile')
return
}
savedProfileRef.current = { name, language, timezone }
setHasProfileChanges(false)
showMessage('success', 'Profile saved successfully!')
} catch {
showMessage('error', 'Failed to save profile')
} finally {
setSaving(false)
}
}
// Track profile changes
useEffect(() => {
if (savedProfileRef.current) {
const hasChanges =
name !== savedProfileRef.current.name ||
language !== savedProfileRef.current.language ||
timezone !== savedProfileRef.current.timezone
setHasProfileChanges(hasChanges)
}
}, [name, language, timezone])
// Initialize saved profile ref
useEffect(() => {
if (user && !savedProfileRef.current) {
savedProfileRef.current = {
name: user.name || '',
language: 'en',
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
}
setName(user.name || '')
}
}, [user])
const handleConnectAccount = async (provider: 'gmail' | 'outlook') => {
if (!user?.$id) return
setConnectingProvider(provider)
try {
const res = await api.getOAuthUrl(provider)
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)
setAccounts(accounts.filter(a => a.id !== accountId))
showMessage('success', 'Account disconnected')
} catch {
showMessage('error', 'Failed to disconnect')
}
}
const handleRecoverEmails = async (accountId: string) => {
if (!user?.$id) return
setRecoveringAccountId(accountId)
try {
const res = await api.recoverEmails(accountId)
if (res.error) {
showMessage('error', res.error.message || 'Recovery failed')
return
}
const data = res.data as { recovered?: number; message?: string } | undefined
const n = data?.recovered ?? 0
const text =
data?.message ||
(n > 0 ? `${n} emails recovered to inbox` : 'No emails found outside inbox')
showMessage('success', text)
} finally {
setRecoveringAccountId(null)
}
}
const handleReSortEmails = async (accountId: string) => {
if (!user?.$id) return
if (
!window.confirm(
'Move messages from Junk, Archive, MailFlow/EmailSorter folders (and similar sort targets) back to INBOX and remove MailFlow category tags? Then run Sort again. Sent/Drafts/Trash are not touched.',
)
) {
return
}
setReSortingAccountId(accountId)
try {
const res = await api.reSortEmails(accountId)
if (res.error) {
showMessage('error', res.error.message || 'Re-sort prep failed')
return
}
const data = res.data as
| { recovered?: number; mailFlowKeywordsStripped?: number; message?: string }
| undefined
showMessage('success', data?.message || 'Re-sort prep completed')
} finally {
setReSortingAccountId(null)
}
}
const handleConnectImap = async (e: React.FormEvent) => {
e.preventDefault()
if (!user?.$id || !imapForm.email.trim() || !imapForm.password) return
setImapConnecting(true)
const res = await api.connectImapAccount({
email: imapForm.email.trim(),
password: imapForm.password,
imapHost: imapForm.imapHost || undefined,
imapPort: imapForm.imapPort || 993,
imapSecure: imapForm.imapSecure,
})
if (res.error) {
const msg = res.error.message || 'Connection failed'
showMessage('error', msg.includes('credentials') || msg.includes('auth') || msg.includes('password') ? 'Login failed check email and password' : msg)
setImapConnecting(false)
return
}
const list = await api.getEmailAccounts()
setAccounts(list.data ?? [])
setShowImapForm(false)
setImapForm({ email: '', password: '', imapHost: 'imap.porkbun.com', imapPort: 993, imapSecure: true })
showMessage('success', 'IMAP account connected')
setImapConnecting(false)
}
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({ vipSenders })
showMessage('success', 'VIP list saved!')
} catch {
showMessage('error', 'Failed to save')
} finally {
setSaving(false)
}
}
const handleManageSubscription = async () => {
if (!user?.$id) return
try {
const res = await api.createPortalSession()
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.email)
if (res.data?.url) {
window.location.href = res.data.url
}
} catch {
showMessage('error', 'Failed to start checkout')
}
}
const tabs = useMemo(() => {
const base = [
{ 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: 'ai-control' as TabType, label: 'Control Panel', icon: Brain },
...(isAdmin ? [{ id: 'name-labels' as TabType, label: 'Name Labels (Team)', icon: Users }] : []),
{ id: 'subscription' as TabType, label: 'Subscription', icon: CreditCard },
{ id: 'privacy' as TabType, label: 'Privacy & Security', icon: Lock },
]
return base
}, [isAdmin])
return (
<div className="min-h-screen bg-slate-50 dark:bg-slate-900">
<header className="bg-white dark:bg-slate-900 border-b border-slate-200 dark:border-slate-700 sticky top-0 z-40">
<div className="w-full 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 dark:text-slate-400" />
<h1 className="text-lg font-semibold text-slate-900 dark:text-slate-100">Settings</h1>
</div>
</div>
</div>
</header>
<main className="w-full 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 dark:bg-green-900/30 text-green-700 dark:text-green-300 border border-green-200 dark:border-green-800'
: 'bg-red-50 dark:bg-red-900/30 text-red-700 dark:text-red-300 border border-red-200 dark:border-red-800'
}`}>
{message.type === 'success' ? <Check className="w-5 h-5 text-green-700 dark:text-green-300" /> : <X className="w-5 h-5 text-red-700 dark:text-red-300" />}
{message.text}
</div>
)}
<div className="flex flex-col lg:flex-row gap-8">
<nav className="lg:w-72 flex-shrink-0">
<div className="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 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 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 border-l-4 border-primary-500 dark:border-primary-400'
: 'text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-700 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 dark:text-primary-400" />
</div>
) : (
<>
{activeTab === 'profile' && (
<div className="space-y-6">
{/* Header with Save Button */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-slate-900 dark:text-slate-100">General Settings</h2>
<p className="text-sm text-slate-500 dark:text-slate-400 mt-1">Manage your account and preferences</p>
</div>
{hasProfileChanges && (
<Button onClick={handleSaveProfile} disabled={saving} size="sm">
{saving ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Save className="w-4 h-4 mr-2" />
)}
Save Changes
</Button>
)}
</div>
{/* Profile Section */}
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<User className="w-5 h-5 text-primary-500 dark:text-primary-400" />
<CardTitle>Profile Information</CardTitle>
</div>
<CardDescription>Your personal account details</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Avatar Section */}
<div className="flex items-start gap-6">
<div className="relative group">
<div className="w-24 h-24 rounded-full bg-gradient-to-br from-primary-400 via-primary-500 to-primary-600 flex items-center justify-center text-white text-3xl font-bold shadow-lg ring-4 ring-white dark:ring-slate-800">
{name?.charAt(0)?.toUpperCase() || email?.charAt(0)?.toUpperCase() || 'U'}
</div>
<button className="absolute bottom-0 right-0 w-8 h-8 rounded-full bg-primary-500 dark:bg-primary-600 text-white flex items-center justify-center shadow-lg hover:bg-primary-600 dark:hover:bg-primary-700 transition-colors opacity-0 group-hover:opacity-100">
<Camera className="w-4 h-4" />
</button>
</div>
<div className="flex-1 space-y-1">
<h3 className="text-xl font-semibold text-slate-900 dark:text-slate-100">
{name || 'User'}
</h3>
<p className="text-slate-500 dark:text-slate-400">{email}</p>
<p className="text-xs text-slate-400 dark:text-slate-500 mt-2">
Member since {user?.$createdAt ? new Date(user.$createdAt).toLocaleDateString('en-US', { month: 'long', year: 'numeric' }) : 'recently'}
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 pt-4 border-t border-slate-200 dark:border-slate-700">
<div>
<Label htmlFor="profile-name" className="flex items-center gap-2">
<Edit2 className="w-4 h-4" />
Full Name
</Label>
<Input
id="profile-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your name"
className="mt-2"
/>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">This is how your name appears to others</p>
</div>
<div>
<Label htmlFor="profile-email" className="flex items-center gap-2">
<Mail className="w-4 h-4" />
Email Address
</Label>
<Input
id="profile-email"
value={email}
disabled
className="mt-2 bg-slate-50 dark:bg-slate-800 cursor-not-allowed"
/>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">Email cannot be changed</p>
</div>
</div>
</CardContent>
</Card>
{/* Preferences Section */}
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<Palette className="w-5 h-5 text-primary-500 dark:text-primary-400" />
<CardTitle>Preferences</CardTitle>
</div>
<CardDescription>Customize your experience</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<Label htmlFor="language" className="flex items-center gap-2 mb-2">
<Globe className="w-4 h-4" />
Language
</Label>
<select
id="language"
value={language}
onChange={(e) => setLanguage(e.target.value)}
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 focus:ring-2 focus:ring-primary-500 focus:border-transparent"
>
<option value="en">English</option>
<option value="de">Deutsch</option>
<option value="fr">Français</option>
<option value="es">Español</option>
<option value="it">Italiano</option>
</select>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">Choose your preferred language</p>
</div>
<div>
<Label htmlFor="timezone" className="flex items-center gap-2 mb-2">
<Clock className="w-4 h-4" />
Timezone
</Label>
<select
id="timezone"
value={timezone}
onChange={(e) => setTimezone(e.target.value)}
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 focus:ring-2 focus:ring-primary-500 focus:border-transparent"
>
<option value="Europe/Berlin">Europe/Berlin (CET/CEST)</option>
<option value="Europe/London">Europe/London (GMT/BST)</option>
<option value="Europe/Paris">Europe/Paris (CET/CEST)</option>
<option value="America/New_York">America/New York (EST/EDT)</option>
<option value="America/Chicago">America/Chicago (CST/CDT)</option>
<option value="America/Denver">America/Denver (MST/MDT)</option>
<option value="America/Los_Angeles">America/Los Angeles (PST/PDT)</option>
<option value="Asia/Tokyo">Asia/Tokyo (JST)</option>
<option value="Asia/Shanghai">Asia/Shanghai (CST)</option>
<option value="Asia/Dubai">Asia/Dubai (GST)</option>
<option value="Australia/Sydney">Australia/Sydney (AEDT/AEST)</option>
<option value="UTC">UTC</option>
</select>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">Set your local timezone</p>
</div>
</div>
</CardContent>
</Card>
{/* Account Information */}
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<Shield className="w-5 h-5 text-primary-500 dark:text-primary-400" />
<CardTitle>Account Information</CardTitle>
</div>
<CardDescription>Your account details and security</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center justify-between p-4 rounded-lg border border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/50">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center">
<Check className="w-5 h-5 text-green-600 dark:text-green-400" />
</div>
<div>
<p className="font-medium text-slate-900 dark:text-slate-100">Account Status</p>
<p className="text-sm text-slate-500 dark:text-slate-400">Active and verified</p>
</div>
</div>
<Badge variant="default" className="bg-green-500 dark:bg-green-600">Active</Badge>
</div>
<div className="flex items-center justify-between p-4 rounded-lg border border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/50">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center">
<Mail className="w-5 h-5 text-blue-600 dark:text-blue-400" />
</div>
<div>
<p className="font-medium text-slate-900 dark:text-slate-100">Email Verification</p>
<p className="text-sm text-slate-500 dark:text-slate-400">{email}</p>
</div>
</div>
<Badge variant="default" className="bg-blue-500 dark:bg-blue-600">Verified</Badge>
</div>
<div className="flex items-center justify-between p-4 rounded-lg border border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/50">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center">
<Clock className="w-5 h-5 text-purple-600 dark:text-purple-400" />
</div>
<div>
<p className="font-medium text-slate-900 dark:text-slate-100">Account Created</p>
<p className="text-sm text-slate-500 dark:text-slate-400">
{user?.$createdAt ? new Date(user.$createdAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
}) : 'Unknown'}
</p>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Sticky Save Bar */}
{hasProfileChanges && (
<div className="sticky bottom-0 z-50 bg-primary-600 dark:bg-primary-700 text-white px-6 py-4 rounded-lg flex items-center justify-between shadow-lg">
<span className="font-medium">You have unsaved changes</span>
<div className="flex items-center gap-3">
<Button
variant="secondary"
onClick={() => {
if (savedProfileRef.current) {
setName(savedProfileRef.current.name)
setLanguage(savedProfileRef.current.language)
setTimezone(savedProfileRef.current.timezone)
setHasProfileChanges(false)
}
}}
disabled={saving}
size="sm"
className="bg-white/20 hover:bg-white/30 text-white border-0"
>
<RotateCcw className="w-4 h-4 mr-2" />
Discard
</Button>
<Button
onClick={handleSaveProfile}
disabled={saving}
size="sm"
className="bg-white text-primary-600 hover:bg-white/90"
>
{saving ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Save className="w-4 h-4 mr-2" />
)}
Save Changes
</Button>
</div>
</div>
)}
</div>
)}
{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 dark:bg-slate-800/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 dark:bg-red-900/50' : account.provider === 'outlook' ? 'bg-blue-100 dark:bg-blue-900/50' : 'bg-slate-100 dark:bg-slate-700/50'
}`}>
<Mail className={`w-6 h-6 ${account.provider === 'gmail' ? 'text-red-600 dark:text-red-400' : account.provider === 'outlook' ? 'text-blue-600 dark:text-blue-400' : 'text-slate-600 dark:text-slate-400'}`} />
</div>
<div>
<p className="font-medium text-slate-900 dark:text-slate-100">{account.email}</p>
<p className="text-sm text-slate-500 dark:text-slate-400 capitalize">{account.provider === 'imap' ? 'IMAP' : account.provider}</p>
</div>
</div>
<div className="flex items-center gap-3 flex-wrap justify-end">
<Badge variant={account.connected ? 'success' : 'secondary'}>
{account.connected ? 'Connected' : 'Disconnected'}
</Badge>
{account.provider === 'imap' && (
<>
<Button
type="button"
variant="outline"
size="sm"
disabled={
recoveringAccountId === account.id ||
reSortingAccountId === account.id
}
onClick={() => handleRecoverEmails(account.id)}
>
{recoveringAccountId === account.id ? (
<Loader2 className="w-4 h-4 animate-spin mr-2" />
) : null}
Recover Emails
</Button>
<Button
type="button"
variant="secondary"
size="sm"
disabled={
recoveringAccountId === account.id ||
reSortingAccountId === account.id
}
onClick={() => handleReSortEmails(account.id)}
title="Reset wrongly sorted mail: move from Junk/Archive/MailFlow folders to INBOX and clear MailFlow tags"
>
{reSortingAccountId === account.id ? (
<Loader2 className="w-4 h-4 animate-spin mr-2" />
) : null}
Re-sort all
</Button>
</>
)}
<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 dark:text-slate-400 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 dark:border-slate-700 rounded-xl hover:border-red-300 dark:hover:border-red-700 hover:bg-red-50 dark:hover:bg-red-900/30 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 dark:text-slate-100">Gmail</p>
<p className="text-sm text-slate-500 dark:text-slate-400">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 dark:border-slate-700 rounded-xl hover:border-blue-300 dark:hover:border-blue-700 hover:bg-blue-50 dark:hover:bg-blue-900/30 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 dark:text-slate-100">Outlook</p>
<p className="text-sm text-slate-500 dark:text-slate-400">Connect Microsoft account</p>
</div>
</button>
<button
type="button"
onClick={() => setShowImapForm(!showImapForm)}
className="flex items-center gap-4 p-4 border-2 border-slate-200 dark:border-slate-700 rounded-xl hover:border-slate-300 dark:hover:border-slate-600 hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-all"
>
<div className="w-12 h-12 rounded-lg bg-slate-100 dark:bg-slate-700 flex items-center justify-center">
<Mail className="w-6 h-6 text-slate-600 dark:text-slate-400" />
</div>
<div className="text-left">
<p className="font-semibold text-slate-900 dark:text-slate-100">IMAP / Other</p>
<p className="text-sm text-slate-500 dark:text-slate-400">Porkbun, Nextcloud Mail, or any IMAP</p>
</div>
</button>
</div>
{showImapForm && (
<form onSubmit={handleConnectImap} className="mt-6 p-4 border border-slate-200 dark:border-slate-700 rounded-xl bg-slate-50 dark:bg-slate-800/50 space-y-4">
<div>
<label htmlFor="imap-email" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Email</label>
<Input
id="imap-email"
type="email"
placeholder="you@example.com"
value={imapForm.email}
onChange={(e) => setImapForm((f) => ({ ...f, email: e.target.value }))}
required
autoComplete="email"
className="bg-white dark:bg-slate-900"
/>
</div>
<div>
<label htmlFor="imap-password" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Password / App password</label>
<Input
id="imap-password"
type="password"
placeholder="••••••••"
value={imapForm.password}
onChange={(e) => setImapForm((f) => ({ ...f, password: e.target.value }))}
required
autoComplete="current-password"
className="bg-white dark:bg-slate-900"
/>
</div>
<details className="text-sm">
<summary className="cursor-pointer text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200">Advanced (host, port, SSL)</summary>
<div className="mt-3 grid grid-cols-1 sm:grid-cols-3 gap-3">
<div>
<label htmlFor="imap-host" className="block text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">IMAP host</label>
<Input
id="imap-host"
type="text"
placeholder="imap.porkbun.com"
value={imapForm.imapHost}
onChange={(e) => setImapForm((f) => ({ ...f, imapHost: e.target.value }))}
className="bg-white dark:bg-slate-900 text-sm"
/>
</div>
<div>
<label htmlFor="imap-port" className="block text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Port</label>
<Input
id="imap-port"
type="number"
min={1}
max={65535}
value={imapForm.imapPort}
onChange={(e) => setImapForm((f) => ({ ...f, imapPort: Number(e.target.value) || 993 }))}
className="bg-white dark:bg-slate-900 text-sm"
/>
</div>
<div className="flex items-end gap-2 pb-2">
<label className="flex items-center gap-2 cursor-pointer text-slate-600 dark:text-slate-400">
<input
type="checkbox"
checked={imapForm.imapSecure}
onChange={(e) => setImapForm((f) => ({ ...f, imapSecure: e.target.checked }))}
className="rounded border-slate-300 dark:border-slate-600"
/>
Use SSL
</label>
</div>
</div>
</details>
<div className="flex gap-2">
<Button type="submit" disabled={imapConnecting}>
{imapConnecting ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : null}
Connect IMAP
</Button>
<Button type="button" variant="outline" onClick={() => { setShowImapForm(false); setImapForm({ email: '', password: '', imapHost: 'imap.porkbun.com', imapPort: 993, imapSecure: true }); }}>
Cancel
</Button>
</div>
</form>
)}
</CardContent>
</Card>
</div>
)}
{activeTab === 'vip' && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="w-5 h-5 text-amber-500 dark:text-amber-400" />
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 dark:bg-amber-900/30 border border-amber-100 dark:border-amber-800 rounded-lg">
<div className="flex items-center gap-3">
<Star className="w-5 h-5 text-amber-500 dark:text-amber-400" />
<span className="text-slate-700 dark:text-slate-300">{vip.email}</span>
</div>
<Button variant="ghost" size="icon" onClick={() => handleRemoveVip(vip.email)}>
<X className="w-4 h-4 text-slate-400 dark:text-slate-500 hover:text-red-500 dark:hover:text-red-400" />
</Button>
</div>
))
) : (
<p className="text-center text-slate-500 dark:text-slate-400 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 === 'ai-control' && (
<div className="space-y-6 w-full">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h2 className="text-2xl font-bold text-slate-900 dark:text-slate-100">Control Panel</h2>
<p className="text-sm text-slate-500 dark:text-slate-400 mt-1">
Configure how AI processes your emails and manages your inbox
</p>
</div>
<div className="flex items-center gap-2">
<Button
variant="secondary"
onClick={handleDiscardChanges}
disabled={!hasUnsavedChanges || saving}
size="sm"
className="flex-1 sm:flex-initial"
>
<RotateCcw className="w-4 h-4 mr-2" />
Reset
</Button>
<Button
onClick={handleSaveControlPanel}
disabled={!hasUnsavedChanges || saving}
size="sm"
className="flex-1 sm:flex-initial"
>
{saving ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Check className="w-4 h-4 mr-2" />}
Save
</Button>
</div>
</div>
{/* Sticky Save Bar */}
{hasUnsavedChanges && (
<div className="sticky top-0 z-50 bg-primary-600 dark:bg-primary-700 text-white px-4 py-3 rounded-lg flex items-center justify-between shadow-lg">
<span className="font-medium">Unsaved changes</span>
<div className="flex items-center gap-2">
<Button
variant="secondary"
onClick={handleDiscardChanges}
disabled={saving}
size="sm"
className="bg-white/20 hover:bg-white/30 text-white border-0"
>
Discard
</Button>
<Button
onClick={handleSaveControlPanel}
disabled={saving}
size="sm"
className="bg-white text-primary-600 hover:bg-white/90"
>
{saving ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Check className="w-4 h-4 mr-2" />}
Save
</Button>
</div>
</div>
)}
{/* Search/Filter Bar */}
<Card>
<CardContent className="py-4">
<div className="flex items-center gap-4">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-slate-400 dark:text-slate-500" />
<Input
placeholder="Search categories or labels..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
/>
</div>
<select
value={filterEnabled}
onChange={(e) => setFilterEnabled((e.target.value || 'all') as 'all' | 'enabled' | 'disabled')}
className="px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100"
>
<option value="all">All</option>
<option value="enabled">Enabled</option>
<option value="disabled">Disabled</option>
</select>
</div>
</CardContent>
</Card>
{/* Tabs */}
<Tabs value={controlPanelTab} onValueChange={(v) => setControlPanelTab((v || 'rules') as 'rules' | 'cleanup' | 'labels')} className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="rules" className="text-sm sm:text-base">Rules</TabsTrigger>
<TabsTrigger value="cleanup" className="text-sm sm:text-base">Cleanup</TabsTrigger>
<TabsTrigger value="labels" className="text-sm sm:text-base">Labels</TabsTrigger>
</TabsList>
{/* Rules Tab */}
<TabsContent value="rules" className="space-y-4 mt-4">
{/* Bulk Actions */}
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100">Category Rules</h3>
<p className="text-sm text-slate-500 dark:text-slate-400 mt-1">Configure how AI handles different email types</p>
</div>
<div className="flex items-center gap-2">
<Button
variant="secondary"
size="sm"
onClick={() => {
const allEnabled = categories.map(c => c.key)
setAiControlSettings({ ...aiControlSettings, enabledCategories: allEnabled })
setCategories(categories.map(c => ({ ...c, enabled: true })))
}}
>
Enable All
</Button>
<Button
variant="secondary"
size="sm"
onClick={() => {
setAiControlSettings({ ...aiControlSettings, enabledCategories: [] })
setCategories(categories.map(c => ({ ...c, enabled: false })))
}}
>
Disable All
</Button>
</div>
</div>
{/* Category Cards Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredCategories.map((category) => {
const action = aiControlSettings.categoryActions[category.key] || category.defaultAction
const actionLabels: Record<string, string> = {
inbox: 'Keep in Inbox',
archive_read: 'Archive & Read',
star: 'Star'
}
return (
<Card
key={category.key}
className="cursor-pointer hover:shadow-md dark:hover:shadow-slate-900/30 transition-all"
onClick={() => {
setSelectedCategory(category)
setShowCategoryPanel(true)
}}
>
<CardContent className="p-5">
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3">
<div
className="w-3 h-3 rounded-full flex-shrink-0"
style={{ backgroundColor: category.color }}
/>
<div className="flex-1 min-w-0">
<h4 className="font-semibold text-slate-900 dark:text-slate-100 truncate">
{category.name}
</h4>
</div>
</div>
<Badge
variant={category.enabled ? 'default' : 'secondary'}
className="ml-2 flex-shrink-0"
>
{category.enabled ? 'Active' : 'Inactive'}
</Badge>
</div>
<p className="text-sm text-slate-600 dark:text-slate-400 mb-4 line-clamp-2">
{category.description}
</p>
{category.enabled && (
<div className="mt-4 pt-4 border-t border-slate-200 dark:border-slate-700">
<div className="flex items-center justify-between">
<span className="text-xs text-slate-500 dark:text-slate-400">Action</span>
<Badge variant="outline" className="text-xs">
{actionLabels[action]}
</Badge>
</div>
</div>
)}
{category.enabled && !aiControlSettings.categoryActions[category.key] && (
<div className="mt-3 text-xs text-amber-600 dark:text-amber-400 flex items-center gap-1">
<AlertTriangle className="w-3 h-3" />
Action required
</div>
)}
</CardContent>
</Card>
)
})}
</div>
</TabsContent>
{/* Cleanup Tab */}
<TabsContent value="cleanup" className="space-y-6 mt-4">
{/* Auto Cleanup Read Emails - Large Toggle Card */}
<Card className="border-slate-200 dark:border-slate-700">
<CardContent className="p-6">
<div className="flex items-start justify-between mb-6">
<div className="flex-1">
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-2">
Auto cleanup read emails
</h3>
<p className="text-sm text-slate-600 dark:text-slate-400">
Automatically clean up emails that have been read after a grace period
</p>
</div>
<button
onClick={() => {
if (!aiControlSettings.cleanup) return
setAiControlSettings({
...aiControlSettings,
cleanup: {
...aiControlSettings.cleanup,
readItems: {
...aiControlSettings.cleanup.readItems,
enabled: !aiControlSettings.cleanup.readItems.enabled,
},
},
})
}}
className={`w-12 h-6 rounded-full transition-colors flex-shrink-0 ${aiControlSettings.cleanup?.readItems.enabled ? 'bg-primary-500 dark:bg-primary-600' : 'bg-slate-300 dark:bg-slate-600'}`}
>
<div className={`w-5 h-5 bg-white dark:bg-slate-200 rounded-full transform transition-transform mx-0.5 ${
aiControlSettings.cleanup?.readItems.enabled ? 'translate-x-6' : 'translate-x-0'
}`} />
</button>
</div>
{aiControlSettings.cleanup?.readItems.enabled && (
<div className="space-y-6 pt-6 border-t border-slate-200 dark:border-slate-700">
{/* Action Selection */}
<div>
<Label htmlFor="read-items-action" className="text-sm font-medium mb-3 block">
Action
</Label>
<select
id="read-items-action"
value={aiControlSettings.cleanup.readItems.action}
onChange={(e) => {
if (!aiControlSettings.cleanup) return
setAiControlSettings({
...aiControlSettings,
cleanup: {
...aiControlSettings.cleanup,
readItems: {
...aiControlSettings.cleanup.readItems,
action: e.target.value as 'archive_read' | 'trash',
},
},
})
}}
className="w-full px-4 py-2.5 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"
>
<option value="archive_read">Archive & Mark Read</option>
<option value="trash">Move to Trash</option>
</select>
</div>
{/* Slider for Days */}
<div>
<div className="flex items-center justify-between mb-3">
<Label className="text-sm font-medium">Grace Period</Label>
<span className="text-sm font-semibold text-slate-900 dark:text-slate-100">
{aiControlSettings.cleanup.readItems.gracePeriodDays} {aiControlSettings.cleanup.readItems.gracePeriodDays === 1 ? 'day' : 'days'}
</span>
</div>
<Slider
value={aiControlSettings.cleanup.readItems.gracePeriodDays}
onValueChange={(value) => {
if (!aiControlSettings.cleanup) return
setAiControlSettings({
...aiControlSettings,
cleanup: {
...aiControlSettings.cleanup,
readItems: {
...aiControlSettings.cleanup.readItems,
gracePeriodDays: value,
},
},
})
}}
min={0}
max={90}
step={1}
/>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-2">
Emails will be cleaned up after this many days
</p>
</div>
{/* Warning Banner */}
{aiControlSettings.cleanup.readItems.action === 'trash' && (
<div className="flex items-start gap-3 p-4 bg-amber-50/50 dark:bg-amber-900/10 border border-amber-200/50 dark:border-amber-800/50 rounded-lg">
<AlertTriangle className="w-5 h-5 text-amber-600 dark:text-amber-400 mt-0.5 flex-shrink-0" />
<div>
<p className="text-sm font-medium text-amber-900 dark:text-amber-100">Delete action selected</p>
<p className="text-xs text-amber-700 dark:text-amber-300 mt-1">
Emails moved to trash can be permanently deleted. Use with caution.
</p>
</div>
</div>
)}
</div>
)}
</CardContent>
</Card>
{/* Delete Promotions After X Days - Large Toggle Card */}
<Card className="border-slate-200 dark:border-slate-700">
<CardContent className="p-6">
<div className="flex items-start justify-between mb-6">
<div className="flex-1">
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-2">
Delete promotions after X days
</h3>
<p className="text-sm text-slate-600 dark:text-slate-400">
Automatically clean up promotional emails and newsletters after a set period
</p>
</div>
<button
onClick={() => {
if (!aiControlSettings.cleanup) return
setAiControlSettings({
...aiControlSettings,
cleanup: {
...aiControlSettings.cleanup,
promotions: {
...aiControlSettings.cleanup.promotions,
enabled: !aiControlSettings.cleanup.promotions.enabled,
},
},
})
}}
className={`w-12 h-6 rounded-full transition-colors flex-shrink-0 ${aiControlSettings.cleanup?.promotions?.enabled ? 'bg-primary-500 dark:bg-primary-600' : 'bg-slate-300 dark:bg-slate-600'}`}
>
<div className={`w-5 h-5 bg-white dark:bg-slate-200 rounded-full transform transition-transform mx-0.5 ${
aiControlSettings.cleanup?.promotions.enabled ? 'translate-x-6' : 'translate-x-0'
}`} />
</button>
</div>
{aiControlSettings.cleanup?.promotions.enabled && (
<div className="space-y-6 pt-6 border-t border-slate-200 dark:border-slate-700">
{/* Action Selection */}
<div>
<Label htmlFor="promotions-action" className="text-sm font-medium mb-3 block">
Action
</Label>
<select
id="promotions-action"
value={aiControlSettings.cleanup.promotions.action}
onChange={(e) => {
if (!aiControlSettings.cleanup) return
setAiControlSettings({
...aiControlSettings,
cleanup: {
...aiControlSettings.cleanup,
promotions: {
...aiControlSettings.cleanup.promotions,
action: e.target.value as 'archive_read' | 'trash',
},
},
})
}}
className="w-full px-4 py-2.5 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"
>
<option value="archive_read">Archive & Mark Read</option>
<option value="trash">Move to Trash</option>
</select>
</div>
{/* Preset Buttons */}
<div>
<Label className="text-sm font-medium mb-3 block">Delete/Archive After</Label>
<div className="flex items-center gap-3 mb-4">
{[7, 14, 30].map((days) => (
<Button
key={days}
variant={aiControlSettings.cleanup?.promotions.deleteAfterDays === days ? 'default' : 'outline'}
size="sm"
onClick={() => {
if (!aiControlSettings.cleanup) return
setAiControlSettings({
...aiControlSettings,
cleanup: {
...aiControlSettings.cleanup,
promotions: {
...aiControlSettings.cleanup.promotions,
deleteAfterDays: days,
},
},
})
}}
className="flex-1"
>
{days} days
</Button>
))}
</div>
<div className="flex items-center justify-between">
<Slider
value={aiControlSettings.cleanup.promotions.deleteAfterDays}
onValueChange={(value) => {
if (!aiControlSettings.cleanup) return
setAiControlSettings({
...aiControlSettings,
cleanup: {
...aiControlSettings.cleanup,
promotions: {
...aiControlSettings.cleanup.promotions,
deleteAfterDays: value,
},
},
})
}}
min={1}
max={90}
step={1}
className="flex-1 mr-4"
/>
<span className="text-sm font-semibold text-slate-900 dark:text-slate-100 min-w-[60px] text-right">
{aiControlSettings.cleanup.promotions.deleteAfterDays} {aiControlSettings.cleanup.promotions.deleteAfterDays === 1 ? 'day' : 'days'}
</span>
</div>
</div>
{/* Match Categories */}
<div>
<Label className="text-sm font-medium mb-3 block">Match Categories</Label>
<div className="flex flex-wrap gap-3">
{categories.filter(c => ['promotions', 'newsletters', 'social'].includes(c.key)).map((category) => (
<button
key={category.key}
onClick={() => {
if (!aiControlSettings.cleanup) return
const current = aiControlSettings.cleanup.promotions.matchCategoriesOrLabels
const updated = current.includes(category.key)
? current.filter(c => c !== category.key)
: [...current, category.key]
setAiControlSettings({
...aiControlSettings,
cleanup: {
...aiControlSettings.cleanup,
promotions: {
...aiControlSettings.cleanup.promotions,
matchCategoriesOrLabels: updated,
},
},
})
}}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors flex items-center gap-2 ${
aiControlSettings.cleanup?.promotions?.matchCategoriesOrLabels?.includes(category.key)
? 'bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 border border-primary-300 dark:border-primary-700'
: 'bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 hover:bg-slate-200 dark:hover:bg-slate-700'
}`}
>
<div className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: category.color }} />
{category.name}
</button>
))}
</div>
</div>
{/* Warning Banner */}
{aiControlSettings.cleanup.promotions.action === 'trash' && (
<div className="flex items-start gap-3 p-4 bg-amber-50/50 dark:bg-amber-900/10 border border-amber-200/50 dark:border-amber-800/50 rounded-lg">
<AlertTriangle className="w-5 h-5 text-amber-600 dark:text-amber-400 mt-0.5 flex-shrink-0" />
<div>
<p className="text-sm font-medium text-amber-900 dark:text-amber-100">Delete action selected</p>
<p className="text-xs text-amber-700 dark:text-amber-300 mt-1">
Promotional emails will be permanently deleted. Use with caution.
</p>
</div>
</div>
)}
</div>
)}
</CardContent>
</Card>
{/* Preview Section */}
{((cleanupPreview && cleanupPreview.length > 0) || cleanupStatus) && (
<Card className="border-slate-200 dark:border-slate-700">
<CardContent className="p-6">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-1 flex items-center gap-2">
<Eye className="w-5 h-5 text-primary-500 dark:text-primary-400" />
Preview
</h3>
<p className="text-sm text-slate-600 dark:text-slate-400">
{(cleanupPreview && cleanupPreview.length > 0)
? `${cleanupPreview.length} emails affected in the last 7 days`
: cleanupStatus?.lastRun
? `Last run: ${new Date(cleanupStatus.lastRun).toLocaleDateString()}`
: 'No preview data available'}
</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={loadCleanupPreview}
disabled={runningCleanup}
>
{runningCleanup ? <Loader2 className="w-4 h-4 animate-spin" /> : <Eye className="w-4 h-4" />}
Refresh
</Button>
</div>
{cleanupPreview && cleanupPreview.length > 0 && (
<div className="space-y-2 max-h-64 overflow-y-auto">
{cleanupPreview.slice(0, 10).map((item) => (
<div key={item.id} className="p-3 rounded-lg border border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/50">
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">{item.subject}</p>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">{item.from} {new Date(item.date).toLocaleDateString()}</p>
<Badge variant="secondary" className="mt-2 text-xs">{item.reason}</Badge>
</div>
))}
</div>
)}
</CardContent>
</Card>
)}
{/* Run Cleanup Now */}
<Card className="border-slate-200 dark:border-slate-700">
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-1">Run Cleanup Now</h3>
<p className="text-sm text-slate-600 dark:text-slate-400">
Manually trigger cleanup for your account
</p>
{cleanupStatus?.lastRun && (
<p className="text-xs text-slate-500 dark:text-slate-400 mt-2">
Last run: {new Date(cleanupStatus.lastRun).toLocaleString()}
{cleanupStatus.lastRunCounts && (
<span className="ml-2">
({cleanupStatus.lastRunCounts.readItems + cleanupStatus.lastRunCounts.promotions} emails processed)
</span>
)}
</p>
)}
</div>
<Button
onClick={handleRunCleanup}
disabled={runningCleanup}
variant="secondary"
size="lg"
>
{runningCleanup ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Play className="w-4 h-4 mr-2" />}
Run Now
</Button>
</div>
</CardContent>
</Card>
</TabsContent>
{/* Labels Tab */}
<TabsContent value="labels" className="space-y-6 mt-4">
{/* Header with Actions */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-slate-100">Company Labels</h3>
<p className="text-sm text-slate-600 dark:text-slate-400 mt-1">Automatically label emails from specific companies</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<select
value={labelSort}
onChange={(e) => setLabelSort((e.target.value || 'name') as 'name' | 'newest')}
className="px-3 py-2 text-sm border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100"
>
<option value="name">Sort: Name</option>
<option value="newest">Sort: Newest</option>
</select>
<Button
variant="secondary"
size="sm"
onClick={() => {
const dataStr = JSON.stringify(companyLabels, null, 2)
const dataBlob = new Blob([dataStr], { type: 'application/json' })
const url = URL.createObjectURL(dataBlob)
const link = document.createElement('a')
link.href = url
link.download = 'company-labels.json'
link.click()
URL.revokeObjectURL(url)
}}
>
<Download className="w-4 h-4 mr-2" />
Export
</Button>
<Button
variant="secondary"
size="sm"
onClick={() => {
setLabelImportErrors([])
const input = document.createElement('input')
input.type = 'file'
input.accept = 'application/json'
input.onchange = async (e) => {
const file = (e.target as HTMLInputElement).files?.[0]
if (!file) return
try {
const text = await file.text()
const imported = JSON.parse(text)
const { labels, errors } = validateLabelImport(imported, companyLabels)
if (errors.length > 0) {
setLabelImportErrors(errors)
showMessage('error', 'Fix import errors before saving')
return
}
setLabelImportErrors([])
setCompanyLabels([...companyLabels, ...labels])
showMessage('success', `Imported ${labels.length} labels`)
} catch {
showMessage('error', 'Invalid JSON file')
setLabelImportErrors([])
}
}
input.click()
}}
>
<Upload className="w-4 h-4 mr-2" />
Import
</Button>
<Button
size="sm"
onClick={() => {
setEditingLabel({ id: '', name: '', condition: '', category: 'promotions', enabled: true })
setShowLabelPanel(true)
}}
>
<Plus className="w-4 h-4 mr-2" />
Add Label
</Button>
</div>
{labelImportErrors.length > 0 && (
<div className="mt-3 w-full rounded-md border border-red-200 dark:border-red-900 bg-red-50 dark:bg-red-950/40 p-3 text-sm text-red-800 dark:text-red-200">
<p className="font-medium mb-1">Import issues</p>
<ul className="list-disc pl-5 space-y-0.5">
{labelImportErrors.map((err, i) => (
<li key={i}>{err}</li>
))}
</ul>
</div>
)}
</div>
{/* Auto-Detection Toggle */}
<Card className="border-slate-200 dark:border-slate-700">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-slate-900 dark:text-slate-100">Auto-Detect Known Companies</p>
<p className="text-sm text-slate-500 dark:text-slate-400 mt-1">Automatically detect and label emails from Amazon, Google, Microsoft, etc.</p>
</div>
<button
onClick={() => {
const newValue = !aiControlSettings.autoDetectCompanies
setAiControlSettings({ ...aiControlSettings, autoDetectCompanies: newValue })
}}
className={`w-12 h-6 rounded-full transition-colors ${aiControlSettings.autoDetectCompanies ? 'bg-primary-500 dark:bg-primary-600' : 'bg-slate-300 dark:bg-slate-600'}`}
>
<div className={`w-5 h-5 bg-white dark:bg-slate-200 rounded-full transform transition-transform mx-0.5 ${
aiControlSettings.autoDetectCompanies ? 'translate-x-6' : 'translate-x-0'
}`} />
</button>
</div>
</CardContent>
</Card>
{/* Labels Table */}
<Card className="border-slate-200 dark:border-slate-700">
<CardContent className="p-0">
{filteredLabels.length > 0 ? (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-slate-200 dark:border-slate-700">
<th className="px-4 sm:px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">Name</th>
<th className="px-4 sm:px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider hidden sm:table-cell">Status</th>
<th className="px-4 sm:px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider hidden md:table-cell">Category</th>
<th className="px-4 sm:px-6 py-3 text-right text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-200 dark:divide-slate-700">
{filteredLabels.map((label) => (
<tr
key={label.id}
className="hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors"
>
<td className="px-4 sm:px-6 py-4">
<div>
<p className="font-medium text-slate-900 dark:text-slate-100">{label.name}</p>
<p className="text-xs text-slate-500 dark:text-slate-400 font-mono mt-1 break-all">{label.condition}</p>
<div className="flex items-center gap-2 mt-2 sm:hidden">
<Badge variant={label.enabled ? 'default' : 'secondary'} className="text-xs">
{label.enabled ? 'Enabled' : 'Disabled'}
</Badge>
{label.category && (
<Badge variant="outline" className="text-xs capitalize">{label.category}</Badge>
)}
</div>
</div>
</td>
<td className="px-4 sm:px-6 py-4 hidden sm:table-cell">
<Badge variant={label.enabled ? 'default' : 'secondary'}>
{label.enabled ? 'Enabled' : 'Disabled'}
</Badge>
</td>
<td className="px-4 sm:px-6 py-4 hidden md:table-cell">
{label.category && (
<Badge variant="outline" className="text-xs capitalize">{label.category}</Badge>
)}
</td>
<td className="px-4 sm:px-6 py-4">
<div className="flex items-center justify-end gap-1 sm:gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => {
setEditingLabel(label)
setShowLabelPanel(true)
}}
className="hidden sm:inline-flex"
>
<Edit2 className="w-4 h-4 mr-1" />
Edit
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => {
setEditingLabel(label)
setShowLabelPanel(true)
}}
className="sm:hidden"
>
<Edit2 className="w-4 h-4" />
</Button>
<button
onClick={async () => {
if (!user?.$id || !label.id) return
try {
await api.saveCompanyLabel({ ...label, enabled: !label.enabled })
setCompanyLabels(companyLabels.map(l => l.id === label.id ? { ...l, enabled: !l.enabled } : l))
showMessage('success', 'Label updated!')
} catch {
showMessage('error', 'Failed to update label')
}
}}
className={`w-10 h-6 rounded-full transition-colors ${label.enabled ? 'bg-primary-500 dark:bg-primary-600' : 'bg-slate-300 dark:bg-slate-600'}`}
title={label.enabled ? 'Disable' : 'Enable'}
>
<div className={`w-4 h-4 bg-white dark:bg-slate-200 rounded-full transform transition-transform mx-1 ${
label.enabled ? 'translate-x-4' : 'translate-x-0'
}`} />
</button>
<Button
variant="ghost"
size="icon"
onClick={async () => {
if (!user?.$id || !label.id) return
if (!confirm('Are you sure you want to delete this label?')) return
try {
await api.deleteCompanyLabel(label.id)
setCompanyLabels(companyLabels.filter(l => l.id !== label.id))
showMessage('success', 'Label deleted!')
} catch {
showMessage('error', 'Failed to delete label')
}
}}
>
<Trash2 className="w-4 h-4 text-red-500 dark:text-red-400" />
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="text-center py-12 text-slate-500 dark:text-slate-400">
<Building2 className="w-12 h-12 mx-auto mb-3 opacity-50" />
<p className="font-medium">{searchQuery ? 'No labels match your search' : 'No labels yet'}</p>
<p className="text-sm mt-1">{searchQuery ? 'Try adjusting your search query' : 'Create your first company label to get started'}</p>
{!searchQuery && (
<Button
variant="secondary"
size="sm"
className="mt-4"
onClick={() => {
setEditingLabel({ id: '', name: '', condition: '', category: 'promotions', enabled: true })
setShowLabelPanel(true)
}}
>
<Plus className="w-4 h-4 mr-2" />
Add Label
</Button>
)}
</div>
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
{/* Category Details Side Panel */}
<SidePanel open={showCategoryPanel} onOpenChange={setShowCategoryPanel}>
<SidePanelContent>
<SidePanelHeader>
<SidePanelCloseButton />
{selectedCategory && (
<>
<div className="flex items-center gap-3 mb-2">
<div
className="w-4 h-4 rounded-full"
style={{ backgroundColor: selectedCategory.color }}
/>
<SidePanelTitle>{selectedCategory.name}</SidePanelTitle>
</div>
<SidePanelDescription>{selectedCategory.description}</SidePanelDescription>
</>
)}
</SidePanelHeader>
{selectedCategory && (
<SidePanelBody>
<div className="space-y-6">
{/* Enable/Disable Toggle */}
<div className="flex items-center justify-between p-4 rounded-lg border border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/50">
<div>
<Label className="text-base font-medium">Enable Category</Label>
<p className="text-sm text-slate-500 dark:text-slate-400 mt-1">
Process emails in this category
</p>
</div>
<button
onClick={() => {
const newEnabled = selectedCategory.enabled
? aiControlSettings.enabledCategories.filter(c => c !== selectedCategory.key)
: [...aiControlSettings.enabledCategories, selectedCategory.key]
setAiControlSettings({ ...aiControlSettings, enabledCategories: newEnabled })
setCategories(categories.map(c => c.key === selectedCategory.key ? { ...c, enabled: !c.enabled } : c))
setSelectedCategory({ ...selectedCategory, enabled: !selectedCategory.enabled })
}}
className={`w-10 h-6 rounded-full transition-colors ${selectedCategory.enabled ? 'bg-primary-500 dark:bg-primary-600' : 'bg-slate-300 dark:bg-slate-600'}`}
>
<div className={`w-4 h-4 bg-white dark:bg-slate-200 rounded-full transform transition-transform mx-1 ${
selectedCategory.enabled ? 'translate-x-4' : 'translate-x-0'
}`} />
</button>
</div>
{/* Action Selection */}
{selectedCategory.enabled && (
<div>
<Label htmlFor="category-action" className="text-base font-medium mb-2 block">
What should happen with these emails?
</Label>
<select
id="category-action"
value={aiControlSettings.categoryActions[selectedCategory.key] || selectedCategory.defaultAction}
onChange={(e) => {
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"
>
<option value="inbox">Keep in Inbox</option>
<option value="archive_read">Archive & Mark Read</option>
<option value="star">Star</option>
</select>
{!aiControlSettings.categoryActions[selectedCategory.key] && (
<div className="mt-2 text-xs text-amber-600 dark:text-amber-400 flex items-center gap-1">
<AlertTriangle className="w-3 h-3" />
Please select an action
</div>
)}
</div>
)}
{/* Advanced Options */}
{selectedCategory.enabled && (
<div>
<button
onClick={() => {
const newSet = new Set(expandedCategories)
if (expandedCategories.has(selectedCategory.key)) {
newSet.delete(selectedCategory.key)
} else {
newSet.add(selectedCategory.key)
}
setExpandedCategories(newSet)
}}
className="flex items-center gap-2 text-sm font-medium text-slate-700 dark:text-slate-300 hover:text-slate-900 dark:hover:text-slate-100 mb-3"
>
{expandedCategories.has(selectedCategory.key) ? <ChevronUp className="w-4 h-4 text-slate-600 dark:text-slate-400" /> : <ChevronDown className="w-4 h-4 text-slate-600 dark:text-slate-400" />}
Advanced Options
</button>
{expandedCategories.has(selectedCategory.key) && (
<div className="mt-3 space-y-4 p-4 rounded-lg border border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/50">
<div>
<Label htmlFor="category-priority">Priority</Label>
<select
id="category-priority"
value={aiControlSettings.categoryAdvanced?.[selectedCategory.key]?.priority || 'medium'}
onChange={(e) => {
const newAdvanced = { ...aiControlSettings.categoryAdvanced }
if (!newAdvanced[selectedCategory.key]) newAdvanced[selectedCategory.key] = {}
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"
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
<div>
<Label htmlFor="category-exclude-keywords">Exclude Keywords</Label>
<Input
id="category-exclude-keywords"
value={(aiControlSettings.categoryAdvanced?.[selectedCategory.key]?.excludeKeywords || []).join(', ')}
onChange={(e) => {
const keywords = e.target.value.split(',').map(k => k.trim()).filter(k => k)
const newAdvanced = { ...aiControlSettings.categoryAdvanced }
if (!newAdvanced[selectedCategory.key]) newAdvanced[selectedCategory.key] = {}
newAdvanced[selectedCategory.key] = { ...newAdvanced[selectedCategory.key], excludeKeywords: keywords }
setAiControlSettings({ ...aiControlSettings, categoryAdvanced: newAdvanced })
}}
placeholder="spam, test, noreply"
className="mt-1"
/>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">Comma-separated keywords to exclude</p>
</div>
</div>
)}
</div>
)}
</div>
</SidePanelBody>
)}
</SidePanelContent>
</SidePanel>
{/* Label Editor Side Panel */}
<SidePanel open={showLabelPanel} onOpenChange={setShowLabelPanel}>
<SidePanelContent>
<SidePanelHeader>
<SidePanelCloseButton />
<SidePanelTitle>
{editingLabel?.id ? 'Edit Label' : 'Add New Label'}
</SidePanelTitle>
<SidePanelDescription>
{editingLabel?.id
? 'Update the company label settings'
: 'Create a new company label to automatically categorize emails'}
</SidePanelDescription>
</SidePanelHeader>
{editingLabel && (
<SidePanelBody>
<div className="space-y-6">
{/* Company Name */}
<div>
<Label htmlFor="label-name" className="text-base font-medium mb-2 block">
Company Name
</Label>
<Input
id="label-name"
placeholder="e.g., Amazon"
value={editingLabel.name}
onChange={(e) => setEditingLabel({ ...editingLabel, name: e.target.value })}
className="w-full"
/>
</div>
{/* Condition */}
<div>
<Label htmlFor="label-condition" className="text-base font-medium mb-2 block">
Condition
</Label>
<Input
id="label-condition"
placeholder="e.g., from:amazon.com OR from:amazon.de"
value={editingLabel.condition}
onChange={(e) => setEditingLabel({ ...editingLabel, condition: e.target.value })}
className="w-full font-mono text-sm"
/>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-2">
Use "from:domain.com" or "subject:keyword" syntax
</p>
</div>
{/* Category */}
<div>
<Label htmlFor="label-category" className="text-base font-medium mb-2 block">
Category
</Label>
<select
id="label-category"
value={editingLabel.category || 'promotions'}
onChange={(e) => setEditingLabel({ ...editingLabel, category: e.target.value })}
className="w-full px-4 py-2.5 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"
>
<option value="promotions">Promotions</option>
<option value="newsletters">Newsletter</option>
<option value="invoices">Invoices</option>
<option value="customers">Clients</option>
</select>
</div>
{/* Enabled Toggle */}
<div className="flex items-center justify-between p-4 rounded-lg border border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/50">
<div>
<Label className="text-base font-medium">Enabled</Label>
<p className="text-sm text-slate-500 dark:text-slate-400 mt-1">
This label will be active and applied to matching emails
</p>
</div>
<button
onClick={() => setEditingLabel({ ...editingLabel, enabled: !editingLabel.enabled })}
className={`w-12 h-6 rounded-full transition-colors ${editingLabel.enabled ? 'bg-primary-500 dark:bg-primary-600' : 'bg-slate-300 dark:bg-slate-600'}`}
>
<div className={`w-5 h-5 bg-white dark:bg-slate-200 rounded-full transform transition-transform mx-0.5 ${
editingLabel.enabled ? 'translate-x-6' : 'translate-x-0'
}`} />
</button>
</div>
</div>
</SidePanelBody>
)}
<SidePanelFooter>
<Button
variant="secondary"
onClick={() => {
setShowLabelPanel(false)
setEditingLabel(null)
}}
>
Cancel
</Button>
<Button
onClick={async () => {
if (!user?.$id || !editingLabel?.name || !editingLabel?.condition) {
showMessage('error', 'Please fill in all required fields')
return
}
try {
const saved = await api.saveCompanyLabel(editingLabel)
if (saved.data) {
if (editingLabel.id) {
setCompanyLabels(companyLabels.map(l => l.id === editingLabel.id ? (saved.data || l) : l))
showMessage('success', 'Label updated!')
} else {
setCompanyLabels([...companyLabels, saved.data])
showMessage('success', 'Label created!')
}
setShowLabelPanel(false)
setEditingLabel(null)
}
} catch {
showMessage('error', editingLabel.id ? 'Failed to update label' : 'Failed to create label')
}
}}
>
{editingLabel?.id ? 'Save Changes' : 'Create Label'}
</Button>
</SidePanelFooter>
</SidePanelContent>
</SidePanel>
</div>
)}
{activeTab === 'name-labels' && isAdmin && (
<div className="space-y-6">
<Card className="border-amber-200 dark:border-amber-900">
<CardHeader>
<div className="flex items-center gap-2">
<Shield className="w-5 h-5 text-amber-600 dark:text-amber-400" />
<CardTitle>Reset sort data (admin)</CardTitle>
</div>
<CardDescription>
Clears email stats, digests, usage, and onboarding progress for the chosen user. Removes the{' '}
<code className="text-xs">$MailFlow-sorted</code> flag from all messages in each IMAP account&apos;s INBOX.
Does not remove email connections or subscriptions.
</CardDescription>
</CardHeader>
<CardContent>
<Button
type="button"
variant="destructive"
disabled={resettingSort}
onClick={async () => {
const email = window.prompt('User email to reset:', 'support@webklar.com')
if (email == null) return
const trimmed = email.trim()
if (!trimmed) {
showMessage('error', 'Email is required')
return
}
if (
!window.confirm(
`Reset ALL sort-related data for ${trimmed}? This cannot be undone.`,
)
) {
return
}
setResettingSort(true)
try {
const res = await api.resetSortData(trimmed)
if (res.error) {
showMessage('error', res.error.message)
return
}
const d = res.data
showMessage(
'success',
`Reset OK. Stats/digests/usage cleared; IMAP $MailFlow-sorted removed from ${d?.imapCleared ?? 0} message(s).`,
)
} finally {
setResettingSort(false)
}
}}
>
{resettingSort ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Resetting
</>
) : (
'Reset sort data'
)}
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<Users className="w-5 h-5 text-primary-500 dark:text-primary-400" />
<CardTitle>Name Labels (Team)</CardTitle>
</div>
<CardDescription>
Personal labels for each team member. The AI will assign emails to a worker when they are clearly for that person (e.g. &quot;für Max&quot;, &quot;an Anna&quot;, subject/body mentions).
</CardDescription>
</CardHeader>
<CardContent>
{nameLabels.length > 0 ? (
<div className="space-y-3">
{nameLabels.map((label) => (
<div
key={label.id || label.name}
className="flex items-center justify-between p-4 rounded-lg border border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/50"
>
<div>
<p className="font-medium text-slate-900 dark:text-slate-100">{label.name}</p>
{label.email && (
<p className="text-sm text-slate-500 dark:text-slate-400">{label.email}</p>
)}
{label.keywords?.length ? (
<p className="text-xs text-slate-400 dark:text-slate-500 mt-1">
Keywords: {label.keywords.join(', ')}
</p>
) : null}
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => {
setEditingNameLabel({ ...label })
setShowNameLabelPanel(true)
}}
>
<Edit2 className="w-4 h-4 mr-1" />
Edit
</Button>
<button
onClick={async () => {
if (!user?.$id || !label.id) return
try {
await api.saveNameLabel({ ...label, enabled: !label.enabled })
setNameLabels(nameLabels.map(l => l.id === label.id ? { ...l, enabled: !l.enabled } : l))
showMessage('success', 'Label updated!')
} catch {
showMessage('error', 'Failed to update label')
}
}}
className={`w-10 h-6 rounded-full transition-colors ${label.enabled ? 'bg-primary-500 dark:bg-primary-600' : 'bg-slate-300 dark:bg-slate-600'}`}
title={label.enabled ? 'Disable' : 'Enable'}
>
<div className={`w-4 h-4 bg-white dark:bg-slate-200 rounded-full transform transition-transform mx-1 ${
label.enabled ? 'translate-x-4' : 'translate-x-0'
}`} />
</button>
<Button
variant="ghost"
size="icon"
onClick={async () => {
if (!user?.$id || !label.id) return
if (!confirm('Delete this name label?')) return
try {
await api.deleteNameLabel(label.id)
setNameLabels(nameLabels.filter(l => l.id !== label.id))
showMessage('success', 'Label deleted!')
} catch {
showMessage('error', 'Failed to delete label')
}
}}
>
<Trash2 className="w-4 h-4 text-red-500 dark:text-red-400" />
</Button>
</div>
</div>
))}
<Button
variant="secondary"
size="sm"
onClick={() => {
setEditingNameLabel({ name: '', enabled: true })
setShowNameLabelPanel(true)
}}
>
<Plus className="w-4 h-4 mr-2" />
Add team member
</Button>
</div>
) : (
<div className="text-center py-12 text-slate-500 dark:text-slate-400">
<Users className="w-12 h-12 mx-auto mb-3 opacity-50" />
<p className="font-medium">No name labels yet</p>
<p className="text-sm mt-1">Add team members so the AI can assign emails to the right person</p>
<Button
variant="secondary"
size="sm"
className="mt-4"
onClick={() => {
setEditingNameLabel({ name: '', enabled: true })
setShowNameLabelPanel(true)
}}
>
<Plus className="w-4 h-4 mr-2" />
Add team member
</Button>
</div>
)}
</CardContent>
</Card>
{/* Name Label Editor Side Panel */}
<SidePanel open={showNameLabelPanel} onOpenChange={setShowNameLabelPanel}>
<SidePanelContent>
<SidePanelHeader>
<SidePanelCloseButton />
<SidePanelTitle>
{editingNameLabel?.id ? 'Edit Name Label' : 'Add Team Member'}
</SidePanelTitle>
<SidePanelDescription>
{editingNameLabel?.id
? 'Update the name label'
: 'Add a team member. The AI will assign emails to this person when they are clearly for them (e.g. &quot;für Max&quot;, subject mentions).'}
</SidePanelDescription>
</SidePanelHeader>
{editingNameLabel && (
<SidePanelBody>
<div className="space-y-6">
<div>
<Label htmlFor="namelabel-name">Name</Label>
<Input
id="namelabel-name"
placeholder="e.g. Max, Anna"
value={editingNameLabel.name}
onChange={(e) => setEditingNameLabel({ ...editingNameLabel, name: e.target.value })}
className="mt-2"
/>
</div>
<div>
<Label htmlFor="namelabel-email">Email (optional)</Label>
<Input
id="namelabel-email"
type="email"
placeholder="max@company.com"
value={editingNameLabel.email || ''}
onChange={(e) => setEditingNameLabel({ ...editingNameLabel, email: e.target.value || undefined })}
className="mt-2"
/>
</div>
<div>
<Label htmlFor="namelabel-keywords">Keywords (optional, comma-separated)</Label>
<Input
id="namelabel-keywords"
placeholder="für Max, an Max, Max bitte"
value={(editingNameLabel.keywords || []).join(', ')}
onChange={(e) => setEditingNameLabel({
...editingNameLabel,
keywords: e.target.value.split(',').map(k => k.trim()).filter(Boolean),
})}
className="mt-2"
/>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">Hints for the AI to recognize emails for this person</p>
</div>
<div className="flex items-center justify-between p-4 rounded-lg border border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/50">
<div>
<Label className="font-medium">Enabled</Label>
<p className="text-sm text-slate-500 dark:text-slate-400">This label will be used when sorting</p>
</div>
<button
onClick={() => setEditingNameLabel({ ...editingNameLabel, enabled: !editingNameLabel.enabled })}
className={`w-12 h-6 rounded-full transition-colors ${editingNameLabel.enabled ? 'bg-primary-500 dark:bg-primary-600' : 'bg-slate-300 dark:bg-slate-600'}`}
>
<div className={`w-5 h-5 bg-white dark:bg-slate-200 rounded-full transform transition-transform mx-0.5 ${
editingNameLabel.enabled ? 'translate-x-6' : 'translate-x-0'
}`} />
</button>
</div>
</div>
</SidePanelBody>
)}
<SidePanelFooter>
<Button variant="secondary" onClick={() => { setShowNameLabelPanel(false); setEditingNameLabel(null) }}>
Cancel
</Button>
<Button
onClick={async () => {
if (!user?.$id || !editingNameLabel?.name?.trim()) {
showMessage('error', 'Please enter a name')
return
}
try {
const saved = await api.saveNameLabel(editingNameLabel)
if (saved.data) {
if (editingNameLabel.id) {
setNameLabels(nameLabels.map(l => l.id === editingNameLabel.id ? (saved.data || l) : l))
showMessage('success', 'Label updated!')
} else {
setNameLabels([...nameLabels, saved.data])
showMessage('success', 'Label created!')
}
setShowNameLabelPanel(false)
setEditingNameLabel(null)
}
} catch {
showMessage('error', editingNameLabel.id ? 'Failed to update' : 'Failed to create')
}
}}
>
{editingNameLabel?.id ? 'Save Changes' : 'Add'}
</Button>
</SidePanelFooter>
</SidePanelContent>
</SidePanel>
</div>
)}
{activeTab === 'referrals' && (
<Card>
<CardHeader>
<CardTitle>Referrals</CardTitle>
<CardDescription>
Share MailFlow and earn rewards
</CardDescription>
</CardHeader>
<CardContent>
{loadingReferral ? (
<div className="text-center py-8">
<Loader2 className="w-6 h-6 animate-spin mx-auto" />
</div>
) : (
<div className="space-y-4">
<div className="p-4 bg-gradient-to-r from-primary-50 to-accent-50 dark:from-primary-900/30 dark:to-accent-900/30 rounded-lg border border-primary-200 dark:border-primary-800">
<p className="text-sm font-semibold text-slate-900 dark:text-slate-100 mb-2">Your referral code</p>
<div className="flex items-center gap-2">
<code className="flex-1 px-3 py-2 bg-white dark:bg-slate-800 border border-primary-200 dark:border-primary-700 rounded-lg font-mono text-lg font-bold text-primary-600 dark:text-primary-400">
{referralData?.referralCode || 'USER-XXXXXX'}
</code>
<Button
size="sm"
onClick={async () => {
const url = `${window.location.origin}?ref=${referralData?.referralCode}`
await navigator.clipboard.writeText(url)
showMessage('success', 'Referral link copied!')
}}
>
<Copy className="w-4 h-4 mr-2" />
Copy link
</Button>
</div>
</div>
<div className="p-4 bg-slate-50 dark:bg-slate-800/50 rounded-lg">
<p className="text-sm text-slate-600 dark:text-slate-400 mb-1">People referred</p>
<p className="text-2xl font-bold text-slate-900 dark:text-slate-100">{referralData?.referralCount || 0}</p>
</div>
</div>
)}
</CardContent>
</Card>
)}
{activeTab === 'privacy' && (
<Card>
<CardHeader>
<CardTitle>Privacy & Security</CardTitle>
<CardDescription>
Understand what data we access, what we store, and how to manage your privacy
</CardDescription>
</CardHeader>
<CardContent>
<PrivacySecurity
connectedAccounts={accounts.map(a => ({ id: a.id, email: a.email, provider: a.provider }))}
onDisconnect={async (accountId) => {
if (!user?.$id) return
try {
const result = await api.disconnectEmailAccount(accountId)
if (result.data) {
setAccounts(accounts.filter(a => a.id !== accountId))
showMessage('success', 'Account disconnected')
}
} catch {
showMessage('error', 'Failed to disconnect account')
}
}}
onDeleteAccount={async () => {
if (!user?.$id) return
if (!confirm('Are you absolutely sure? This cannot be undone.')) return
try {
const result = await api.deleteAccount()
if (result.data) {
showMessage('success', 'Account deleted. Redirecting...')
setTimeout(() => {
window.location.href = '/'
}, 2000)
}
} catch {
showMessage('error', 'Failed to delete account')
}
}}
/>
</CardContent>
</Card>
)}
{activeTab === 'subscription' && (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Current Subscription</CardTitle>
<CardDescription>Manage your MailFlow subscription</CardDescription>
</CardHeader>
<CardContent>
{loading ? (
<p className="text-sm text-slate-500 dark:text-slate-400 py-6">
Loading subscription
</p>
) : !subscription ? (
<div className="space-y-3 py-2">
<p className="text-sm text-slate-600 dark:text-slate-400">
Subscription status could not be loaded. Make sure you are signed in and the API is running.
</p>
<Button
variant="outline"
size="sm"
onClick={async () => {
const r = await api.getSubscriptionStatus()
if (r.data) {
setSubscription(r.data)
showMessage('success', 'Subscription loaded')
} else {
showMessage('error', r.error?.message || 'Failed to load subscription')
}
}}
>
Retry
</Button>
</div>
) : (
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 p-4 bg-gradient-to-r from-primary-50 to-accent-50 dark:from-primary-900/30 dark:to-accent-900/30 rounded-xl border border-primary-100 dark:border-primary-800">
<div className="flex items-center gap-4">
<div className="w-14 h-14 rounded-xl bg-white dark:bg-slate-800 shadow-sm flex items-center justify-center shrink-0">
<Crown className="w-7 h-7 text-primary-500 dark:text-primary-400" />
</div>
<div>
<div className="flex flex-wrap items-center gap-2">
<h3 className="font-bold text-lg text-slate-900 dark:text-slate-100">
{subscriptionTitle(subscription)}
</h3>
{(() => {
const b = subscriptionBadge(subscription)
return b.label ? (
<Badge variant={b.variant}>{b.label}</Badge>
) : null
})()}
</div>
{subscription.currentPeriodEnd && (
<p className="text-sm text-slate-500 dark:text-slate-400 mt-1">
Next billing:{' '}
{new Date(subscription.currentPeriodEnd).toLocaleDateString('en-US')}
</p>
)}
</div>
</div>
<Button className="shrink-0" 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 dark:border-primary-600 bg-primary-50 dark:bg-primary-900/30' : 'border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800'
}`}>
{plan.popular && (
<div className="absolute -top-3 left-1/2 -translate-x-1/2">
<Badge className="bg-primary-500 dark:bg-primary-600 text-white">Popular</Badge>
</div>
)}
<h3 className="font-bold text-lg text-slate-900 dark:text-slate-100">{plan.name}</h3>
<div className="mt-2 mb-4">
<span className="text-3xl font-bold text-slate-900 dark:text-slate-100">${plan.price}</span>
<span className="text-slate-500 dark:text-slate-400">/month</span>
</div>
<ul className="space-y-2 mb-6">
{plan.features.map((feature, fi) => (
<li key={`${plan.id}-${fi}`} className="flex items-center gap-2 text-sm text-slate-600 dark:text-slate-400">
<Check className="w-4 h-4 text-green-500 dark:text-green-400" />
{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>
)
}