refactor: Consistent styling across all Command Center pages

CHANGES:
- All pages now use PageContainer (max-w-7xl) for consistent width
- Unified StatCard component usage across all pages
- All tables now use PremiumTable with consistent styling
- Consistent filter/tab design across pages
- Updated SectionHeader with compact mode option
- Consistent card styling with backdrop blur effects
- Unified form input styling across all pages
- Settings page redesigned with consistent card layouts

PAGES UPDATED:
- Dashboard: PageContainer, SectionHeader, StatCard integration
- Watchlist: Full redesign with PremiumTable
- Portfolio: Full redesign with PremiumTable
- Settings: Consistent card styling and layout
- Auctions & Intelligence: Already using consistent components
This commit is contained in:
yves.gugger
2025-12-10 10:34:23 +01:00
parent b66c3b360d
commit 20f43dbc8d
5 changed files with 1012 additions and 1290 deletions

View File

@ -5,6 +5,7 @@ import { useSearchParams } from 'next/navigation'
import { useStore } from '@/lib/store' import { useStore } from '@/lib/store'
import { api } from '@/lib/api' import { api } from '@/lib/api'
import { CommandCenterLayout } from '@/components/CommandCenterLayout' import { CommandCenterLayout } from '@/components/CommandCenterLayout'
import { PremiumTable, StatCard, PageContainer, Badge, SectionHeader } from '@/components/PremiumTable'
import { Toast, useToast } from '@/components/Toast' import { Toast, useToast } from '@/components/Toast'
import { import {
Eye, Eye,
@ -13,7 +14,6 @@ import {
Gavel, Gavel,
Clock, Clock,
Bell, Bell,
ArrowRight,
ExternalLink, ExternalLink,
Sparkles, Sparkles,
ChevronRight, ChevronRight,
@ -59,9 +59,6 @@ export default function DashboardPage() {
const [quickDomain, setQuickDomain] = useState('') const [quickDomain, setQuickDomain] = useState('')
const [addingDomain, setAddingDomain] = useState(false) const [addingDomain, setAddingDomain] = useState(false)
// Note: checkAuth is called in CommandCenterLayout, no need to duplicate here
// Auth redirect is also handled by CommandCenterLayout
// Check for upgrade success // Check for upgrade success
useEffect(() => { useEffect(() => {
if (searchParams.get('upgraded') === 'true') { if (searchParams.get('upgraded') === 'true') {
@ -131,37 +128,39 @@ export default function DashboardPage() {
> >
{toast && <Toast message={toast.message} type={toast.type} onClose={hideToast} />} {toast && <Toast message={toast.message} type={toast.type} onClose={hideToast} />}
<div className="max-w-7xl mx-auto space-y-8"> <PageContainer>
{/* Quick Add */} {/* Quick Add */}
<div className="relative p-6 bg-gradient-to-r from-accent/10 via-accent/5 to-transparent border border-accent/20 rounded-2xl overflow-hidden"> <div className="relative p-5 sm:p-6 bg-gradient-to-r from-accent/10 via-accent/5 to-transparent border border-accent/20 rounded-2xl overflow-hidden">
<div className="absolute top-0 right-0 w-64 h-64 bg-accent/5 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2" /> <div className="absolute top-0 right-0 w-64 h-64 bg-accent/5 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2" />
<div className="relative"> <div className="relative">
<h2 className="text-lg font-semibold text-foreground mb-4 flex items-center gap-2"> <h2 className="text-base font-semibold text-foreground mb-4 flex items-center gap-2">
<div className="w-8 h-8 bg-accent/20 rounded-lg flex items-center justify-center"> <div className="w-8 h-8 bg-accent/20 rounded-lg flex items-center justify-center">
<Search className="w-4 h-4 text-accent" /> <Search className="w-4 h-4 text-accent" />
</div> </div>
Quick Add to Watchlist Quick Add to Watchlist
</h2> </h2>
<form onSubmit={handleQuickAdd} className="flex gap-3"> <form onSubmit={handleQuickAdd} className="flex flex-col sm:flex-row gap-3">
<input <div className="relative flex-1">
type="text" <Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-foreground-subtle" />
value={quickDomain} <input
onChange={(e) => setQuickDomain(e.target.value)} type="text"
placeholder="Enter domain to track (e.g., dream.com)" value={quickDomain}
className="flex-1 h-12 px-5 bg-background/80 backdrop-blur-sm border border-border/50 rounded-xl onChange={(e) => setQuickDomain(e.target.value)}
text-foreground placeholder:text-foreground-subtle placeholder="Enter domain to track (e.g., dream.com)"
focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/30 className="w-full h-11 pl-11 pr-4 bg-background/80 backdrop-blur-sm border border-border/50 rounded-xl
shadow-[0_2px_8px_-2px_rgba(0,0,0,0.1)]" text-sm text-foreground placeholder:text-foreground-subtle
/> focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/30"
/>
</div>
<button <button
type="submit" type="submit"
disabled={addingDomain || !quickDomain.trim()} disabled={addingDomain || !quickDomain.trim()}
className="flex items-center gap-2 h-12 px-6 bg-gradient-to-r from-accent to-accent/80 text-background rounded-xl className="flex items-center justify-center gap-2 h-11 px-6 bg-gradient-to-r from-accent to-accent/80 text-background rounded-xl
font-medium hover:shadow-[0_0_20px_-5px_rgba(16,185,129,0.4)] transition-all font-medium hover:shadow-[0_0_20px_-5px_rgba(16,185,129,0.4)] transition-all
disabled:opacity-50 disabled:cursor-not-allowed" disabled:opacity-50 disabled:cursor-not-allowed"
> >
<Plus className="w-4 h-4" /> <Plus className="w-4 h-4" />
Add <span>Add</span>
</button> </button>
</form> </form>
</div> </div>
@ -169,269 +168,219 @@ export default function DashboardPage() {
{/* Stats Overview */} {/* Stats Overview */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<Link <Link href="/watchlist" className="group">
href="/watchlist" <StatCard
className="group relative p-5 bg-gradient-to-br from-background-secondary/60 to-background-secondary/30 border border-border/50 rounded-2xl title="Domains Watched"
hover:border-accent/30 transition-all duration-300 overflow-hidden" value={totalDomains}
> icon={Eye}
<div className="absolute inset-0 bg-accent/5 opacity-0 group-hover:opacity-100 transition-opacity" /> />
<div className="relative">
<div className="flex items-start justify-between mb-4">
<div className="w-11 h-11 bg-foreground/5 border border-border/30 rounded-xl flex items-center justify-center group-hover:bg-accent/10 group-hover:border-accent/20 transition-all">
<Eye className="w-5 h-5 text-foreground-muted group-hover:text-accent transition-colors" />
</div>
<ChevronRight className="w-5 h-5 text-foreground-subtle group-hover:text-accent
group-hover:translate-x-0.5 transition-all" />
</div>
<p className="text-3xl font-display text-foreground mb-1">{totalDomains}</p>
<p className="text-sm text-foreground-muted">Domains Watched</p>
</div>
</Link> </Link>
<Link href="/watchlist?filter=available" className="group">
<Link <StatCard
href="/watchlist?filter=available" title="Available Now"
className={clsx( value={availableDomains.length}
"group relative p-5 border rounded-2xl transition-all duration-300 overflow-hidden", icon={Sparkles}
availableDomains.length > 0 accent={availableDomains.length > 0}
? "bg-gradient-to-br from-accent/15 to-accent/5 border-accent/30 hover:border-accent/50 shadow-[0_0_30px_-10px_rgba(16,185,129,0.2)]" />
: "bg-gradient-to-br from-background-secondary/60 to-background-secondary/30 border-border/50 hover:border-accent/30"
)}
>
{availableDomains.length > 0 && <div className="absolute inset-0 bg-accent/5 animate-pulse" />}
<div className="relative">
<div className="flex items-start justify-between mb-4">
<div className={clsx(
"w-11 h-11 rounded-xl flex items-center justify-center border transition-all",
availableDomains.length > 0 ? "bg-accent/20 border-accent/30" : "bg-foreground/5 border-border/30"
)}>
<Sparkles className={clsx(
"w-5 h-5",
availableDomains.length > 0 ? "text-accent" : "text-foreground-muted"
)} />
</div>
{availableDomains.length > 0 && (
<span className="px-2.5 py-1 bg-accent text-background text-xs font-bold rounded-lg shadow-[0_0_10px_rgba(16,185,129,0.3)]">
POUNCE!
</span>
)}
</div>
<p className={clsx(
"text-3xl font-display mb-1",
availableDomains.length > 0 ? "text-accent" : "text-foreground"
)}>
{availableDomains.length}
</p>
<p className="text-sm text-foreground-muted">Available Now</p>
</div>
</Link> </Link>
<Link href="/portfolio" className="group">
<Link <StatCard
href="/portfolio" title="Portfolio"
className="group relative p-5 bg-gradient-to-br from-background-secondary/60 to-background-secondary/30 border border-border/50 rounded-2xl value={0}
hover:border-accent/30 transition-all duration-300 overflow-hidden" icon={Briefcase}
> />
<div className="absolute inset-0 bg-accent/5 opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="relative">
<div className="flex items-start justify-between mb-4">
<div className="w-11 h-11 bg-foreground/5 border border-border/30 rounded-xl flex items-center justify-center group-hover:bg-accent/10 group-hover:border-accent/20 transition-all">
<Briefcase className="w-5 h-5 text-foreground-muted group-hover:text-accent transition-colors" />
</div>
<ChevronRight className="w-5 h-5 text-foreground-subtle group-hover:text-accent
group-hover:translate-x-0.5 transition-all" />
</div>
<p className="text-3xl font-display text-foreground mb-1">0</p>
<p className="text-sm text-foreground-muted">Portfolio Domains</p>
</div>
</Link> </Link>
<StatCard
<div className="relative p-5 bg-gradient-to-br from-background-secondary/60 to-background-secondary/30 border border-border/50 rounded-2xl overflow-hidden"> title="Plan"
<div className="absolute top-0 right-0 w-24 h-24 bg-accent/10 rounded-full blur-2xl -translate-y-1/2 translate-x-1/2" /> value={tierName}
<div className="relative"> subtitle={`${subscription?.domains_used || 0}/${subscription?.domain_limit || 5} slots`}
<div className="flex items-start justify-between mb-4"> icon={TierIcon}
<div className="w-11 h-11 bg-accent/10 border border-accent/20 rounded-xl flex items-center justify-center"> />
<TierIcon className="w-5 h-5 text-accent" />
</div>
</div>
<p className="text-3xl font-display text-foreground mb-1">{tierName}</p>
<p className="text-sm text-foreground-muted">
{subscription?.domains_used || 0}/{subscription?.domain_limit || 5} slots used
</p>
</div>
</div>
</div> </div>
{/* Activity Feed + Market Pulse */} {/* Activity Feed + Market Pulse */}
<div className="grid lg:grid-cols-2 gap-6"> <div className="grid lg:grid-cols-2 gap-6">
{/* Activity Feed */} {/* Activity Feed */}
<div className="p-6 bg-gradient-to-br from-background-secondary/60 to-background-secondary/30 border border-border/50 rounded-2xl"> <div className="relative overflow-hidden rounded-2xl border border-border/40 bg-gradient-to-b from-background-secondary/40 to-background-secondary/20 backdrop-blur-sm">
<div className="flex items-center justify-between mb-6"> <div className="p-5 border-b border-border/30">
<h2 className="text-lg font-semibold text-foreground flex items-center gap-3"> <SectionHeader
<div className="w-9 h-9 bg-accent/10 border border-accent/20 rounded-lg flex items-center justify-center"> title="Activity Feed"
<Activity className="w-4 h-4 text-accent" /> icon={Activity}
</div> compact
Activity Feed action={
</h2> <Link href="/watchlist" className="text-sm text-accent hover:text-accent/80 transition-colors">
<Link href="/watchlist" className="text-sm text-accent hover:text-accent/80 transition-colors"> View all
View all </Link>
</Link> }
/>
</div> </div>
<div className="p-5">
{availableDomains.length > 0 ? ( {availableDomains.length > 0 ? (
<div className="space-y-3"> <div className="space-y-3">
{availableDomains.slice(0, 4).map((domain) => ( {availableDomains.slice(0, 4).map((domain) => (
<div <div
key={domain.id} key={domain.id}
className="flex items-center gap-4 p-3 bg-accent/5 border border-accent/20 rounded-xl" className="flex items-center gap-4 p-3 bg-accent/5 border border-accent/20 rounded-xl"
>
<div className="relative">
<span className="w-3 h-3 bg-accent rounded-full block" />
<span className="absolute inset-0 bg-accent rounded-full animate-ping opacity-50" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground truncate">{domain.name}</p>
<p className="text-xs text-accent">Available for registration!</p>
</div>
<a
href={`https://www.namecheap.com/domains/registration/results/?domain=${domain.name}`}
target="_blank"
rel="noopener noreferrer"
className="text-xs font-medium text-accent hover:underline flex items-center gap-1"
> >
Register <ExternalLink className="w-3 h-3" /> <div className="relative">
</a> <span className="w-3 h-3 bg-accent rounded-full block" />
</div> <span className="absolute inset-0 bg-accent rounded-full animate-ping opacity-50" />
))} </div>
{availableDomains.length > 4 && ( <div className="flex-1 min-w-0">
<p className="text-center text-sm text-foreground-muted"> <p className="text-sm font-medium text-foreground truncate">{domain.name}</p>
+{availableDomains.length - 4} more available <p className="text-xs text-accent">Available for registration!</p>
</div>
<a
href={`https://www.namecheap.com/domains/registration/results/?domain=${domain.name}`}
target="_blank"
rel="noopener noreferrer"
className="text-xs font-medium text-accent hover:underline flex items-center gap-1"
>
Register <ExternalLink className="w-3 h-3" />
</a>
</div>
))}
{availableDomains.length > 4 && (
<p className="text-center text-sm text-foreground-muted">
+{availableDomains.length - 4} more available
</p>
)}
</div>
) : totalDomains > 0 ? (
<div className="text-center py-8">
<Bell className="w-10 h-10 text-foreground-subtle mx-auto mb-3" />
<p className="text-foreground-muted">All domains are still registered</p>
<p className="text-sm text-foreground-subtle mt-1">
We're monitoring {totalDomains} domains for you
</p> </p>
)} </div>
</div> ) : (
) : totalDomains > 0 ? ( <div className="text-center py-8">
<div className="text-center py-8"> <Plus className="w-10 h-10 text-foreground-subtle mx-auto mb-3" />
<Bell className="w-10 h-10 text-foreground-subtle mx-auto mb-3" /> <p className="text-foreground-muted">No domains tracked yet</p>
<p className="text-foreground-muted">All domains are still registered</p> <p className="text-sm text-foreground-subtle mt-1">
<p className="text-sm text-foreground-subtle mt-1"> Add a domain above to start monitoring
We're monitoring {totalDomains} domains for you </p>
</p> </div>
</div> )}
) : ( </div>
<div className="text-center py-8">
<Plus className="w-10 h-10 text-foreground-subtle mx-auto mb-3" />
<p className="text-foreground-muted">No domains tracked yet</p>
<p className="text-sm text-foreground-subtle mt-1">
Add a domain above to start monitoring
</p>
</div>
)}
</div> </div>
{/* Market Pulse */} {/* Market Pulse */}
<div className="p-6 bg-gradient-to-br from-background-secondary/60 to-background-secondary/30 border border-border/50 rounded-2xl"> <div className="relative overflow-hidden rounded-2xl border border-border/40 bg-gradient-to-b from-background-secondary/40 to-background-secondary/20 backdrop-blur-sm">
<div className="flex items-center justify-between mb-6"> <div className="p-5 border-b border-border/30">
<h2 className="text-lg font-semibold text-foreground flex items-center gap-3"> <SectionHeader
<div className="w-9 h-9 bg-accent/10 border border-accent/20 rounded-lg flex items-center justify-center"> title="Market Pulse"
<Gavel className="w-4 h-4 text-accent" /> icon={Gavel}
</div> compact
Market Pulse action={
</h2> <Link href="/auctions" className="text-sm text-accent hover:text-accent/80 transition-colors">
<Link href="/market" className="text-sm text-accent hover:text-accent/80 transition-colors"> View all →
View all → </Link>
</Link> }
/>
</div>
<div className="p-5">
{loadingAuctions ? (
<div className="space-y-3">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-14 bg-foreground/5 rounded-xl animate-pulse" />
))}
</div>
) : hotAuctions.length > 0 ? (
<div className="space-y-3">
{hotAuctions.map((auction, idx) => (
<a
key={`${auction.domain}-${idx}`}
href={auction.affiliate_url || '#'}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-4 p-3 bg-foreground/5 rounded-xl
hover:bg-foreground/10 transition-colors group"
>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground truncate">{auction.domain}</p>
<p className="text-xs text-foreground-muted flex items-center gap-2">
<Clock className="w-3 h-3" />
{auction.time_remaining}
<span className="text-foreground-subtle">• {auction.platform}</span>
</p>
</div>
<div className="text-right">
<p className="text-sm font-semibold text-foreground">${auction.current_bid}</p>
<p className="text-xs text-foreground-subtle">current bid</p>
</div>
<ExternalLink className="w-4 h-4 text-foreground-subtle group-hover:text-foreground" />
</a>
))}
</div>
) : (
<div className="text-center py-8">
<Gavel className="w-10 h-10 text-foreground-subtle mx-auto mb-3" />
<p className="text-foreground-muted">No auctions ending soon</p>
</div>
)}
</div> </div>
{loadingAuctions ? (
<div className="space-y-3">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-14 bg-foreground/5 rounded-xl animate-pulse" />
))}
</div>
) : hotAuctions.length > 0 ? (
<div className="space-y-3">
{hotAuctions.map((auction, idx) => (
<a
key={`${auction.domain}-${idx}`}
href={auction.affiliate_url || '#'}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-4 p-3 bg-foreground/5 rounded-xl
hover:bg-foreground/10 transition-colors group"
>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground truncate">{auction.domain}</p>
<p className="text-xs text-foreground-muted flex items-center gap-2">
<Clock className="w-3 h-3" />
{auction.time_remaining}
<span className="text-foreground-subtle">• {auction.platform}</span>
</p>
</div>
<div className="text-right">
<p className="text-sm font-semibold text-foreground">${auction.current_bid}</p>
<p className="text-xs text-foreground-subtle">current bid</p>
</div>
<ExternalLink className="w-4 h-4 text-foreground-subtle group-hover:text-foreground" />
</a>
))}
</div>
) : (
<div className="text-center py-8">
<Gavel className="w-10 h-10 text-foreground-subtle mx-auto mb-3" />
<p className="text-foreground-muted">No auctions ending soon</p>
</div>
)}
</div> </div>
</div> </div>
{/* Trending TLDs */} {/* Trending TLDs */}
<div className="p-6 bg-gradient-to-br from-background-secondary/60 to-background-secondary/30 border border-border/50 rounded-2xl"> <div className="relative overflow-hidden rounded-2xl border border-border/40 bg-gradient-to-b from-background-secondary/40 to-background-secondary/20 backdrop-blur-sm">
<div className="flex items-center justify-between mb-6"> <div className="p-5 border-b border-border/30">
<h2 className="text-lg font-semibold text-foreground flex items-center gap-3"> <SectionHeader
<div className="w-9 h-9 bg-accent/10 border border-accent/20 rounded-lg flex items-center justify-center"> title="Trending TLDs"
<TrendingUp className="w-4 h-4 text-accent" /> icon={TrendingUp}
</div> compact
Trending TLDs action={
</h2> <Link href="/intelligence" className="text-sm text-accent hover:text-accent/80 transition-colors">
<Link href="/intelligence" className="text-sm text-accent hover:text-accent/80 transition-colors"> View all →
View all →
</Link>
</div>
{loadingTlds ? (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-24 bg-foreground/5 rounded-xl animate-pulse" />
))}
</div>
) : (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{trendingTlds.map((tld) => (
<Link
key={tld.tld}
href={`/tld-pricing/${tld.tld}`}
className="group relative p-4 bg-background/50 border border-border/50 rounded-xl
hover:border-accent/30 transition-all duration-300 overflow-hidden"
>
<div className="absolute inset-0 bg-accent/5 opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="relative">
<div className="flex items-center justify-between mb-3">
<span className="font-mono text-2xl font-semibold text-foreground group-hover:text-accent transition-colors">.{tld.tld}</span>
<span className={clsx(
"text-xs font-bold px-2.5 py-1 rounded-lg border",
(tld.price_change || 0) > 0
? "text-orange-400 bg-orange-400/10 border-orange-400/20"
: "text-accent bg-accent/10 border-accent/20"
)}>
{(tld.price_change || 0) > 0 ? '+' : ''}{(tld.price_change || 0).toFixed(1)}%
</span>
</div>
<p className="text-sm text-foreground-muted truncate">{tld.reason}</p>
</div>
</Link> </Link>
))} }
</div> />
)} </div>
<div className="p-5">
{loadingTlds ? (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-24 bg-foreground/5 rounded-xl animate-pulse" />
))}
</div>
) : trendingTlds.length > 0 ? (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{trendingTlds.map((tld) => (
<Link
key={tld.tld}
href={`/tld-pricing/${tld.tld}`}
className="group relative p-4 bg-foreground/5 border border-border/30 rounded-xl
hover:border-accent/30 transition-all duration-300 overflow-hidden"
>
<div className="absolute inset-0 bg-accent/5 opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="relative">
<div className="flex items-center justify-between mb-3">
<span className="font-mono text-2xl font-semibold text-foreground group-hover:text-accent transition-colors">.{tld.tld}</span>
<span className={clsx(
"text-xs font-bold px-2.5 py-1 rounded-lg border",
(tld.price_change || 0) > 0
? "text-orange-400 bg-orange-400/10 border-orange-400/20"
: "text-accent bg-accent/10 border-accent/20"
)}>
{(tld.price_change || 0) > 0 ? '+' : ''}{(tld.price_change || 0).toFixed(1)}%
</span>
</div>
<p className="text-sm text-foreground-muted truncate">{tld.reason}</p>
</div>
</Link>
))}
</div>
) : (
<div className="text-center py-8">
<TrendingUp className="w-10 h-10 text-foreground-subtle mx-auto mb-3" />
<p className="text-foreground-muted">No trending TLDs available</p>
</div>
)}
</div>
</div> </div>
</div> </PageContainer>
</CommandCenterLayout> </CommandCenterLayout>
) )
} }

