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
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:
@ -1,11 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState, useMemo, useCallback, memo } from 'react'
|
||||
import { useEffect, useState, useMemo, useCallback } from 'react'
|
||||
import { useStore } from '@/lib/store'
|
||||
import { api } from '@/lib/api'
|
||||
import { TerminalLayout } from '@/components/TerminalLayout'
|
||||
import { CommandCenterLayout } from '@/components/CommandCenterLayout'
|
||||
import {
|
||||
ExternalLink,
|
||||
Loader2,
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
@ -16,28 +15,39 @@ import {
|
||||
Search,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Info,
|
||||
ArrowRight,
|
||||
Lock,
|
||||
Sparkles,
|
||||
BarChart3,
|
||||
Activity,
|
||||
Zap,
|
||||
Filter,
|
||||
Check,
|
||||
Eye,
|
||||
ShieldCheck,
|
||||
Diamond,
|
||||
Minus
|
||||
} from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
import Link from 'next/link'
|
||||
|
||||
// ============================================================================
|
||||
// TIER ACCESS LEVELS
|
||||
// TYPES
|
||||
// ============================================================================
|
||||
|
||||
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 {
|
||||
switch (tier) {
|
||||
@ -48,175 +58,7 @@ function getTierLevel(tier: UserTier): number {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 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
|
||||
}
|
||||
const formatPrice = (p: number) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(p)
|
||||
|
||||
// ============================================================================
|
||||
// MAIN PAGE
|
||||
@ -225,30 +67,23 @@ interface TLDData {
|
||||
export default function IntelPage() {
|
||||
const { subscription } = useStore()
|
||||
|
||||
// Determine user tier
|
||||
const userTier: UserTier = (subscription?.tier as UserTier) || 'scout'
|
||||
const tierLevel = getTierLevel(userTier)
|
||||
|
||||
// Feature access checks
|
||||
const canSeeRenewal = tierLevel >= 2 // Trader+
|
||||
const canSee3yTrend = tierLevel >= 3 // Tycoon only
|
||||
const canSeeFullHistory = tierLevel >= 3 // Tycoon only
|
||||
const canSeeRenewal = tierLevel >= 2
|
||||
const canSee3yTrend = tierLevel >= 3
|
||||
|
||||
// Data
|
||||
const [tldData, setTldData] = useState<TLDData[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const [total, setTotal] = useState(0)
|
||||
|
||||
// Filters
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [filterType, setFilterType] = useState<'all' | 'tech' | 'geo' | 'budget'>('all')
|
||||
|
||||
// Sort
|
||||
const [sortField, setSortField] = useState<SortField>('popularity')
|
||||
const [sortDirection, setSortDirection] = useState<SortDirection>('asc')
|
||||
|
||||
// Load Data
|
||||
const loadData = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
@ -296,18 +131,12 @@ export default function IntelPage() {
|
||||
}
|
||||
}, [sortField, canSeeRenewal, canSee3yTrend])
|
||||
|
||||
// Transform & Filter
|
||||
const filteredData = useMemo(() => {
|
||||
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']
|
||||
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')
|
||||
|
||||
// Budget filter: registration under $10
|
||||
if (filterType === 'budget') data = data.filter(t => t.min_price < 10)
|
||||
|
||||
if (searchQuery) {
|
||||
@ -341,295 +170,277 @@ export default function IntelPage() {
|
||||
return { lowest, hottest, traps, avgRenewal }
|
||||
}, [tldData])
|
||||
|
||||
const formatPrice = (p: number) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(p)
|
||||
|
||||
return (
|
||||
<TerminalLayout hideHeaderSearch={true}>
|
||||
<div className="relative font-sans text-zinc-100 selection:bg-emerald-500/30">
|
||||
<CommandCenterLayout minimal>
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* HEADER */}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
<section className="pt-6 lg:pt-8 pb-6">
|
||||
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-6">
|
||||
<div className="space-y-3">
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<BarChart3 className="w-4 h-4 text-accent" />
|
||||
<span className="text-[10px] font-mono tracking-wide text-accent">Pricing Analytics</span>
|
||||
</div>
|
||||
|
||||
{/* Ambient Background Glow */}
|
||||
<div className="fixed inset-0 pointer-events-none overflow-hidden">
|
||||
<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="absolute bottom-0 left-1/4 w-[600px] h-[500px] bg-blue-500/5 rounded-full blur-[100px] mix-blend-screen" />
|
||||
<h1 className="font-display text-[2rem] lg:text-[2.5rem] leading-[1] tracking-[-0.02em]">
|
||||
<span className="text-white">TLD Intel</span>
|
||||
<span className="text-white/30 ml-3">{total}</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-6 lg:gap-8">
|
||||
<div className="text-right">
|
||||
<div className="text-xl font-display text-accent">{formatPrice(stats.lowest)}</div>
|
||||
<div className="text-[9px] tracking-wide text-white/30 font-mono">Lowest Entry</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>
|
||||
</section>
|
||||
|
||||
<div className="relative z-10 max-w-[1600px] mx-auto p-4 md:p-8 space-y-8">
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* 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>
|
||||
|
||||
{/* 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 className="flex-1" />
|
||||
|
||||
{/* Quick Stats Pills */}
|
||||
<div className="flex gap-2">
|
||||
<div className={clsx(
|
||||
"px-3 py-1.5 rounded-full border flex items-center gap-2 text-xs font-medium",
|
||||
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 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>
|
||||
|
||||
{/* 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>
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* 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
|
||||
|
||||
{/* 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>
|
||||
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>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</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 for Scout users */}
|
||||
{userTier === 'scout' && (
|
||||
<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="w-12 h-12 bg-emerald-500/10 rounded-full flex items-center justify-center mx-auto mb-4 text-emerald-400">
|
||||
<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>
|
||||
</TerminalLayout>
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* 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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -415,7 +415,7 @@ export default function PortfolioPage() {
|
||||
<AlertCircle className="w-5 h-5" />
|
||||
<p className="text-sm flex-1">{error}</p>
|
||||
<button onClick={() => setError(null)}><X className="w-4 h-4" /></button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
@ -435,16 +435,16 @@ export default function PortfolioPage() {
|
||||
<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">
|
||||
Track your domain investments, monitor valuations, and calculate ROI. Know exactly how your portfolio is performing.
|
||||
</p>
|
||||
<Link
|
||||
href="/pricing"
|
||||
</p>
|
||||
<Link
|
||||
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"
|
||||
>
|
||||
>
|
||||
Upgrade to Trader <ArrowRight className="w-4 h-4" />
|
||||
</Link>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats Grid */}
|
||||
{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-1 text-center">Status</div>
|
||||
<div className="hidden md:block md:col-span-2 text-right">Actions</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<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>
|
||||
{domain.purchase_date && (
|
||||
<div className="text-[10px] text-zinc-600">{formatDate(domain.purchase_date)}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Value */}
|
||||
<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">
|
||||
{!domain.is_sold && (
|
||||
<>
|
||||
<button
|
||||
<button
|
||||
onClick={() => openListModal(domain)}
|
||||
className="p-2 rounded-lg text-zinc-600 hover:text-amber-400 hover:bg-amber-500/10 transition-all"
|
||||
title="List for sale"
|
||||
>
|
||||
>
|
||||
<Tag className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleRefreshValue(domain)}
|
||||
className="p-2 rounded-lg text-zinc-600 hover:text-blue-400 hover:bg-blue-500/10 transition-all"
|
||||
title="Refresh value"
|
||||
>
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openSellModal(domain)}
|
||||
className="p-2 rounded-lg text-zinc-600 hover:text-emerald-400 hover:bg-emerald-500/10 transition-all"
|
||||
title="Record sale"
|
||||
>
|
||||
<DollarSign className="w-4 h-4" />
|
||||
</button>
|
||||
>
|
||||
<DollarSign className="w-4 h-4" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
<button
|
||||
onClick={() => openEditModal(domain)}
|
||||
className="p-2 rounded-lg text-zinc-600 hover:text-white hover:bg-white/10 transition-all"
|
||||
title="Edit"
|
||||
@ -626,22 +626,22 @@ export default function PortfolioPage() {
|
||||
onClick={() => handleDelete(domain)}
|
||||
className="p-2 rounded-lg text-zinc-600 hover:text-rose-400 hover:bg-rose-500/10 transition-all"
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
{/* Add Modal */}
|
||||
{showAddModal && (
|
||||
{/* Add Modal */}
|
||||
{showAddModal && (
|
||||
<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="p-6 border-b border-white/5">
|
||||
@ -650,55 +650,55 @@ export default function PortfolioPage() {
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={formData.domain}
|
||||
onChange={(e) => setFormData({ ...formData, domain: e.target.value })}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Purchase Date</label>
|
||||
<input
|
||||
<input
|
||||
type="date"
|
||||
value={formData.purchase_date}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Purchase Price</label>
|
||||
<div className="relative">
|
||||
<DollarSign className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-500" />
|
||||
<input
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={formData.purchase_price}
|
||||
onChange={(e) => setFormData({ ...formData, purchase_price: e.target.value })}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<input
|
||||
type="text"
|
||||
<input
|
||||
type="text"
|
||||
value={formData.registrar}
|
||||
onChange={(e) => setFormData({ ...formData, registrar: e.target.value })}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Renewal Date</label>
|
||||
<input
|
||||
@ -722,29 +722,29 @@ export default function PortfolioPage() {
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAddModal(false)}
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
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"
|
||||
>
|
||||
>
|
||||
{saving ? <Loader2 className="w-5 h-5 animate-spin" /> : <Plus className="w-5 h-5" />}
|
||||
{saving ? 'Adding...' : 'Add Domain'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* Edit Modal */}
|
||||
{showEditModal && selectedDomain && (
|
||||
{/* Edit Modal */}
|
||||
{showEditModal && selectedDomain && (
|
||||
<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="p-6 border-b border-white/5">
|
||||
@ -753,8 +753,8 @@ export default function PortfolioPage() {
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleEdit} className="p-6 space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Purchase Date</label>
|
||||
<input
|
||||
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>
|
||||
<div className="relative">
|
||||
<DollarSign className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-500" />
|
||||
<input
|
||||
type="number"
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={formData.purchase_price}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<input
|
||||
type="text"
|
||||
<input
|
||||
type="text"
|
||||
value={formData.registrar}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Renewal Date</label>
|
||||
<input
|
||||
@ -796,7 +796,7 @@ export default function PortfolioPage() {
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@ -810,29 +810,29 @@ export default function PortfolioPage() {
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowEditModal(false)}
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
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"
|
||||
>
|
||||
>
|
||||
{saving ? <Loader2 className="w-5 h-5 animate-spin" /> : <CheckCircle className="w-5 h-5" />}
|
||||
{saving ? 'Saving...' : 'Save Changes'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* 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="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">
|
||||
@ -843,25 +843,25 @@ export default function PortfolioPage() {
|
||||
<p className="text-sm text-zinc-400 mt-1">
|
||||
Congratulations on selling <strong className="text-white">{selectedDomain.domain}</strong>!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<input
|
||||
<input
|
||||
type="date"
|
||||
required
|
||||
required
|
||||
value={sellData.sale_date}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2">Sale Price *</label>
|
||||
<div className="relative">
|
||||
<DollarSign className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-emerald-500" />
|
||||
<input
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
required
|
||||
@ -869,8 +869,8 @@ export default function PortfolioPage() {
|
||||
onChange={(e) => setSellData({ ...sellData, sale_price: e.target.value })}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
{selectedDomain.purchase_price && sellData.sale_price && (
|
||||
<div className={clsx(
|
||||
"mt-2 text-sm font-medium",
|
||||
@ -879,30 +879,30 @@ export default function PortfolioPage() {
|
||||
{parseFloat(sellData.sale_price) > selectedDomain.purchase_price ? '📈' : '📉'}
|
||||
{' '}ROI: {(((parseFloat(sellData.sale_price) - selectedDomain.purchase_price) / selectedDomain.purchase_price) * 100).toFixed(1)}%
|
||||
{' '}(${(parseFloat(sellData.sale_price) - selectedDomain.purchase_price).toLocaleString()} profit)
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowSellModal(false)}
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
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"
|
||||
>
|
||||
>
|
||||
{saving ? <Loader2 className="w-5 h-5 animate-spin" /> : <DollarSign className="w-5 h-5" />}
|
||||
{saving ? 'Saving...' : 'Record Sale'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* List for Sale Modal */}
|
||||
@ -917,7 +917,7 @@ export default function PortfolioPage() {
|
||||
<p className="text-sm text-zinc-400 mt-1">
|
||||
Put <strong className="text-white">{selectedDomain.domain}</strong> on the marketplace
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleListForSale} className="p-6 space-y-4">
|
||||
<div>
|
||||
@ -931,14 +931,14 @@ export default function PortfolioPage() {
|
||||
onChange={(e) => setListData({ ...listData, asking_price: e.target.value })}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
{selectedDomain.estimated_value && (
|
||||
<p className="mt-2 text-xs text-zinc-500">
|
||||
Estimated value: <span className="text-amber-400">{formatCurrency(selectedDomain.estimated_value)}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<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="make_offer">Make Offer Only</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
💡 After creating the listing, you'll need to verify domain ownership via DNS before it goes live on the marketplace.
|
||||
</p>
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<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 ? 'Creating...' : 'Create Listing'}
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TerminalLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import { useStore } from '@/lib/store'
|
||||
import { api } from '@/lib/api'
|
||||
import { TerminalLayout } from '@/components/TerminalLayout'
|
||||
import { CommandCenterLayout } from '@/components/CommandCenterLayout'
|
||||
import {
|
||||
Plus,
|
||||
Target,
|
||||
@ -12,62 +12,21 @@ import {
|
||||
Trash2,
|
||||
Power,
|
||||
PowerOff,
|
||||
Eye,
|
||||
Bell,
|
||||
MessageSquare,
|
||||
Loader2,
|
||||
X,
|
||||
AlertCircle,
|
||||
CheckCircle,
|
||||
TrendingUp,
|
||||
Filter,
|
||||
Clock,
|
||||
DollarSign,
|
||||
Hash,
|
||||
Tag,
|
||||
Crown,
|
||||
Activity
|
||||
} from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
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
|
||||
// ============================================================================
|
||||
@ -111,19 +70,16 @@ export default function SniperAlertsPage() {
|
||||
const [deletingId, setDeletingId] = useState<number | null>(null)
|
||||
const [togglingId, setTogglingId] = useState<number | null>(null)
|
||||
|
||||
// Tier-based limits
|
||||
const tier = subscription?.tier || 'scout'
|
||||
const alertLimits: Record<string, number> = { scout: 2, trader: 10, tycoon: 50 }
|
||||
const maxAlerts = alertLimits[tier] || 2
|
||||
const canAddMore = alerts.length < maxAlerts
|
||||
const isTycoon = tier === 'tycoon'
|
||||
|
||||
// Stats
|
||||
const activeAlerts = alerts.filter(a => a.is_active).length
|
||||
const totalMatches = alerts.reduce((sum, a) => sum + a.matches_count, 0)
|
||||
const totalNotifications = alerts.reduce((sum, a) => sum + a.notifications_sent, 0)
|
||||
|
||||
// Load alerts
|
||||
const loadAlerts = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
@ -140,7 +96,6 @@ export default function SniperAlertsPage() {
|
||||
loadAlerts()
|
||||
}, [loadAlerts])
|
||||
|
||||
// Toggle alert active status
|
||||
const handleToggle = async (id: number, currentStatus: boolean) => {
|
||||
setTogglingId(id)
|
||||
try {
|
||||
@ -156,7 +111,6 @@ export default function SniperAlertsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// Delete alert
|
||||
const handleDelete = async (id: number, name: string) => {
|
||||
if (!confirm(`Delete alert "${name}"?`)) return
|
||||
|
||||
@ -172,293 +126,276 @@ export default function SniperAlertsPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<TerminalLayout>
|
||||
<div className="relative min-h-screen">
|
||||
{/* Ambient Background Glow */}
|
||||
<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" />
|
||||
<div className="absolute top-1/3 right-1/4 w-96 h-96 bg-blue-500/5 rounded-full blur-3xl" />
|
||||
</div>
|
||||
<CommandCenterLayout minimal>
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* HEADER */}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
<section className="pt-6 lg:pt-8 pb-6">
|
||||
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-6">
|
||||
<div className="space-y-3">
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<Target className="w-4 h-4 text-accent" />
|
||||
<span className="text-[10px] font-mono tracking-wide text-accent">Automated Alerts</span>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative z-10 space-y-8">
|
||||
{/* 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>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<StatCard
|
||||
label="Active Alerts"
|
||||
value={activeAlerts}
|
||||
subValue={`/ ${maxAlerts} slots`}
|
||||
icon={Target}
|
||||
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"
|
||||
/>
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* ALERTS LIST */}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
<section className="py-6 border-t border-white/[0.08]">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<Loader2 className="w-6 h-6 text-accent animate-spin" />
|
||||
</div>
|
||||
|
||||
{/* Alerts List */}
|
||||
<div className="bg-zinc-900/40 border border-white/5 rounded-xl overflow-hidden backdrop-blur-sm">
|
||||
{/* Header */}
|
||||
<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>
|
||||
) : alerts.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">
|
||||
<Target className="w-6 h-6 text-white/20" />
|
||||
</div>
|
||||
|
||||
{/* Loading */}
|
||||
{loading && (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<Loader2 className="w-8 h-8 text-emerald-500 animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{!loading && alerts.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-center px-4">
|
||||
<div className="w-16 h-16 rounded-full bg-white/5 flex items-center justify-center mb-4">
|
||||
<Target className="w-8 h-8 text-zinc-600" />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-white mb-2">No Alerts Yet</h3>
|
||||
<p className="text-sm text-zinc-500 max-w-md mb-6">
|
||||
Create your first sniper alert to get notified when domains matching your criteria appear in auctions.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
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"
|
||||
>
|
||||
<Plus className="w-4 h-4" /> Create First Alert
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Alerts Grid */}
|
||||
{!loading && alerts.length > 0 && (
|
||||
<div className="divide-y divide-white/5">
|
||||
{alerts.map((alert) => (
|
||||
<div key={alert.id} className="p-6 hover:bg-white/[0.02] transition-colors group">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
{/* Alert Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="text-lg font-bold text-white truncate">{alert.name}</h3>
|
||||
{alert.is_active ? (
|
||||
<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>
|
||||
<p className="text-white/40 text-sm mb-2">No alerts yet</p>
|
||||
<p className="text-white/25 text-xs mb-6 max-w-sm mx-auto">
|
||||
Create your first sniper alert to get notified when matching domains appear
|
||||
</p>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Create First Alert
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{alerts.map((alert) => (
|
||||
<div
|
||||
key={alert.id}
|
||||
className={clsx(
|
||||
"group border transition-all",
|
||||
alert.is_active
|
||||
? "bg-white/[0.02] border-white/[0.08] hover:border-accent/30"
|
||||
: "bg-white/[0.01] border-white/[0.04]"
|
||||
)}
|
||||
>
|
||||
<div className="p-5">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="text-base font-medium text-white truncate">{alert.name}</h3>
|
||||
{alert.is_active ? (
|
||||
<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 className="w-1.5 h-1.5 bg-accent animate-pulse" />
|
||||
Active
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-2 py-0.5 text-[9px] font-mono bg-white/5 text-white/40 border border-white/10">
|
||||
Paused
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Criteria Pills */}
|
||||
<div className="flex flex-wrap gap-2 mb-3">
|
||||
{alert.tlds && (
|
||||
<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
|
||||
{isTycoon && alert.notify_sms && (
|
||||
<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">
|
||||
<Crown className="w-3 h-3" />
|
||||
SMS
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Bell className="w-3 h-3" />
|
||||
{alert.notifications_sent} sent
|
||||
</span>
|
||||
{alert.last_matched_at && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
Last: {new Date(alert.last_matched_at).toLocaleDateString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handleToggle(alert.id, alert.is_active)}
|
||||
disabled={togglingId === alert.id}
|
||||
className={clsx(
|
||||
"p-2 rounded-lg border transition-all",
|
||||
alert.is_active
|
||||
? "bg-emerald-500/10 border-emerald-500/20 text-emerald-400 hover:bg-emerald-500/20"
|
||||
: "bg-zinc-800/50 border-zinc-700 text-zinc-500 hover:bg-zinc-800"
|
||||
)}
|
||||
title={alert.is_active ? "Pause" : "Activate"}
|
||||
>
|
||||
{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>
|
||||
{alert.description && (
|
||||
<p className="text-sm text-white/40 mb-3">{alert.description}</p>
|
||||
)}
|
||||
|
||||
<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>
|
||||
<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>
|
||||
)}
|
||||
{alert.keywords && (
|
||||
<span className="px-2 py-0.5 text-[10px] font-mono bg-accent/10 text-accent border border-accent/20">
|
||||
+{alert.keywords}
|
||||
</span>
|
||||
)}
|
||||
{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>
|
||||
|
||||
<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 className="flex items-center gap-4 text-[10px] text-white/30 font-mono">
|
||||
<span className="flex items-center gap-1">
|
||||
<Zap className="w-3 h-3" />
|
||||
{alert.matches_count} matches
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Bell className="w-3 h-3" />
|
||||
{alert.notifications_sent} sent
|
||||
</span>
|
||||
{alert.last_matched_at && (
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
{new Date(alert.last_matched_at).toLocaleDateString()}
|
||||
</span>
|
||||
)}
|
||||
</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>
|
||||
|
||||
{/* 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>
|
||||
</TerminalLayout>
|
||||
</section>
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* 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}
|
||||
>
|
||||
<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()}
|
||||
>
|
||||
{/* 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="p-2 rounded-lg border bg-emerald-500/10 border-emerald-500/20">
|
||||
<Target className="w-5 h-5 text-emerald-400" />
|
||||
</div>
|
||||
<Target className="w-5 h-5 text-accent" />
|
||||
<div>
|
||||
<h3 className="font-bold text-lg text-white">{isEditing ? 'Edit Alert' : 'Create Sniper Alert'}</h3>
|
||||
<p className="text-xs text-zinc-500">Set precise criteria for domain matching</p>
|
||||
<h3 className="font-medium text-white">{isEditing ? 'Edit Alert' : 'Create Sniper Alert'}</h3>
|
||||
<p className="text-xs text-white/40">Set criteria for domain matching</p>
|
||||
</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" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-6 max-h-[70vh] overflow-y-auto">
|
||||
{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" />
|
||||
<p className="text-sm flex-1">{error}</p>
|
||||
</div>
|
||||
@ -577,85 +512,85 @@ function CreateEditModal({ alert, onClose, onSuccess, isTycoon }: {
|
||||
|
||||
{/* Basic Info */}
|
||||
<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>
|
||||
<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
|
||||
type="text"
|
||||
value={form.name}
|
||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||
placeholder="e.g. Premium 4L .com domains"
|
||||
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>
|
||||
<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
|
||||
value={form.description}
|
||||
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
||||
placeholder="Optional description"
|
||||
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>
|
||||
|
||||
{/* Filters */}
|
||||
<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>
|
||||
<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
|
||||
type="text"
|
||||
value={form.tlds}
|
||||
onChange={(e) => setForm({ ...form, tlds: e.target.value })}
|
||||
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>
|
||||
<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
|
||||
type="text"
|
||||
value={form.platforms}
|
||||
onChange={(e) => setForm({ ...form, platforms: e.target.value })}
|
||||
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>
|
||||
<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
|
||||
type="text"
|
||||
value={form.keywords}
|
||||
onChange={(e) => setForm({ ...form, keywords: e.target.value })}
|
||||
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>
|
||||
<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
|
||||
type="text"
|
||||
value={form.exclude_keywords}
|
||||
onChange={(e) => setForm({ ...form, exclude_keywords: e.target.value })}
|
||||
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 className="grid grid-cols-2 gap-4">
|
||||
<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
|
||||
type="number"
|
||||
value={form.min_length}
|
||||
@ -663,12 +598,12 @@ function CreateEditModal({ alert, onClose, onSuccess, isTycoon }: {
|
||||
placeholder="1"
|
||||
min="1"
|
||||
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>
|
||||
<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
|
||||
type="number"
|
||||
value={form.max_length}
|
||||
@ -676,14 +611,14 @@ function CreateEditModal({ alert, onClose, onSuccess, isTycoon }: {
|
||||
placeholder="63"
|
||||
min="1"
|
||||
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 className="grid grid-cols-2 gap-4">
|
||||
<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
|
||||
type="number"
|
||||
value={form.min_price}
|
||||
@ -691,12 +626,12 @@ function CreateEditModal({ alert, onClose, onSuccess, isTycoon }: {
|
||||
placeholder="0"
|
||||
min="0"
|
||||
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>
|
||||
<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
|
||||
type="number"
|
||||
value={form.max_price}
|
||||
@ -704,100 +639,62 @@ function CreateEditModal({ alert, onClose, onSuccess, isTycoon }: {
|
||||
placeholder="10000"
|
||||
min="0"
|
||||
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 className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<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">
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-3 cursor-pointer p-3 border border-white/[0.06] hover:bg-white/[0.02] transition-colors">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.no_numbers}
|
||||
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 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
|
||||
type="checkbox"
|
||||
checked={form.no_hyphens}
|
||||
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>
|
||||
</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>
|
||||
|
||||
{/* Notifications */}
|
||||
<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
|
||||
type="checkbox"
|
||||
checked={form.notify_email}
|
||||
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" />
|
||||
<span className="text-sm text-zinc-300 flex-1">Email notifications</span>
|
||||
<Bell className="w-4 h-4 text-accent" />
|
||||
<span className="text-sm text-white/60 flex-1">Email notifications</span>
|
||||
</label>
|
||||
|
||||
<label className={clsx(
|
||||
"flex items-center gap-3 cursor-pointer p-3 rounded-lg border transition-colors",
|
||||
isTycoon ? "border-amber-500/20 hover:bg-amber-500/5" : "border-white/5 opacity-50 cursor-not-allowed"
|
||||
"flex items-center gap-3 cursor-pointer p-3 border transition-colors",
|
||||
isTycoon ? "border-amber-400/20 hover:bg-amber-400/[0.02]" : "border-white/[0.06] opacity-50 cursor-not-allowed"
|
||||
)}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.notify_sms}
|
||||
onChange={(e) => isTycoon && setForm({ ...form, notify_sms: e.target.checked })}
|
||||
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" />
|
||||
<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" />}
|
||||
</label>
|
||||
</div>
|
||||
@ -807,14 +704,14 @@ function CreateEditModal({ alert, onClose, onSuccess, isTycoon }: {
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
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 ? (
|
||||
<>
|
||||
@ -824,7 +721,7 @@ function CreateEditModal({ alert, onClose, onSuccess, isTycoon }: {
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
{isEditing ? 'Update Alert' : 'Create Alert'}
|
||||
{isEditing ? 'Update' : 'Create'}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
@ -834,4 +731,3 @@ function CreateEditModal({ alert, onClose, onSuccess, isTycoon }: {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,6 @@ import {
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
AlertCircle,
|
||||
ArrowUpRight,
|
||||
MousePointer,
|
||||
Target,
|
||||
Wallet,
|
||||
@ -17,85 +16,42 @@ import {
|
||||
ChevronRight,
|
||||
Copy,
|
||||
Check,
|
||||
ExternalLink,
|
||||
XCircle,
|
||||
Sparkles,
|
||||
Loader2
|
||||
} from 'lucide-react'
|
||||
import { api, YieldDomain, YieldTransaction } from '@/lib/api'
|
||||
import { useStore } from '@/lib/store'
|
||||
import { CommandCenterLayout } from '@/components/CommandCenterLayout'
|
||||
import clsx from 'clsx'
|
||||
|
||||
// Stats Card Component
|
||||
function StatsCard({
|
||||
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',
|
||||
}
|
||||
// ============================================================================
|
||||
// STATUS BADGE
|
||||
// ============================================================================
|
||||
|
||||
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 }) {
|
||||
const config: Record<string, { color: string; icon: any }> = {
|
||||
active: { color: 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30', icon: CheckCircle2 },
|
||||
pending: { color: 'bg-amber-500/20 text-amber-400 border-amber-500/30', icon: Clock },
|
||||
verifying: { color: 'bg-blue-500/20 text-blue-400 border-blue-500/30', icon: RefreshCw },
|
||||
paused: { color: 'bg-zinc-500/20 text-zinc-400 border-zinc-500/30', icon: AlertCircle },
|
||||
error: { color: 'bg-red-500/20 text-red-400 border-red-500/30', icon: XCircle },
|
||||
active: { color: 'bg-accent/10 text-accent border-accent/20', icon: CheckCircle2 },
|
||||
pending: { color: 'bg-amber-400/10 text-amber-400 border-amber-400/20', icon: Clock },
|
||||
verifying: { color: 'bg-blue-400/10 text-blue-400 border-blue-400/20', icon: RefreshCw },
|
||||
paused: { color: 'bg-white/5 text-white/40 border-white/10', icon: AlertCircle },
|
||||
error: { color: 'bg-red-400/10 text-red-400 border-red-400/20', icon: XCircle },
|
||||
}
|
||||
|
||||
const { color, icon: Icon } = config[status] || config.pending
|
||||
|
||||
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" />
|
||||
{status}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
// Activate Domain Modal
|
||||
// ============================================================================
|
||||
// ACTIVATE MODAL
|
||||
// ============================================================================
|
||||
|
||||
function ActivateModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
@ -115,7 +71,6 @@ function ActivateModal({
|
||||
|
||||
const handleAnalyze = async () => {
|
||||
if (!domain.trim()) return
|
||||
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
@ -163,19 +118,17 @@ function ActivateModal({
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-black/70 backdrop-blur-sm" onClick={onClose} />
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center">
|
||||
<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 */}
|
||||
<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="p-2 rounded-lg bg-emerald-500/20">
|
||||
<Sparkles className="w-5 h-5 text-emerald-400" />
|
||||
</div>
|
||||
<Sparkles className="w-5 h-5 text-accent" />
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">Activate Domain for Yield</h2>
|
||||
<p className="text-sm text-zinc-400">Turn your parked domains into passive income</p>
|
||||
<h2 className="font-medium text-white">Activate Domain for Yield</h2>
|
||||
<p className="text-xs text-white/40">Turn parked domains into passive income</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -185,19 +138,19 @@ function ActivateModal({
|
||||
{step === 'input' && (
|
||||
<div className="space-y-4">
|
||||
<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
|
||||
type="text"
|
||||
value={domain}
|
||||
onChange={(e) => setDomain(e.target.value)}
|
||||
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()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{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}
|
||||
</div>
|
||||
)}
|
||||
@ -205,7 +158,7 @@ function ActivateModal({
|
||||
<button
|
||||
onClick={handleAnalyze}
|
||||
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 ? (
|
||||
<>
|
||||
@ -224,28 +177,28 @@ function ActivateModal({
|
||||
|
||||
{step === 'analyze' && analysis && (
|
||||
<div className="space-y-5">
|
||||
{/* Intent Detection Results */}
|
||||
<div className="p-4 rounded-xl bg-zinc-800/50 border border-zinc-700">
|
||||
<div className="p-4 bg-white/[0.02] border border-white/[0.08]">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-sm text-zinc-400">Detected Intent</span>
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||
analysis.intent.confidence > 0.7 ? 'bg-emerald-500/20 text-emerald-400' :
|
||||
analysis.intent.confidence > 0.4 ? 'bg-amber-500/20 text-amber-400' :
|
||||
'bg-zinc-500/20 text-zinc-400'
|
||||
}`}>
|
||||
<span className="text-xs text-white/40">Detected Intent</span>
|
||||
<span className={clsx(
|
||||
"px-2 py-0.5 text-[10px] font-mono",
|
||||
analysis.intent.confidence > 0.7 ? 'bg-accent/10 text-accent' :
|
||||
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
|
||||
</span>
|
||||
</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.subcategory && (
|
||||
<span className="text-zinc-400"> / {analysis.intent.subcategory.replace('_', ' ')}</span>
|
||||
<span className="text-white/40"> / {analysis.intent.subcategory.replace('_', ' ')}</span>
|
||||
)}
|
||||
</p>
|
||||
{analysis.intent.keywords_matched.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{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]}
|
||||
</span>
|
||||
))}
|
||||
@ -253,37 +206,33 @@ function ActivateModal({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Value Estimate */}
|
||||
<div className="p-4 rounded-xl bg-gradient-to-br from-emerald-500/10 to-transparent border border-emerald-500/30">
|
||||
<span className="text-sm text-zinc-400">Estimated Monthly Revenue</span>
|
||||
<div className="p-4 bg-accent/5 border border-accent/20">
|
||||
<span className="text-xs text-white/40">Estimated Monthly Revenue</span>
|
||||
<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}
|
||||
</span>
|
||||
<span className="text-zinc-400">-</span>
|
||||
<span className="text-3xl font-bold text-emerald-400">
|
||||
<span className="text-white/40">-</span>
|
||||
<span className="text-2xl font-display text-accent">
|
||||
{analysis.value.estimated_monthly_max}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-zinc-500 mt-2">
|
||||
Based on intent category, geo-targeting, and partner rates
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Monetization Potential */}
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-zinc-800/50">
|
||||
<span className="text-sm text-zinc-400">Monetization Potential</span>
|
||||
<span className={`font-medium ${
|
||||
analysis.monetization_potential === 'high' ? 'text-emerald-400' :
|
||||
<div className="flex items-center justify-between p-3 bg-white/[0.02] border border-white/[0.06]">
|
||||
<span className="text-xs text-white/40">Monetization Potential</span>
|
||||
<span className={clsx(
|
||||
"font-medium text-sm",
|
||||
analysis.monetization_potential === 'high' ? 'text-accent' :
|
||||
analysis.monetization_potential === 'medium' ? 'text-amber-400' :
|
||||
'text-zinc-400'
|
||||
}`}>
|
||||
'text-white/40'
|
||||
)}>
|
||||
{analysis.monetization_potential.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{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}
|
||||
</div>
|
||||
)}
|
||||
@ -291,14 +240,14 @@ function ActivateModal({
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
onClick={handleActivate}
|
||||
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 ? (
|
||||
<>
|
||||
@ -319,29 +268,27 @@ function ActivateModal({
|
||||
{step === 'dns' && dnsInstructions && (
|
||||
<div className="space-y-5">
|
||||
<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">
|
||||
<CheckCircle2 className="w-6 h-6 text-emerald-400" />
|
||||
<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-accent" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-white">Domain Registered!</h3>
|
||||
<p className="text-sm text-zinc-400 mt-1">Complete DNS setup to start earning</p>
|
||||
<h3 className="font-medium text-white">Domain Registered!</h3>
|
||||
<p className="text-xs text-white/40 mt-1">Complete DNS setup to start earning</p>
|
||||
</div>
|
||||
|
||||
{/* Nameserver Option */}
|
||||
<div className="p-4 rounded-xl bg-zinc-800/50 border border-zinc-700">
|
||||
<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="p-4 bg-white/[0.02] border border-white/[0.08]">
|
||||
<h4 className="text-xs font-medium text-white mb-3">Option 1: Nameservers</h4>
|
||||
<div className="space-y-2">
|
||||
{dnsInstructions.nameservers.map((ns: string, i: number) => (
|
||||
<div key={i} className="flex items-center justify-between p-2 rounded bg-zinc-900">
|
||||
<code className="text-sm text-emerald-400">{ns}</code>
|
||||
<div key={i} className="flex items-center justify-between p-2 bg-black/30">
|
||||
<code className="text-sm text-accent font-mono">{ns}</code>
|
||||
<button
|
||||
onClick={() => copyToClipboard(ns, `ns-${i}`)}
|
||||
className="p-1 hover:bg-zinc-700 rounded"
|
||||
className="p-1 hover:bg-white/10"
|
||||
>
|
||||
{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>
|
||||
</div>
|
||||
@ -349,25 +296,23 @@ function ActivateModal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CNAME Option */}
|
||||
<div className="p-4 rounded-xl bg-zinc-800/50 border border-zinc-700">
|
||||
<h4 className="text-sm font-medium text-white mb-3">Option 2: CNAME Record</h4>
|
||||
<p className="text-xs text-zinc-500 mb-3">Or add a CNAME record:</p>
|
||||
<div className="flex items-center justify-between p-2 rounded bg-zinc-900">
|
||||
<div>
|
||||
<span className="text-xs text-zinc-500">Host: </span>
|
||||
<code className="text-sm text-white">{dnsInstructions.cname_host}</code>
|
||||
<span className="text-xs text-zinc-500 mx-2">→</span>
|
||||
<code className="text-sm text-emerald-400">{dnsInstructions.cname_target}</code>
|
||||
<div className="p-4 bg-white/[0.02] border border-white/[0.08]">
|
||||
<h4 className="text-xs font-medium text-white mb-3">Option 2: CNAME Record</h4>
|
||||
<div className="flex items-center justify-between p-2 bg-black/30">
|
||||
<div className="text-sm">
|
||||
<span className="text-white/40">Host: </span>
|
||||
<code className="text-white font-mono">{dnsInstructions.cname_host}</code>
|
||||
<span className="text-white/40 mx-2">→</span>
|
||||
<code className="text-accent font-mono">{dnsInstructions.cname_target}</code>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => copyToClipboard(dnsInstructions.cname_target, 'cname')}
|
||||
className="p-1 hover:bg-zinc-700 rounded"
|
||||
className="p-1 hover:bg-white/10"
|
||||
>
|
||||
{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>
|
||||
</div>
|
||||
@ -375,7 +320,7 @@ function ActivateModal({
|
||||
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
@ -387,7 +332,10 @@ function ActivateModal({
|
||||
)
|
||||
}
|
||||
|
||||
// Main Yield Page
|
||||
// ============================================================================
|
||||
// MAIN PAGE
|
||||
// ============================================================================
|
||||
|
||||
export default function YieldPage() {
|
||||
const { subscription } = useStore()
|
||||
const [loading, setLoading] = useState(true)
|
||||
@ -416,162 +364,185 @@ export default function YieldPage() {
|
||||
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
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-gradient-to-br from-emerald-500/20 to-purple-500/20">
|
||||
<TrendingUp className="w-6 h-6 text-emerald-400" />
|
||||
<CommandCenterLayout minimal>
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* HEADER */}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
<section className="pt-6 lg:pt-8 pb-6">
|
||||
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-6">
|
||||
<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>
|
||||
Yield
|
||||
</h1>
|
||||
<p className="text-zinc-400 mt-1">Turn parked domains into passive income</p>
|
||||
|
||||
<h1 className="font-display text-[2rem] lg:text-[2.5rem] leading-[1] tracking-[-0.02em]">
|
||||
<span className="text-white">Yield</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-white/40 text-sm max-w-md">
|
||||
Turn parked domains into passive income with intent-based monetization
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
{stats && (
|
||||
<div className="flex gap-6">
|
||||
<div className="text-right">
|
||||
<div className="text-xl font-display text-accent">{stats.currency} {stats.monthly_revenue.toLocaleString()}</div>
|
||||
<div className="text-[9px] tracking-wide text-white/30 font-mono">Monthly</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<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>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-xl font-display text-white">{stats.monthly_clicks.toLocaleString()}</div>
|
||||
<div className="text-[9px] tracking-wide text-white/30 font-mono">Clicks</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
disabled={refreshing}
|
||||
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"
|
||||
>
|
||||
<RefreshCw className={clsx("w-4 h-4", refreshing && "animate-spin")} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setShowActivateModal(true)}
|
||||
className="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" />
|
||||
Add Domain
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
disabled={refreshing}
|
||||
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
|
||||
onClick={() => setShowActivateModal(true)}
|
||||
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"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Add Domain
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* STATS */}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{stats && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<StatsCard
|
||||
label="Monthly Revenue"
|
||||
value={`${stats.currency} ${stats.monthly_revenue.toLocaleString()}`}
|
||||
subValue={`Lifetime: ${stats.currency} ${stats.lifetime_revenue.toLocaleString()}`}
|
||||
icon={DollarSign}
|
||||
color="emerald"
|
||||
/>
|
||||
<StatsCard
|
||||
label="Active Domains"
|
||||
value={stats.active_domains}
|
||||
subValue={`${stats.pending_domains} pending`}
|
||||
icon={Zap}
|
||||
color="blue"
|
||||
/>
|
||||
<StatsCard
|
||||
label="Monthly Clicks"
|
||||
value={stats.monthly_clicks.toLocaleString()}
|
||||
subValue={`${stats.monthly_conversions} conversions`}
|
||||
icon={MousePointer}
|
||||
color="amber"
|
||||
/>
|
||||
<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>
|
||||
<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 */}
|
||||
<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" />
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* 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>
|
||||
<h3 className="text-lg font-medium text-white mb-2">No yield domains yet</h3>
|
||||
<p className="text-zinc-400 mb-6 max-w-md mx-auto">
|
||||
Activate your first domain to start generating passive income from visitor intent routing.
|
||||
<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>
|
||||
<button
|
||||
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" />
|
||||
Add Your First Domain
|
||||
Add First Domain
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<table className="w-full min-w-[800px]">
|
||||
<thead>
|
||||
<tr className="text-left text-xs uppercase tracking-wider text-zinc-500 bg-zinc-800/50">
|
||||
<th className="px-4 py-3">Domain</th>
|
||||
<th className="px-4 py-3">Status</th>
|
||||
<th className="px-4 py-3">Intent</th>
|
||||
<th className="px-4 py-3">Clicks</th>
|
||||
<th className="px-4 py-3">Conversions</th>
|
||||
<th className="px-4 py-3 text-right">Revenue</th>
|
||||
<th className="px-4 py-3"></th>
|
||||
<tr className="text-xs text-white/40 border-b border-white/[0.06]">
|
||||
<th className="text-left py-3 px-4 font-medium">Domain</th>
|
||||
<th className="text-left py-3 px-4 font-medium">Status</th>
|
||||
<th className="text-left py-3 px-4 font-medium">Intent</th>
|
||||
<th className="text-right py-3 px-4 font-medium">Clicks</th>
|
||||
<th className="text-right py-3 px-4 font-medium">Conversions</th>
|
||||
<th className="text-right py-3 px-4 font-medium">Revenue</th>
|
||||
<th className="text-right py-3 px-4 font-medium"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-zinc-800">
|
||||
<tbody>
|
||||
{dashboard?.domains?.map((domain: YieldDomain) => (
|
||||
<tr key={domain.id} className="hover:bg-zinc-800/30 transition-colors">
|
||||
<td className="px-4 py-4">
|
||||
<tr key={domain.id} className="group border-b border-white/[0.04] hover:bg-white/[0.02]">
|
||||
<td className="py-3 px-4">
|
||||
<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()}
|
||||
</div>
|
||||
<span className="font-medium text-white">{domain.domain}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-4">
|
||||
<td className="py-3 px-4">
|
||||
<StatusBadge status={domain.status} />
|
||||
</td>
|
||||
<td className="px-4 py-4">
|
||||
<span className="text-sm text-zinc-300 capitalize">
|
||||
<td className="py-3 px-4">
|
||||
<span className="text-sm text-white/60 capitalize">
|
||||
{domain.detected_intent?.replace('_', ' ') || '-'}
|
||||
</span>
|
||||
{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)}%)
|
||||
</span>
|
||||
)}
|
||||
</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()}
|
||||
</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()}
|
||||
</td>
|
||||
<td className="px-4 py-4 text-right">
|
||||
<span className="font-medium text-emerald-400">
|
||||
<td className="py-3 px-4 text-right">
|
||||
<span className="font-medium text-accent font-mono">
|
||||
{domain.currency} {domain.total_revenue.toLocaleString()}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-4 text-right">
|
||||
<button className="p-1.5 hover:bg-zinc-700 rounded-lg transition-colors">
|
||||
<ChevronRight className="w-4 h-4 text-zinc-400" />
|
||||
<td className="py-3 px-4 text-right">
|
||||
<button className="p-1.5 hover:bg-white/10 transition-colors opacity-0 group-hover:opacity-100">
|
||||
<ChevronRight className="w-4 h-4 text-white/40" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@ -580,48 +551,49 @@ export default function YieldPage() {
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Recent Transactions */}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* RECENT TRANSACTIONS */}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{dashboard?.recent_transactions?.length > 0 && (
|
||||
<div className="bg-zinc-900/50 border border-zinc-800 rounded-xl overflow-hidden">
|
||||
<div className="p-4 border-b border-zinc-800">
|
||||
<h2 className="text-lg font-semibold text-white">Recent Activity</h2>
|
||||
</div>
|
||||
<div className="divide-y divide-zinc-800">
|
||||
<section className="py-6 border-t border-white/[0.06]">
|
||||
<h2 className="text-xs font-mono text-white/40 tracking-wide mb-4">Recent Activity</h2>
|
||||
<div className="space-y-px">
|
||||
{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={`w-8 h-8 rounded-full flex items-center justify-center ${
|
||||
tx.event_type === 'sale' ? 'bg-emerald-500/20' :
|
||||
tx.event_type === 'lead' ? 'bg-blue-500/20' :
|
||||
'bg-zinc-700'
|
||||
}`}>
|
||||
<div className={clsx(
|
||||
"w-8 h-8 flex items-center justify-center",
|
||||
tx.event_type === 'sale' ? 'bg-accent/10' :
|
||||
tx.event_type === 'lead' ? 'bg-blue-400/10' :
|
||||
'bg-white/5'
|
||||
)}>
|
||||
{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' ? (
|
||||
<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>
|
||||
<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 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)}
|
||||
</p>
|
||||
<p className="text-xs text-zinc-500">
|
||||
<p className="text-[10px] text-white/30 font-mono">
|
||||
{new Date(tx.created_at).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Activate Modal */}
|
||||
@ -630,7 +602,6 @@ export default function YieldPage() {
|
||||
onClose={() => setShowActivateModal(false)}
|
||||
onSuccess={fetchDashboard}
|
||||
/>
|
||||
</div>
|
||||
</CommandCenterLayout>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user