Intel, Sniper, Yield pages redesign
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled

This commit is contained in:
2025-12-12 22:45:55 +01:00
parent b820690478
commit b7fa3632bf
4 changed files with 959 additions and 1281 deletions

View File

@ -1,11 +1,10 @@
'use client' 'use client'
import { useEffect, useState, useMemo, useCallback, memo } from 'react' import { useEffect, useState, useMemo, useCallback } from 'react'
import { useStore } from '@/lib/store' import { useStore } from '@/lib/store'
import { api } from '@/lib/api' import { api } from '@/lib/api'
import { TerminalLayout } from '@/components/TerminalLayout' import { CommandCenterLayout } from '@/components/CommandCenterLayout'
import { import {
ExternalLink,
Loader2, Loader2,
TrendingUp, TrendingUp,
TrendingDown, TrendingDown,
@ -16,28 +15,39 @@ import {
Search, Search,
ChevronDown, ChevronDown,
ChevronUp, ChevronUp,
Info,
ArrowRight, ArrowRight,
Lock, Lock,
Sparkles, Sparkles,
BarChart3, BarChart3,
Activity,
Zap, Zap,
Filter,
Check,
Eye,
ShieldCheck,
Diamond,
Minus Minus
} from 'lucide-react' } from 'lucide-react'
import clsx from 'clsx' import clsx from 'clsx'
import Link from 'next/link' import Link from 'next/link'
// ============================================================================ // ============================================================================
// TIER ACCESS LEVELS // TYPES
// ============================================================================ // ============================================================================
type UserTier = 'scout' | 'trader' | 'tycoon' type UserTier = 'scout' | 'trader' | 'tycoon'
type SortField = 'tld' | 'price' | 'renewal' | 'change' | 'change3y' | 'risk' | 'popularity'
type SortDirection = 'asc' | 'desc'
interface TLDData {
tld: string
min_price: number
avg_price: number
max_price: number
min_renewal_price: number
avg_renewal_price: number
price_change_7d: number
price_change_1y: number
price_change_3y: number
risk_level: 'low' | 'medium' | 'high'
risk_reason: string
popularity_rank?: number
type?: string
}
function getTierLevel(tier: UserTier): number { function getTierLevel(tier: UserTier): number {
switch (tier) { switch (tier) {
@ -48,175 +58,7 @@ function getTierLevel(tier: UserTier): number {
} }
} }
// ============================================================================ const formatPrice = (p: number) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(p)
// SHARED COMPONENTS
// ============================================================================
const Tooltip = memo(({ children, content }: { children: React.ReactNode; content: string }) => (
<div className="relative flex items-center group/tooltip w-fit">
{children}
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-zinc-900 border border-zinc-800 rounded text-[10px] text-zinc-300 whitespace-nowrap opacity-0 group-hover/tooltip:opacity-100 transition-opacity pointer-events-none z-50 shadow-xl max-w-xs text-center">
{content}
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-px border-4 border-transparent border-t-zinc-800" />
</div>
</div>
))
Tooltip.displayName = 'Tooltip'
const LockedFeature = memo(({ requiredTier, currentTier }: { requiredTier: UserTier; currentTier: UserTier }) => {
const tierNames = { scout: 'Scout', trader: 'Trader', tycoon: 'Tycoon' }
return (
<Tooltip content={`Upgrade to ${tierNames[requiredTier]} to unlock`}>
<div className="flex items-center gap-1.5 text-zinc-600 cursor-help px-2 py-1 rounded bg-zinc-900/50 border border-zinc-800 hover:bg-zinc-900 transition-colors">
<Lock className="w-3 h-3" />
<span className="text-[10px] font-medium uppercase tracking-wider">Locked</span>
</div>
</Tooltip>
)
})
LockedFeature.displayName = 'LockedFeature'
const StatCard = memo(({
label,
value,
subValue,
icon: Icon,
highlight,
locked = false,
lockTooltip
}: {
label: string
value: string | number
subValue?: string
icon: any
highlight?: boolean
locked?: boolean
lockTooltip?: string
}) => (
<div className={clsx(
"bg-zinc-900/40 border p-4 relative overflow-hidden group hover:border-white/10 transition-colors",
highlight ? "border-emerald-500/30" : "border-white/5"
)}>
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
<Icon className="w-16 h-16" />
</div>
<div className="relative z-10">
<div className="flex items-center gap-2 text-zinc-400 mb-1">
<Icon className={clsx("w-4 h-4", highlight && "text-emerald-400")} />
<span className="text-xs font-medium uppercase tracking-wider">{label}</span>
</div>
{locked ? (
<Tooltip content={lockTooltip || 'Upgrade to unlock'}>
<div className="flex items-center gap-2 text-zinc-600 cursor-help mt-1">
<Lock className="w-5 h-5" />
<span className="text-2xl font-bold"></span>
</div>
</Tooltip>
) : (
<div className="flex items-baseline gap-2">
<span className="text-2xl font-bold text-white tracking-tight">{value}</span>
{subValue && <span className="text-xs text-zinc-500 font-medium">{subValue}</span>}
</div>
)}
{highlight && (
<div className="mt-2 text-[10px] font-medium px-1.5 py-0.5 w-fit rounded border text-emerald-400 border-emerald-400/20 bg-emerald-400/5">
LIVE
</div>
)}
</div>
</div>
))
StatCard.displayName = 'StatCard'
const FilterToggle = memo(({ active, onClick, label, icon: Icon }: {
active: boolean
onClick: () => void
label: string
icon?: any
}) => (
<button
onClick={onClick}
className={clsx(
"px-4 py-1.5 rounded-md text-xs font-medium transition-all flex items-center gap-2 whitespace-nowrap border",
active
? "bg-zinc-800 text-white border-zinc-600 shadow-sm"
: "bg-transparent text-zinc-400 border-zinc-800 hover:text-zinc-200 hover:bg-white/5"
)}
>
{Icon && <Icon className="w-3.5 h-3.5" />}
{label}
</button>
))
FilterToggle.displayName = 'FilterToggle'
type SortField = 'tld' | 'price' | 'renewal' | 'change' | 'change3y' | 'risk' | 'popularity'
type SortDirection = 'asc' | 'desc'
const SortableHeader = memo(({
label, field, currentSort, currentDirection, onSort, align = 'left', tooltip, locked = false, lockTooltip
}: {
label: string; field: SortField; currentSort: SortField; currentDirection: SortDirection; onSort: (field: SortField) => void; align?: 'left'|'center'|'right'; tooltip?: string; locked?: boolean; lockTooltip?: string
}) => {
const isActive = currentSort === field
return (
<div className={clsx(
"flex items-center gap-1",
align === 'right' && "justify-end ml-auto",
align === 'center' && "justify-center mx-auto"
)}>
<button
onClick={() => !locked && onSort(field)}
disabled={locked}
className={clsx(
"flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider transition-all group select-none py-2",
locked ? "text-zinc-600 cursor-not-allowed" : isActive ? "text-zinc-300" : "text-zinc-500 hover:text-zinc-400"
)}
>
{label}
{locked ? (
<Tooltip content={lockTooltip || 'Upgrade to unlock'}>
<Lock className="w-2.5 h-2.5 text-zinc-600" />
</Tooltip>
) : (
<div className={clsx("flex flex-col -space-y-1 transition-opacity", isActive ? "opacity-100" : "opacity-0 group-hover:opacity-30")}>
<ChevronUp className={clsx("w-2 h-2", isActive && currentDirection === 'asc' ? "text-zinc-300" : "text-zinc-600")} />
<ChevronDown className={clsx("w-2 h-2", isActive && currentDirection === 'desc' ? "text-zinc-300" : "text-zinc-600")} />
</div>
)}
</button>
{tooltip && !locked && (
<Tooltip content={tooltip}>
<Info className="w-3 h-3 text-zinc-700 hover:text-zinc-500 transition-colors cursor-help" />
</Tooltip>
)}
</div>
)
})
SortableHeader.displayName = 'SortableHeader'
// ============================================================================
// TYPES
// ============================================================================
interface TLDData {
tld: string
min_price: number
avg_price: number
max_price: number
min_renewal_price: number
avg_renewal_price: number
cheapest_registrar?: string
cheapest_registrar_url?: string
price_change_7d: number
price_change_1y: number
price_change_3y: number
risk_level: 'low' | 'medium' | 'high'
risk_reason: string
popularity_rank?: number
type?: string
}
// ============================================================================ // ============================================================================
// MAIN PAGE // MAIN PAGE
@ -225,30 +67,23 @@ interface TLDData {
export default function IntelPage() { export default function IntelPage() {
const { subscription } = useStore() const { subscription } = useStore()
// Determine user tier
const userTier: UserTier = (subscription?.tier as UserTier) || 'scout' const userTier: UserTier = (subscription?.tier as UserTier) || 'scout'
const tierLevel = getTierLevel(userTier) const tierLevel = getTierLevel(userTier)
// Feature access checks const canSeeRenewal = tierLevel >= 2
const canSeeRenewal = tierLevel >= 2 // Trader+ const canSee3yTrend = tierLevel >= 3
const canSee3yTrend = tierLevel >= 3 // Tycoon only
const canSeeFullHistory = tierLevel >= 3 // Tycoon only
// Data
const [tldData, setTldData] = useState<TLDData[]>([]) const [tldData, setTldData] = useState<TLDData[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [refreshing, setRefreshing] = useState(false) const [refreshing, setRefreshing] = useState(false)
const [total, setTotal] = useState(0) const [total, setTotal] = useState(0)
// Filters
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const [filterType, setFilterType] = useState<'all' | 'tech' | 'geo' | 'budget'>('all') const [filterType, setFilterType] = useState<'all' | 'tech' | 'geo' | 'budget'>('all')
// Sort
const [sortField, setSortField] = useState<SortField>('popularity') const [sortField, setSortField] = useState<SortField>('popularity')
const [sortDirection, setSortDirection] = useState<SortDirection>('asc') const [sortDirection, setSortDirection] = useState<SortDirection>('asc')
// Load Data
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
setLoading(true) setLoading(true)
try { try {
@ -296,18 +131,12 @@ export default function IntelPage() {
} }
}, [sortField, canSeeRenewal, canSee3yTrend]) }, [sortField, canSeeRenewal, canSee3yTrend])
// Transform & Filter
const filteredData = useMemo(() => { const filteredData = useMemo(() => {
let data = tldData let data = tldData
// Tech filter: common tech-related TLDs
const techTlds = ['ai', 'io', 'app', 'dev', 'tech', 'cloud', 'digital', 'software', 'code', 'systems', 'network', 'data', 'cyber', 'online', 'web', 'api', 'hosting'] const techTlds = ['ai', 'io', 'app', 'dev', 'tech', 'cloud', 'digital', 'software', 'code', 'systems', 'network', 'data', 'cyber', 'online', 'web', 'api', 'hosting']
if (filterType === 'tech') data = data.filter(t => techTlds.includes(t.tld) || t.tld === 'io' || t.tld === 'ai') if (filterType === 'tech') data = data.filter(t => techTlds.includes(t.tld) || t.tld === 'io' || t.tld === 'ai')
// Geo filter: all country-code TLDs (ccTLD type from backend)
if (filterType === 'geo') data = data.filter(t => t.type === 'ccTLD') if (filterType === 'geo') data = data.filter(t => t.type === 'ccTLD')
// Budget filter: registration under $10
if (filterType === 'budget') data = data.filter(t => t.min_price < 10) if (filterType === 'budget') data = data.filter(t => t.min_price < 10)
if (searchQuery) { if (searchQuery) {
@ -341,295 +170,277 @@ export default function IntelPage() {
return { lowest, hottest, traps, avgRenewal } return { lowest, hottest, traps, avgRenewal }
}, [tldData]) }, [tldData])
const formatPrice = (p: number) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(p)
return ( return (
<TerminalLayout hideHeaderSearch={true}> <CommandCenterLayout minimal>
<div className="relative font-sans text-zinc-100 selection:bg-emerald-500/30"> {/* ═══════════════════════════════════════════════════════════════════════ */}
{/* HEADER */}
{/* Ambient Background Glow */} {/* ═══════════════════════════════════════════════════════════════════════ */}
<div className="fixed inset-0 pointer-events-none overflow-hidden"> <section className="pt-6 lg:pt-8 pb-6">
<div className="absolute top-0 right-1/4 w-[800px] h-[600px] bg-emerald-500/5 rounded-full blur-[120px] mix-blend-screen" /> <div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-6">
<div className="absolute bottom-0 left-1/4 w-[600px] h-[500px] bg-blue-500/5 rounded-full blur-[100px] mix-blend-screen" /> <div className="space-y-3">
</div> <div className="inline-flex items-center gap-2">
<BarChart3 className="w-4 h-4 text-accent" />
<div className="relative z-10 max-w-[1600px] mx-auto p-4 md:p-8 space-y-8"> <span className="text-[10px] font-mono tracking-wide text-accent">Pricing Analytics</span>
{/* Header Section */}
<div className="flex flex-col md:flex-row justify-between items-start md:items-end gap-6 border-b border-white/5 pb-8">
<div className="space-y-2">
<div className="flex items-center gap-3">
<div className="h-8 w-1 bg-emerald-500 rounded-full shadow-[0_0_10px_rgba(16,185,129,0.5)]" />
<h1 className="text-3xl font-bold tracking-tight text-white">TLD Intelligence</h1>
</div>
<p className="text-zinc-400 max-w-lg">
Inflation Monitor & Pricing Analytics across 800+ TLDs.
</p>
</div> </div>
{/* Quick Stats Pills */} <h1 className="font-display text-[2rem] lg:text-[2.5rem] leading-[1] tracking-[-0.02em]">
<div className="flex gap-2"> <span className="text-white">TLD Intel</span>
<div className={clsx( <span className="text-white/30 ml-3">{total}</span>
"px-3 py-1.5 rounded-full border flex items-center gap-2 text-xs font-medium", </h1>
userTier === 'tycoon' ? "bg-amber-500/5 border-amber-500/20 text-amber-400" :
userTier === 'trader' ? "bg-blue-500/5 border-blue-500/20 text-blue-400" :
"bg-white/5 border-white/10 text-zinc-300"
)}>
<Diamond className="w-3.5 h-3.5" />
{userTier === 'tycoon' ? 'Tycoon Access' : userTier === 'trader' ? 'Trader Access' : 'Scout Access'}
</div>
<div className="px-3 py-1.5 rounded-full bg-white/5 border border-white/10 flex items-center gap-2 text-xs font-medium text-zinc-300">
<Activity className="w-3.5 h-3.5 text-emerald-400" />
{total} Tracked
</div>
</div>
</div>
{/* Metric Grid */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<StatCard
label="Tracked TLDs"
value={total}
icon={Globe}
highlight={true}
/>
<StatCard
label="Lowest Entry"
value={formatPrice(stats.lowest)}
subValue="Registration"
icon={DollarSign}
/>
<StatCard
label="Avg. Renewal"
value={canSeeRenewal ? formatPrice(stats.avgRenewal) : '—'}
subValue={canSeeRenewal ? "/ year" : undefined}
icon={RefreshCw}
locked={!canSeeRenewal}
lockTooltip="Upgrade to Trader to see renewal prices"
/>
<StatCard
label="Renewal Traps"
value={stats.traps}
subValue="High Risk"
icon={AlertTriangle}
/>
</div>
{/* Control Bar */}
<div className="sticky top-4 z-30 bg-black/80 backdrop-blur-md border border-white/10 rounded-xl p-2 flex flex-col md:flex-row gap-4 items-center justify-between shadow-2xl">
{/* Filter Pills */}
<div className="flex items-center gap-2 overflow-x-auto w-full pb-1 md:pb-0 scrollbar-hide">
<FilterToggle active={filterType === 'all'} onClick={() => setFilterType('all')} label="All TLDs" />
<FilterToggle active={filterType === 'tech'} onClick={() => setFilterType('tech')} label="Tech" icon={Zap} />
<FilterToggle active={filterType === 'geo'} onClick={() => setFilterType('geo')} label="Geo / National" icon={Globe} />
<FilterToggle active={filterType === 'budget'} onClick={() => setFilterType('budget')} label="Budget <$10" icon={DollarSign} />
</div>
{/* Refresh Button (Mobile) */}
<button
onClick={handleRefresh}
className="md:hidden p-2 text-zinc-400 hover:text-white"
>
<RefreshCw className={clsx("w-4 h-4", refreshing && "animate-spin")} />
</button>
{/* Search Filter */}
<div className="relative w-full md:w-64 flex-shrink-0">
<Search className="absolute left-3 top-2.5 w-4 h-4 text-zinc-500" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search TLDs..."
className="w-full bg-black/50 border border-white/10 rounded-lg pl-9 pr-4 py-2 text-sm text-white placeholder:text-zinc-600 focus:outline-none focus:border-white/20 transition-all"
/>
</div>
</div>
{/* DATA GRID */}
<div className="bg-zinc-900/40 border border-white/5 rounded-xl overflow-hidden backdrop-blur-sm">
{/* Unified Table Header - Use a wrapper with min-width to force scrolling instead of breaking */}
<div className="overflow-x-auto">
<div className="min-w-[1000px]"> {/* Force minimum width */}
<div className="grid grid-cols-12 gap-4 px-6 py-3 bg-white/[0.02] border-b border-white/5 text-[11px] font-semibold text-zinc-500 uppercase tracking-wider sticky top-0 z-20 backdrop-blur-sm items-center">
<div className="col-span-2">
<SortableHeader label="Extension" field="tld" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} />
</div>
<div className="col-span-2 text-right">
<SortableHeader label="Reg. Price" field="price" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} align="right" />
</div>
<div className="col-span-2 text-right">
<SortableHeader
label="Renewal"
field="renewal"
currentSort={sortField}
currentDirection={sortDirection}
onSort={handleSort}
align="right"
locked={!canSeeRenewal}
lockTooltip="Upgrade to Trader to unlock"
/>
</div>
<div className="col-span-2 text-center">
<SortableHeader label="Trend (1y)" field="change" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} align="center" />
</div>
<div className="col-span-2 text-center">
{canSee3yTrend ? (
<SortableHeader
label="Trend (3y)"
field="change3y"
currentSort={sortField}
currentDirection={sortDirection}
onSort={handleSort}
align="center"
locked={!canSeeFullHistory}
/>
) : (
<span className="text-zinc-700 select-none">Trend (3y)</span>
)}
</div>
<div className="col-span-1 text-center">
<SortableHeader label="Risk" field="risk" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} align="center" />
</div>
<div className="col-span-1 text-right py-2">Action</div>
</div>
{/* Rows */}
{loading ? (
<div className="flex flex-col items-center justify-center py-32 space-y-4">
<Loader2 className="w-8 h-8 text-emerald-500 animate-spin" />
<p className="text-zinc-500 text-sm animate-pulse">Analyzing registry data...</p>
</div>
) : filteredData.length === 0 ? (
<div className="flex flex-col items-center justify-center py-20 text-center">
<div className="w-16 h-16 rounded-full bg-white/5 flex items-center justify-center mb-4">
<Search className="w-8 h-8 text-zinc-600" />
</div>
<h3 className="text-lg font-medium text-white mb-1">No TLDs found</h3>
<p className="text-zinc-500 text-sm max-w-xs mx-auto">
Try adjusting your filters or search query.
</p>
</div>
) : (
<div className="divide-y divide-white/5">
{filteredData.map((tld) => {
const isTrap = tld.min_renewal_price > tld.min_price * 1.5
const trend = tld.price_change_1y || 0
const trend3y = tld.price_change_3y || 0
return (
<div key={tld.tld} className="grid grid-cols-12 gap-4 px-6 py-4 items-center hover:bg-white/[0.04] transition-all group relative">
{/* TLD */}
<div className="col-span-2">
<Link href={`/terminal/intel/${tld.tld}`} className="flex items-center gap-2 group/link">
<span className="font-mono font-bold text-white text-[15px] group-hover/link:text-emerald-400 transition-colors">.{tld.tld}</span>
</Link>
</div>
{/* Price */}
<div className="col-span-2 text-right">
<span className="font-mono font-medium text-white whitespace-nowrap">{formatPrice(tld.min_price)}</span>
</div>
{/* Renewal (Trader+) */}
<div className="col-span-2 text-right flex items-center justify-end gap-2">
{canSeeRenewal ? (
<>
<span className={clsx("font-mono font-medium whitespace-nowrap", isTrap ? "text-amber-400" : "text-zinc-400")}>
{formatPrice(tld.min_renewal_price)}
</span>
{isTrap && (
<Tooltip content={`Renewal is ${(tld.min_renewal_price/tld.min_price).toFixed(1)}x higher!`}>
<AlertTriangle className="w-3.5 h-3.5 text-amber-400 cursor-help flex-shrink-0" />
</Tooltip>
)}
</>
) : (
<LockedFeature requiredTier="trader" currentTier={userTier} />
)}
</div>
{/* Trend 1y */}
<div className="col-span-2 flex justify-center">
<div className={clsx("flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium font-mono whitespace-nowrap",
trend > 5 ? "text-orange-400 bg-orange-400/5 border border-orange-400/20" :
trend < -5 ? "text-emerald-400 bg-emerald-400/5 border border-emerald-400/20" :
"text-zinc-400 bg-zinc-800/50 border border-zinc-700"
)}>
{trend > 0 ? <TrendingUp className="w-3 h-3" /> : trend < 0 ? <TrendingDown className="w-3 h-3" /> : <Minus className="w-3 h-3" />}
{Math.abs(trend)}%
</div>
</div>
{/* Trend 3y */}
<div className="col-span-2 flex justify-center">
{canSee3yTrend ? (
<div className={clsx("flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium font-mono whitespace-nowrap",
trend3y > 10 ? "text-orange-400 bg-orange-400/5 border border-orange-400/20" :
trend3y < -10 ? "text-emerald-400 bg-emerald-400/5 border border-emerald-400/20" :
"text-zinc-400 bg-zinc-800/50 border border-zinc-700"
)}>
{trend3y > 0 ? <TrendingUp className="w-3 h-3" /> : trend3y < 0 ? <TrendingDown className="w-3 h-3" /> : <Minus className="w-3 h-3" />}
{Math.abs(trend3y)}%
</div>
) : (
<span className="text-zinc-700 text-xs"></span>
)}
</div>
{/* Risk */}
<div className="col-span-1 flex justify-center">
<Tooltip content={tld.risk_reason || 'Standard risk profile'}>
<div className="w-16 h-1.5 rounded-full overflow-hidden bg-zinc-800 cursor-help">
<div className={clsx("h-full rounded-full",
tld.risk_level === 'low' ? "w-1/3 bg-emerald-500" :
tld.risk_level === 'medium' ? "w-2/3 bg-amber-500" :
"w-full bg-red-500"
)} />
</div>
</Tooltip>
</div>
{/* Action */}
<div className="col-span-1 flex justify-end items-center gap-3">
<Link
href={`/terminal/intel/${tld.tld}`}
className="h-8 px-3 flex items-center gap-2 rounded-lg text-xs font-bold transition-all bg-white text-black hover:bg-zinc-200 shadow-white/10 opacity-0 group-hover:opacity-100 uppercase tracking-wide whitespace-nowrap"
>
<ArrowRight className="w-3 h-3" />
</Link>
</div>
</div>
)
})}
</div>
)}
</div>
</div>
</div> </div>
{/* Upgrade CTA for Scout users */} <div className="flex gap-6 lg:gap-8">
{userTier === 'scout' && ( <div className="text-right">
<div className="mt-8 p-6 rounded-2xl bg-gradient-to-br from-zinc-900 to-zinc-900/50 border border-white/5 text-center"> <div className="text-xl font-display text-accent">{formatPrice(stats.lowest)}</div>
<div className="w-12 h-12 bg-emerald-500/10 rounded-full flex items-center justify-center mx-auto mb-4 text-emerald-400"> <div className="text-[9px] tracking-wide text-white/30 font-mono">Lowest Entry</div>
<BarChart3 className="w-6 h-6" />
</div>
<h3 className="text-lg font-bold text-white mb-2">Unlock Full TLD Intelligence</h3>
<p className="text-sm text-zinc-400 mb-4 max-w-md mx-auto">
See renewal prices, identify renewal traps, and access detailed price history charts with Trader or Tycoon.
</p>
<Link
href="/pricing"
className="inline-flex items-center gap-2 px-6 py-3 bg-white text-black font-bold rounded-lg hover:bg-zinc-200 transition-all"
>
<Sparkles className="w-4 h-4" />
Upgrade Now
</Link>
</div> </div>
)} <div className="text-right">
<div className="text-xl font-display text-amber-400">{stats.traps}</div>
<div className="text-[9px] tracking-wide text-white/30 font-mono">High Risk</div>
</div>
{canSeeRenewal && (
<div className="text-right">
<div className="text-xl font-display text-white">{formatPrice(stats.avgRenewal)}</div>
<div className="text-[9px] tracking-wide text-white/30 font-mono">Avg Renewal</div>
</div>
)}
</div>
</div> </div>
</div> </section>
</TerminalLayout>
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* FILTERS */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
<section className="pb-6 border-b border-white/[0.08]">
<div className="flex flex-wrap items-center gap-2">
<button
onClick={() => setFilterType('all')}
className={clsx(
"px-3 py-1.5 text-xs font-medium transition-colors",
filterType === 'all' ? "bg-white/10 text-white" : "text-white/40 hover:text-white/60"
)}
>
All TLDs
</button>
<button
onClick={() => setFilterType('tech')}
className={clsx(
"px-3 py-1.5 text-xs font-medium transition-colors flex items-center gap-1.5",
filterType === 'tech' ? "bg-accent/20 text-accent" : "text-white/40 hover:text-white/60"
)}
>
<Zap className="w-3 h-3" />
Tech
</button>
<button
onClick={() => setFilterType('geo')}
className={clsx(
"px-3 py-1.5 text-xs font-medium transition-colors flex items-center gap-1.5",
filterType === 'geo' ? "bg-white/10 text-white" : "text-white/40 hover:text-white/60"
)}
>
<Globe className="w-3 h-3" />
Geo / National
</button>
<button
onClick={() => setFilterType('budget')}
className={clsx(
"px-3 py-1.5 text-xs font-medium transition-colors flex items-center gap-1.5",
filterType === 'budget' ? "bg-white/10 text-white" : "text-white/40 hover:text-white/60"
)}
>
<DollarSign className="w-3 h-3" />
Budget {'<'}$10
</button>
<div className="flex-1" />
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-white/30" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search TLDs..."
className="bg-[#050505] border border-white/10 pl-9 pr-4 py-2 text-sm text-white placeholder:text-white/25 outline-none focus:border-accent/40 w-48 lg:w-64"
/>
</div>
<button
onClick={handleRefresh}
disabled={refreshing}
className="p-2 text-white/30 hover:text-white transition-colors"
>
<RefreshCw className={clsx("w-4 h-4", refreshing && "animate-spin")} />
</button>
</div>
</section>
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* TABLE */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
<section className="py-6">
{loading ? (
<div className="flex items-center justify-center py-20">
<Loader2 className="w-6 h-6 text-accent animate-spin" />
</div>
) : filteredData.length === 0 ? (
<div className="text-center py-16">
<div className="w-14 h-14 mx-auto bg-white/[0.02] border border-white/10 flex items-center justify-center mb-4">
<Search className="w-6 h-6 text-white/20" />
</div>
<p className="text-white/40 text-sm">No TLDs found</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full min-w-[900px]">
<thead>
<tr className="text-xs text-white/40 border-b border-white/[0.06]">
<th className="text-left py-3 px-4 font-medium">
<button onClick={() => handleSort('tld')} className="flex items-center gap-1 hover:text-white/60">
Extension
{sortField === 'tld' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
</th>
<th className="text-right py-3 px-4 font-medium">
<button onClick={() => handleSort('price')} className="flex items-center gap-1 ml-auto hover:text-white/60">
Reg. Price
{sortField === 'price' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
</th>
<th className="text-right py-3 px-4 font-medium">
<button
onClick={() => canSeeRenewal && handleSort('renewal')}
className={clsx("flex items-center gap-1 ml-auto", canSeeRenewal ? "hover:text-white/60" : "opacity-50 cursor-not-allowed")}
>
Renewal
{!canSeeRenewal && <Lock className="w-3 h-3" />}
{sortField === 'renewal' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
</th>
<th className="text-center py-3 px-4 font-medium">
<button onClick={() => handleSort('change')} className="flex items-center gap-1 mx-auto hover:text-white/60">
1y Trend
{sortField === 'change' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
</th>
<th className="text-center py-3 px-4 font-medium">
<button
onClick={() => canSee3yTrend && handleSort('change3y')}
className={clsx("flex items-center gap-1 mx-auto", canSee3yTrend ? "hover:text-white/60" : "opacity-50 cursor-not-allowed")}
>
3y Trend
{!canSee3yTrend && <Lock className="w-3 h-3" />}
</button>
</th>
<th className="text-center py-3 px-4 font-medium">Risk</th>
<th className="text-right py-3 px-4 font-medium"></th>
</tr>
</thead>
<tbody>
{filteredData.map((tld) => {
const isTrap = tld.min_renewal_price > tld.min_price * 1.5
const trend = tld.price_change_1y || 0
const trend3y = tld.price_change_3y || 0
return (
<tr key={tld.tld} className="group border-b border-white/[0.04] hover:bg-white/[0.02]">
<td className="py-3 px-4">
<Link href={`/terminal/intel/${tld.tld}`} className="font-mono font-medium text-white hover:text-accent transition-colors">
.{tld.tld}
</Link>
</td>
<td className="py-3 px-4 text-right font-mono text-white">
{formatPrice(tld.min_price)}
</td>
<td className="py-3 px-4 text-right">
{canSeeRenewal ? (
<div className="flex items-center justify-end gap-1.5">
<span className={clsx("font-mono", isTrap ? "text-amber-400" : "text-white/50")}>
{formatPrice(tld.min_renewal_price)}
</span>
{isTrap && <AlertTriangle className="w-3.5 h-3.5 text-amber-400" />}
</div>
) : (
<span className="text-white/20 flex items-center gap-1 justify-end">
<Lock className="w-3 h-3" />
Trader+
</span>
)}
</td>
<td className="py-3 px-4 text-center">
<span className={clsx(
"inline-flex items-center gap-1 text-xs font-mono px-2 py-0.5",
trend > 5 ? "text-orange-400 bg-orange-400/10" :
trend < -5 ? "text-accent bg-accent/10" :
"text-white/40 bg-white/5"
)}>
{trend > 0 ? <TrendingUp className="w-3 h-3" /> : trend < 0 ? <TrendingDown className="w-3 h-3" /> : <Minus className="w-3 h-3" />}
{Math.abs(trend)}%
</span>
</td>
<td className="py-3 px-4 text-center">
{canSee3yTrend ? (
<span className={clsx(
"inline-flex items-center gap-1 text-xs font-mono px-2 py-0.5",
trend3y > 10 ? "text-orange-400 bg-orange-400/10" :
trend3y < -10 ? "text-accent bg-accent/10" :
"text-white/40 bg-white/5"
)}>
{trend3y > 0 ? <TrendingUp className="w-3 h-3" /> : trend3y < 0 ? <TrendingDown className="w-3 h-3" /> : <Minus className="w-3 h-3" />}
{Math.abs(trend3y)}%
</span>
) : (
<span className="text-white/20"></span>
)}
</td>
<td className="py-3 px-4 text-center">
<div className="w-16 h-1.5 mx-auto bg-white/10 overflow-hidden" title={tld.risk_reason || 'Standard risk'}>
<div className={clsx(
"h-full",
tld.risk_level === 'low' ? "w-1/3 bg-accent" :
tld.risk_level === 'medium' ? "w-2/3 bg-amber-400" :
"w-full bg-red-400"
)} />
</div>
</td>
<td className="py-3 px-4 text-right">
<Link
href={`/terminal/intel/${tld.tld}`}
className="opacity-0 group-hover:opacity-100 inline-flex items-center gap-1 text-xs text-white/50 hover:text-white transition-all"
>
<ArrowRight className="w-4 h-4" />
</Link>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)}
</section>
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* UPGRADE CTA */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
{userTier === 'scout' && (
<section className="py-8 border-t border-white/[0.06]">
<div className="text-center max-w-md mx-auto">
<Sparkles className="w-8 h-8 text-accent mx-auto mb-4" />
<h3 className="font-display text-xl text-white mb-2">Unlock Full Intel</h3>
<p className="text-sm text-white/40 mb-6">
See renewal prices, identify traps, and access 3-year price history with Trader or Tycoon.
</p>
<Link
href="/pricing"
className="inline-flex items-center gap-2 px-6 py-3 bg-white text-black font-semibold hover:bg-white/90 transition-colors"
>
Upgrade Now
</Link>
</div>
</section>
)}
</CommandCenterLayout>
) )
} }

View File

@ -164,7 +164,7 @@ export default function PortfolioPage() {
notes: '', notes: '',
tags: '', tags: '',
}) })
const [sellData, setSellData] = useState({ const [sellData, setSellData] = useState({
sale_date: new Date().toISOString().split('T')[0], sale_date: new Date().toISOString().split('T')[0],
sale_price: '', sale_price: '',
@ -415,7 +415,7 @@ export default function PortfolioPage() {
<AlertCircle className="w-5 h-5" /> <AlertCircle className="w-5 h-5" />
<p className="text-sm flex-1">{error}</p> <p className="text-sm flex-1">{error}</p>
<button onClick={() => setError(null)}><X className="w-4 h-4" /></button> <button onClick={() => setError(null)}><X className="w-4 h-4" /></button>
</div> </div>
)} )}
{success && ( {success && (
@ -435,16 +435,16 @@ export default function PortfolioPage() {
<h2 className="text-2xl font-bold text-white mb-2">Unlock Portfolio Management</h2> <h2 className="text-2xl font-bold text-white mb-2">Unlock Portfolio Management</h2>
<p className="text-zinc-400 mb-6 max-w-md mx-auto"> <p className="text-zinc-400 mb-6 max-w-md mx-auto">
Track your domain investments, monitor valuations, and calculate ROI. Know exactly how your portfolio is performing. Track your domain investments, monitor valuations, and calculate ROI. Know exactly how your portfolio is performing.
</p> </p>
<Link <Link
href="/pricing" href="/pricing"
className="inline-flex items-center gap-2 px-6 py-3 bg-blue-500 text-white font-bold rounded-xl hover:bg-blue-400 transition-all shadow-lg shadow-blue-500/20" className="inline-flex items-center gap-2 px-6 py-3 bg-blue-500 text-white font-bold rounded-xl hover:bg-blue-400 transition-all shadow-lg shadow-blue-500/20"
> >
Upgrade to Trader <ArrowRight className="w-4 h-4" /> Upgrade to Trader <ArrowRight className="w-4 h-4" />
</Link> </Link>
</div> </div>
</div> </div>
)} )}
{/* Stats Grid */} {/* Stats Grid */}
{canUsePortfolio && summary && ( {canUsePortfolio && summary && (
@ -494,7 +494,7 @@ export default function PortfolioPage() {
<div className="hidden md:block md:col-span-2 text-right">ROI</div> <div className="hidden md:block md:col-span-2 text-right">ROI</div>
<div className="hidden md:block md:col-span-1 text-center">Status</div> <div className="hidden md:block md:col-span-1 text-center">Status</div>
<div className="hidden md:block md:col-span-2 text-right">Actions</div> <div className="hidden md:block md:col-span-2 text-right">Actions</div>
</div> </div>
{loading ? ( {loading ? (
<div className="flex items-center justify-center py-20"> <div className="flex items-center justify-center py-20">
@ -547,8 +547,8 @@ export default function PortfolioPage() {
<div className="font-mono text-zinc-400">{formatCurrency(domain.purchase_price)}</div> <div className="font-mono text-zinc-400">{formatCurrency(domain.purchase_price)}</div>
{domain.purchase_date && ( {domain.purchase_date && (
<div className="text-[10px] text-zinc-600">{formatDate(domain.purchase_date)}</div> <div className="text-[10px] text-zinc-600">{formatDate(domain.purchase_date)}</div>
)} )}
</div> </div>
{/* Value */} {/* Value */}
<div className="hidden md:block col-span-2 text-right"> <div className="hidden md:block col-span-2 text-right">
@ -592,30 +592,30 @@ export default function PortfolioPage() {
<div className="hidden md:flex col-span-2 justify-end gap-1"> <div className="hidden md:flex col-span-2 justify-end gap-1">
{!domain.is_sold && ( {!domain.is_sold && (
<> <>
<button <button
onClick={() => openListModal(domain)} onClick={() => openListModal(domain)}
className="p-2 rounded-lg text-zinc-600 hover:text-amber-400 hover:bg-amber-500/10 transition-all" className="p-2 rounded-lg text-zinc-600 hover:text-amber-400 hover:bg-amber-500/10 transition-all"
title="List for sale" title="List for sale"
> >
<Tag className="w-4 h-4" /> <Tag className="w-4 h-4" />
</button> </button>
<button <button
onClick={() => handleRefreshValue(domain)} onClick={() => handleRefreshValue(domain)}
className="p-2 rounded-lg text-zinc-600 hover:text-blue-400 hover:bg-blue-500/10 transition-all" className="p-2 rounded-lg text-zinc-600 hover:text-blue-400 hover:bg-blue-500/10 transition-all"
title="Refresh value" title="Refresh value"
> >
<RefreshCw className="w-4 h-4" /> <RefreshCw className="w-4 h-4" />
</button> </button>
<button <button
onClick={() => openSellModal(domain)} onClick={() => openSellModal(domain)}
className="p-2 rounded-lg text-zinc-600 hover:text-emerald-400 hover:bg-emerald-500/10 transition-all" className="p-2 rounded-lg text-zinc-600 hover:text-emerald-400 hover:bg-emerald-500/10 transition-all"
title="Record sale" title="Record sale"
> >
<DollarSign className="w-4 h-4" /> <DollarSign className="w-4 h-4" />
</button> </button>
</> </>
)} )}
<button <button
onClick={() => openEditModal(domain)} onClick={() => openEditModal(domain)}
className="p-2 rounded-lg text-zinc-600 hover:text-white hover:bg-white/10 transition-all" className="p-2 rounded-lg text-zinc-600 hover:text-white hover:bg-white/10 transition-all"
title="Edit" title="Edit"
@ -626,22 +626,22 @@ export default function PortfolioPage() {
onClick={() => handleDelete(domain)} onClick={() => handleDelete(domain)}
className="p-2 rounded-lg text-zinc-600 hover:text-rose-400 hover:bg-rose-500/10 transition-all" className="p-2 rounded-lg text-zinc-600 hover:text-rose-400 hover:bg-rose-500/10 transition-all"
title="Delete" title="Delete"
> >
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</button> </button>
</div> </div>
</div> </div>
))} ))}
</div> </div>
)} )}
</div> </div>
)} )}
</div> </div>
{/* Add Modal */} {/* Add Modal */}
{showAddModal && ( {showAddModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-md"> <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-md">
<div className="w-full max-w-lg bg-[#0A0A0A] border border-white/10 rounded-2xl shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200"> <div className="w-full max-w-lg bg-[#0A0A0A] border border-white/10 rounded-2xl shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200">
<div className="p-6 border-b border-white/5"> <div className="p-6 border-b border-white/5">
@ -650,55 +650,55 @@ export default function PortfolioPage() {
</div> </div>
<form onSubmit={handleAdd} className="p-6 space-y-4"> <form onSubmit={handleAdd} className="p-6 space-y-4">
<div> <div>
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Domain Name *</label> <label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Domain Name *</label>
<input <input
type="text" type="text"
required required
value={formData.domain} value={formData.domain}
onChange={(e) => setFormData({ ...formData, domain: e.target.value })} onChange={(e) => setFormData({ ...formData, domain: e.target.value })}
placeholder="example.com" placeholder="example.com"
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-blue-500/50 transition-all font-mono" className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-blue-500/50 transition-all font-mono"
/> />
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Purchase Date</label> <label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Purchase Date</label>
<input <input
type="date" type="date"
value={formData.purchase_date} value={formData.purchase_date}
onChange={(e) => setFormData({ ...formData, purchase_date: e.target.value })} onChange={(e) => setFormData({ ...formData, purchase_date: e.target.value })}
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white focus:outline-none focus:border-blue-500/50 transition-all" className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white focus:outline-none focus:border-blue-500/50 transition-all"
/> />
</div> </div>
<div> <div>
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Purchase Price</label> <label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Purchase Price</label>
<div className="relative"> <div className="relative">
<DollarSign className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-500" /> <DollarSign className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-500" />
<input <input
type="number" type="number"
step="0.01" step="0.01"
value={formData.purchase_price} value={formData.purchase_price}
onChange={(e) => setFormData({ ...formData, purchase_price: e.target.value })} onChange={(e) => setFormData({ ...formData, purchase_price: e.target.value })}
placeholder="0.00" placeholder="0.00"
className="w-full pl-9 pr-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-blue-500/50 transition-all font-mono" className="w-full pl-9 pr-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-blue-500/50 transition-all font-mono"
/> />
</div> </div>
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Registrar</label> <label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Registrar</label>
<input <input
type="text" type="text"
value={formData.registrar} value={formData.registrar}
onChange={(e) => setFormData({ ...formData, registrar: e.target.value })} onChange={(e) => setFormData({ ...formData, registrar: e.target.value })}
placeholder="Namecheap, GoDaddy..." placeholder="Namecheap, GoDaddy..."
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-blue-500/50 transition-all" className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-blue-500/50 transition-all"
/> />
</div> </div>
<div> <div>
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Renewal Date</label> <label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Renewal Date</label>
<input <input
@ -722,29 +722,29 @@ export default function PortfolioPage() {
</div> </div>
<div className="flex gap-3 pt-4"> <div className="flex gap-3 pt-4">
<button <button
type="button" type="button"
onClick={() => setShowAddModal(false)} onClick={() => setShowAddModal(false)}
className="flex-1 px-4 py-3 border border-white/10 text-zinc-400 rounded-xl hover:bg-white/5 hover:text-white transition-all font-medium" className="flex-1 px-4 py-3 border border-white/10 text-zinc-400 rounded-xl hover:bg-white/5 hover:text-white transition-all font-medium"
> >
Cancel Cancel
</button> </button>
<button <button
type="submit" type="submit"
disabled={saving} disabled={saving}
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-blue-500 text-white font-bold rounded-xl hover:bg-blue-400 transition-all disabled:opacity-50 shadow-lg shadow-blue-500/20" className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-blue-500 text-white font-bold rounded-xl hover:bg-blue-400 transition-all disabled:opacity-50 shadow-lg shadow-blue-500/20"
> >
{saving ? <Loader2 className="w-5 h-5 animate-spin" /> : <Plus className="w-5 h-5" />} {saving ? <Loader2 className="w-5 h-5 animate-spin" /> : <Plus className="w-5 h-5" />}
{saving ? 'Adding...' : 'Add Domain'} {saving ? 'Adding...' : 'Add Domain'}
</button> </button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
)} )}
{/* Edit Modal */} {/* Edit Modal */}
{showEditModal && selectedDomain && ( {showEditModal && selectedDomain && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-md"> <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-md">
<div className="w-full max-w-lg bg-[#0A0A0A] border border-white/10 rounded-2xl shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200"> <div className="w-full max-w-lg bg-[#0A0A0A] border border-white/10 rounded-2xl shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200">
<div className="p-6 border-b border-white/5"> <div className="p-6 border-b border-white/5">
@ -753,8 +753,8 @@ export default function PortfolioPage() {
</div> </div>
<form onSubmit={handleEdit} className="p-6 space-y-4"> <form onSubmit={handleEdit} className="p-6 space-y-4">
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Purchase Date</label> <label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Purchase Date</label>
<input <input
type="date" type="date"
@ -767,27 +767,27 @@ export default function PortfolioPage() {
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Purchase Price</label> <label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Purchase Price</label>
<div className="relative"> <div className="relative">
<DollarSign className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-500" /> <DollarSign className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-500" />
<input <input
type="number" type="number"
step="0.01" step="0.01"
value={formData.purchase_price} value={formData.purchase_price}
onChange={(e) => setFormData({ ...formData, purchase_price: e.target.value })} onChange={(e) => setFormData({ ...formData, purchase_price: e.target.value })}
className="w-full pl-9 pr-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white focus:outline-none focus:border-blue-500/50 transition-all font-mono" className="w-full pl-9 pr-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white focus:outline-none focus:border-blue-500/50 transition-all font-mono"
/> />
</div> </div>
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Registrar</label> <label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Registrar</label>
<input <input
type="text" type="text"
value={formData.registrar} value={formData.registrar}
onChange={(e) => setFormData({ ...formData, registrar: e.target.value })} onChange={(e) => setFormData({ ...formData, registrar: e.target.value })}
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white focus:outline-none focus:border-blue-500/50 transition-all" className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white focus:outline-none focus:border-blue-500/50 transition-all"
/> />
</div> </div>
<div> <div>
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Renewal Date</label> <label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Renewal Date</label>
<input <input
@ -796,7 +796,7 @@ export default function PortfolioPage() {
onChange={(e) => setFormData({ ...formData, renewal_date: e.target.value })} onChange={(e) => setFormData({ ...formData, renewal_date: e.target.value })}
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white focus:outline-none focus:border-blue-500/50 transition-all" className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white focus:outline-none focus:border-blue-500/50 transition-all"
/> />
</div> </div>
</div> </div>
<div> <div>
@ -810,29 +810,29 @@ export default function PortfolioPage() {
</div> </div>
<div className="flex gap-3 pt-4"> <div className="flex gap-3 pt-4">
<button <button
type="button" type="button"
onClick={() => setShowEditModal(false)} onClick={() => setShowEditModal(false)}
className="flex-1 px-4 py-3 border border-white/10 text-zinc-400 rounded-xl hover:bg-white/5 hover:text-white transition-all font-medium" className="flex-1 px-4 py-3 border border-white/10 text-zinc-400 rounded-xl hover:bg-white/5 hover:text-white transition-all font-medium"
> >
Cancel Cancel
</button> </button>
<button <button
type="submit" type="submit"
disabled={saving} disabled={saving}
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-blue-500 text-white font-bold rounded-xl hover:bg-blue-400 transition-all disabled:opacity-50 shadow-lg shadow-blue-500/20" className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-blue-500 text-white font-bold rounded-xl hover:bg-blue-400 transition-all disabled:opacity-50 shadow-lg shadow-blue-500/20"
> >
{saving ? <Loader2 className="w-5 h-5 animate-spin" /> : <CheckCircle className="w-5 h-5" />} {saving ? <Loader2 className="w-5 h-5 animate-spin" /> : <CheckCircle className="w-5 h-5" />}
{saving ? 'Saving...' : 'Save Changes'} {saving ? 'Saving...' : 'Save Changes'}
</button> </button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
)} )}
{/* Sell Modal */} {/* Sell Modal */}
{showSellModal && selectedDomain && ( {showSellModal && selectedDomain && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-md"> <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-md">
<div className="w-full max-w-md bg-[#0A0A0A] border border-white/10 rounded-2xl shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200"> <div className="w-full max-w-md bg-[#0A0A0A] border border-white/10 rounded-2xl shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200">
<div className="p-6 border-b border-white/5 bg-gradient-to-r from-emerald-500/10 to-transparent"> <div className="p-6 border-b border-white/5 bg-gradient-to-r from-emerald-500/10 to-transparent">
@ -843,25 +843,25 @@ export default function PortfolioPage() {
<p className="text-sm text-zinc-400 mt-1"> <p className="text-sm text-zinc-400 mt-1">
Congratulations on selling <strong className="text-white">{selectedDomain.domain}</strong>! Congratulations on selling <strong className="text-white">{selectedDomain.domain}</strong>!
</p> </p>
</div> </div>
<form onSubmit={handleSell} className="p-6 space-y-4"> <form onSubmit={handleSell} className="p-6 space-y-4">
<div> <div>
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Sale Date *</label> <label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Sale Date *</label>
<input <input
type="date" type="date"
required required
value={sellData.sale_date} value={sellData.sale_date}
onChange={(e) => setSellData({ ...sellData, sale_date: e.target.value })} onChange={(e) => setSellData({ ...sellData, sale_date: e.target.value })}
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white focus:outline-none focus:border-emerald-500/50 transition-all" className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white focus:outline-none focus:border-emerald-500/50 transition-all"
/> />
</div> </div>
<div> <div>
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Sale Price *</label> <label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Sale Price *</label>
<div className="relative"> <div className="relative">
<DollarSign className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-emerald-500" /> <DollarSign className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-emerald-500" />
<input <input
type="number" type="number"
step="0.01" step="0.01"
required required
@ -869,8 +869,8 @@ export default function PortfolioPage() {
onChange={(e) => setSellData({ ...sellData, sale_price: e.target.value })} onChange={(e) => setSellData({ ...sellData, sale_price: e.target.value })}
placeholder="0.00" placeholder="0.00"
className="w-full pl-9 pr-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all font-mono text-lg" className="w-full pl-9 pr-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all font-mono text-lg"
/> />
</div> </div>
{selectedDomain.purchase_price && sellData.sale_price && ( {selectedDomain.purchase_price && sellData.sale_price && (
<div className={clsx( <div className={clsx(
"mt-2 text-sm font-medium", "mt-2 text-sm font-medium",
@ -879,30 +879,30 @@ export default function PortfolioPage() {
{parseFloat(sellData.sale_price) > selectedDomain.purchase_price ? '📈' : '📉'} {parseFloat(sellData.sale_price) > selectedDomain.purchase_price ? '📈' : '📉'}
{' '}ROI: {(((parseFloat(sellData.sale_price) - selectedDomain.purchase_price) / selectedDomain.purchase_price) * 100).toFixed(1)}% {' '}ROI: {(((parseFloat(sellData.sale_price) - selectedDomain.purchase_price) / selectedDomain.purchase_price) * 100).toFixed(1)}%
{' '}(${(parseFloat(sellData.sale_price) - selectedDomain.purchase_price).toLocaleString()} profit) {' '}(${(parseFloat(sellData.sale_price) - selectedDomain.purchase_price).toLocaleString()} profit)
</div> </div>
)} )}
</div> </div>
<div className="flex gap-3 pt-4"> <div className="flex gap-3 pt-4">
<button <button
type="button" type="button"
onClick={() => setShowSellModal(false)} onClick={() => setShowSellModal(false)}
className="flex-1 px-4 py-3 border border-white/10 text-zinc-400 rounded-xl hover:bg-white/5 hover:text-white transition-all font-medium" className="flex-1 px-4 py-3 border border-white/10 text-zinc-400 rounded-xl hover:bg-white/5 hover:text-white transition-all font-medium"
> >
Cancel Cancel
</button> </button>
<button <button
type="submit" type="submit"
disabled={saving} disabled={saving}
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-emerald-500 text-white font-bold rounded-xl hover:bg-emerald-400 transition-all disabled:opacity-50 shadow-lg shadow-emerald-500/20" className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-emerald-500 text-white font-bold rounded-xl hover:bg-emerald-400 transition-all disabled:opacity-50 shadow-lg shadow-emerald-500/20"
> >
{saving ? <Loader2 className="w-5 h-5 animate-spin" /> : <DollarSign className="w-5 h-5" />} {saving ? <Loader2 className="w-5 h-5 animate-spin" /> : <DollarSign className="w-5 h-5" />}
{saving ? 'Saving...' : 'Record Sale'} {saving ? 'Saving...' : 'Record Sale'}
</button> </button>
</div>
</form>
</div> </div>
</div> </form>
</div>
</div>
)} )}
{/* List for Sale Modal */} {/* List for Sale Modal */}
@ -917,8 +917,8 @@ export default function PortfolioPage() {
<p className="text-sm text-zinc-400 mt-1"> <p className="text-sm text-zinc-400 mt-1">
Put <strong className="text-white">{selectedDomain.domain}</strong> on the marketplace Put <strong className="text-white">{selectedDomain.domain}</strong> on the marketplace
</p> </p>
</div> </div>
<form onSubmit={handleListForSale} className="p-6 space-y-4"> <form onSubmit={handleListForSale} className="p-6 space-y-4">
<div> <div>
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Asking Price</label> <label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Asking Price</label>
@ -931,14 +931,14 @@ export default function PortfolioPage() {
onChange={(e) => setListData({ ...listData, asking_price: e.target.value })} onChange={(e) => setListData({ ...listData, asking_price: e.target.value })}
placeholder="Leave empty for 'Make Offer'" placeholder="Leave empty for 'Make Offer'"
className="w-full pl-9 pr-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-amber-500/50 transition-all font-mono text-lg" className="w-full pl-9 pr-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-amber-500/50 transition-all font-mono text-lg"
/> />
</div> </div>
{selectedDomain.estimated_value && ( {selectedDomain.estimated_value && (
<p className="mt-2 text-xs text-zinc-500"> <p className="mt-2 text-xs text-zinc-500">
Estimated value: <span className="text-amber-400">{formatCurrency(selectedDomain.estimated_value)}</span> Estimated value: <span className="text-amber-400">{formatCurrency(selectedDomain.estimated_value)}</span>
</p> </p>
)} )}
</div> </div>
<div> <div>
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Price Type</label> <label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Price Type</label>
@ -951,13 +951,13 @@ export default function PortfolioPage() {
<option value="fixed">Fixed Price</option> <option value="fixed">Fixed Price</option>
<option value="make_offer">Make Offer Only</option> <option value="make_offer">Make Offer Only</option>
</select> </select>
</div> </div>
<div className="p-4 bg-amber-500/5 border border-amber-500/10 rounded-xl"> <div className="p-4 bg-amber-500/5 border border-amber-500/10 rounded-xl">
<p className="text-xs text-amber-400/80 leading-relaxed"> <p className="text-xs text-amber-400/80 leading-relaxed">
💡 After creating the listing, you'll need to verify domain ownership via DNS before it goes live on the marketplace. 💡 After creating the listing, you'll need to verify domain ownership via DNS before it goes live on the marketplace.
</p> </p>
</div> </div>
<div className="flex gap-3 pt-4"> <div className="flex gap-3 pt-4">
<button <button
@ -974,13 +974,13 @@ export default function PortfolioPage() {
> >
{saving ? <Loader2 className="w-5 h-5 animate-spin" /> : <Tag className="w-5 h-5" />} {saving ? <Loader2 className="w-5 h-5 animate-spin" /> : <Tag className="w-5 h-5" />}
{saving ? 'Creating...' : 'Create Listing'} {saving ? 'Creating...' : 'Create Listing'}
</button> </button>
</div> </div>
</form> </form>
</div> </div>
</div>
)}
</div> </div>
)}
</div>
</TerminalLayout> </TerminalLayout>
) )
} }

View File

@ -3,7 +3,7 @@
import { useEffect, useState, useCallback } from 'react' import { useEffect, useState, useCallback } from 'react'
import { useStore } from '@/lib/store' import { useStore } from '@/lib/store'
import { api } from '@/lib/api' import { api } from '@/lib/api'
import { TerminalLayout } from '@/components/TerminalLayout' import { CommandCenterLayout } from '@/components/CommandCenterLayout'
import { import {
Plus, Plus,
Target, Target,
@ -12,62 +12,21 @@ import {
Trash2, Trash2,
Power, Power,
PowerOff, PowerOff,
Eye,
Bell, Bell,
MessageSquare, MessageSquare,
Loader2, Loader2,
X, X,
AlertCircle, AlertCircle,
CheckCircle, CheckCircle,
TrendingUp,
Filter,
Clock, Clock,
DollarSign, DollarSign,
Hash, Hash,
Tag,
Crown, Crown,
Activity Activity
} from 'lucide-react' } from 'lucide-react'
import clsx from 'clsx' import clsx from 'clsx'
import Link from 'next/link' import Link from 'next/link'
// ============================================================================
// SHARED COMPONENTS
// ============================================================================
const StatCard = ({ label, value, subValue, icon: Icon, highlight, trend }: {
label: string
value: string | number
subValue?: string
icon: any
highlight?: boolean
trend?: 'up' | 'down' | 'neutral' | 'active'
}) => (
<div className={clsx(
"bg-zinc-900/40 border p-4 relative overflow-hidden group hover:border-white/10 transition-colors h-full",
highlight ? "border-emerald-500/30" : "border-white/5"
)}>
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
<Icon className="w-16 h-16" />
</div>
<div className="relative z-10">
<div className="flex items-center gap-2 text-zinc-400 mb-1">
<Icon className={clsx("w-4 h-4", (highlight || trend === 'active' || trend === 'up') && "text-emerald-400")} />
<span className="text-xs font-medium uppercase tracking-wider">{label}</span>
</div>
<div className="flex items-baseline gap-2">
<span className="text-2xl font-bold text-white tracking-tight">{value}</span>
{subValue && <span className="text-xs text-zinc-500 font-medium">{subValue}</span>}
</div>
{highlight && (
<div className="mt-2 text-[10px] font-medium px-1.5 py-0.5 w-fit rounded border text-emerald-400 border-emerald-400/20 bg-emerald-400/5">
LIVE
</div>
)}
</div>
</div>
)
// ============================================================================ // ============================================================================
// INTERFACES // INTERFACES
// ============================================================================ // ============================================================================
@ -111,19 +70,16 @@ export default function SniperAlertsPage() {
const [deletingId, setDeletingId] = useState<number | null>(null) const [deletingId, setDeletingId] = useState<number | null>(null)
const [togglingId, setTogglingId] = useState<number | null>(null) const [togglingId, setTogglingId] = useState<number | null>(null)
// Tier-based limits
const tier = subscription?.tier || 'scout' const tier = subscription?.tier || 'scout'
const alertLimits: Record<string, number> = { scout: 2, trader: 10, tycoon: 50 } const alertLimits: Record<string, number> = { scout: 2, trader: 10, tycoon: 50 }
const maxAlerts = alertLimits[tier] || 2 const maxAlerts = alertLimits[tier] || 2
const canAddMore = alerts.length < maxAlerts const canAddMore = alerts.length < maxAlerts
const isTycoon = tier === 'tycoon' const isTycoon = tier === 'tycoon'
// Stats
const activeAlerts = alerts.filter(a => a.is_active).length const activeAlerts = alerts.filter(a => a.is_active).length
const totalMatches = alerts.reduce((sum, a) => sum + a.matches_count, 0) const totalMatches = alerts.reduce((sum, a) => sum + a.matches_count, 0)
const totalNotifications = alerts.reduce((sum, a) => sum + a.notifications_sent, 0) const totalNotifications = alerts.reduce((sum, a) => sum + a.notifications_sent, 0)
// Load alerts
const loadAlerts = useCallback(async () => { const loadAlerts = useCallback(async () => {
setLoading(true) setLoading(true)
try { try {
@ -140,7 +96,6 @@ export default function SniperAlertsPage() {
loadAlerts() loadAlerts()
}, [loadAlerts]) }, [loadAlerts])
// Toggle alert active status
const handleToggle = async (id: number, currentStatus: boolean) => { const handleToggle = async (id: number, currentStatus: boolean) => {
setTogglingId(id) setTogglingId(id)
try { try {
@ -156,7 +111,6 @@ export default function SniperAlertsPage() {
} }
} }
// Delete alert
const handleDelete = async (id: number, name: string) => { const handleDelete = async (id: number, name: string) => {
if (!confirm(`Delete alert "${name}"?`)) return if (!confirm(`Delete alert "${name}"?`)) return
@ -172,293 +126,276 @@ export default function SniperAlertsPage() {
} }
return ( return (
<TerminalLayout> <CommandCenterLayout minimal>
<div className="relative min-h-screen"> {/* ═══════════════════════════════════════════════════════════════════════ */}
{/* Ambient Background Glow */} {/* HEADER */}
<div className="fixed inset-0 pointer-events-none"> {/* ═══════════════════════════════════════════════════════════════════════ */}
<div className="absolute top-0 left-1/4 w-96 h-96 bg-emerald-500/5 rounded-full blur-3xl" /> <section className="pt-6 lg:pt-8 pb-6">
<div className="absolute top-1/3 right-1/4 w-96 h-96 bg-blue-500/5 rounded-full blur-3xl" /> <div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-6">
</div> <div className="space-y-3">
<div className="inline-flex items-center gap-2">
{/* Content */} <Target className="w-4 h-4 text-accent" />
<div className="relative z-10 space-y-8"> <span className="text-[10px] font-mono tracking-wide text-accent">Automated Alerts</span>
{/* Header */}
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<div className="flex items-center gap-3">
<div className="h-8 w-1 bg-emerald-500 rounded-full shadow-[0_0_10px_rgba(16,185,129,0.5)]" />
<h1 className="text-3xl font-bold tracking-tight text-white">Sniper Alerts</h1>
</div>
<p className="text-zinc-400 mt-1 max-w-2xl">
Get notified when domains matching your exact criteria hit the market. Set it, forget it, and pounce when the time is right.
</p>
</div> </div>
<h1 className="font-display text-[2rem] lg:text-[2.5rem] leading-[1] tracking-[-0.02em]">
<span className="text-white">Sniper</span>
<span className="text-white/30 ml-3">{alerts.length}/{maxAlerts}</span>
</h1>
<p className="text-white/40 text-sm max-w-md">
Get notified when domains matching your criteria hit the market
</p>
</div>
<div className="flex items-center gap-4">
<div className="flex gap-6">
<div className="text-right">
<div className="text-xl font-display text-accent">{activeAlerts}</div>
<div className="text-[9px] tracking-wide text-white/30 font-mono">Active</div>
</div>
<div className="text-right">
<div className="text-xl font-display text-white">{totalMatches}</div>
<div className="text-[9px] tracking-wide text-white/30 font-mono">Matches</div>
</div>
<div className="text-right">
<div className="text-xl font-display text-white">{totalNotifications}</div>
<div className="text-[9px] tracking-wide text-white/30 font-mono">Sent</div>
</div>
</div>
<button <button
onClick={() => setShowCreateModal(true)} onClick={() => setShowCreateModal(true)}
disabled={!canAddMore} disabled={!canAddMore}
className="px-4 py-2 bg-emerald-500 text-white font-medium rounded-lg hover:bg-emerald-400 transition-all shadow-lg shadow-emerald-500/20 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 whitespace-nowrap" className={clsx(
"flex items-center gap-2 px-4 py-2 text-sm font-semibold transition-colors",
canAddMore
? "bg-accent text-black hover:bg-white"
: "bg-white/10 text-white/40 cursor-not-allowed"
)}
> >
<Plus className="w-4 h-4" /> New Alert <Plus className="w-4 h-4" />
New Alert
</button> </button>
</div> </div>
</div>
</section>
{/* Stats */} {/* ═══════════════════════════════════════════════════════════════════════ */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4"> {/* ALERTS LIST */}
<StatCard {/* ═══════════════════════════════════════════════════════════════════════ */}
label="Active Alerts" <section className="py-6 border-t border-white/[0.08]">
value={activeAlerts} {loading ? (
subValue={`/ ${maxAlerts} slots`} <div className="flex items-center justify-center py-20">
icon={Target} <Loader2 className="w-6 h-6 text-accent animate-spin" />
trend="active"
highlight={activeAlerts > 0}
/>
<StatCard
label="Total Matches"
value={totalMatches}
subValue="All time"
icon={Zap}
trend={totalMatches > 0 ? 'up' : 'neutral'}
/>
<StatCard
label="Notifications"
value={totalNotifications}
subValue="Sent"
icon={Bell}
trend={totalNotifications > 0 ? 'up' : 'neutral'}
/>
<StatCard
label="Monitoring"
value={alerts.length}
subValue="Total alerts"
icon={Activity}
trend="neutral"
/>
</div> </div>
) : alerts.length === 0 ? (
{/* Alerts List */} <div className="text-center py-16">
<div className="bg-zinc-900/40 border border-white/5 rounded-xl overflow-hidden backdrop-blur-sm"> <div className="w-14 h-14 mx-auto bg-white/[0.02] border border-white/10 flex items-center justify-center mb-4">
{/* Header */} <Target className="w-6 h-6 text-white/20" />
<div className="px-6 py-4 bg-white/[0.02] border-b border-white/5 flex items-center justify-between">
<h2 className="text-sm font-semibold text-zinc-400 uppercase tracking-wider flex items-center gap-2">
<Filter className="w-4 h-4" />
Your Alerts
</h2>
<span className="text-xs text-zinc-600">{alerts.length} / {maxAlerts}</span>
</div> </div>
<p className="text-white/40 text-sm mb-2">No alerts yet</p>
{/* Loading */} <p className="text-white/25 text-xs mb-6 max-w-sm mx-auto">
{loading && ( Create your first sniper alert to get notified when matching domains appear
<div className="flex items-center justify-center py-20"> </p>
<Loader2 className="w-8 h-8 text-emerald-500 animate-spin" /> <button
</div> onClick={() => setShowCreateModal(true)}
)} className="inline-flex items-center gap-2 px-4 py-2 bg-accent text-black text-sm font-semibold hover:bg-white transition-colors"
>
{/* Empty State */} <Plus className="w-4 h-4" />
{!loading && alerts.length === 0 && ( Create First Alert
<div className="flex flex-col items-center justify-center py-20 text-center px-4"> </button>
<div className="w-16 h-16 rounded-full bg-white/5 flex items-center justify-center mb-4"> </div>
<Target className="w-8 h-8 text-zinc-600" /> ) : (
</div> <div className="space-y-3">
<h3 className="text-lg font-bold text-white mb-2">No Alerts Yet</h3> {alerts.map((alert) => (
<p className="text-sm text-zinc-500 max-w-md mb-6"> <div
Create your first sniper alert to get notified when domains matching your criteria appear in auctions. key={alert.id}
</p> className={clsx(
<button "group border transition-all",
onClick={() => setShowCreateModal(true)} alert.is_active
className="px-4 py-2 bg-emerald-500 text-white font-medium rounded-lg hover:bg-emerald-400 transition-all shadow-lg shadow-emerald-500/20 flex items-center gap-2" ? "bg-white/[0.02] border-white/[0.08] hover:border-accent/30"
> : "bg-white/[0.01] border-white/[0.04]"
<Plus className="w-4 h-4" /> Create First Alert )}
</button> >
</div> <div className="p-5">
)} <div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0">
{/* Alerts Grid */} <div className="flex items-center gap-3 mb-2">
{!loading && alerts.length > 0 && ( <h3 className="text-base font-medium text-white truncate">{alert.name}</h3>
<div className="divide-y divide-white/5"> {alert.is_active ? (
{alerts.map((alert) => ( <span className="px-2 py-0.5 text-[9px] font-mono bg-accent/10 text-accent border border-accent/20 flex items-center gap-1">
<div key={alert.id} className="p-6 hover:bg-white/[0.02] transition-colors group"> <div className="w-1.5 h-1.5 bg-accent animate-pulse" />
<div className="flex items-start justify-between gap-4"> Active
{/* Alert Info */} </span>
<div className="flex-1 min-w-0"> ) : (
<div className="flex items-center gap-3 mb-2"> <span className="px-2 py-0.5 text-[9px] font-mono bg-white/5 text-white/40 border border-white/10">
<h3 className="text-lg font-bold text-white truncate">{alert.name}</h3> Paused
{alert.is_active ? ( </span>
<span className="px-2 py-0.5 rounded-full text-[10px] font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 flex items-center gap-1">
<div className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
ACTIVE
</span>
) : (
<span className="px-2 py-0.5 rounded-full text-[10px] font-bold bg-zinc-800 text-zinc-500 border border-zinc-700">
PAUSED
</span>
)}
{isTycoon && alert.notify_sms && (
<span className="px-2 py-0.5 rounded-full text-[10px] font-bold bg-amber-500/10 text-amber-400 border border-amber-500/20 flex items-center gap-1">
<Crown className="w-3 h-3" />
SMS
</span>
)}
</div>
{alert.description && (
<p className="text-sm text-zinc-400 mb-3">{alert.description}</p>
)} )}
{isTycoon && alert.notify_sms && (
{/* Criteria Pills */} <span className="px-2 py-0.5 text-[9px] font-mono bg-amber-400/10 text-amber-400 border border-amber-400/20 flex items-center gap-1">
<div className="flex flex-wrap gap-2 mb-3"> <Crown className="w-3 h-3" />
{alert.tlds && ( SMS
<span className="px-2 py-1 rounded-lg bg-blue-500/10 text-blue-400 text-xs font-medium border border-blue-500/20">
{alert.tlds}
</span>
)}
{alert.keywords && (
<span className="px-2 py-1 rounded-lg bg-emerald-500/10 text-emerald-400 text-xs font-medium border border-emerald-500/20">
+{alert.keywords}
</span>
)}
{alert.exclude_keywords && (
<span className="px-2 py-1 rounded-lg bg-rose-500/10 text-rose-400 text-xs font-medium border border-rose-500/20">
-{alert.exclude_keywords}
</span>
)}
{(alert.min_length || alert.max_length) && (
<span className="px-2 py-1 rounded-lg bg-zinc-800 text-zinc-400 text-xs font-medium border border-zinc-700 flex items-center gap-1">
<Hash className="w-3 h-3" />
{alert.min_length || 1}-{alert.max_length || 63} chars
</span>
)}
{(alert.min_price || alert.max_price) && (
<span className="px-2 py-1 rounded-lg bg-zinc-800 text-zinc-400 text-xs font-medium border border-zinc-700 flex items-center gap-1">
<DollarSign className="w-3 h-3" />
{alert.min_price ? `$${alert.min_price}+` : ''}{alert.max_price ? ` - $${alert.max_price}` : ''}
</span>
)}
{alert.no_numbers && (
<span className="px-2 py-1 rounded-lg bg-zinc-800 text-zinc-400 text-xs font-medium border border-zinc-700">
No numbers
</span>
)}
{alert.no_hyphens && (
<span className="px-2 py-1 rounded-lg bg-zinc-800 text-zinc-400 text-xs font-medium border border-zinc-700">
No hyphens
</span>
)}
</div>
{/* Stats */}
<div className="flex items-center gap-4 text-xs text-zinc-500">
<span className="flex items-center gap-1">
<Zap className="w-3 h-3" />
{alert.matches_count} matches
</span> </span>
<span className="flex items-center gap-1"> )}
<Bell className="w-3 h-3" /> </div>
{alert.notifications_sent} sent
{alert.description && (
<p className="text-sm text-white/40 mb-3">{alert.description}</p>
)}
<div className="flex flex-wrap gap-1.5 mb-3">
{alert.tlds && (
<span className="px-2 py-0.5 text-[10px] font-mono bg-blue-400/10 text-blue-400 border border-blue-400/20">
{alert.tlds}
</span> </span>
{alert.last_matched_at && ( )}
<span className="flex items-center gap-1"> {alert.keywords && (
<Clock className="w-3 h-3" /> <span className="px-2 py-0.5 text-[10px] font-mono bg-accent/10 text-accent border border-accent/20">
Last: {new Date(alert.last_matched_at).toLocaleDateString()} +{alert.keywords}
</span> </span>
)} )}
</div> {alert.exclude_keywords && (
<span className="px-2 py-0.5 text-[10px] font-mono bg-rose-400/10 text-rose-400 border border-rose-400/20">
-{alert.exclude_keywords}
</span>
)}
{(alert.min_length || alert.max_length) && (
<span className="px-2 py-0.5 text-[10px] font-mono bg-white/5 text-white/50 border border-white/10 flex items-center gap-1">
<Hash className="w-3 h-3" />
{alert.min_length || 1}-{alert.max_length || 63}
</span>
)}
{(alert.min_price || alert.max_price) && (
<span className="px-2 py-0.5 text-[10px] font-mono bg-white/5 text-white/50 border border-white/10 flex items-center gap-1">
<DollarSign className="w-3 h-3" />
{alert.min_price ? `$${alert.min_price}` : ''}{alert.max_price ? ` - $${alert.max_price}` : '+'}
</span>
)}
{alert.no_numbers && (
<span className="px-2 py-0.5 text-[10px] font-mono bg-white/5 text-white/50 border border-white/10">
No digits
</span>
)}
{alert.no_hyphens && (
<span className="px-2 py-0.5 text-[10px] font-mono bg-white/5 text-white/50 border border-white/10">
No hyphens
</span>
)}
</div> </div>
{/* Actions */} <div className="flex items-center gap-4 text-[10px] text-white/30 font-mono">
<div className="flex items-center gap-2"> <span className="flex items-center gap-1">
<button <Zap className="w-3 h-3" />
onClick={() => handleToggle(alert.id, alert.is_active)} {alert.matches_count} matches
disabled={togglingId === alert.id} </span>
className={clsx( <span className="flex items-center gap-1">
"p-2 rounded-lg border transition-all", <Bell className="w-3 h-3" />
alert.is_active {alert.notifications_sent} sent
? "bg-emerald-500/10 border-emerald-500/20 text-emerald-400 hover:bg-emerald-500/20" </span>
: "bg-zinc-800/50 border-zinc-700 text-zinc-500 hover:bg-zinc-800" {alert.last_matched_at && (
)} <span className="flex items-center gap-1">
title={alert.is_active ? "Pause" : "Activate"} <Clock className="w-3 h-3" />
> {new Date(alert.last_matched_at).toLocaleDateString()}
{togglingId === alert.id ? ( </span>
<Loader2 className="w-4 h-4 animate-spin" /> )}
) : alert.is_active ? (
<Power className="w-4 h-4" />
) : (
<PowerOff className="w-4 h-4" />
)}
</button>
<button
onClick={() => setEditingAlert(alert)}
className="p-2 rounded-lg border bg-zinc-800/50 border-zinc-700 text-zinc-400 hover:text-white hover:bg-zinc-800 transition-all"
title="Edit"
>
<Edit2 className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(alert.id, alert.name)}
disabled={deletingId === alert.id}
className="p-2 rounded-lg border bg-zinc-800/50 border-zinc-700 text-zinc-400 hover:text-rose-400 hover:border-rose-500/30 transition-all disabled:opacity-50"
title="Delete"
>
{deletingId === alert.id ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Trash2 className="w-4 h-4" />
)}
</button>
</div> </div>
</div> </div>
<div className="flex items-center gap-2">
<button
onClick={() => handleToggle(alert.id, alert.is_active)}
disabled={togglingId === alert.id}
className={clsx(
"w-8 h-8 flex items-center justify-center border transition-colors",
alert.is_active
? "bg-accent/10 border-accent/20 text-accent hover:bg-accent/20"
: "bg-white/5 border-white/10 text-white/40 hover:bg-white/10"
)}
>
{togglingId === alert.id ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : alert.is_active ? (
<Power className="w-4 h-4" />
) : (
<PowerOff className="w-4 h-4" />
)}
</button>
<button
onClick={() => setEditingAlert(alert)}
className="w-8 h-8 flex items-center justify-center border bg-white/5 border-white/10 text-white/40 hover:text-white hover:bg-white/10 transition-colors"
>
<Edit2 className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(alert.id, alert.name)}
disabled={deletingId === alert.id}
className="w-8 h-8 flex items-center justify-center border bg-white/5 border-white/10 text-white/40 hover:text-rose-400 hover:border-rose-400/30 transition-colors"
>
{deletingId === alert.id ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Trash2 className="w-4 h-4" />
)}
</button>
</div>
</div> </div>
))} </div>
</div> </div>
)} ))}
</div> </div>
{/* Upgrade CTA */}
{!canAddMore && (
<div className="p-6 bg-gradient-to-br from-amber-900/20 to-black border border-amber-500/20 rounded-2xl text-center">
<Crown className="w-12 h-12 text-amber-400 mx-auto mb-4" />
<h3 className="text-xl font-bold text-white mb-2">Alert Limit Reached</h3>
<p className="text-zinc-400 mb-4">
You've created {maxAlerts} alerts. Upgrade to add more.
</p>
<div className="flex gap-4 justify-center text-sm">
<div className="px-4 py-2 bg-white/5 rounded-lg">
<span className="text-zinc-500">Trader:</span> <span className="text-white font-bold">10 alerts</span>
</div>
<div className="px-4 py-2 bg-amber-500/10 border border-amber-500/20 rounded-lg">
<span className="text-zinc-500">Tycoon:</span> <span className="text-amber-400 font-bold">50 alerts + SMS</span>
</div>
</div>
<Link
href="/pricing"
className="inline-flex items-center gap-2 px-6 py-3 bg-amber-500 text-white font-bold rounded-xl hover:bg-amber-400 transition-all shadow-lg shadow-amber-500/20 mt-4"
>
Upgrade Now
</Link>
</div>
)}
</div>
{/* Create/Edit Modal */}
{(showCreateModal || editingAlert) && (
<CreateEditModal
alert={editingAlert}
onClose={() => {
setShowCreateModal(false)
setEditingAlert(null)
}}
onSuccess={() => {
loadAlerts()
setShowCreateModal(false)
setEditingAlert(null)
}}
isTycoon={isTycoon}
/>
)} )}
</div> </section>
</TerminalLayout>
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* UPGRADE CTA */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
{!canAddMore && (
<section className="py-8 border-t border-white/[0.06]">
<div className="text-center max-w-md mx-auto">
<Crown className="w-8 h-8 text-amber-400 mx-auto mb-4" />
<h3 className="font-display text-xl text-white mb-2">Alert Limit Reached</h3>
<p className="text-sm text-white/40 mb-4">
You've created {maxAlerts} alerts. Upgrade for more.
</p>
<div className="flex gap-4 justify-center text-xs mb-6">
<div className="px-3 py-2 bg-white/5 border border-white/10">
<span className="text-white/40">Trader:</span> <span className="text-white font-medium">10 alerts</span>
</div>
<div className="px-3 py-2 bg-amber-400/10 border border-amber-400/20">
<span className="text-white/40">Tycoon:</span> <span className="text-amber-400 font-medium">50 + SMS</span>
</div>
</div>
<Link
href="/pricing"
className="inline-flex items-center gap-2 px-6 py-3 bg-amber-400 text-black font-semibold hover:bg-amber-300 transition-colors"
>
Upgrade Now
</Link>
</div>
</section>
)}
{/* Create/Edit Modal */}
{(showCreateModal || editingAlert) && (
<CreateEditModal
alert={editingAlert}
onClose={() => {
setShowCreateModal(false)
setEditingAlert(null)
}}
onSuccess={() => {
loadAlerts()
setShowCreateModal(false)
setEditingAlert(null)
}}
isTycoon={isTycoon}
/>
)}
</CommandCenterLayout>
) )
} }
@ -548,28 +485,26 @@ function CreateEditModal({ alert, onClose, onSuccess, isTycoon }: {
onClick={onClose} onClick={onClose}
> >
<div <div
className="w-full max-w-2xl bg-[#0A0A0A] border border-white/10 rounded-2xl shadow-2xl my-8" className="w-full max-w-2xl bg-[#050505] border border-white/10 my-8"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
{/* Header */} {/* Header */}
<div className="flex items-center justify-between p-6 border-b border-white/5 bg-white/[0.02]"> <div className="flex items-center justify-between p-6 border-b border-white/[0.08]">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 rounded-lg border bg-emerald-500/10 border-emerald-500/20"> <Target className="w-5 h-5 text-accent" />
<Target className="w-5 h-5 text-emerald-400" />
</div>
<div> <div>
<h3 className="font-bold text-lg text-white">{isEditing ? 'Edit Alert' : 'Create Sniper Alert'}</h3> <h3 className="font-medium text-white">{isEditing ? 'Edit Alert' : 'Create Sniper Alert'}</h3>
<p className="text-xs text-zinc-500">Set precise criteria for domain matching</p> <p className="text-xs text-white/40">Set criteria for domain matching</p>
</div> </div>
</div> </div>
<button onClick={onClose} className="p-2 hover:bg-white/10 rounded-full text-zinc-500 hover:text-white transition-colors"> <button onClick={onClose} className="p-2 hover:bg-white/10 text-white/40 hover:text-white transition-colors">
<X className="w-5 h-5" /> <X className="w-5 h-5" />
</button> </button>
</div> </div>
<form onSubmit={handleSubmit} className="p-6 space-y-6 max-h-[70vh] overflow-y-auto"> <form onSubmit={handleSubmit} className="p-6 space-y-6 max-h-[70vh] overflow-y-auto">
{error && ( {error && (
<div className="p-4 bg-rose-500/10 border border-rose-500/20 rounded-xl flex items-center gap-3 text-rose-400"> <div className="p-4 bg-rose-500/10 border border-rose-500/20 flex items-center gap-3 text-rose-400">
<AlertCircle className="w-5 h-5" /> <AlertCircle className="w-5 h-5" />
<p className="text-sm flex-1">{error}</p> <p className="text-sm flex-1">{error}</p>
</div> </div>
@ -577,85 +512,85 @@ function CreateEditModal({ alert, onClose, onSuccess, isTycoon }: {
{/* Basic Info */} {/* Basic Info */}
<div className="space-y-4"> <div className="space-y-4">
<h4 className="text-sm font-bold text-zinc-400 uppercase tracking-wider">Basic Info</h4> <h4 className="text-xs font-mono text-white/40 tracking-wide">Basic Info</h4>
<div> <div>
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Alert Name *</label> <label className="block text-xs text-white/50 mb-2">Alert Name *</label>
<input <input
type="text" type="text"
value={form.name} value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })} onChange={(e) => setForm({ ...form, name: e.target.value })}
placeholder="e.g. Premium 4L .com domains" placeholder="e.g. Premium 4L .com domains"
required required
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all" className="w-full px-4 py-3 bg-white/5 border border-white/10 text-white placeholder:text-white/25 outline-none focus:border-accent/50 transition-all"
/> />
</div> </div>
<div> <div>
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Description</label> <label className="block text-xs text-white/50 mb-2">Description</label>
<textarea <textarea
value={form.description} value={form.description}
onChange={(e) => setForm({ ...form, description: e.target.value })} onChange={(e) => setForm({ ...form, description: e.target.value })}
placeholder="Optional description" placeholder="Optional description"
rows={2} rows={2}
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all resize-none" className="w-full px-4 py-3 bg-white/5 border border-white/10 text-white placeholder:text-white/25 outline-none focus:border-accent/50 transition-all resize-none"
/> />
</div> </div>
</div> </div>
{/* Filters */} {/* Filters */}
<div className="space-y-4"> <div className="space-y-4">
<h4 className="text-sm font-bold text-zinc-400 uppercase tracking-wider">Criteria</h4> <h4 className="text-xs font-mono text-white/40 tracking-wide">Criteria</h4>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">TLDs</label> <label className="block text-xs text-white/50 mb-2">TLDs</label>
<input <input
type="text" type="text"
value={form.tlds} value={form.tlds}
onChange={(e) => setForm({ ...form, tlds: e.target.value })} onChange={(e) => setForm({ ...form, tlds: e.target.value })}
placeholder="com,io,ai" placeholder="com,io,ai"
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all font-mono text-sm" className="w-full px-4 py-3 bg-white/5 border border-white/10 text-white placeholder:text-white/25 outline-none focus:border-accent/50 font-mono text-sm"
/> />
</div> </div>
<div> <div>
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Platforms</label> <label className="block text-xs text-white/50 mb-2">Platforms</label>
<input <input
type="text" type="text"
value={form.platforms} value={form.platforms}
onChange={(e) => setForm({ ...form, platforms: e.target.value })} onChange={(e) => setForm({ ...form, platforms: e.target.value })}
placeholder="godaddy,sedo" placeholder="godaddy,sedo"
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all font-mono text-sm" className="w-full px-4 py-3 bg-white/5 border border-white/10 text-white placeholder:text-white/25 outline-none focus:border-accent/50 font-mono text-sm"
/> />
</div> </div>
</div> </div>
<div> <div>
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Must Contain</label> <label className="block text-xs text-white/50 mb-2">Must Contain</label>
<input <input
type="text" type="text"
value={form.keywords} value={form.keywords}
onChange={(e) => setForm({ ...form, keywords: e.target.value })} onChange={(e) => setForm({ ...form, keywords: e.target.value })}
placeholder="crypto,web3,ai" placeholder="crypto,web3,ai"
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all font-mono text-sm" className="w-full px-4 py-3 bg-white/5 border border-white/10 text-white placeholder:text-white/25 outline-none focus:border-accent/50 font-mono text-sm"
/> />
</div> </div>
<div> <div>
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Must NOT Contain</label> <label className="block text-xs text-white/50 mb-2">Must Not Contain</label>
<input <input
type="text" type="text"
value={form.exclude_keywords} value={form.exclude_keywords}
onChange={(e) => setForm({ ...form, exclude_keywords: e.target.value })} onChange={(e) => setForm({ ...form, exclude_keywords: e.target.value })}
placeholder="xxx,adult" placeholder="xxx,adult"
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all font-mono text-sm" className="w-full px-4 py-3 bg-white/5 border border-white/10 text-white placeholder:text-white/25 outline-none focus:border-accent/50 font-mono text-sm"
/> />
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Min Length</label> <label className="block text-xs text-white/50 mb-2">Min Length</label>
<input <input
type="number" type="number"
value={form.min_length} value={form.min_length}
@ -663,12 +598,12 @@ function CreateEditModal({ alert, onClose, onSuccess, isTycoon }: {
placeholder="1" placeholder="1"
min="1" min="1"
max="63" max="63"
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all" className="w-full px-4 py-3 bg-white/5 border border-white/10 text-white placeholder:text-white/25 outline-none focus:border-accent/50"
/> />
</div> </div>
<div> <div>
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Max Length</label> <label className="block text-xs text-white/50 mb-2">Max Length</label>
<input <input
type="number" type="number"
value={form.max_length} value={form.max_length}
@ -676,14 +611,14 @@ function CreateEditModal({ alert, onClose, onSuccess, isTycoon }: {
placeholder="63" placeholder="63"
min="1" min="1"
max="63" max="63"
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all" className="w-full px-4 py-3 bg-white/5 border border-white/10 text-white placeholder:text-white/25 outline-none focus:border-accent/50"
/> />
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Min Price (USD)</label> <label className="block text-xs text-white/50 mb-2">Min Price (USD)</label>
<input <input
type="number" type="number"
value={form.min_price} value={form.min_price}
@ -691,12 +626,12 @@ function CreateEditModal({ alert, onClose, onSuccess, isTycoon }: {
placeholder="0" placeholder="0"
min="0" min="0"
step="0.01" step="0.01"
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all" className="w-full px-4 py-3 bg-white/5 border border-white/10 text-white placeholder:text-white/25 outline-none focus:border-accent/50"
/> />
</div> </div>
<div> <div>
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Max Price (USD)</label> <label className="block text-xs text-white/50 mb-2">Max Price (USD)</label>
<input <input
type="number" type="number"
value={form.max_price} value={form.max_price}
@ -704,100 +639,62 @@ function CreateEditModal({ alert, onClose, onSuccess, isTycoon }: {
placeholder="10000" placeholder="10000"
min="0" min="0"
step="0.01" step="0.01"
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all" className="w-full px-4 py-3 bg-white/5 border border-white/10 text-white placeholder:text-white/25 outline-none focus:border-accent/50"
/> />
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="space-y-2">
<div> <label className="flex items-center gap-3 cursor-pointer p-3 border border-white/[0.06] hover:bg-white/[0.02] transition-colors">
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Max Bids</label>
<input
type="number"
value={form.max_bids}
onChange={(e) => setForm({ ...form, max_bids: e.target.value })}
placeholder="Low competition"
min="0"
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all"
/>
</div>
<div>
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Ending Within (hours)</label>
<input
type="number"
value={form.ending_within_hours}
onChange={(e) => setForm({ ...form, ending_within_hours: e.target.value })}
placeholder="24"
min="1"
max="168"
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all"
/>
</div>
</div>
<div className="space-y-3">
<label className="flex items-center gap-3 cursor-pointer p-3 rounded-lg border border-white/5 hover:bg-white/5 transition-colors">
<input <input
type="checkbox" type="checkbox"
checked={form.no_numbers} checked={form.no_numbers}
onChange={(e) => setForm({ ...form, no_numbers: e.target.checked })} onChange={(e) => setForm({ ...form, no_numbers: e.target.checked })}
className="w-5 h-5 rounded border-white/20 bg-black text-emerald-500 focus:ring-emerald-500 focus:ring-offset-0" className="w-4 h-4 border-white/20 bg-black text-accent focus:ring-accent focus:ring-offset-0"
/> />
<span className="text-sm text-zinc-300">No numbers in domain</span> <span className="text-sm text-white/60">No numbers in domain</span>
</label> </label>
<label className="flex items-center gap-3 cursor-pointer p-3 rounded-lg border border-white/5 hover:bg-white/5 transition-colors"> <label className="flex items-center gap-3 cursor-pointer p-3 border border-white/[0.06] hover:bg-white/[0.02] transition-colors">
<input <input
type="checkbox" type="checkbox"
checked={form.no_hyphens} checked={form.no_hyphens}
onChange={(e) => setForm({ ...form, no_hyphens: e.target.checked })} onChange={(e) => setForm({ ...form, no_hyphens: e.target.checked })}
className="w-5 h-5 rounded border-white/20 bg-black text-emerald-500 focus:ring-emerald-500 focus:ring-offset-0" className="w-4 h-4 border-white/20 bg-black text-accent focus:ring-accent focus:ring-offset-0"
/> />
<span className="text-sm text-zinc-300">No hyphens in domain</span> <span className="text-sm text-white/60">No hyphens in domain</span>
</label> </label>
</div> </div>
<div>
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Exclude Characters</label>
<input
type="text"
value={form.exclude_chars}
onChange={(e) => setForm({ ...form, exclude_chars: e.target.value })}
placeholder="q,x,z"
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all font-mono text-sm"
/>
</div>
</div> </div>
{/* Notifications */} {/* Notifications */}
<div className="space-y-4"> <div className="space-y-4">
<h4 className="text-sm font-bold text-zinc-400 uppercase tracking-wider">Notifications</h4> <h4 className="text-xs font-mono text-white/40 tracking-wide">Notifications</h4>
<label className="flex items-center gap-3 cursor-pointer p-3 rounded-lg border border-white/5 hover:bg-white/5 transition-colors"> <label className="flex items-center gap-3 cursor-pointer p-3 border border-white/[0.06] hover:bg-white/[0.02] transition-colors">
<input <input
type="checkbox" type="checkbox"
checked={form.notify_email} checked={form.notify_email}
onChange={(e) => setForm({ ...form, notify_email: e.target.checked })} onChange={(e) => setForm({ ...form, notify_email: e.target.checked })}
className="w-5 h-5 rounded border-white/20 bg-black text-emerald-500 focus:ring-emerald-500 focus:ring-offset-0" className="w-4 h-4 border-white/20 bg-black text-accent focus:ring-accent focus:ring-offset-0"
/> />
<Bell className="w-4 h-4 text-emerald-400" /> <Bell className="w-4 h-4 text-accent" />
<span className="text-sm text-zinc-300 flex-1">Email notifications</span> <span className="text-sm text-white/60 flex-1">Email notifications</span>
</label> </label>
<label className={clsx( <label className={clsx(
"flex items-center gap-3 cursor-pointer p-3 rounded-lg border transition-colors", "flex items-center gap-3 cursor-pointer p-3 border transition-colors",
isTycoon ? "border-amber-500/20 hover:bg-amber-500/5" : "border-white/5 opacity-50 cursor-not-allowed" isTycoon ? "border-amber-400/20 hover:bg-amber-400/[0.02]" : "border-white/[0.06] opacity-50 cursor-not-allowed"
)}> )}>
<input <input
type="checkbox" type="checkbox"
checked={form.notify_sms} checked={form.notify_sms}
onChange={(e) => isTycoon && setForm({ ...form, notify_sms: e.target.checked })} onChange={(e) => isTycoon && setForm({ ...form, notify_sms: e.target.checked })}
disabled={!isTycoon} disabled={!isTycoon}
className="w-5 h-5 rounded border-white/20 bg-black text-amber-500 focus:ring-amber-500 focus:ring-offset-0 disabled:opacity-50" className="w-4 h-4 border-white/20 bg-black text-amber-400 focus:ring-amber-400 focus:ring-offset-0 disabled:opacity-50"
/> />
<MessageSquare className="w-4 h-4 text-amber-400" /> <MessageSquare className="w-4 h-4 text-amber-400" />
<span className="text-sm text-zinc-300 flex-1">SMS notifications</span> <span className="text-sm text-white/60 flex-1">SMS notifications</span>
{!isTycoon && <Crown className="w-4 h-4 text-amber-400" />} {!isTycoon && <Crown className="w-4 h-4 text-amber-400" />}
</label> </label>
</div> </div>
@ -807,14 +704,14 @@ function CreateEditModal({ alert, onClose, onSuccess, isTycoon }: {
<button <button
type="button" type="button"
onClick={onClose} onClick={onClose}
className="flex-1 px-4 py-3 border border-white/10 text-zinc-400 rounded-xl hover:bg-white/5 hover:text-white transition-all font-medium" className="flex-1 px-4 py-3 border border-white/10 text-white/60 hover:bg-white/5 hover:text-white transition-all font-medium"
> >
Cancel Cancel
</button> </button>
<button <button
type="submit" type="submit"
disabled={loading || !form.name} disabled={loading || !form.name}
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-emerald-500 text-white font-bold rounded-xl hover:bg-emerald-400 transition-all disabled:opacity-50 shadow-lg shadow-emerald-500/20" className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-accent text-black font-semibold hover:bg-white transition-all disabled:opacity-50"
> >
{loading ? ( {loading ? (
<> <>
@ -824,7 +721,7 @@ function CreateEditModal({ alert, onClose, onSuccess, isTycoon }: {
) : ( ) : (
<> <>
<CheckCircle className="w-5 h-5" /> <CheckCircle className="w-5 h-5" />
{isEditing ? 'Update Alert' : 'Create Alert'} {isEditing ? 'Update' : 'Create'}
</> </>
)} )}
</button> </button>
@ -834,4 +731,3 @@ function CreateEditModal({ alert, onClose, onSuccess, isTycoon }: {
</div> </div>
) )
} }

View File

@ -9,7 +9,6 @@ import {
CheckCircle2, CheckCircle2,
Clock, Clock,
AlertCircle, AlertCircle,
ArrowUpRight,
MousePointer, MousePointer,
Target, Target,
Wallet, Wallet,
@ -17,85 +16,42 @@ import {
ChevronRight, ChevronRight,
Copy, Copy,
Check, Check,
ExternalLink,
XCircle, XCircle,
Sparkles, Sparkles,
Loader2 Loader2
} from 'lucide-react' } from 'lucide-react'
import { api, YieldDomain, YieldTransaction } from '@/lib/api' import { api, YieldDomain, YieldTransaction } from '@/lib/api'
import { useStore } from '@/lib/store' import { useStore } from '@/lib/store'
import { CommandCenterLayout } from '@/components/CommandCenterLayout'
import clsx from 'clsx'
// Stats Card Component // ============================================================================
function StatsCard({ // STATUS BADGE
label, // ============================================================================
value,
subValue,
icon: Icon,
trend,
color = 'emerald'
}: {
label: string
value: string | number
subValue?: string
icon: any
trend?: number
color?: 'emerald' | 'blue' | 'amber' | 'purple'
}) {
const colorClasses = {
emerald: 'from-emerald-500/20 to-emerald-500/5 text-emerald-400 border-emerald-500/30',
blue: 'from-blue-500/20 to-blue-500/5 text-blue-400 border-blue-500/30',
amber: 'from-amber-500/20 to-amber-500/5 text-amber-400 border-amber-500/30',
purple: 'from-purple-500/20 to-purple-500/5 text-purple-400 border-purple-500/30',
}
return (
<div className={`
relative overflow-hidden rounded-xl border bg-gradient-to-br p-5
${colorClasses[color]}
`}>
<div className="flex items-start justify-between">
<div>
<p className="text-xs uppercase tracking-wider text-zinc-400 mb-1">{label}</p>
<p className="text-2xl font-bold text-white">{value}</p>
{subValue && (
<p className="text-sm text-zinc-400 mt-1">{subValue}</p>
)}
</div>
<div className="p-2 rounded-lg bg-black/20">
<Icon className="w-5 h-5" />
</div>
</div>
{trend !== undefined && (
<div className={`mt-3 flex items-center gap-1 text-xs ${trend >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
<ArrowUpRight className={`w-3 h-3 ${trend < 0 ? 'rotate-180' : ''}`} />
<span>{Math.abs(trend)}% vs last month</span>
</div>
)}
</div>
)
}
// Domain Status Badge
function StatusBadge({ status }: { status: string }) { function StatusBadge({ status }: { status: string }) {
const config: Record<string, { color: string; icon: any }> = { const config: Record<string, { color: string; icon: any }> = {
active: { color: 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30', icon: CheckCircle2 }, active: { color: 'bg-accent/10 text-accent border-accent/20', icon: CheckCircle2 },
pending: { color: 'bg-amber-500/20 text-amber-400 border-amber-500/30', icon: Clock }, pending: { color: 'bg-amber-400/10 text-amber-400 border-amber-400/20', icon: Clock },
verifying: { color: 'bg-blue-500/20 text-blue-400 border-blue-500/30', icon: RefreshCw }, verifying: { color: 'bg-blue-400/10 text-blue-400 border-blue-400/20', icon: RefreshCw },
paused: { color: 'bg-zinc-500/20 text-zinc-400 border-zinc-500/30', icon: AlertCircle }, paused: { color: 'bg-white/5 text-white/40 border-white/10', icon: AlertCircle },
error: { color: 'bg-red-500/20 text-red-400 border-red-500/30', icon: XCircle }, error: { color: 'bg-red-400/10 text-red-400 border-red-400/20', icon: XCircle },
} }
const { color, icon: Icon } = config[status] || config.pending const { color, icon: Icon } = config[status] || config.pending
return ( return (
<span className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs border ${color}`}> <span className={`inline-flex items-center gap-1.5 px-2 py-0.5 text-[10px] font-mono border ${color}`}>
<Icon className="w-3 h-3" /> <Icon className="w-3 h-3" />
{status} {status}
</span> </span>
) )
} }
// Activate Domain Modal // ============================================================================
// ACTIVATE MODAL
// ============================================================================
function ActivateModal({ function ActivateModal({
isOpen, isOpen,
onClose, onClose,
@ -115,7 +71,6 @@ function ActivateModal({
const handleAnalyze = async () => { const handleAnalyze = async () => {
if (!domain.trim()) return if (!domain.trim()) return
setLoading(true) setLoading(true)
setError(null) setError(null)
@ -163,19 +118,17 @@ function ActivateModal({
if (!isOpen) return null if (!isOpen) return null
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center"> <div className="fixed inset-0 z-[100] flex items-center justify-center">
<div className="absolute inset-0 bg-black/70 backdrop-blur-sm" onClick={onClose} /> <div className="absolute inset-0 bg-black/80 backdrop-blur-sm" onClick={onClose} />
<div className="relative bg-zinc-900 border border-zinc-800 rounded-2xl w-full max-w-lg mx-4 overflow-hidden"> <div className="relative bg-[#050505] border border-white/10 w-full max-w-lg mx-4">
{/* Header */} {/* Header */}
<div className="p-6 border-b border-zinc-800"> <div className="p-6 border-b border-white/[0.08]">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-emerald-500/20"> <Sparkles className="w-5 h-5 text-accent" />
<Sparkles className="w-5 h-5 text-emerald-400" />
</div>
<div> <div>
<h2 className="text-lg font-semibold text-white">Activate Domain for Yield</h2> <h2 className="font-medium text-white">Activate Domain for Yield</h2>
<p className="text-sm text-zinc-400">Turn your parked domains into passive income</p> <p className="text-xs text-white/40">Turn parked domains into passive income</p>
</div> </div>
</div> </div>
</div> </div>
@ -185,19 +138,19 @@ function ActivateModal({
{step === 'input' && ( {step === 'input' && (
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="block text-sm text-zinc-400 mb-2">Domain Name</label> <label className="block text-xs text-white/50 mb-2">Domain Name</label>
<input <input
type="text" type="text"
value={domain} value={domain}
onChange={(e) => setDomain(e.target.value)} onChange={(e) => setDomain(e.target.value)}
placeholder="e.g. zahnarzt-zuerich.ch" placeholder="e.g. zahnarzt-zuerich.ch"
className="w-full px-4 py-3 bg-zinc-800 border border-zinc-700 rounded-lg text-white placeholder:text-zinc-500 focus:outline-none focus:border-emerald-500" className="w-full px-4 py-3 bg-white/5 border border-white/10 text-white placeholder:text-white/25 outline-none focus:border-accent/50"
onKeyDown={(e) => e.key === 'Enter' && handleAnalyze()} onKeyDown={(e) => e.key === 'Enter' && handleAnalyze()}
/> />
</div> </div>
{error && ( {error && (
<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/30 text-red-400 text-sm"> <div className="p-3 bg-red-400/10 border border-red-400/20 text-red-400 text-sm">
{error} {error}
</div> </div>
)} )}
@ -205,7 +158,7 @@ function ActivateModal({
<button <button
onClick={handleAnalyze} onClick={handleAnalyze}
disabled={loading || !domain.trim()} disabled={loading || !domain.trim()}
className="w-full py-3 px-4 bg-emerald-600 hover:bg-emerald-500 disabled:bg-zinc-700 disabled:text-zinc-400 text-white font-medium rounded-lg transition-colors flex items-center justify-center gap-2" className="w-full py-3 px-4 bg-accent text-black font-semibold hover:bg-white disabled:bg-white/10 disabled:text-white/40 transition-colors flex items-center justify-center gap-2"
> >
{loading ? ( {loading ? (
<> <>
@ -224,28 +177,28 @@ function ActivateModal({
{step === 'analyze' && analysis && ( {step === 'analyze' && analysis && (
<div className="space-y-5"> <div className="space-y-5">
{/* Intent Detection Results */} <div className="p-4 bg-white/[0.02] border border-white/[0.08]">
<div className="p-4 rounded-xl bg-zinc-800/50 border border-zinc-700">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<span className="text-sm text-zinc-400">Detected Intent</span> <span className="text-xs text-white/40">Detected Intent</span>
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${ <span className={clsx(
analysis.intent.confidence > 0.7 ? 'bg-emerald-500/20 text-emerald-400' : "px-2 py-0.5 text-[10px] font-mono",
analysis.intent.confidence > 0.4 ? 'bg-amber-500/20 text-amber-400' : analysis.intent.confidence > 0.7 ? 'bg-accent/10 text-accent' :
'bg-zinc-500/20 text-zinc-400' analysis.intent.confidence > 0.4 ? 'bg-amber-400/10 text-amber-400' :
}`}> 'bg-white/5 text-white/40'
)}>
{Math.round(analysis.intent.confidence * 100)}% confidence {Math.round(analysis.intent.confidence * 100)}% confidence
</span> </span>
</div> </div>
<p className="text-lg font-semibold text-white capitalize"> <p className="text-base font-medium text-white capitalize">
{analysis.intent.category.replace('_', ' ')} {analysis.intent.category.replace('_', ' ')}
{analysis.intent.subcategory && ( {analysis.intent.subcategory && (
<span className="text-zinc-400"> / {analysis.intent.subcategory.replace('_', ' ')}</span> <span className="text-white/40"> / {analysis.intent.subcategory.replace('_', ' ')}</span>
)} )}
</p> </p>
{analysis.intent.keywords_matched.length > 0 && ( {analysis.intent.keywords_matched.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1"> <div className="mt-2 flex flex-wrap gap-1">
{analysis.intent.keywords_matched.map((kw: string, i: number) => ( {analysis.intent.keywords_matched.map((kw: string, i: number) => (
<span key={i} className="px-2 py-0.5 bg-zinc-700 rounded text-xs text-zinc-300"> <span key={i} className="px-2 py-0.5 bg-white/5 text-[10px] font-mono text-white/60">
{kw.split('~')[0]} {kw.split('~')[0]}
</span> </span>
))} ))}
@ -253,37 +206,33 @@ function ActivateModal({
)} )}
</div> </div>
{/* Value Estimate */} <div className="p-4 bg-accent/5 border border-accent/20">
<div className="p-4 rounded-xl bg-gradient-to-br from-emerald-500/10 to-transparent border border-emerald-500/30"> <span className="text-xs text-white/40">Estimated Monthly Revenue</span>
<span className="text-sm text-zinc-400">Estimated Monthly Revenue</span>
<div className="flex items-baseline gap-2 mt-1"> <div className="flex items-baseline gap-2 mt-1">
<span className="text-3xl font-bold text-emerald-400"> <span className="text-2xl font-display text-accent">
{analysis.value.currency} {analysis.value.estimated_monthly_min} {analysis.value.currency} {analysis.value.estimated_monthly_min}
</span> </span>
<span className="text-zinc-400">-</span> <span className="text-white/40">-</span>
<span className="text-3xl font-bold text-emerald-400"> <span className="text-2xl font-display text-accent">
{analysis.value.estimated_monthly_max} {analysis.value.estimated_monthly_max}
</span> </span>
</div> </div>
<p className="text-xs text-zinc-500 mt-2">
Based on intent category, geo-targeting, and partner rates
</p>
</div> </div>
{/* Monetization Potential */} <div className="flex items-center justify-between p-3 bg-white/[0.02] border border-white/[0.06]">
<div className="flex items-center justify-between p-3 rounded-lg bg-zinc-800/50"> <span className="text-xs text-white/40">Monetization Potential</span>
<span className="text-sm text-zinc-400">Monetization Potential</span> <span className={clsx(
<span className={`font-medium ${ "font-medium text-sm",
analysis.monetization_potential === 'high' ? 'text-emerald-400' : analysis.monetization_potential === 'high' ? 'text-accent' :
analysis.monetization_potential === 'medium' ? 'text-amber-400' : analysis.monetization_potential === 'medium' ? 'text-amber-400' :
'text-zinc-400' 'text-white/40'
}`}> )}>
{analysis.monetization_potential.toUpperCase()} {analysis.monetization_potential.toUpperCase()}
</span> </span>
</div> </div>
{error && ( {error && (
<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/30 text-red-400 text-sm"> <div className="p-3 bg-red-400/10 border border-red-400/20 text-red-400 text-sm">
{error} {error}
</div> </div>
)} )}
@ -291,14 +240,14 @@ function ActivateModal({
<div className="flex gap-3"> <div className="flex gap-3">
<button <button
onClick={() => setStep('input')} onClick={() => setStep('input')}
className="flex-1 py-3 px-4 bg-zinc-800 hover:bg-zinc-700 text-white font-medium rounded-lg transition-colors" className="flex-1 py-3 px-4 bg-white/5 border border-white/10 text-white/60 hover:bg-white/10 hover:text-white transition-colors"
> >
Back Back
</button> </button>
<button <button
onClick={handleActivate} onClick={handleActivate}
disabled={loading} disabled={loading}
className="flex-1 py-3 px-4 bg-emerald-600 hover:bg-emerald-500 disabled:bg-zinc-700 text-white font-medium rounded-lg transition-colors flex items-center justify-center gap-2" className="flex-1 py-3 px-4 bg-accent text-black font-semibold hover:bg-white disabled:bg-white/10 disabled:text-white/40 transition-colors flex items-center justify-center gap-2"
> >
{loading ? ( {loading ? (
<> <>
@ -319,29 +268,27 @@ function ActivateModal({
{step === 'dns' && dnsInstructions && ( {step === 'dns' && dnsInstructions && (
<div className="space-y-5"> <div className="space-y-5">
<div className="text-center mb-4"> <div className="text-center mb-4">
<div className="w-12 h-12 rounded-full bg-emerald-500/20 flex items-center justify-center mx-auto mb-3"> <div className="w-12 h-12 bg-accent/10 flex items-center justify-center mx-auto mb-3">
<CheckCircle2 className="w-6 h-6 text-emerald-400" /> <CheckCircle2 className="w-6 h-6 text-accent" />
</div> </div>
<h3 className="text-lg font-semibold text-white">Domain Registered!</h3> <h3 className="font-medium text-white">Domain Registered!</h3>
<p className="text-sm text-zinc-400 mt-1">Complete DNS setup to start earning</p> <p className="text-xs text-white/40 mt-1">Complete DNS setup to start earning</p>
</div> </div>
{/* Nameserver Option */} <div className="p-4 bg-white/[0.02] border border-white/[0.08]">
<div className="p-4 rounded-xl bg-zinc-800/50 border border-zinc-700"> <h4 className="text-xs font-medium text-white mb-3">Option 1: Nameservers</h4>
<h4 className="text-sm font-medium text-white mb-3">Option 1: Nameservers</h4>
<p className="text-xs text-zinc-500 mb-3">Point your domain to our nameservers:</p>
<div className="space-y-2"> <div className="space-y-2">
{dnsInstructions.nameservers.map((ns: string, i: number) => ( {dnsInstructions.nameservers.map((ns: string, i: number) => (
<div key={i} className="flex items-center justify-between p-2 rounded bg-zinc-900"> <div key={i} className="flex items-center justify-between p-2 bg-black/30">
<code className="text-sm text-emerald-400">{ns}</code> <code className="text-sm text-accent font-mono">{ns}</code>
<button <button
onClick={() => copyToClipboard(ns, `ns-${i}`)} onClick={() => copyToClipboard(ns, `ns-${i}`)}
className="p-1 hover:bg-zinc-700 rounded" className="p-1 hover:bg-white/10"
> >
{copied === `ns-${i}` ? ( {copied === `ns-${i}` ? (
<Check className="w-4 h-4 text-emerald-400" /> <Check className="w-4 h-4 text-accent" />
) : ( ) : (
<Copy className="w-4 h-4 text-zinc-400" /> <Copy className="w-4 h-4 text-white/40" />
)} )}
</button> </button>
</div> </div>
@ -349,25 +296,23 @@ function ActivateModal({
</div> </div>
</div> </div>
{/* CNAME Option */} <div className="p-4 bg-white/[0.02] border border-white/[0.08]">
<div className="p-4 rounded-xl bg-zinc-800/50 border border-zinc-700"> <h4 className="text-xs font-medium text-white mb-3">Option 2: CNAME Record</h4>
<h4 className="text-sm font-medium text-white mb-3">Option 2: CNAME Record</h4> <div className="flex items-center justify-between p-2 bg-black/30">
<p className="text-xs text-zinc-500 mb-3">Or add a CNAME record:</p> <div className="text-sm">
<div className="flex items-center justify-between p-2 rounded bg-zinc-900"> <span className="text-white/40">Host: </span>
<div> <code className="text-white font-mono">{dnsInstructions.cname_host}</code>
<span className="text-xs text-zinc-500">Host: </span> <span className="text-white/40 mx-2"></span>
<code className="text-sm text-white">{dnsInstructions.cname_host}</code> <code className="text-accent font-mono">{dnsInstructions.cname_target}</code>
<span className="text-xs text-zinc-500 mx-2"></span>
<code className="text-sm text-emerald-400">{dnsInstructions.cname_target}</code>
</div> </div>
<button <button
onClick={() => copyToClipboard(dnsInstructions.cname_target, 'cname')} onClick={() => copyToClipboard(dnsInstructions.cname_target, 'cname')}
className="p-1 hover:bg-zinc-700 rounded" className="p-1 hover:bg-white/10"
> >
{copied === 'cname' ? ( {copied === 'cname' ? (
<Check className="w-4 h-4 text-emerald-400" /> <Check className="w-4 h-4 text-accent" />
) : ( ) : (
<Copy className="w-4 h-4 text-zinc-400" /> <Copy className="w-4 h-4 text-white/40" />
)} )}
</button> </button>
</div> </div>
@ -375,7 +320,7 @@ function ActivateModal({
<button <button
onClick={handleDone} onClick={handleDone}
className="w-full py-3 px-4 bg-emerald-600 hover:bg-emerald-500 text-white font-medium rounded-lg transition-colors" className="w-full py-3 px-4 bg-accent text-black font-semibold hover:bg-white transition-colors"
> >
Done Done
</button> </button>
@ -387,7 +332,10 @@ function ActivateModal({
) )
} }
// Main Yield Page // ============================================================================
// MAIN PAGE
// ============================================================================
export default function YieldPage() { export default function YieldPage() {
const { subscription } = useStore() const { subscription } = useStore()
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
@ -416,162 +364,185 @@ export default function YieldPage() {
fetchDashboard() fetchDashboard()
} }
if (loading) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="flex flex-col items-center gap-4">
<div className="w-10 h-10 border-2 border-emerald-500 border-t-transparent rounded-full animate-spin" />
<p className="text-zinc-400">Loading yield dashboard...</p>
</div>
</div>
)
}
const stats = dashboard?.stats const stats = dashboard?.stats
return ( return (
<div className="p-6 space-y-6"> <CommandCenterLayout minimal>
{/* Header */} {/* ═══════════════════════════════════════════════════════════════════════ */}
<div className="flex items-center justify-between"> {/* HEADER */}
<div> {/* ═══════════════════════════════════════════════════════════════════════ */}
<h1 className="text-2xl font-bold text-white flex items-center gap-3"> <section className="pt-6 lg:pt-8 pb-6">
<div className="p-2 rounded-lg bg-gradient-to-br from-emerald-500/20 to-purple-500/20"> <div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-6">
<TrendingUp className="w-6 h-6 text-emerald-400" /> <div className="space-y-3">
<div className="inline-flex items-center gap-2">
<TrendingUp className="w-4 h-4 text-accent" />
<span className="text-[10px] font-mono tracking-wide text-accent">Passive Income</span>
</div> </div>
Yield
</h1> <h1 className="font-display text-[2rem] lg:text-[2.5rem] leading-[1] tracking-[-0.02em]">
<p className="text-zinc-400 mt-1">Turn parked domains into passive income</p> <span className="text-white">Yield</span>
</div> </h1>
<div className="flex items-center gap-3"> <p className="text-white/40 text-sm max-w-md">
<button Turn parked domains into passive income with intent-based monetization
onClick={handleRefresh} </p>
disabled={refreshing} </div>
className="p-2 rounded-lg bg-zinc-800 hover:bg-zinc-700 text-zinc-400 hover:text-white transition-colors"
>
<RefreshCw className={`w-5 h-5 ${refreshing ? 'animate-spin' : ''}`} />
</button>
<button <div className="flex items-center gap-4">
onClick={() => setShowActivateModal(true)} {stats && (
className="flex items-center gap-2 px-4 py-2 bg-emerald-600 hover:bg-emerald-500 text-white font-medium rounded-lg transition-colors" <div className="flex gap-6">
> <div className="text-right">
<Plus className="w-4 h-4" /> <div className="text-xl font-display text-accent">{stats.currency} {stats.monthly_revenue.toLocaleString()}</div>
Add Domain <div className="text-[9px] tracking-wide text-white/30 font-mono">Monthly</div>
</button> </div>
</div> <div className="text-right">
</div> <div className="text-xl font-display text-white">{stats.active_domains}</div>
<div className="text-[9px] tracking-wide text-white/30 font-mono">Active</div>
{/* Stats Grid */} </div>
{stats && ( <div className="text-right">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> <div className="text-xl font-display text-white">{stats.monthly_clicks.toLocaleString()}</div>
<StatsCard <div className="text-[9px] tracking-wide text-white/30 font-mono">Clicks</div>
label="Monthly Revenue" </div>
value={`${stats.currency} ${stats.monthly_revenue.toLocaleString()}`} </div>
subValue={`Lifetime: ${stats.currency} ${stats.lifetime_revenue.toLocaleString()}`} )}
icon={DollarSign}
color="emerald" <div className="flex items-center gap-2">
/> <button
<StatsCard onClick={handleRefresh}
label="Active Domains" disabled={refreshing}
value={stats.active_domains} className="w-9 h-9 flex items-center justify-center border border-white/10 text-white/40 hover:text-white hover:bg-white/5 transition-colors"
subValue={`${stats.pending_domains} pending`} >
icon={Zap} <RefreshCw className={clsx("w-4 h-4", refreshing && "animate-spin")} />
color="blue" </button>
/>
<StatsCard <button
label="Monthly Clicks" onClick={() => setShowActivateModal(true)}
value={stats.monthly_clicks.toLocaleString()} className="flex items-center gap-2 px-4 py-2 bg-accent text-black text-sm font-semibold hover:bg-white transition-colors"
subValue={`${stats.monthly_conversions} conversions`} >
icon={MousePointer} <Plus className="w-4 h-4" />
color="amber" Add Domain
/> </button>
<StatsCard
label="Pending Payout"
value={`${stats.currency} ${stats.pending_payout.toLocaleString()}`}
subValue={stats.next_payout_date ? `Next: ${new Date(stats.next_payout_date).toLocaleDateString()}` : undefined}
icon={Wallet}
color="purple"
/>
</div>
)}
{/* Domains Table */}
<div className="bg-zinc-900/50 border border-zinc-800 rounded-xl overflow-hidden">
<div className="p-4 border-b border-zinc-800 flex items-center justify-between">
<h2 className="text-lg font-semibold text-white">Your Yield Domains</h2>
<span className="text-sm text-zinc-400">{dashboard?.domains?.length || 0} domains</span>
</div>
{dashboard?.domains?.length === 0 ? (
<div className="p-12 text-center">
<div className="w-16 h-16 rounded-full bg-zinc-800 flex items-center justify-center mx-auto mb-4">
<TrendingUp className="w-8 h-8 text-zinc-600" />
</div> </div>
<h3 className="text-lg font-medium text-white mb-2">No yield domains yet</h3> </div>
<p className="text-zinc-400 mb-6 max-w-md mx-auto"> </div>
Activate your first domain to start generating passive income from visitor intent routing. </section>
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* STATS */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
{stats && (
<section className="pb-6 border-b border-white/[0.08]">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<div className="p-4 bg-accent/5 border border-accent/20">
<DollarSign className="w-5 h-5 text-accent mb-2" />
<div className="text-xl font-display text-white">{stats.currency} {stats.monthly_revenue.toLocaleString()}</div>
<div className="text-[10px] text-white/40 font-mono">Monthly Revenue</div>
<div className="text-[10px] text-white/30 mt-1">Lifetime: {stats.currency} {stats.lifetime_revenue.toLocaleString()}</div>
</div>
<div className="p-4 bg-white/[0.02] border border-white/[0.08]">
<Zap className="w-5 h-5 text-blue-400 mb-2" />
<div className="text-xl font-display text-white">{stats.active_domains}</div>
<div className="text-[10px] text-white/40 font-mono">Active Domains</div>
<div className="text-[10px] text-white/30 mt-1">{stats.pending_domains} pending</div>
</div>
<div className="p-4 bg-white/[0.02] border border-white/[0.08]">
<MousePointer className="w-5 h-5 text-amber-400 mb-2" />
<div className="text-xl font-display text-white">{stats.monthly_clicks.toLocaleString()}</div>
<div className="text-[10px] text-white/40 font-mono">Monthly Clicks</div>
<div className="text-[10px] text-white/30 mt-1">{stats.monthly_conversions} conversions</div>
</div>
<div className="p-4 bg-white/[0.02] border border-white/[0.08]">
<Wallet className="w-5 h-5 text-purple-400 mb-2" />
<div className="text-xl font-display text-white">{stats.currency} {stats.pending_payout.toLocaleString()}</div>
<div className="text-[10px] text-white/40 font-mono">Pending Payout</div>
{stats.next_payout_date && (
<div className="text-[10px] text-white/30 mt-1">Next: {new Date(stats.next_payout_date).toLocaleDateString()}</div>
)}
</div>
</div>
</section>
)}
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* DOMAINS TABLE */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
<section className="py-6">
{loading ? (
<div className="flex items-center justify-center py-20">
<Loader2 className="w-6 h-6 text-accent animate-spin" />
</div>
) : dashboard?.domains?.length === 0 ? (
<div className="text-center py-16">
<div className="w-14 h-14 mx-auto bg-white/[0.02] border border-white/10 flex items-center justify-center mb-4">
<TrendingUp className="w-6 h-6 text-white/20" />
</div>
<p className="text-white/40 text-sm mb-2">No yield domains yet</p>
<p className="text-white/25 text-xs mb-6 max-w-sm mx-auto">
Activate your first domain to start generating passive income
</p> </p>
<button <button
onClick={() => setShowActivateModal(true)} onClick={() => setShowActivateModal(true)}
className="inline-flex items-center gap-2 px-6 py-3 bg-emerald-600 hover:bg-emerald-500 text-white font-medium rounded-lg transition-colors" className="inline-flex items-center gap-2 px-4 py-2 bg-accent text-black text-sm font-semibold hover:bg-white transition-colors"
> >
<Plus className="w-4 h-4" /> <Plus className="w-4 h-4" />
Add Your First Domain Add First Domain
</button> </button>
</div> </div>
) : ( ) : (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <table className="w-full min-w-[800px]">
<thead> <thead>
<tr className="text-left text-xs uppercase tracking-wider text-zinc-500 bg-zinc-800/50"> <tr className="text-xs text-white/40 border-b border-white/[0.06]">
<th className="px-4 py-3">Domain</th> <th className="text-left py-3 px-4 font-medium">Domain</th>
<th className="px-4 py-3">Status</th> <th className="text-left py-3 px-4 font-medium">Status</th>
<th className="px-4 py-3">Intent</th> <th className="text-left py-3 px-4 font-medium">Intent</th>
<th className="px-4 py-3">Clicks</th> <th className="text-right py-3 px-4 font-medium">Clicks</th>
<th className="px-4 py-3">Conversions</th> <th className="text-right py-3 px-4 font-medium">Conversions</th>
<th className="px-4 py-3 text-right">Revenue</th> <th className="text-right py-3 px-4 font-medium">Revenue</th>
<th className="px-4 py-3"></th> <th className="text-right py-3 px-4 font-medium"></th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-zinc-800"> <tbody>
{dashboard?.domains?.map((domain: YieldDomain) => ( {dashboard?.domains?.map((domain: YieldDomain) => (
<tr key={domain.id} className="hover:bg-zinc-800/30 transition-colors"> <tr key={domain.id} className="group border-b border-white/[0.04] hover:bg-white/[0.02]">
<td className="px-4 py-4"> <td className="py-3 px-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-zinc-800 flex items-center justify-center text-emerald-400 text-xs font-bold"> <div className="w-8 h-8 bg-white/5 border border-white/10 flex items-center justify-center text-accent text-xs font-bold">
{domain.domain.charAt(0).toUpperCase()} {domain.domain.charAt(0).toUpperCase()}
</div> </div>
<span className="font-medium text-white">{domain.domain}</span> <span className="font-medium text-white">{domain.domain}</span>
</div> </div>
</td> </td>
<td className="px-4 py-4"> <td className="py-3 px-4">
<StatusBadge status={domain.status} /> <StatusBadge status={domain.status} />
</td> </td>
<td className="px-4 py-4"> <td className="py-3 px-4">
<span className="text-sm text-zinc-300 capitalize"> <span className="text-sm text-white/60 capitalize">
{domain.detected_intent?.replace('_', ' ') || '-'} {domain.detected_intent?.replace('_', ' ') || '-'}
</span> </span>
{domain.intent_confidence > 0 && ( {domain.intent_confidence > 0 && (
<span className="text-xs text-zinc-500 ml-2"> <span className="text-[10px] text-white/30 ml-2 font-mono">
({Math.round(domain.intent_confidence * 100)}%) ({Math.round(domain.intent_confidence * 100)}%)
</span> </span>
)} )}
</td> </td>
<td className="px-4 py-4 text-zinc-300"> <td className="py-3 px-4 text-right text-white/60 font-mono">
{domain.total_clicks.toLocaleString()} {domain.total_clicks.toLocaleString()}
</td> </td>
<td className="px-4 py-4 text-zinc-300"> <td className="py-3 px-4 text-right text-white/60 font-mono">
{domain.total_conversions.toLocaleString()} {domain.total_conversions.toLocaleString()}
</td> </td>
<td className="px-4 py-4 text-right"> <td className="py-3 px-4 text-right">
<span className="font-medium text-emerald-400"> <span className="font-medium text-accent font-mono">
{domain.currency} {domain.total_revenue.toLocaleString()} {domain.currency} {domain.total_revenue.toLocaleString()}
</span> </span>
</td> </td>
<td className="px-4 py-4 text-right"> <td className="py-3 px-4 text-right">
<button className="p-1.5 hover:bg-zinc-700 rounded-lg transition-colors"> <button className="p-1.5 hover:bg-white/10 transition-colors opacity-0 group-hover:opacity-100">
<ChevronRight className="w-4 h-4 text-zinc-400" /> <ChevronRight className="w-4 h-4 text-white/40" />
</button> </button>
</td> </td>
</tr> </tr>
@ -580,57 +551,57 @@ export default function YieldPage() {
</table> </table>
</div> </div>
)} )}
</div> </section>
{/* Recent Transactions */} {/* ═══════════════════════════════════════════════════════════════════════ */}
{/* RECENT TRANSACTIONS */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
{dashboard?.recent_transactions?.length > 0 && ( {dashboard?.recent_transactions?.length > 0 && (
<div className="bg-zinc-900/50 border border-zinc-800 rounded-xl overflow-hidden"> <section className="py-6 border-t border-white/[0.06]">
<div className="p-4 border-b border-zinc-800"> <h2 className="text-xs font-mono text-white/40 tracking-wide mb-4">Recent Activity</h2>
<h2 className="text-lg font-semibold text-white">Recent Activity</h2> <div className="space-y-px">
</div>
<div className="divide-y divide-zinc-800">
{dashboard.recent_transactions.slice(0, 5).map((tx: YieldTransaction) => ( {dashboard.recent_transactions.slice(0, 5).map((tx: YieldTransaction) => (
<div key={tx.id} className="p-4 flex items-center justify-between hover:bg-zinc-800/30 transition-colors"> <div key={tx.id} className="flex items-center justify-between p-4 bg-white/[0.01] border border-white/[0.04] hover:bg-white/[0.02] transition-colors">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${ <div className={clsx(
tx.event_type === 'sale' ? 'bg-emerald-500/20' : "w-8 h-8 flex items-center justify-center",
tx.event_type === 'lead' ? 'bg-blue-500/20' : tx.event_type === 'sale' ? 'bg-accent/10' :
'bg-zinc-700' tx.event_type === 'lead' ? 'bg-blue-400/10' :
}`}> 'bg-white/5'
)}>
{tx.event_type === 'sale' ? ( {tx.event_type === 'sale' ? (
<DollarSign className="w-4 h-4 text-emerald-400" /> <DollarSign className="w-4 h-4 text-accent" />
) : tx.event_type === 'lead' ? ( ) : tx.event_type === 'lead' ? (
<Target className="w-4 h-4 text-blue-400" /> <Target className="w-4 h-4 text-blue-400" />
) : ( ) : (
<MousePointer className="w-4 h-4 text-zinc-400" /> <MousePointer className="w-4 h-4 text-white/40" />
)} )}
</div> </div>
<div> <div>
<p className="text-sm font-medium text-white capitalize">{tx.event_type}</p> <p className="text-sm font-medium text-white capitalize">{tx.event_type}</p>
<p className="text-xs text-zinc-500">{tx.partner_slug}</p> <p className="text-[10px] text-white/30 font-mono">{tx.partner_slug}</p>
</div> </div>
</div> </div>
<div className="text-right"> <div className="text-right">
<p className="text-sm font-medium text-emerald-400"> <p className="text-sm font-medium text-accent font-mono">
+{tx.currency} {tx.net_amount.toFixed(2)} +{tx.currency} {tx.net_amount.toFixed(2)}
</p> </p>
<p className="text-xs text-zinc-500"> <p className="text-[10px] text-white/30 font-mono">
{new Date(tx.created_at).toLocaleDateString()} {new Date(tx.created_at).toLocaleDateString()}
</p> </p>
</div> </div>
</div> </div>
))} ))}
</div> </div>
</div> </section>
)} )}
{/* Activate Modal */} {/* Activate Modal */}
<ActivateModal <ActivateModal
isOpen={showActivateModal} isOpen={showActivateModal}
onClose={() => setShowActivateModal(false)} onClose={() => setShowActivateModal(false)}
onSuccess={fetchDashboard} onSuccess={fetchDashboard}
/> />
</div> </CommandCenterLayout>
) )
} }