View File

@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'
import { useStore } from '@/lib/store' import { useStore } from '@/lib/store'
import { api, PortfolioDomain, PortfolioSummary, DomainValuation } from '@/lib/api' import { api, PortfolioDomain, PortfolioSummary, DomainValuation } from '@/lib/api'
import { CommandCenterLayout } from '@/components/CommandCenterLayout' import { CommandCenterLayout } from '@/components/CommandCenterLayout'
import { PremiumTable, StatCard, PageContainer, TableActionButton } from '@/components/PremiumTable'
import { Toast, useToast } from '@/components/Toast' import { Toast, useToast } from '@/components/Toast'
import { import {
Plus, Plus,
@ -15,12 +16,12 @@ import {
RefreshCw, RefreshCw,
Loader2, Loader2,
TrendingUp, TrendingUp,
TrendingDown,
Tag,
ExternalLink,
Sparkles, Sparkles,
ArrowUpRight, ArrowUpRight,
X, X,
Briefcase,
PiggyBank,
ShoppingCart,
} from 'lucide-react' } from 'lucide-react'
import clsx from 'clsx' import clsx from 'clsx'
import Link from 'next/link' import Link from 'next/link'
@ -230,53 +231,31 @@ export default function PortfolioPage() {
<button <button
onClick={() => setShowAddModal(true)} onClick={() => setShowAddModal(true)}
disabled={!canAddMore} disabled={!canAddMore}
className="flex items-center gap-2 h-9 px-4 bg-accent text-background rounded-lg className="flex items-center gap-2 h-9 px-4 bg-gradient-to-r from-accent to-accent/80 text-background
font-medium text-sm hover:bg-accent-hover transition-colors rounded-lg font-medium text-sm hover:shadow-[0_0_20px_-5px_rgba(16,185,129,0.4)]
disabled:opacity-50 disabled:cursor-not-allowed" transition-all disabled:opacity-50 disabled:cursor-not-allowed"
> >
<Plus className="w-4 h-4" /> <Plus className="w-4 h-4" />
Add Domain <span className="hidden sm:inline">Add Domain</span>
</button> </button>
} }
> >
{toast && <Toast message={toast.message} type={toast.type} onClose={hideToast} />} {toast && <Toast message={toast.message} type={toast.type} onClose={hideToast} />}
<div className="max-w-7xl mx-auto space-y-6"> <PageContainer>
{/* Summary Stats */} {/* Summary Stats */}
{summary && ( <div className="grid grid-cols-2 lg:grid-cols-5 gap-4">
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4"> <StatCard title="Total Domains" value={summary?.total_domains || 0} icon={Briefcase} />
<div className="p-5 bg-background-secondary/50 border border-border rounded-xl"> <StatCard title="Total Invested" value={`$${(summary?.total_invested || 0).toLocaleString()}`} icon={DollarSign} />
<p className="text-sm text-foreground-muted mb-1">Total Domains</p> <StatCard title="Est. Value" value={`$${(summary?.total_value || 0).toLocaleString()}`} icon={TrendingUp} />
<p className="text-2xl font-display text-foreground">{summary.total_domains}</p> <StatCard
</div> title="Profit/Loss"
<div className="p-5 bg-background-secondary/50 border border-border rounded-xl"> value={`${(summary?.total_profit || 0) >= 0 ? '+' : ''}$${(summary?.total_profit || 0).toLocaleString()}`}
<p className="text-sm text-foreground-muted mb-1">Total Invested</p> icon={PiggyBank}
<p className="text-2xl font-display text-foreground">${summary.total_invested?.toLocaleString() || 0}</p> accent={(summary?.total_profit || 0) >= 0}
</div> />
<div className="p-5 bg-background-secondary/50 border border-border rounded-xl"> <StatCard title="Sold" value={summary?.sold_domains || 0} icon={ShoppingCart} />
<p className="text-sm text-foreground-muted mb-1">Est. Value</p> </div>
<p className="text-2xl font-display text-foreground">${summary.total_value?.toLocaleString() || 0}</p>
</div>
<div className={clsx(
"p-5 border rounded-xl",
(summary.total_profit || 0) >= 0
? "bg-accent/5 border-accent/20"
: "bg-red-500/5 border-red-500/20"
)}>
<p className="text-sm text-foreground-muted mb-1">Profit/Loss</p>
<p className={clsx(
"text-2xl font-display",
(summary.total_profit || 0) >= 0 ? "text-accent" : "text-red-400"
)}>
{(summary.total_profit || 0) >= 0 ? '+' : ''}${summary.total_profit?.toLocaleString() || 0}
</p>
</div>
<div className="p-5 bg-background-secondary/50 border border-border rounded-xl">
<p className="text-sm text-foreground-muted mb-1">Sold</p>
<p className="text-2xl font-display text-foreground">{summary.sold_domains || 0}</p>
</div>
</div>
)}
{!canAddMore && ( {!canAddMore && (
<div className="flex items-center justify-between p-4 bg-amber-500/10 border border-amber-500/20 rounded-xl"> <div className="flex items-center justify-between p-4 bg-amber-500/10 border border-amber-500/20 rounded-xl">
@ -292,176 +271,177 @@ export default function PortfolioPage() {
</div> </div>
)} )}
{/* Domain List */} {/* Portfolio Table */}
{loading ? ( <PremiumTable
<div className="space-y-3"> data={portfolio}
{[...Array(3)].map((_, i) => ( keyExtractor={(d) => d.id}
<div key={i} className="h-24 bg-background-secondary/50 border border-border rounded-xl animate-pulse" /> loading={loading}
))} emptyIcon={<Briefcase className="w-12 h-12 text-foreground-subtle" />}
</div> emptyTitle="Your portfolio is empty"
) : portfolio.length === 0 ? ( emptyDescription="Add your first domain to start tracking investments"
<div className="text-center py-16 bg-background-secondary/30 border border-border rounded-xl"> columns={[
<div className="w-16 h-16 bg-foreground/5 rounded-2xl flex items-center justify-center mx-auto mb-4"> {
<Plus className="w-8 h-8 text-foreground-subtle" /> key: 'domain',
</div> header: 'Domain',
<p className="text-foreground-muted mb-2">Your portfolio is empty</p> render: (domain) => (
<p className="text-sm text-foreground-subtle mb-4">Add your first domain to start tracking investments</p> <div>
<button <span className="font-mono font-medium text-foreground">{domain.domain}</span>
onClick={() => setShowAddModal(true)} {domain.registrar && (
className="inline-flex items-center gap-2 px-4 py-2 bg-accent text-background rounded-lg font-medium" <p className="text-xs text-foreground-muted flex items-center gap-1 mt-0.5">
> <Building className="w-3 h-3" /> {domain.registrar}
<Plus className="w-4 h-4" /> </p>
Add Domain
</button>
</div>
) : (
<div className="space-y-3">
{portfolio.map((domain) => (
<div
key={domain.id}
className="group p-5 bg-background-secondary/50 border border-border rounded-xl
hover:border-foreground/20 transition-all"
>
<div className="flex flex-col lg:flex-row lg:items-center gap-4">
{/* Domain Info */}
<div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold text-foreground mb-2">{domain.domain}</h3>
<div className="flex flex-wrap gap-4 text-sm text-foreground-muted">
{domain.purchase_price && (
<span className="flex items-center gap-1.5">
<DollarSign className="w-3.5 h-3.5" />
Bought: ${domain.purchase_price}
</span>
)}
{domain.registrar && (
<span className="flex items-center gap-1.5">
<Building className="w-3.5 h-3.5" />
{domain.registrar}
</span>
)}
{domain.renewal_date && (
<span className="flex items-center gap-1.5">
<Calendar className="w-3.5 h-3.5" />
Renews: {new Date(domain.renewal_date).toLocaleDateString()}
</span>
)}
</div>
</div>
{/* Valuation */}
{domain.current_valuation && (
<div className="text-right">
<p className="text-xl font-semibold text-foreground">
${domain.current_valuation.toLocaleString()}
</p>
<p className="text-xs text-foreground-subtle">Est. Value</p>
</div>
)} )}
{/* Actions */}
<div className="flex items-center gap-2">
<button
onClick={() => handleValuate(domain)}
className="p-2 text-foreground-muted hover:text-accent hover:bg-accent/10 rounded-lg transition-colors"
title="Get valuation"
>
<Sparkles className="w-4 h-4" />
</button>
<button
onClick={() => handleRefresh(domain)}
disabled={refreshingId === domain.id}
className="p-2 text-foreground-muted hover:bg-foreground/5 rounded-lg transition-colors"
title="Refresh valuation"
>
<RefreshCw className={clsx("w-4 h-4", refreshingId === domain.id && "animate-spin")} />
</button>
<button
onClick={() => openEditModal(domain)}
className="p-2 text-foreground-muted hover:bg-foreground/5 rounded-lg transition-colors"
title="Edit"
>
<Edit2 className="w-4 h-4" />
</button>
<button
onClick={() => openSellModal(domain)}
className="px-3 py-2 text-sm font-medium text-accent hover:bg-accent/10 rounded-lg transition-colors"
>
Sell
</button>
<button
onClick={() => handleDelete(domain)}
className="p-2 text-foreground-muted hover:text-red-400 hover:bg-red-400/10 rounded-lg transition-colors"
title="Remove"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div> </div>
</div> ),
))} },
</div> {
)} key: 'purchase',
</div> header: 'Purchase',
hideOnMobile: true,
render: (domain) => (
<div>
{domain.purchase_price && (
<span className="font-medium text-foreground">${domain.purchase_price.toLocaleString()}</span>
)}
{domain.purchase_date && (
<p className="text-xs text-foreground-muted flex items-center gap-1 mt-0.5">
<Calendar className="w-3 h-3" /> {new Date(domain.purchase_date).toLocaleDateString()}
</p>
)}
</div>
),
},
{
key: 'valuation',
header: 'Est. Value',
align: 'right',
render: (domain) => (
domain.current_valuation ? (
<span className="font-semibold text-foreground">${domain.current_valuation.toLocaleString()}</span>
) : (
<span className="text-foreground-muted">—</span>
)
),
},
{
key: 'renewal',
header: 'Renewal',
hideOnMobile: true,
hideOnTablet: true,
render: (domain) => (
domain.renewal_date ? (
<span className="text-sm text-foreground-muted">
{new Date(domain.renewal_date).toLocaleDateString()}
</span>
) : (
<span className="text-foreground-subtle">—</span>
)
),
},
{
key: 'actions',
header: '',
align: 'right',
render: (domain) => (
<div className="flex items-center gap-1 justify-end">
<TableActionButton
icon={Sparkles}
onClick={() => handleValuate(domain)}
title="Get valuation"
/>
<TableActionButton
icon={RefreshCw}
onClick={() => handleRefresh(domain)}
loading={refreshingId === domain.id}
title="Refresh valuation"
/>
<TableActionButton
icon={Edit2}
onClick={() => openEditModal(domain)}
title="Edit"
/>
<button
onClick={(e) => { e.stopPropagation(); openSellModal(domain) }}
className="px-3 py-2 text-xs font-medium text-accent hover:bg-accent/10 rounded-lg transition-colors"
>
Sell
</button>
<TableActionButton
icon={Trash2}
onClick={() => handleDelete(domain)}
variant="danger"
title="Remove"
/>
</div>
),
},
]}
/>
</PageContainer>
{/* Add Modal */} {/* Add Modal */}
{showAddModal && ( {showAddModal && (
<Modal title="Add Domain to Portfolio" onClose={() => setShowAddModal(false)}> <Modal title="Add Domain to Portfolio" onClose={() => setShowAddModal(false)}>
<form onSubmit={handleAddDomain} className="space-y-4"> <form onSubmit={handleAddDomain} className="space-y-4">
<div> <div>
<label className="block text-sm text-foreground-muted mb-1">Domain *</label> <label className="block text-sm text-foreground-muted mb-1.5">Domain *</label>
<input <input
type="text" type="text"
value={addForm.domain} value={addForm.domain}
onChange={(e) => setAddForm({ ...addForm, domain: e.target.value })} onChange={(e) => setAddForm({ ...addForm, domain: e.target.value })}
placeholder="example.com" placeholder="example.com"
className="w-full h-10 px-3 bg-background border border-border rounded-lg text-foreground" className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl text-foreground
focus:outline-none focus:border-accent/50 transition-all"
required required
/> />
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm text-foreground-muted mb-1">Purchase Price</label> <label className="block text-sm text-foreground-muted mb-1.5">Purchase Price</label>
<input <input
type="number" type="number"
value={addForm.purchase_price} value={addForm.purchase_price}
onChange={(e) => setAddForm({ ...addForm, purchase_price: e.target.value })} onChange={(e) => setAddForm({ ...addForm, purchase_price: e.target.value })}
placeholder="100" placeholder="100"
className="w-full h-10 px-3 bg-background border border-border rounded-lg text-foreground" className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl text-foreground
focus:outline-none focus:border-accent/50"
/> />
</div> </div>
<div> <div>
<label className="block text-sm text-foreground-muted mb-1">Purchase Date</label> <label className="block text-sm text-foreground-muted mb-1.5">Purchase Date</label>
<input <input
type="date" type="date"
value={addForm.purchase_date} value={addForm.purchase_date}
onChange={(e) => setAddForm({ ...addForm, purchase_date: e.target.value })} onChange={(e) => setAddForm({ ...addForm, purchase_date: e.target.value })}
className="w-full h-10 px-3 bg-background border border-border rounded-lg text-foreground" className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl text-foreground
focus:outline-none focus:border-accent/50"
/> />
</div> </div>
</div> </div>
<div> <div>
<label className="block text-sm text-foreground-muted mb-1">Registrar</label> <label className="block text-sm text-foreground-muted mb-1.5">Registrar</label>
<input <input
type="text" type="text"
value={addForm.registrar} value={addForm.registrar}
onChange={(e) => setAddForm({ ...addForm, registrar: e.target.value })} onChange={(e) => setAddForm({ ...addForm, registrar: e.target.value })}
placeholder="Namecheap" placeholder="Namecheap"
className="w-full h-10 px-3 bg-background border border-border rounded-lg text-foreground" className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl text-foreground
focus:outline-none focus:border-accent/50"
/> />
</div> </div>
<div className="flex justify-end gap-3"> <div className="flex justify-end gap-3 pt-2">
<button <button
type="button" type="button"
onClick={() => setShowAddModal(false)} onClick={() => setShowAddModal(false)}
className="px-4 py-2 text-foreground-muted hover:text-foreground" className="px-4 py-2.5 text-foreground-muted hover:text-foreground transition-colors"
> >
Cancel Cancel
</button> </button>
<button <button
type="submit" type="submit"
disabled={addingDomain || !addForm.domain.trim()} disabled={addingDomain || !addForm.domain.trim()}
className="flex items-center gap-2 px-4 py-2 bg-accent text-background rounded-lg font-medium className="flex items-center gap-2 px-5 py-2.5 bg-gradient-to-r from-accent to-accent/80
disabled:opacity-50" text-background rounded-xl font-medium disabled:opacity-50 transition-all"
> >
{addingDomain && <Loader2 className="w-4 h-4 animate-spin" />} {addingDomain && <Loader2 className="w-4 h-4 animate-spin" />}
Add Domain Add Domain
@ -471,12 +451,110 @@ export default function PortfolioPage() {
</Modal> </Modal>
)} )}
{/* Edit Modal */}
{showEditModal && selectedDomain && (
<Modal title={`Edit ${selectedDomain.domain}`} onClose={() => setShowEditModal(false)}>
<form onSubmit={handleEditDomain} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm text-foreground-muted mb-1.5">Purchase Price</label>
<input
type="number"
value={editForm.purchase_price}
onChange={(e) => setEditForm({ ...editForm, purchase_price: e.target.value })}
className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl text-foreground
focus:outline-none focus:border-accent/50"
/>
</div>
<div>
<label className="block text-sm text-foreground-muted mb-1.5">Registrar</label>
<input
type="text"
value={editForm.registrar}
onChange={(e) => setEditForm({ ...editForm, registrar: e.target.value })}
className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl text-foreground
focus:outline-none focus:border-accent/50"
/>
</div>
</div>
<div className="flex justify-end gap-3 pt-2">
<button
type="button"
onClick={() => setShowEditModal(false)}
className="px-4 py-2.5 text-foreground-muted hover:text-foreground transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={savingEdit}
className="flex items-center gap-2 px-5 py-2.5 bg-gradient-to-r from-accent to-accent/80
text-background rounded-xl font-medium disabled:opacity-50 transition-all"
>
{savingEdit && <Loader2 className="w-4 h-4 animate-spin" />}
Save Changes
</button>
</div>
</form>
</Modal>
)}
{/* Sell Modal */}
{showSellModal && selectedDomain && (
<Modal title={`Sell ${selectedDomain.domain}`} onClose={() => setShowSellModal(false)}>
<form onSubmit={handleSellDomain} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm text-foreground-muted mb-1.5">Sale Price *</label>
<input
type="number"
value={sellForm.sale_price}
onChange={(e) => setSellForm({ ...sellForm, sale_price: e.target.value })}
placeholder="1000"
className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl text-foreground
focus:outline-none focus:border-accent/50"
required
/>
</div>
<div>
<label className="block text-sm text-foreground-muted mb-1.5">Sale Date</label>
<input
type="date"
value={sellForm.sale_date}
onChange={(e) => setSellForm({ ...sellForm, sale_date: e.target.value })}
className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl text-foreground
focus:outline-none focus:border-accent/50"
/>
</div>
</div>
<div className="flex justify-end gap-3 pt-2">
<button
type="button"
onClick={() => setShowSellModal(false)}
className="px-4 py-2.5 text-foreground-muted hover:text-foreground transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={processingSale || !sellForm.sale_price}
className="flex items-center gap-2 px-5 py-2.5 bg-gradient-to-r from-accent to-accent/80
text-background rounded-xl font-medium disabled:opacity-50 transition-all"
>
{processingSale && <Loader2 className="w-4 h-4 animate-spin" />}
Mark as Sold
</button>
</div>
</form>
</Modal>
)}
{/* Valuation Modal */} {/* Valuation Modal */}
{showValuationModal && ( {showValuationModal && (
<Modal title="Domain Valuation" onClose={() => { setShowValuationModal(false); setValuation(null); }}> <Modal title="Domain Valuation" onClose={() => { setShowValuationModal(false); setValuation(null); }}>
{valuatingDomain ? ( {valuatingDomain ? (
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-12">
<Loader2 className="w-6 h-6 animate-spin text-accent" /> <Loader2 className="w-8 h-8 animate-spin text-accent" />
</div> </div>
) : valuation ? ( ) : valuation ? (
<div className="space-y-4"> <div className="space-y-4">
@ -485,11 +563,11 @@ export default function PortfolioPage() {
<p className="text-sm text-foreground-muted mt-1">Estimated Value</p> <p className="text-sm text-foreground-muted mt-1">Estimated Value</p>
</div> </div>
<div className="space-y-2 text-sm"> <div className="space-y-2 text-sm">
<div className="flex justify-between"> <div className="flex justify-between p-3 bg-foreground/5 rounded-lg">
<span className="text-foreground-muted">Confidence</span> <span className="text-foreground-muted">Confidence</span>
<span className="text-foreground capitalize">{valuation.confidence}</span> <span className="text-foreground capitalize font-medium">{valuation.confidence}</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between p-3 bg-foreground/5 rounded-lg">
<span className="text-foreground-muted">Formula</span> <span className="text-foreground-muted">Formula</span>
<span className="text-foreground font-mono text-xs">{valuation.valuation_formula}</span> <span className="text-foreground font-mono text-xs">{valuation.valuation_formula}</span>
</div> </div>
@ -502,7 +580,7 @@ export default function PortfolioPage() {
) )
} }
// Simple Modal Component // Modal Component
function Modal({ title, children, onClose }: { title: string; children: React.ReactNode; onClose: () => void }) { function Modal({ title, children, onClose }: { title: string; children: React.ReactNode; onClose: () => void }) {
return ( return (
<div <div
@ -510,20 +588,19 @@ function Modal({ title, children, onClose }: { title: string; children: React.Re
onClick={onClose} onClick={onClose}
> >
<div <div
className="w-full max-w-md bg-background-secondary border border-border rounded-2xl shadow-2xl" className="w-full max-w-md bg-background-secondary border border-border/50 rounded-2xl shadow-2xl"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<div className="flex items-center justify-between p-4 border-b border-border"> <div className="flex items-center justify-between p-5 border-b border-border/50">
<h3 className="text-lg font-semibold text-foreground">{title}</h3> <h3 className="text-lg font-semibold text-foreground">{title}</h3>
<button onClick={onClose} className="text-foreground-muted hover:text-foreground"> <button onClick={onClose} className="p-1 text-foreground-muted hover:text-foreground transition-colors">
<X className="w-5 h-5" /> <X className="w-5 h-5" />
</button> </button>
</div> </div>
<div className="p-4"> <div className="p-5">
{children} {children}
</div> </div>
</div> </div>
</div> </div>
) )
} }

View File

@ -3,6 +3,7 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { CommandCenterLayout } from '@/components/CommandCenterLayout' import { CommandCenterLayout } from '@/components/CommandCenterLayout'
import { PageContainer } from '@/components/PremiumTable'
import { useStore } from '@/lib/store' import { useStore } from '@/lib/store'
import { api, PriceAlert } from '@/lib/api' import { api, PriceAlert } from '@/lib/api'
import { import {
@ -41,7 +42,7 @@ export default function SettingsPage() {
email: '', email: '',
}) })
// Notification preferences (local state - would be persisted via API in production) // Notification preferences
const [notificationPrefs, setNotificationPrefs] = useState({ const [notificationPrefs, setNotificationPrefs] = useState({
domain_availability: true, domain_availability: true,
price_alerts: true, price_alerts: true,
@ -99,7 +100,6 @@ export default function SettingsPage() {
try { try {
await api.updateMe({ name: profileForm.name || undefined }) await api.updateMe({ name: profileForm.name || undefined })
// Update store with new user info
const { checkAuth } = useStore.getState() const { checkAuth } = useStore.getState()
await checkAuth() await checkAuth()
setSuccess('Profile updated successfully') setSuccess('Profile updated successfully')
@ -116,7 +116,6 @@ export default function SettingsPage() {
setSuccess(null) setSuccess(null)
try { try {
// Store in localStorage for now (would be API in production)
localStorage.setItem('notification_prefs', JSON.stringify(notificationPrefs)) localStorage.setItem('notification_prefs', JSON.stringify(notificationPrefs))
setSuccess('Notification preferences saved') setSuccess('Notification preferences saved')
} catch (err) { } catch (err) {
@ -126,7 +125,6 @@ export default function SettingsPage() {
} }
} }
// Load notification preferences from localStorage
useEffect(() => { useEffect(() => {
const saved = localStorage.getItem('notification_prefs') const saved = localStorage.getItem('notification_prefs')
if (saved) { if (saved) {
@ -184,536 +182,382 @@ export default function SettingsPage() {
title="Settings" title="Settings"
subtitle="Manage your account" subtitle="Manage your account"
> >
<PageContainer>
{/* Messages */}
{error && (
<div className="p-4 bg-red-500/10 border border-red-500/20 rounded-xl flex items-center gap-3">
<AlertCircle className="w-5 h-5 text-red-400 shrink-0" />
<p className="text-sm text-red-400 flex-1">{error}</p>
<button onClick={() => setError(null)} className="text-red-400 hover:text-red-300">
<Trash2 className="w-4 h-4" />
</button>
</div>
)}
<main className="max-w-5xl mx-auto"> {success && (
<div className="space-y-8"> <div className="p-4 bg-accent/10 border border-accent/20 rounded-xl flex items-center gap-3">
<Check className="w-5 h-5 text-accent shrink-0" />
<p className="text-sm text-accent flex-1">{success}</p>
<button onClick={() => setSuccess(null)} className="text-accent hover:text-accent/80">
<Trash2 className="w-4 h-4" />
</button>
</div>
)}
{/* Messages */} <div className="flex flex-col lg:flex-row gap-6">
{error && ( {/* Sidebar */}
<div className="mb-6 p-4 bg-danger/5 border border-danger/20 rounded-2xl flex items-center gap-3"> <div className="lg:w-72 shrink-0 space-y-5">
<AlertCircle className="w-5 h-5 text-danger shrink-0" /> {/* Mobile: Horizontal scroll tabs */}
<p className="text-body-sm text-danger flex-1">{error}</p> <nav className="lg:hidden flex gap-2 overflow-x-auto pb-2 -mx-4 px-4 scrollbar-hide">
<button onClick={() => setError(null)} className="text-danger hover:text-danger/80"> {tabs.map((tab) => (
<Trash2 className="w-4 h-4" /> <button
</button> key={tab.id}
</div> onClick={() => setActiveTab(tab.id)}
)} className={clsx(
"flex items-center gap-2.5 px-5 py-3 text-sm font-medium rounded-xl whitespace-nowrap transition-all",
activeTab === tab.id
? "bg-gradient-to-r from-accent to-accent/80 text-background shadow-lg shadow-accent/20"
: "bg-background-secondary/50 text-foreground-muted hover:text-foreground border border-border/50"
)}
>
<tab.icon className="w-4 h-4" />
{tab.label}
</button>
))}
</nav>
{/* Desktop: Vertical tabs */}
<nav className="hidden lg:block p-2 bg-gradient-to-b from-background-secondary/40 to-background-secondary/20 border border-border/40 rounded-2xl backdrop-blur-sm">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={clsx(
"w-full flex items-center gap-3 px-5 py-3.5 text-sm font-medium rounded-xl transition-all",
activeTab === tab.id
? "bg-gradient-to-r from-accent to-accent/80 text-background shadow-lg shadow-accent/20"
: "text-foreground-muted hover:text-foreground hover:bg-foreground/5"
)}
>
<tab.icon className="w-4 h-4" />
{tab.label}
</button>
))}
</nav>
{success && ( {/* Plan info */}
<div className="mb-6 p-4 bg-accent/5 border border-accent/20 rounded-2xl flex items-center gap-3"> <div className="hidden lg:block p-5 bg-accent/5 border border-accent/20 rounded-2xl">
<Check className="w-5 h-5 text-accent shrink-0" /> <div className="flex items-center gap-2 mb-3">
<p className="text-body-sm text-accent flex-1">{success}</p> {isProOrHigher ? <Crown className="w-5 h-5 text-accent" /> : <Zap className="w-5 h-5 text-accent" />}
<button onClick={() => setSuccess(null)} className="text-accent hover:text-accent/80"> <span className="text-sm font-semibold text-foreground">{tierName} Plan</span>
<Trash2 className="w-4 h-4" />
</button>
</div>
)}
<div className="flex flex-col lg:flex-row gap-8 animate-slide-up">
{/* Sidebar - Horizontal scroll on mobile, vertical on desktop */}
<div className="lg:w-72 shrink-0">
{/* Mobile: Horizontal scroll tabs */}
<nav className="lg:hidden flex gap-2 overflow-x-auto pb-2 -mx-4 px-4 scrollbar-hide">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={clsx(
"flex items-center gap-2.5 px-5 py-3 text-sm font-medium rounded-xl whitespace-nowrap transition-all duration-300",
activeTab === tab.id
? "bg-accent text-background shadow-lg shadow-accent/20"
: "bg-background-secondary/50 text-foreground-muted hover:text-foreground border border-border hover:border-accent/30"
)}
>
<tab.icon className="w-4 h-4" />
{tab.label}
</button>
))}
</nav>
{/* Desktop: Vertical tabs */}
<nav className="hidden lg:block p-2 bg-background-secondary/50 border border-border rounded-2xl">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={clsx(
"w-full flex items-center gap-3 px-5 py-3.5 text-sm font-medium rounded-xl transition-all duration-300",
activeTab === tab.id
? "bg-accent text-background shadow-lg shadow-accent/20"
: "text-foreground-muted hover:text-foreground hover:bg-foreground/5"
)}
>
<tab.icon className="w-4 h-4" />
{tab.label}
</button>
))}
</nav>
{/* Plan info - hidden on mobile, shown in content area instead */}
<div className="hidden lg:block mt-5 p-6 bg-accent/5 border border-accent/20 rounded-2xl">
<div className="flex items-center gap-2 mb-3">
{isProOrHigher ? <Crown className="w-5 h-5 text-accent" /> : <Zap className="w-5 h-5 text-accent" />}
<span className="text-sm font-semibold text-foreground">{tierName} Plan</span>
</div>
<p className="text-xs text-foreground-muted mb-4">
{subscription?.domains_used || 0} / {subscription?.domain_limit || 5} domains tracked
</p>
{!isProOrHigher && (
<Link
href="/pricing"
className="flex items-center justify-center gap-2 w-full py-2.5 bg-accent text-background text-ui-sm font-medium rounded-xl hover:bg-accent-hover transition-all"
>
Upgrade
<ChevronRight className="w-3.5 h-3.5" />
</Link>
)}
</div> </div>
</div> <p className="text-xs text-foreground-muted mb-4">
{subscription?.domains_used || 0} / {subscription?.domain_limit || 5} domains tracked
{/* Content */} </p>
<div className="flex-1 min-w-0"> {!isProOrHigher && (
{/* Profile Tab */} <Link
{activeTab === 'profile' && ( href="/pricing"
<div className="p-6 sm:p-8 bg-background-secondary/30 border border-border rounded-2xl"> className="flex items-center justify-center gap-2 w-full py-2.5 bg-gradient-to-r from-accent to-accent/80 text-background text-sm font-medium rounded-xl hover:shadow-[0_0_20px_-5px_rgba(16,185,129,0.4)] transition-all"
<h2 className="text-body-lg font-medium text-foreground mb-6">Profile Information</h2> >
Upgrade
<form onSubmit={handleSaveProfile} className="space-y-5"> <ChevronRight className="w-3.5 h-3.5" />
<div> </Link>
<label className="block text-ui-sm text-foreground-muted mb-2">Name</label>
<input
type="text"
value={profileForm.name}
onChange={(e) => setProfileForm({ ...profileForm, name: e.target.value })}
placeholder="Your name"
className="w-full px-4 py-3.5 bg-background border border-border rounded-xl text-body text-foreground
placeholder:text-foreground-subtle focus:outline-none focus:ring-1 focus:ring-accent/50 focus:border-accent/50 transition-all"
/>
</div>
<div>
<label className="block text-ui-sm text-foreground-muted mb-2">Email</label>
<input
type="email"
value={profileForm.email}
disabled
className="w-full px-4 py-3.5 bg-background-tertiary border border-border rounded-xl text-body text-foreground-muted cursor-not-allowed"
/>
<p className="text-ui-xs text-foreground-subtle mt-1.5">Email cannot be changed</p>
</div>
<button
type="submit"
disabled={saving}
className="px-6 py-3.5 bg-foreground text-background text-ui font-medium rounded-xl
hover:bg-foreground/90 disabled:opacity-50 transition-all flex items-center gap-2 shadow-lg shadow-foreground/10"
>
{saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Check className="w-4 h-4" />}
Save Changes
</button>
</form>
</div>
)}
{/* Notifications Tab */}
{activeTab === 'notifications' && (
<div className="space-y-6">
<div className="p-6 sm:p-8 bg-background-secondary/30 border border-border rounded-2xl">
<h2 className="text-body-lg font-medium text-foreground mb-5">Email Preferences</h2>
<div className="space-y-3">
<label className="flex items-center justify-between p-4 bg-background border border-border rounded-xl cursor-pointer hover:border-foreground/20 transition-colors">
<div>
<p className="text-body-sm font-medium text-foreground">Domain Availability</p>
<p className="text-body-xs text-foreground-muted">Get notified when watched domains become available</p>
</div>
<input
type="checkbox"
checked={notificationPrefs.domain_availability}
onChange={(e) => setNotificationPrefs({ ...notificationPrefs, domain_availability: e.target.checked })}
className="w-5 h-5 accent-accent cursor-pointer"
/>
</label>
<label className="flex items-center justify-between p-4 bg-background border border-border rounded-xl cursor-pointer hover:border-foreground/20 transition-colors">
<div>
<p className="text-body-sm font-medium text-foreground">Price Alerts</p>
<p className="text-body-xs text-foreground-muted">Get notified when TLD prices change</p>
</div>
<input
type="checkbox"
checked={notificationPrefs.price_alerts}
onChange={(e) => setNotificationPrefs({ ...notificationPrefs, price_alerts: e.target.checked })}
className="w-5 h-5 accent-accent cursor-pointer"
/>
</label>
<label className="flex items-center justify-between p-4 bg-background border border-border rounded-xl cursor-pointer hover:border-foreground/20 transition-colors">
<div>
<p className="text-body-sm font-medium text-foreground">Weekly Digest</p>
<p className="text-body-xs text-foreground-muted">Receive a weekly summary of your portfolio</p>
</div>
<input
type="checkbox"
checked={notificationPrefs.weekly_digest}
onChange={(e) => setNotificationPrefs({ ...notificationPrefs, weekly_digest: e.target.checked })}
className="w-5 h-5 accent-accent cursor-pointer"
/>
</label>
</div>
<button
onClick={handleSaveNotifications}
disabled={savingNotifications}
className="mt-5 px-6 py-3 bg-foreground text-background text-ui font-medium rounded-xl
hover:bg-foreground/90 disabled:opacity-50 transition-all flex items-center gap-2"
>
{savingNotifications ? <Loader2 className="w-4 h-4 animate-spin" /> : <Check className="w-4 h-4" />}
Save Preferences
</button>
</div>
{/* Active Price Alerts */}
<div className="p-6 sm:p-8 bg-background-secondary/30 border border-border rounded-2xl">
<h2 className="text-body-lg font-medium text-foreground mb-5">Active Price Alerts</h2>
{loadingAlerts ? (
<div className="py-10 flex items-center justify-center">
<Loader2 className="w-6 h-6 animate-spin text-accent" />
</div>
) : priceAlerts.length === 0 ? (
<div className="py-12 text-center border border-dashed border-border/50 rounded-xl bg-background/30">
<Bell className="w-10 h-10 text-foreground-subtle mx-auto mb-4" />
<p className="text-body text-foreground-muted mb-3">No price alerts set</p>
<Link
href="/tld-pricing"
className="text-accent hover:text-accent-hover text-body-sm font-medium"
>
Browse TLD prices
</Link>
</div>
) : (
<div className="space-y-2">
{priceAlerts.map((alert) => (
<div
key={alert.id}
className="flex items-center justify-between p-4 bg-background border border-border rounded-xl hover:border-foreground/20 transition-colors"
>
<div className="flex items-center gap-3">
<div className="relative">
<div className={clsx(
"w-2.5 h-2.5 rounded-full",
alert.is_active ? "bg-accent" : "bg-foreground-subtle"
)} />
{alert.is_active && (
<span className="absolute inset-0 rounded-full bg-accent animate-ping opacity-40" />
)}
</div>
<div>
<Link
href={`/tld-pricing/${alert.tld}`}
className="text-body-sm font-mono font-medium text-foreground hover:text-accent transition-colors"
>
.{alert.tld}
</Link>
<p className="text-body-xs text-foreground-muted">
Alert on {alert.threshold_percent}% change
{alert.target_price && ` or below $${alert.target_price}`}
</p>
</div>
</div>
<button
onClick={() => handleDeletePriceAlert(alert.tld, alert.id)}
disabled={deletingAlertId === alert.id}
className="p-2 text-foreground-subtle hover:text-danger hover:bg-danger/10 rounded-lg transition-all"
>
{deletingAlertId === alert.id ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Trash2 className="w-4 h-4" />
)}
</button>
</div>
))}
</div>
)}
</div>
</div>
)}
{/* Billing Tab */}
{activeTab === 'billing' && (
<div className="space-y-6">
{/* Current Plan */}
<div className="p-6 sm:p-8 bg-background-secondary/30 border border-border rounded-2xl">
<h2 className="text-body-lg font-medium text-foreground mb-6">Your Current Plan</h2>
<div className="p-5 bg-accent/5 border border-accent/20 rounded-xl 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-xl font-semibold text-foreground">{tierName}</p>
<p className="text-body-sm text-foreground-muted">
{tierName === 'Scout' ? 'Free forever' : tierName === 'Trader' ? '$9/month' : '$29/month'}
</p>
</div>
</div>
<span className={clsx(
"px-3 py-1.5 text-ui-xs font-medium rounded-full",
isProOrHigher ? "bg-accent/10 text-accent" : "bg-foreground/5 text-foreground-muted"
)}>
{isProOrHigher ? 'Active' : 'Free'}
</span>
</div>
{/* Plan Stats */}
<div className="grid grid-cols-3 gap-4 p-4 bg-background/50 rounded-xl mb-4">
<div className="text-center">
<p className="text-2xl font-semibold text-foreground">{subscription?.domain_limit || 5}</p>
<p className="text-xs text-foreground-muted">Domains</p>
</div>
<div className="text-center border-x border-border/50">
<p className="text-2xl font-semibold text-foreground">
{subscription?.check_frequency === 'realtime' ? '10m' :
subscription?.check_frequency === 'hourly' ? '1h' : '24h'}
</p>
<p className="text-xs text-foreground-muted">Check Interval</p>
</div>
<div className="text-center">
<p className="text-2xl font-semibold text-foreground">
{subscription?.portfolio_limit === -1 ? '∞' : subscription?.portfolio_limit || 0}
</p>
<p className="text-xs text-foreground-muted">Portfolio</p>
</div>
</div>
{isProOrHigher ? (
<button
onClick={handleOpenBillingPortal}
className="w-full py-3 bg-background text-foreground text-ui font-medium rounded-xl border border-border
hover:border-foreground/20 transition-all flex items-center justify-center gap-2"
>
<ExternalLink className="w-4 h-4" />
Manage Subscription
</button>
) : (
<Link
href="/pricing"
className="w-full py-3 bg-accent text-background text-ui font-medium rounded-xl
hover:bg-accent-hover transition-all flex items-center justify-center gap-2 shadow-lg shadow-accent/20"
>
<Zap className="w-4 h-4" />
Upgrade Plan
</Link>
)}
</div>
{/* Plan Features */}
<h3 className="text-body-sm font-medium text-foreground mb-3">Your Plan Includes</h3>
<ul className="grid grid-cols-2 gap-2">
<li className="flex items-center gap-2 text-body-sm">
<Check className="w-4 h-4 text-accent" />
<span className="text-foreground">{subscription?.domain_limit || 5} Watchlist Domains</span>
</li>
<li className="flex items-center gap-2 text-body-sm">
<Check className="w-4 h-4 text-accent" />
<span className="text-foreground">
{subscription?.check_frequency === 'realtime' ? '10-minute' :
subscription?.check_frequency === 'hourly' ? 'Hourly' : 'Daily'} Scans
</span>
</li>
<li className="flex items-center gap-2 text-body-sm">
<Check className="w-4 h-4 text-accent" />
<span className="text-foreground">Email Alerts</span>
</li>
<li className="flex items-center gap-2 text-body-sm">
<Check className="w-4 h-4 text-accent" />
<span className="text-foreground">TLD Price Data</span>
</li>
{subscription?.features?.domain_valuation && (
<li className="flex items-center gap-2 text-body-sm">
<Check className="w-4 h-4 text-accent" />
<span className="text-foreground">Domain Valuation</span>
</li>
)}
{(subscription?.portfolio_limit ?? 0) !== 0 && (
<li className="flex items-center gap-2 text-body-sm">
<Check className="w-4 h-4 text-accent" />
<span className="text-foreground">
{subscription?.portfolio_limit === -1 ? 'Unlimited' : subscription?.portfolio_limit} Portfolio
</span>
</li>
)}
{subscription?.features?.expiration_tracking && (
<li className="flex items-center gap-2 text-body-sm">
<Check className="w-4 h-4 text-accent" />
<span className="text-foreground">Expiry Tracking</span>
</li>
)}
{(subscription?.history_days ?? 0) !== 0 && (
<li className="flex items-center gap-2 text-body-sm">
<Check className="w-4 h-4 text-accent" />
<span className="text-foreground">
{subscription?.history_days === -1 ? 'Full' : `${subscription?.history_days}-day`} History
</span>
</li>
)}
</ul>
</div>
{/* Compare All Plans */}
<div className="p-6 sm:p-8 bg-background-secondary/30 border border-border rounded-2xl">
<h2 className="text-body-lg font-medium text-foreground mb-6">Compare All Plans</h2>
<div className="overflow-x-auto -mx-2">
<table className="w-full min-w-[500px]">
<thead>
<tr className="border-b border-border">
<th className="text-left py-3 px-3 text-body-sm font-medium text-foreground-muted">Feature</th>
<th className={clsx(
"text-center py-3 px-3 text-body-sm font-medium",
tierName === 'Scout' ? "text-accent" : "text-foreground-muted"
)}>Scout</th>
<th className={clsx(
"text-center py-3 px-3 text-body-sm font-medium",
tierName === 'Trader' ? "text-accent" : "text-foreground-muted"
)}>Trader</th>
<th className={clsx(
"text-center py-3 px-3 text-body-sm font-medium",
tierName === 'Tycoon' ? "text-accent" : "text-foreground-muted"
)}>Tycoon</th>
</tr>
</thead>
<tbody>
<tr className="border-b border-border/50">
<td className="py-3 px-3 text-body-sm text-foreground">Price</td>
<td className="py-3 px-3 text-center text-body-sm text-foreground">Free</td>
<td className="py-3 px-3 text-center text-body-sm text-foreground">$9/mo</td>
<td className="py-3 px-3 text-center text-body-sm text-foreground">$29/mo</td>
</tr>
<tr className="border-b border-border/50">
<td className="py-3 px-3 text-body-sm text-foreground">Watchlist Domains</td>
<td className="py-3 px-3 text-center text-body-sm text-foreground">5</td>
<td className="py-3 px-3 text-center text-body-sm text-foreground">50</td>
<td className="py-3 px-3 text-center text-body-sm text-foreground">500</td>
</tr>
<tr className="border-b border-border/50">
<td className="py-3 px-3 text-body-sm text-foreground">Scan Frequency</td>
<td className="py-3 px-3 text-center text-body-sm text-foreground">Daily</td>
<td className="py-3 px-3 text-center text-body-sm text-foreground">Hourly</td>
<td className="py-3 px-3 text-center text-body-sm text-accent font-medium">10 min</td>
</tr>
<tr className="border-b border-border/50">
<td className="py-3 px-3 text-body-sm text-foreground">Portfolio</td>
<td className="py-3 px-3 text-center text-body-sm text-foreground-muted"></td>
<td className="py-3 px-3 text-center text-body-sm text-foreground">25</td>
<td className="py-3 px-3 text-center text-body-sm text-foreground">Unlimited</td>
</tr>
<tr className="border-b border-border/50">
<td className="py-3 px-3 text-body-sm text-foreground">Domain Valuation</td>
<td className="py-3 px-3 text-center text-body-sm text-foreground-muted"></td>
<td className="py-3 px-3 text-center"><Check className="w-5 h-5 text-accent mx-auto" /></td>
<td className="py-3 px-3 text-center"><Check className="w-5 h-5 text-accent mx-auto" /></td>
</tr>
<tr className="border-b border-border/50">
<td className="py-3 px-3 text-body-sm text-foreground">Price History</td>
<td className="py-3 px-3 text-center text-body-sm text-foreground-muted"></td>
<td className="py-3 px-3 text-center text-body-sm text-foreground">90 days</td>
<td className="py-3 px-3 text-center text-body-sm text-foreground">Unlimited</td>
</tr>
<tr>
<td className="py-3 px-3 text-body-sm text-foreground">Expiry Tracking</td>
<td className="py-3 px-3 text-center text-body-sm text-foreground-muted"></td>
<td className="py-3 px-3 text-center"><Check className="w-5 h-5 text-accent mx-auto" /></td>
<td className="py-3 px-3 text-center"><Check className="w-5 h-5 text-accent mx-auto" /></td>
</tr>
</tbody>
</table>
</div>
{!isProOrHigher && (
<div className="mt-6 text-center">
<Link
href="/pricing"
className="inline-flex items-center gap-2 px-6 py-3 bg-accent text-background text-ui font-medium rounded-xl
hover:bg-accent-hover transition-all shadow-lg shadow-accent/20"
>
<Zap className="w-4 h-4" />
Upgrade Now
</Link>
</div>
)}
</div>
</div>
)}
{/* Security Tab */}
{activeTab === 'security' && (
<div className="space-y-6">
<div className="p-6 sm:p-8 bg-background-secondary/30 border border-border rounded-2xl">
<h2 className="text-body-lg font-medium text-foreground mb-4">Password</h2>
<p className="text-body-sm text-foreground-muted mb-5">
Change your password or reset it if you've forgotten it.
</p>
<Link
href="/forgot-password"
className="inline-flex items-center gap-2 px-5 py-3 bg-background border border-border text-foreground text-ui font-medium rounded-xl
hover:border-foreground/20 transition-all"
>
<Key className="w-4 h-4" />
Change Password
</Link>
</div>
<div className="p-6 sm:p-8 bg-background-secondary/30 border border-border rounded-2xl">
<h2 className="text-body-lg font-medium text-foreground mb-5">Account Security</h2>
<div className="space-y-3">
<div className="flex items-center justify-between p-4 bg-background border border-border rounded-xl">
<div>
<p className="text-body-sm font-medium text-foreground">Email Verified</p>
<p className="text-body-xs text-foreground-muted">Your email address has been verified</p>
</div>
<div className="w-8 h-8 bg-accent/10 rounded-lg flex items-center justify-center">
<Check className="w-4 h-4 text-accent" />
</div>
</div>
<div className="flex items-center justify-between p-4 bg-background border border-border rounded-xl">
<div>
<p className="text-body-sm font-medium text-foreground">Two-Factor Authentication</p>
<p className="text-body-xs text-foreground-muted">Coming soon</p>
</div>
<span className="text-ui-xs px-2.5 py-1 bg-foreground/5 text-foreground-muted rounded-full">Soon</span>
</div>
</div>
</div>
<div className="p-6 sm:p-8 bg-danger/5 border border-danger/20 rounded-2xl">
<h2 className="text-body-lg font-medium text-danger mb-2">Danger Zone</h2>
<p className="text-body-sm text-foreground-muted mb-5">
Permanently delete your account and all associated data.
</p>
<button
className="px-5 py-3 bg-danger text-white text-ui font-medium rounded-xl hover:bg-danger/90 transition-all"
>
Delete Account
</button>
</div>
</div>
)} )}
</div> </div>
</div> </div>
{/* Content */}
<div className="flex-1 min-w-0">
{/* Profile Tab */}
{activeTab === 'profile' && (
<div className="relative overflow-hidden rounded-2xl border border-border/40 bg-gradient-to-b from-background-secondary/40 to-background-secondary/20 backdrop-blur-sm p-6">
<h2 className="text-lg font-medium text-foreground mb-6">Profile Information</h2>
<form onSubmit={handleSaveProfile} className="space-y-5">
<div>
<label className="block text-sm text-foreground-muted mb-2">Name</label>
<input
type="text"
value={profileForm.name}
onChange={(e) => setProfileForm({ ...profileForm, name: e.target.value })}
placeholder="Your name"
className="w-full h-11 px-4 bg-background border border-border/50 rounded-xl text-foreground
placeholder:text-foreground-subtle focus:outline-none focus:border-accent/50 transition-all"
/>
</div>
<div>
<label className="block text-sm text-foreground-muted mb-2">Email</label>
<input
type="email"
value={profileForm.email}
disabled
className="w-full h-11 px-4 bg-foreground/5 border border-border/50 rounded-xl text-foreground-muted cursor-not-allowed"
/>
<p className="text-xs text-foreground-subtle mt-1.5">Email cannot be changed</p>
</div>
<button
type="submit"
disabled={saving}
className="flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-accent to-accent/80 text-background font-medium rounded-xl
hover:shadow-[0_0_20px_-5px_rgba(16,185,129,0.4)] disabled:opacity-50 transition-all"
>
{saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Check className="w-4 h-4" />}
Save Changes
</button>
</form>
</div>
)}
{/* Notifications Tab */}
{activeTab === 'notifications' && (
<div className="space-y-6">
<div className="relative overflow-hidden rounded-2xl border border-border/40 bg-gradient-to-b from-background-secondary/40 to-background-secondary/20 backdrop-blur-sm p-6">
<h2 className="text-lg font-medium text-foreground mb-5">Email Preferences</h2>
<div className="space-y-3">
{[
{ key: 'domain_availability', label: 'Domain Availability', desc: 'Get notified when watched domains become available' },
{ key: 'price_alerts', label: 'Price Alerts', desc: 'Get notified when TLD prices change' },
{ key: 'weekly_digest', label: 'Weekly Digest', desc: 'Receive a weekly summary of your portfolio' },
].map((item) => (
<label key={item.key} className="flex items-center justify-between p-4 bg-foreground/5 border border-border/30 rounded-xl cursor-pointer hover:border-foreground/20 transition-colors">
<div>
<p className="text-sm font-medium text-foreground">{item.label}</p>
<p className="text-xs text-foreground-muted">{item.desc}</p>
</div>
<input
type="checkbox"
checked={notificationPrefs[item.key as keyof typeof notificationPrefs]}
onChange={(e) => setNotificationPrefs({ ...notificationPrefs, [item.key]: e.target.checked })}
className="w-5 h-5 accent-accent cursor-pointer"
/>
</label>
))}
</div>
<button
onClick={handleSaveNotifications}
disabled={savingNotifications}
className="mt-5 flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-accent to-accent/80 text-background font-medium rounded-xl
hover:shadow-[0_0_20px_-5px_rgba(16,185,129,0.4)] disabled:opacity-50 transition-all"
>
{savingNotifications ? <Loader2 className="w-4 h-4 animate-spin" /> : <Check className="w-4 h-4" />}
Save Preferences
</button>
</div>
{/* Active Price Alerts */}
<div className="relative overflow-hidden rounded-2xl border border-border/40 bg-gradient-to-b from-background-secondary/40 to-background-secondary/20 backdrop-blur-sm p-6">
<h2 className="text-lg font-medium text-foreground mb-5">Active Price Alerts</h2>
{loadingAlerts ? (
<div className="py-10 flex items-center justify-center">
<Loader2 className="w-6 h-6 animate-spin text-accent" />
</div>
) : priceAlerts.length === 0 ? (
<div className="py-12 text-center border border-dashed border-border/50 rounded-xl bg-foreground/5">
<Bell className="w-10 h-10 text-foreground-subtle mx-auto mb-4" />
<p className="text-foreground-muted mb-3">No price alerts set</p>
<Link href="/intelligence" className="text-accent hover:text-accent/80 text-sm font-medium">
Browse TLD prices
</Link>
</div>
) : (
<div className="space-y-2">
{priceAlerts.map((alert) => (
<div
key={alert.id}
className="flex items-center justify-between p-4 bg-foreground/5 border border-border/30 rounded-xl hover:border-foreground/20 transition-colors"
>
<div className="flex items-center gap-3">
<div className="relative">
<div className={clsx(
"w-2.5 h-2.5 rounded-full",
alert.is_active ? "bg-accent" : "bg-foreground-subtle"
)} />
{alert.is_active && (
<span className="absolute inset-0 rounded-full bg-accent animate-ping opacity-40" />
)}
</div>
<div>
<Link
href={`/tld-pricing/${alert.tld}`}
className="text-sm font-mono font-medium text-foreground hover:text-accent transition-colors"
>
.{alert.tld}
</Link>
<p className="text-xs text-foreground-muted">
Alert on {alert.threshold_percent}% change
{alert.target_price && ` or below $${alert.target_price}`}
</p>
</div>
</div>
<button
onClick={() => handleDeletePriceAlert(alert.tld, alert.id)}
disabled={deletingAlertId === alert.id}
className="p-2 text-foreground-subtle hover:text-red-400 hover:bg-red-400/10 rounded-lg transition-all"
>
{deletingAlertId === alert.id ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Trash2 className="w-4 h-4" />
)}
</button>
</div>
))}
</div>
)}
</div>
</div>
)}
{/* Billing Tab */}
{activeTab === 'billing' && (
<div className="space-y-6">
{/* Current Plan */}
<div className="relative overflow-hidden rounded-2xl border border-border/40 bg-gradient-to-b from-background-secondary/40 to-background-secondary/20 backdrop-blur-sm p-6">
<h2 className="text-lg font-medium text-foreground mb-6">Your Current Plan</h2>
<div className="p-5 bg-accent/5 border border-accent/20 rounded-xl 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-xl font-semibold text-foreground">{tierName}</p>
<p className="text-sm text-foreground-muted">
{tierName === 'Scout' ? 'Free forever' : tierName === 'Trader' ? '$9/month' : '$29/month'}
</p>
</div>
</div>
<span className={clsx(
"px-3 py-1.5 text-xs font-medium rounded-full",
isProOrHigher ? "bg-accent/10 text-accent" : "bg-foreground/5 text-foreground-muted"
)}>
{isProOrHigher ? 'Active' : 'Free'}
</span>
</div>
{/* Plan Stats */}
<div className="grid grid-cols-3 gap-4 p-4 bg-background/50 rounded-xl mb-4">
<div className="text-center">
<p className="text-2xl font-semibold text-foreground">{subscription?.domain_limit || 5}</p>
<p className="text-xs text-foreground-muted">Domains</p>
</div>
<div className="text-center border-x border-border/50">
<p className="text-2xl font-semibold text-foreground">
{subscription?.check_frequency === 'realtime' ? '10m' :
subscription?.check_frequency === 'hourly' ? '1h' : '24h'}
</p>
<p className="text-xs text-foreground-muted">Check Interval</p>
</div>
<div className="text-center">
<p className="text-2xl font-semibold text-foreground">
{subscription?.portfolio_limit === -1 ? '∞' : subscription?.portfolio_limit || 0}
</p>
<p className="text-xs text-foreground-muted">Portfolio</p>
</div>
</div>
{isProOrHigher ? (
<button
onClick={handleOpenBillingPortal}
className="w-full flex items-center justify-center gap-2 py-3 bg-background text-foreground font-medium rounded-xl border border-border/50
hover:border-foreground/20 transition-all"
>
<ExternalLink className="w-4 h-4" />
Manage Subscription
</button>
) : (
<Link
href="/pricing"
className="w-full flex items-center justify-center gap-2 py-3 bg-gradient-to-r from-accent to-accent/80 text-background font-medium rounded-xl
hover:shadow-[0_0_20px_-5px_rgba(16,185,129,0.4)] transition-all"
>
<Zap className="w-4 h-4" />
Upgrade Plan
</Link>
)}
</div>
{/* Plan Features */}
<h3 className="text-sm font-medium text-foreground mb-3">Your Plan 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-sm">
<Check className="w-4 h-4 text-accent" />
<span className="text-foreground">{feature}</span>
</li>
))}
</ul>
</div>
</div>
)}
{/* Security Tab */}
{activeTab === 'security' && (
<div className="space-y-6">
<div className="relative overflow-hidden rounded-2xl border border-border/40 bg-gradient-to-b from-background-secondary/40 to-background-secondary/20 backdrop-blur-sm p-6">
<h2 className="text-lg font-medium text-foreground mb-4">Password</h2>
<p className="text-sm text-foreground-muted mb-5">
Change your password or reset it if you've forgotten it.
</p>
<Link
href="/forgot-password"
className="inline-flex items-center gap-2 px-5 py-3 bg-foreground/5 border border-border/50 text-foreground font-medium rounded-xl
hover:border-foreground/20 transition-all"
>
<Key className="w-4 h-4" />
Change Password
</Link>
</div>
<div className="relative overflow-hidden rounded-2xl border border-border/40 bg-gradient-to-b from-background-secondary/40 to-background-secondary/20 backdrop-blur-sm p-6">
<h2 className="text-lg font-medium text-foreground mb-5">Account Security</h2>
<div className="space-y-3">
<div className="flex items-center justify-between p-4 bg-foreground/5 border border-border/30 rounded-xl">
<div>
<p className="text-sm font-medium text-foreground">Email Verified</p>
<p className="text-xs text-foreground-muted">Your email address has been verified</p>
</div>
<div className="w-8 h-8 bg-accent/10 rounded-lg flex items-center justify-center">
<Check className="w-4 h-4 text-accent" />
</div>
</div>
<div className="flex items-center justify-between p-4 bg-foreground/5 border border-border/30 rounded-xl">
<div>
<p className="text-sm font-medium text-foreground">Two-Factor Authentication</p>
<p className="text-xs text-foreground-muted">Coming soon</p>
</div>
<span className="text-xs px-2.5 py-1 bg-foreground/5 text-foreground-muted rounded-full border border-border/30">Soon</span>
</div>
</div>
</div>
<div className="relative overflow-hidden rounded-2xl border border-red-500/20 bg-red-500/5 p-6">
<h2 className="text-lg font-medium text-red-400 mb-2">Danger Zone</h2>
<p className="text-sm text-foreground-muted mb-5">
Permanently delete your account and all associated data.
</p>
<button
className="px-5 py-3 bg-red-500 text-white font-medium rounded-xl hover:bg-red-500/90 transition-all"
>
Delete Account
</button>
</div>
</div>
)}
</div>
</div> </div>
</main> </PageContainer>
</CommandCenterLayout> </CommandCenterLayout>
) )
} }

View File

@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'
import { useStore } from '@/lib/store' import { useStore } from '@/lib/store'
import { api } from '@/lib/api' import { api } from '@/lib/api'
import { CommandCenterLayout } from '@/components/CommandCenterLayout' import { CommandCenterLayout } from '@/components/CommandCenterLayout'
import { PremiumTable, Badge, StatCard, PageContainer, TableActionButton } from '@/components/PremiumTable'
import { Toast, useToast } from '@/components/Toast' import { Toast, useToast } from '@/components/Toast'
import { import {
Plus, Plus,
@ -14,10 +15,11 @@ import {
BellOff, BellOff,
History, History,
ExternalLink, ExternalLink,
MoreVertical,
Search, Search,
Filter, Eye,
Sparkles,
ArrowUpRight, ArrowUpRight,
X,
} from 'lucide-react' } from 'lucide-react'
import clsx from 'clsx' import clsx from 'clsx'
import Link from 'next/link' import Link from 'next/link'
@ -29,57 +31,6 @@ interface DomainHistory {
checked_at: string checked_at: string
} }
// Status indicator component with traffic light system
function StatusIndicator({ domain }: { domain: any }) {
// Determine status based on domain data
let status: 'available' | 'watching' | 'stable' = 'stable'
let label = 'Stable'
let description = 'Domain is registered and active'
if (domain.is_available) {
status = 'available'
label = 'Available'
description = 'Domain is available for registration!'
} else if (domain.status === 'checking' || domain.status === 'pending') {
status = 'watching'
label = 'Watching'
description = 'Monitoring for changes'
}
const colors = {
available: 'bg-accent text-accent',
watching: 'bg-amber-400 text-amber-400',
stable: 'bg-foreground-muted text-foreground-muted',
}
return (
<div className="flex items-center gap-3">
<div className="relative">
<span className={clsx(
"block w-3 h-3 rounded-full",
colors[status].split(' ')[0]
)} />
{status === 'available' && (
<span className={clsx(
"absolute inset-0 rounded-full animate-ping opacity-75",
colors[status].split(' ')[0]
)} />
)}
</div>
<div>
<p className={clsx(
"text-sm font-medium",
status === 'available' ? 'text-accent' :
status === 'watching' ? 'text-amber-400' : 'text-foreground-muted'
)}>
{label}
</p>
<p className="text-xs text-foreground-subtle hidden sm:block">{description}</p>
</div>
</div>
)
}
export default function WatchlistPage() { export default function WatchlistPage() {
const { domains, addDomain, deleteDomain, refreshDomain, subscription } = useStore() const { domains, addDomain, deleteDomain, refreshDomain, subscription } = useStore()
const { toast, showToast, hideToast } = useToast() const { toast, showToast, hideToast } = useToast()
@ -97,11 +48,9 @@ export default function WatchlistPage() {
// Filter domains // Filter domains
const filteredDomains = domains?.filter(domain => { const filteredDomains = domains?.filter(domain => {
// Search filter
if (searchQuery && !domain.name.toLowerCase().includes(searchQuery.toLowerCase())) { if (searchQuery && !domain.name.toLowerCase().includes(searchQuery.toLowerCase())) {
return false return false
} }
// Status filter
if (filterStatus === 'available' && !domain.is_available) return false if (filterStatus === 'available' && !domain.is_available) return false
if (filterStatus === 'watching' && domain.is_available) return false if (filterStatus === 'watching' && domain.is_available) return false
return true return true
@ -110,6 +59,9 @@ export default function WatchlistPage() {
// Stats // Stats
const availableCount = domains?.filter(d => d.is_available).length || 0 const availableCount = domains?.filter(d => d.is_available).length || 0
const watchingCount = domains?.filter(d => !d.is_available).length || 0 const watchingCount = domains?.filter(d => !d.is_available).length || 0
const domainsUsed = domains?.length || 0
const domainLimit = subscription?.domain_limit || 5
const canAddMore = domainsUsed < domainLimit
const handleAddDomain = async (e: React.FormEvent) => { const handleAddDomain = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
@ -168,29 +120,6 @@ export default function WatchlistPage() {
} }
} }
const loadHistory = async (domainId: number) => {
if (selectedDomainId === domainId) {
setSelectedDomainId(null)
setDomainHistory(null)
return
}
setSelectedDomainId(domainId)
setLoadingHistory(true)
try {
const history = await api.getDomainHistory(domainId)
setDomainHistory(history)
} catch (err) {
setDomainHistory([])
} finally {
setLoadingHistory(false)
}
}
const domainLimit = subscription?.domain_limit || 5
const domainsUsed = domains?.length || 0
const canAddMore = domainsUsed < domainLimit
return ( return (
<CommandCenterLayout <CommandCenterLayout
title="Watchlist" title="Watchlist"
@ -198,65 +127,43 @@ export default function WatchlistPage() {
> >
{toast && <Toast message={toast.message} type={toast.type} onClose={hideToast} />} {toast && <Toast message={toast.message} type={toast.type} onClose={hideToast} />}
<div className="max-w-6xl mx-auto space-y-6"> <PageContainer>
{/* Stats Cards */} {/* Stats Cards */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<div className="p-5 bg-gradient-to-br from-background-secondary/60 to-background-secondary/30 border border-border/50 rounded-2xl"> <StatCard title="Total Watched" value={domainsUsed} icon={Eye} />
<p className="text-xs text-foreground-subtle uppercase tracking-wider mb-2">Total Watched</p> <StatCard title="Available" value={availableCount} icon={Sparkles} accent />
<p className="text-3xl font-display text-foreground">{domainsUsed}</p> <StatCard title="Watching" value={watchingCount} subtitle="still registered" icon={RefreshCw} />
</div> <StatCard title="Limit" value={domainLimit === -1 ? '∞' : domainLimit} subtitle="max domains" icon={Bell} />
<div className="relative p-5 bg-gradient-to-br from-accent/15 to-accent/5 border border-accent/30 rounded-2xl overflow-hidden">
<div className="absolute top-0 right-0 w-20 h-20 bg-accent/10 rounded-full blur-2xl" />
<div className="relative">
<p className="text-xs text-foreground-subtle uppercase tracking-wider mb-2">Available</p>
<p className="text-3xl font-display text-accent">{availableCount}</p>
</div>
</div>
<div className="p-5 bg-gradient-to-br from-background-secondary/60 to-background-secondary/30 border border-border/50 rounded-2xl">
<p className="text-xs text-foreground-subtle uppercase tracking-wider mb-2">Watching</p>
<p className="text-3xl font-display text-foreground">{watchingCount}</p>
</div>
<div className="p-5 bg-gradient-to-br from-background-secondary/60 to-background-secondary/30 border border-border/50 rounded-2xl">
<p className="text-xs text-foreground-subtle uppercase tracking-wider mb-2">Limit</p>
<p className="text-3xl font-display text-foreground">{domainLimit === -1 ? '∞' : domainLimit}</p>
</div>
</div> </div>
{/* Add Domain Form */} {/* Add Domain Form */}
<form onSubmit={handleAddDomain} className="flex gap-3"> <div className="flex flex-col sm:flex-row gap-3">
<div className="flex-1 relative"> <div className="relative flex-1">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-foreground-subtle" />
<input <input
type="text" type="text"
value={newDomain} value={newDomain}
onChange={(e) => setNewDomain(e.target.value)} onChange={(e) => setNewDomain(e.target.value)}
placeholder="Enter domain to track (e.g., dream.com)" placeholder="Enter domain to track (e.g., dream.com)"
disabled={!canAddMore} disabled={!canAddMore}
className={clsx( onKeyDown={(e) => e.key === 'Enter' && handleAddDomain(e)}
"w-full h-12 px-5 bg-background-secondary/50 border border-border/50 rounded-xl", className="w-full h-11 pl-11 pr-4 bg-background-secondary/50 border border-border/50 rounded-xl
"text-foreground placeholder:text-foreground-subtle", text-sm text-foreground placeholder:text-foreground-subtle
"focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent/30", focus:outline-none focus:border-accent/50 transition-all
"disabled:opacity-50 disabled:cursor-not-allowed", disabled:opacity-50 disabled:cursor-not-allowed"
"shadow-[0_2px_8px_-2px_rgba(0,0,0,0.1)]"
)}
/> />
</div> </div>
<button <button
type="submit" onClick={handleAddDomain}
disabled={adding || !newDomain.trim() || !canAddMore} disabled={adding || !newDomain.trim() || !canAddMore}
className={clsx( className="flex items-center justify-center gap-2 h-11 px-6 bg-gradient-to-r from-accent to-accent/80
"flex items-center gap-2 h-12 px-6 rounded-xl font-medium transition-all", text-background rounded-xl font-medium hover:shadow-[0_0_20px_-5px_rgba(16,185,129,0.4)]
"bg-gradient-to-r from-accent to-accent/80 text-background hover:shadow-[0_0_20px_-5px_rgba(16,185,129,0.4)]", disabled:opacity-50 disabled:cursor-not-allowed transition-all"
"disabled:opacity-50 disabled:cursor-not-allowed"
)}
> >
{adding ? ( {adding ? <Loader2 className="w-4 h-4 animate-spin" /> : <Plus className="w-4 h-4" />}
<Loader2 className="w-4 h-4 animate-spin" /> <span>Add Domain</span>
) : (
<Plus className="w-4 h-4" />
)}
Add
</button> </button>
</form> </div>
{!canAddMore && ( {!canAddMore && (
<div className="flex items-center justify-between p-4 bg-amber-500/10 border border-amber-500/20 rounded-xl"> <div className="flex items-center justify-between p-4 bg-amber-500/10 border border-amber-500/20 rounded-xl">
@ -274,225 +181,165 @@ export default function WatchlistPage() {
{/* Filters */} {/* Filters */}
<div className="flex flex-col sm:flex-row gap-3 sm:items-center sm:justify-between"> <div className="flex flex-col sm:flex-row gap-3 sm:items-center sm:justify-between">
<div className="flex gap-2"> <div className="flex flex-wrap items-center gap-2 p-1.5 bg-background-secondary/30 border border-border/30 rounded-2xl w-fit">
<button {[
onClick={() => setFilterStatus('all')} { id: 'all' as const, label: 'All', count: domainsUsed },
className={clsx( { id: 'available' as const, label: 'Available', count: availableCount, color: 'accent' },
"px-4 py-2 text-sm rounded-lg transition-colors", { id: 'watching' as const, label: 'Watching', count: watchingCount },
filterStatus === 'all' ].map((tab) => (
? "bg-foreground/10 text-foreground" <button
: "text-foreground-muted hover:bg-foreground/5" key={tab.id}
)} onClick={() => setFilterStatus(tab.id)}
> className={clsx(
All ({domainsUsed}) "flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-xl transition-all",
</button> filterStatus === tab.id
<button ? tab.color === 'accent'
onClick={() => setFilterStatus('available')} ? "bg-gradient-to-r from-accent to-accent/80 text-background shadow-lg shadow-accent/20"
className={clsx( : "bg-foreground/10 text-foreground"
"px-4 py-2 text-sm rounded-lg transition-colors flex items-center gap-2", : "text-foreground-muted hover:text-foreground hover:bg-foreground/5"
filterStatus === 'available' )}
? "bg-accent/10 text-accent" >
: "text-foreground-muted hover:bg-foreground/5" {tab.id === 'available' && <span className="w-2 h-2 rounded-full bg-accent" />}
)} {tab.id === 'watching' && <span className="w-2 h-2 rounded-full bg-foreground-muted" />}
> <span>{tab.label}</span>
<span className="w-2 h-2 rounded-full bg-accent" /> <span className={clsx(
Available ({availableCount}) "text-xs px-1.5 py-0.5 rounded",
</button> filterStatus === tab.id ? "bg-background/20" : "bg-foreground/10"
<button )}>{tab.count}</span>
onClick={() => setFilterStatus('watching')} </button>
className={clsx( ))}
"px-4 py-2 text-sm rounded-lg transition-colors flex items-center gap-2",
filterStatus === 'watching'
? "bg-foreground/10 text-foreground"
: "text-foreground-muted hover:bg-foreground/5"
)}
>
<span className="w-2 h-2 rounded-full bg-foreground-muted" />
Watching ({watchingCount})
</button>
</div> </div>
<div className="relative"> <div className="relative max-w-xs">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-foreground-muted" /> <Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-foreground-muted" />
<input <input
type="text" type="text"
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search domains..." placeholder="Filter domains..."
className="w-full sm:w-64 h-10 pl-9 pr-4 bg-background-secondary border border-border rounded-lg className="w-full h-10 pl-10 pr-10 bg-background-secondary/50 border border-border/50 rounded-xl
text-sm text-foreground placeholder:text-foreground-subtle text-sm text-foreground placeholder:text-foreground-subtle
focus:outline-none focus:border-accent" focus:outline-none focus:border-accent/50"
/> />
{searchQuery && (
<button onClick={() => setSearchQuery('')} className="absolute right-3 top-1/2 -translate-y-1/2 text-foreground-subtle hover:text-foreground">
<X className="w-4 h-4" />
</button>
)}
</div> </div>
</div> </div>
{/* Domain List */} {/* Domain Table */}
<div className="space-y-3"> <PremiumTable
{filteredDomains.length === 0 ? ( data={filteredDomains}
<div className="text-center py-16 bg-background-secondary/30 border border-border rounded-xl"> keyExtractor={(d) => d.id}
{domainsUsed === 0 ? ( emptyIcon={<Eye className="w-12 h-12 text-foreground-subtle" />}
<> emptyTitle={domainsUsed === 0 ? "Your watchlist is empty" : "No domains match your filters"}
<div className="w-16 h-16 bg-foreground/5 rounded-2xl flex items-center justify-center mx-auto mb-4"> emptyDescription={domainsUsed === 0 ? "Add a domain above to start tracking" : "Try adjusting your filter criteria"}
<Plus className="w-8 h-8 text-foreground-subtle" /> columns={[
</div> {
<p className="text-foreground-muted mb-2">Your watchlist is empty</p> key: 'domain',
<p className="text-sm text-foreground-subtle">Add a domain above to start tracking</p> header: 'Domain',
</> render: (domain) => (
) : ( <div className="flex items-center gap-3">
<> <div className="relative">
<Filter className="w-8 h-8 text-foreground-subtle mx-auto mb-4" /> <span className={clsx(
<p className="text-foreground-muted">No domains match your filters</p> "block w-3 h-3 rounded-full",
</> domain.is_available ? "bg-accent" : "bg-foreground-muted/50"
)} )} />
</div>
) : (
filteredDomains.map((domain) => (
<div
key={domain.id}
className={clsx(
"group p-4 sm:p-5 rounded-xl border transition-all duration-200",
domain.is_available
? "bg-accent/5 border-accent/20 hover:border-accent/40"
: "bg-background-secondary/50 border-border hover:border-foreground/20"
)}
>
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
{/* Domain Name + Status */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-lg font-semibold text-foreground truncate">
{domain.name}
</h3>
{domain.is_available && (
<span className="shrink-0 px-2 py-0.5 bg-accent/20 text-accent text-xs font-semibold rounded-full">
GRAB IT!
</span>
)}
</div>
<StatusIndicator domain={domain} />
</div>
{/* Actions */}
<div className="flex items-center gap-2 shrink-0">
{/* Notify Toggle */}
<button
onClick={() => handleToggleNotify(domain.id, domain.notify_on_available)}
disabled={togglingNotifyId === domain.id}
className={clsx(
"p-2 rounded-lg transition-colors",
domain.notify_on_available
? "bg-accent/10 text-accent hover:bg-accent/20"
: "text-foreground-muted hover:bg-foreground/5"
)}
title={domain.notify_on_available ? "Disable alerts" : "Enable alerts"}
>
{togglingNotifyId === domain.id ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : domain.notify_on_available ? (
<Bell className="w-4 h-4" />
) : (
<BellOff className="w-4 h-4" />
)}
</button>
{/* History */}
<button
onClick={() => loadHistory(domain.id)}
className={clsx(
"p-2 rounded-lg transition-colors",
selectedDomainId === domain.id
? "bg-foreground/10 text-foreground"
: "text-foreground-muted hover:bg-foreground/5"
)}
title="View history"
>
<History className="w-4 h-4" />
</button>
{/* Refresh */}
<button
onClick={() => handleRefresh(domain.id)}
disabled={refreshingId === domain.id}
className="p-2 rounded-lg text-foreground-muted hover:bg-foreground/5 transition-colors"
title="Refresh status"
>
<RefreshCw className={clsx(
"w-4 h-4",
refreshingId === domain.id && "animate-spin"
)} />
</button>
{/* Delete */}
<button
onClick={() => handleDelete(domain.id, domain.name)}
disabled={deletingId === domain.id}
className="p-2 rounded-lg text-foreground-muted hover:text-red-400 hover:bg-red-400/10 transition-colors"
title="Remove"
>
{deletingId === domain.id ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Trash2 className="w-4 h-4" />
)}
</button>
{/* External Link (if available) */}
{domain.is_available && ( {domain.is_available && (
<a <span className="absolute inset-0 rounded-full bg-accent animate-ping opacity-50" />
href={`https://www.namecheap.com/domains/registration/results/?domain=${domain.name}`} )}
target="_blank" </div>
rel="noopener noreferrer" <div>
className="flex items-center gap-2 px-4 py-2 bg-accent text-background rounded-lg <span className="font-mono font-medium text-foreground">{domain.name}</span>
font-medium text-sm hover:bg-accent-hover transition-colors" {domain.is_available && (
> <Badge variant="success" size="xs" className="ml-2">AVAILABLE</Badge>
Register
<ExternalLink className="w-3 h-3" />
</a>
)} )}
</div> </div>
</div> </div>
),
{/* History Panel */} },
{selectedDomainId === domain.id && ( {
<div className="mt-4 pt-4 border-t border-border/50"> key: 'status',
<h4 className="text-sm font-medium text-foreground-muted mb-3">Status History</h4> header: 'Status',
{loadingHistory ? ( hideOnMobile: true,
<div className="flex items-center gap-2 text-foreground-muted"> render: (domain) => (
<Loader2 className="w-4 h-4 animate-spin" /> <span className={clsx(
<span className="text-sm">Acquiring targets...</span> "text-sm",
</div> domain.is_available ? "text-accent font-medium" : "text-foreground-muted"
) : domainHistory && domainHistory.length > 0 ? ( )}>
<div className="space-y-2"> {domain.is_available ? 'Ready to register!' : 'Monitoring...'}
{domainHistory.slice(0, 5).map((entry) => ( </span>
<div ),
key={entry.id} },
className="flex items-center gap-3 text-sm" {
> key: 'notifications',
<span className={clsx( header: 'Alerts',
"w-2 h-2 rounded-full", align: 'center',
entry.is_available ? "bg-accent" : "bg-foreground-muted" hideOnMobile: true,
)} /> render: (domain) => (
<span className="text-foreground-muted"> <button
{new Date(entry.checked_at).toLocaleDateString()} at{' '} onClick={(e) => {
{new Date(entry.checked_at).toLocaleTimeString()} e.stopPropagation()
</span> handleToggleNotify(domain.id, domain.notify_on_available)
<span className="text-foreground"> }}
{entry.is_available ? 'Available' : 'Registered'} disabled={togglingNotifyId === domain.id}
</span> className={clsx(
</div> "p-2 rounded-lg transition-colors",
))} domain.notify_on_available
</div> ? "bg-accent/10 text-accent hover:bg-accent/20"
) : ( : "text-foreground-muted hover:bg-foreground/5"
<p className="text-sm text-foreground-subtle">No history available yet</p> )}
)} title={domain.notify_on_available ? "Disable alerts" : "Enable alerts"}
</div> >
)} {togglingNotifyId === domain.id ? (
</div> <Loader2 className="w-4 h-4 animate-spin" />
)) ) : domain.notify_on_available ? (
)} <Bell className="w-4 h-4" />
</div> ) : (
</div> <BellOff className="w-4 h-4" />
)}
</button>
),
},
{
key: 'actions',
header: '',
align: 'right',
render: (domain) => (
<div className="flex items-center gap-1 justify-end">
<TableActionButton
icon={RefreshCw}
onClick={() => handleRefresh(domain.id)}
loading={refreshingId === domain.id}
title="Refresh status"
/>
<TableActionButton
icon={Trash2}
onClick={() => handleDelete(domain.id, domain.name)}
variant="danger"
loading={deletingId === domain.id}
title="Remove"
/>
{domain.is_available && (
<a
href={`https://www.namecheap.com/domains/registration/results/?domain=${domain.name}`}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="flex items-center gap-1.5 px-3 py-2 bg-accent text-background text-xs font-medium
rounded-lg hover:bg-accent-hover transition-colors ml-1"
>
Register <ExternalLink className="w-3 h-3" />
</a>
)}
</div>
),
},
]}
/>
</PageContainer>
</CommandCenterLayout> </CommandCenterLayout>
) )
} }

View File

@ -397,22 +397,27 @@ export function SectionHeader({
subtitle, subtitle,
icon: Icon, icon: Icon,
action, action,
compact = false,
}: { }: {
title: string title: string
subtitle?: string subtitle?: string
icon?: React.ComponentType<{ className?: string }> icon?: React.ComponentType<{ className?: string }>
action?: ReactNode action?: ReactNode
compact?: boolean
}) { }) {
return ( return (
<div className="flex items-center justify-between mb-6"> <div className={clsx("flex items-center justify-between", !compact && "mb-6")}>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{Icon && ( {Icon && (
<div className="w-10 h-10 bg-accent/10 border border-accent/20 rounded-xl flex items-center justify-center"> <div className={clsx(
<Icon className="w-5 h-5 text-accent" /> "bg-accent/10 border border-accent/20 rounded-xl flex items-center justify-center",
compact ? "w-9 h-9" : "w-10 h-10"
)}>
<Icon className={clsx(compact ? "w-4 h-4" : "w-5 h-5", "text-accent")} />
</div> </div>
)} )}
<div> <div>
<h2 className="text-lg font-semibold text-foreground">{title}</h2> <h2 className={clsx(compact ? "text-base" : "text-lg", "font-semibold text-foreground")}>{title}</h2>
{subtitle && <p className="text-sm text-foreground-muted">{subtitle}</p>} {subtitle && <p className="text-sm text-foreground-muted">{subtitle}</p>}
</div> </div>
</div> </div>