feat: Ultimate plan switcher in settings
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
This commit is contained in:
@ -22,12 +22,93 @@ import {
|
||||
TrendingUp,
|
||||
X,
|
||||
Settings,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
Sparkles,
|
||||
Clock,
|
||||
Target,
|
||||
BarChart3,
|
||||
MessageSquare,
|
||||
Database,
|
||||
} from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import clsx from 'clsx'
|
||||
|
||||
type SettingsTab = 'profile' | 'notifications' | 'billing' | 'security'
|
||||
|
||||
// Plan definitions
|
||||
const PLANS = [
|
||||
{
|
||||
id: 'scout',
|
||||
name: 'Scout',
|
||||
icon: Zap,
|
||||
price: 0,
|
||||
period: 'forever',
|
||||
description: 'Perfect for getting started',
|
||||
color: 'white',
|
||||
features: [
|
||||
{ icon: Target, text: '5 Watchlist Domains' },
|
||||
{ icon: Clock, text: 'Daily Checks' },
|
||||
{ icon: Bell, text: '2 Sniper Alerts' },
|
||||
{ icon: BarChart3, text: 'Basic Market Data' },
|
||||
],
|
||||
limits: {
|
||||
domains: 5,
|
||||
alerts: 2,
|
||||
listings: 0,
|
||||
portfolio: 0,
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'trader',
|
||||
name: 'Trader',
|
||||
icon: TrendingUp,
|
||||
price: 9,
|
||||
period: '/month',
|
||||
description: 'For serious domain investors',
|
||||
color: 'accent',
|
||||
badge: 'Popular',
|
||||
features: [
|
||||
{ icon: Target, text: '50 Watchlist Domains' },
|
||||
{ icon: Clock, text: 'Hourly Checks' },
|
||||
{ icon: Bell, text: '10 Sniper Alerts' },
|
||||
{ icon: Sparkles, text: 'Domain Valuation' },
|
||||
{ icon: Database, text: '25 Portfolio Domains' },
|
||||
{ icon: MessageSquare, text: '5 Marketplace Listings' },
|
||||
],
|
||||
limits: {
|
||||
domains: 50,
|
||||
alerts: 10,
|
||||
listings: 5,
|
||||
portfolio: 25,
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'tycoon',
|
||||
name: 'Tycoon',
|
||||
icon: Crown,
|
||||
price: 29,
|
||||
period: '/month',
|
||||
description: 'Maximum power & control',
|
||||
color: 'amber',
|
||||
badge: 'Full Power',
|
||||
features: [
|
||||
{ icon: Target, text: '500 Watchlist Domains' },
|
||||
{ icon: Clock, text: '10-Minute Checks' },
|
||||
{ icon: Bell, text: '50 Sniper Alerts' },
|
||||
{ icon: Sparkles, text: 'Full SEO Metrics' },
|
||||
{ icon: Database, text: 'Unlimited Portfolio' },
|
||||
{ icon: MessageSquare, text: '50 Featured Listings' },
|
||||
],
|
||||
limits: {
|
||||
domains: 500,
|
||||
alerts: 50,
|
||||
listings: 50,
|
||||
portfolio: -1,
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
export default function SettingsPage() {
|
||||
const router = useRouter()
|
||||
const { user, isAuthenticated, isLoading, checkAuth, subscription } = useStore()
|
||||
@ -52,6 +133,7 @@ export default function SettingsPage() {
|
||||
const [priceAlerts, setPriceAlerts] = useState<PriceAlert[]>([])
|
||||
const [loadingAlerts, setLoadingAlerts] = useState(false)
|
||||
const [deletingAlertId, setDeletingAlertId] = useState<number | null>(null)
|
||||
const [changingPlan, setChangingPlan] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
checkAuth()
|
||||
@ -153,6 +235,52 @@ export default function SettingsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const handlePlanChange = async (planId: string) => {
|
||||
setChangingPlan(planId)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
if (planId === 'scout') {
|
||||
// Downgrade to free - cancel subscription
|
||||
await api.cancelSubscription()
|
||||
setSuccess('Downgraded to Scout. Your premium features will remain active until the end of your billing period.')
|
||||
await checkAuth()
|
||||
} else {
|
||||
// Upgrade to paid plan
|
||||
const successUrl = `${window.location.origin}/terminal/settings?upgraded=${planId}`
|
||||
const cancelUrl = `${window.location.origin}/terminal/settings?cancelled=true`
|
||||
const { checkout_url } = await api.createCheckoutSession(planId, successUrl, cancelUrl)
|
||||
window.location.href = checkout_url
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to change plan')
|
||||
} finally {
|
||||
setChangingPlan(null)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle URL params for upgrade success/cancel
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const upgraded = params.get('upgraded')
|
||||
const cancelled = params.get('cancelled')
|
||||
|
||||
if (upgraded) {
|
||||
setSuccess(`🎉 Welcome to ${upgraded.charAt(0).toUpperCase() + upgraded.slice(1)}! Your account has been upgraded.`)
|
||||
setActiveTab('billing')
|
||||
// Clean URL
|
||||
window.history.replaceState({}, '', '/terminal/settings')
|
||||
// Refresh subscription data
|
||||
checkAuth()
|
||||
}
|
||||
|
||||
if (cancelled) {
|
||||
setError('Checkout was cancelled. No changes were made.')
|
||||
setActiveTab('billing')
|
||||
window.history.replaceState({}, '', '/terminal/settings')
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-[#020202]">
|
||||
@ -429,85 +557,208 @@ export default function SettingsPage() {
|
||||
{/* Billing Tab */}
|
||||
{activeTab === 'billing' && (
|
||||
<div className="space-y-6">
|
||||
{/* Plan Switcher */}
|
||||
<div className="border border-white/[0.08] bg-white/[0.01] p-6">
|
||||
<h2 className="text-sm font-mono text-white/40 tracking-wide mb-6">Current Plan</h2>
|
||||
|
||||
<div className="p-5 bg-accent/5 border border-accent/20 mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{tierName === 'Tycoon' ? <Crown className="w-6 h-6 text-accent" /> :
|
||||
tierName === 'Trader' ? <TrendingUp className="w-6 h-6 text-accent" /> :
|
||||
<Zap className="w-6 h-6 text-accent" />}
|
||||
<div>
|
||||
<p className="text-lg font-display text-white">{tierName}</p>
|
||||
<p className="text-xs text-white/40">
|
||||
{tierName === 'Scout' ? 'Free forever' : tierName === 'Trader' ? '$9/month' : '$29/month'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className={clsx(
|
||||
"px-2 py-1 text-[10px] font-mono",
|
||||
isProOrHigher ? "bg-accent/10 text-accent" : "bg-white/5 text-white/40"
|
||||
)}>
|
||||
{isProOrHigher ? 'Active' : 'Free'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Plan Stats */}
|
||||
<div className="grid grid-cols-3 gap-4 p-4 bg-black/30 mb-4">
|
||||
<div className="text-center">
|
||||
<p className="text-xl font-display text-white">{subscription?.domain_limit || 5}</p>
|
||||
<p className="text-[10px] text-white/40 font-mono">Domains</p>
|
||||
</div>
|
||||
<div className="text-center border-x border-white/10">
|
||||
<p className="text-xl font-display text-white">
|
||||
{subscription?.check_frequency === 'realtime' ? '10m' :
|
||||
subscription?.check_frequency === 'hourly' ? '1h' : '24h'}
|
||||
</p>
|
||||
<p className="text-[10px] text-white/40 font-mono">Interval</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-xl font-display text-white">
|
||||
{subscription?.portfolio_limit === -1 ? '∞' : subscription?.portfolio_limit || 0}
|
||||
</p>
|
||||
<p className="text-[10px] text-white/40 font-mono">Portfolio</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isProOrHigher ? (
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-sm font-mono text-white/40 tracking-wide">Choose Your Plan</h2>
|
||||
{isProOrHigher && (
|
||||
<button
|
||||
onClick={handleOpenBillingPortal}
|
||||
className="w-full flex items-center justify-center gap-2 py-3 bg-black/50 text-white text-sm font-medium border border-white/10 hover:border-white/20 transition-all"
|
||||
className="flex items-center gap-2 px-3 py-1.5 text-[10px] font-mono text-white/50 border border-white/10 hover:border-white/20 hover:text-white transition-all"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
Manage Subscription
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
Manage Billing
|
||||
</button>
|
||||
) : (
|
||||
<Link
|
||||
href="/pricing"
|
||||
className="w-full flex items-center justify-center gap-2 py-3 bg-accent text-black text-sm font-semibold hover:bg-white transition-all"
|
||||
>
|
||||
<Zap className="w-4 h-4" />
|
||||
Upgrade Plan
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Plan Features */}
|
||||
<h3 className="text-xs font-mono text-white/40 mb-3">Includes</h3>
|
||||
<ul className="grid grid-cols-2 gap-2">
|
||||
{[
|
||||
`${subscription?.domain_limit || 5} Watchlist Domains`,
|
||||
`${subscription?.check_frequency === 'realtime' ? '10-minute' : subscription?.check_frequency === 'hourly' ? 'Hourly' : 'Daily'} Scans`,
|
||||
'Email Alerts',
|
||||
'TLD Price Data',
|
||||
].map((feature) => (
|
||||
<li key={feature} className="flex items-center gap-2 text-xs">
|
||||
<Check className="w-3 h-3 text-accent" />
|
||||
<span className="text-white/60">{feature}</span>
|
||||
{/* Plan Cards */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
{PLANS.map((plan) => {
|
||||
const isCurrent = tierName.toLowerCase() === plan.id
|
||||
const isUpgrade =
|
||||
(tierName === 'Scout' && (plan.id === 'trader' || plan.id === 'tycoon')) ||
|
||||
(tierName === 'Trader' && plan.id === 'tycoon')
|
||||
const isDowngrade =
|
||||
(tierName === 'Tycoon' && (plan.id === 'trader' || plan.id === 'scout')) ||
|
||||
(tierName === 'Trader' && plan.id === 'scout')
|
||||
|
||||
return (
|
||||
<div
|
||||
key={plan.id}
|
||||
className={clsx(
|
||||
"relative p-5 border transition-all",
|
||||
isCurrent
|
||||
? "border-accent bg-accent/5"
|
||||
: "border-white/[0.08] bg-white/[0.01] hover:border-white/20"
|
||||
)}
|
||||
>
|
||||
{/* Badge */}
|
||||
{plan.badge && (
|
||||
<div className={clsx(
|
||||
"absolute -top-2.5 left-4 px-2 py-0.5 text-[9px] font-mono uppercase tracking-wider",
|
||||
plan.id === 'trader' ? "bg-accent text-black" : "bg-amber-500 text-black"
|
||||
)}>
|
||||
{plan.badge}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Current Badge */}
|
||||
{isCurrent && (
|
||||
<div className="absolute -top-2.5 right-4 px-2 py-0.5 text-[9px] font-mono uppercase tracking-wider bg-white text-black">
|
||||
Current
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3 mb-4 mt-2">
|
||||
<div className={clsx(
|
||||
"w-10 h-10 flex items-center justify-center border",
|
||||
plan.id === 'tycoon' ? "border-amber-500/30 bg-amber-500/10" :
|
||||
plan.id === 'trader' ? "border-accent/30 bg-accent/10" :
|
||||
"border-white/10 bg-white/5"
|
||||
)}>
|
||||
<plan.icon className={clsx(
|
||||
"w-5 h-5",
|
||||
plan.id === 'tycoon' ? "text-amber-400" :
|
||||
plan.id === 'trader' ? "text-accent" :
|
||||
"text-white/60"
|
||||
)} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-display text-white">{plan.name}</h3>
|
||||
<p className="text-[10px] text-white/40">{plan.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Price */}
|
||||
<div className="flex items-baseline gap-1 mb-5">
|
||||
{plan.price === 0 ? (
|
||||
<span className="text-3xl font-mono text-white">Free</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-3xl font-mono text-white">${plan.price}</span>
|
||||
<span className="text-sm text-white/40">{plan.period}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Features */}
|
||||
<ul className="space-y-2 mb-5">
|
||||
{plan.features.map((feature, idx) => (
|
||||
<li key={idx} className="flex items-center gap-2">
|
||||
<feature.icon className="w-3.5 h-3.5 text-accent shrink-0" />
|
||||
<span className="text-xs text-white/70">{feature.text}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* Action Button */}
|
||||
{isCurrent ? (
|
||||
<div className="w-full py-3 text-center text-sm font-mono text-accent border border-accent/30 bg-accent/5">
|
||||
<Check className="w-4 h-4 inline mr-2" />
|
||||
Active Plan
|
||||
</div>
|
||||
) : isUpgrade ? (
|
||||
<button
|
||||
onClick={() => handlePlanChange(plan.id)}
|
||||
disabled={changingPlan === plan.id}
|
||||
className="w-full flex items-center justify-center gap-2 py-3 bg-accent text-black text-sm font-semibold hover:bg-white disabled:opacity-50 transition-all"
|
||||
>
|
||||
{changingPlan === plan.id ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<ArrowUp className="w-4 h-4" />
|
||||
Upgrade to {plan.name}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
) : isDowngrade ? (
|
||||
<button
|
||||
onClick={() => handlePlanChange(plan.id)}
|
||||
disabled={changingPlan === plan.id}
|
||||
className="w-full flex items-center justify-center gap-2 py-3 text-white/60 text-sm font-medium border border-white/10 hover:border-white/30 hover:text-white disabled:opacity-50 transition-all"
|
||||
>
|
||||
{changingPlan === plan.id ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<ArrowDown className="w-4 h-4" />
|
||||
Downgrade
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Usage Stats */}
|
||||
<div className="border border-white/[0.08] bg-white/[0.01] p-6">
|
||||
<h2 className="text-sm font-mono text-white/40 tracking-wide mb-4">Current Usage</h2>
|
||||
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="p-4 bg-white/[0.02] border border-white/[0.06]">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[10px] font-mono text-white/40">Watchlist</span>
|
||||
<Target className="w-3.5 h-3.5 text-accent" />
|
||||
</div>
|
||||
<p className="text-xl font-mono text-white">
|
||||
{subscription?.domains_used || 0}
|
||||
<span className="text-white/30">/{subscription?.domain_limit || 5}</span>
|
||||
</p>
|
||||
<div className="mt-2 h-1 bg-white/10 overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-accent transition-all"
|
||||
style={{ width: `${Math.min(100, ((subscription?.domains_used || 0) / (subscription?.domain_limit || 5)) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-white/[0.02] border border-white/[0.06]">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[10px] font-mono text-white/40">Check Speed</span>
|
||||
<Clock className="w-3.5 h-3.5 text-accent" />
|
||||
</div>
|
||||
<p className="text-xl font-mono text-white">
|
||||
{subscription?.check_frequency === 'realtime' ? '10m' :
|
||||
subscription?.check_frequency === 'hourly' ? '1h' : '24h'}
|
||||
</p>
|
||||
<p className="text-[10px] text-white/30 mt-1">Interval</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-white/[0.02] border border-white/[0.06]">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[10px] font-mono text-white/40">Portfolio</span>
|
||||
<Database className="w-3.5 h-3.5 text-accent" />
|
||||
</div>
|
||||
<p className="text-xl font-mono text-white">
|
||||
{subscription?.portfolio_limit === -1 ? '∞' : subscription?.portfolio_limit || 0}
|
||||
</p>
|
||||
<p className="text-[10px] text-white/30 mt-1">Slots</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-white/[0.02] border border-white/[0.06]">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[10px] font-mono text-white/40">Status</span>
|
||||
<Shield className="w-3.5 h-3.5 text-accent" />
|
||||
</div>
|
||||
<p className="text-xl font-mono text-accent capitalize">
|
||||
{subscription?.status || 'active'}
|
||||
</p>
|
||||
<p className="text-[10px] text-white/30 mt-1">Subscription</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Help Text */}
|
||||
<div className="text-center py-4">
|
||||
<p className="text-xs text-white/30">
|
||||
Need help? <Link href="/contact" className="text-accent hover:underline">Contact support</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user