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:
593
client/src/pages/Settings.tsx
Normal file
593
client/src/pages/Settings.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user