'use client'
import { useEffect, useState, useMemo, useCallback, memo } from 'react'
import { useStore } from '@/lib/store'
import { api } from '@/lib/api'
import { TerminalLayout } from '@/components/TerminalLayout'
import {
ExternalLink,
Loader2,
TrendingUp,
TrendingDown,
Globe,
DollarSign,
AlertTriangle,
RefreshCw,
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
// ============================================================================
type UserTier = 'scout' | 'trader' | 'tycoon'
function getTierLevel(tier: UserTier): number {
switch (tier) {
case 'tycoon': return 3
case 'trader': return 2
case 'scout': return 1
default: return 1
}
}
// ============================================================================
// SHARED COMPONENTS
// ============================================================================
const Tooltip = memo(({ children, content }: { children: React.ReactNode; content: string }) => (
))
Tooltip.displayName = 'Tooltip'
const LockedFeature = memo(({ requiredTier, currentTier }: { requiredTier: UserTier; currentTier: UserTier }) => {
const tierNames = { scout: 'Scout', trader: 'Trader', tycoon: 'Tycoon' }
return (
Locked
)
})
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
}) => (
{label}
{locked ? (
—
) : (
{value}
{subValue && {subValue}}
)}
{highlight && (
● LIVE
)}
))
StatCard.displayName = 'StatCard'
const FilterToggle = memo(({ active, onClick, label, icon: Icon }: {
active: boolean
onClick: () => void
label: string
icon?: any
}) => (
))
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 (
{tooltip && !locked && (
)}
)
})
SortableHeader.displayName = 'SortableHeader'
// ============================================================================
// TYPES
// ============================================================================
interface TLDData {
tld: string
min_price: number
avg_price: number
max_price: number
min_renewal_price: number
avg_renewal_price: number
cheapest_registrar?: string
cheapest_registrar_url?: string
price_change_7d: number
price_change_1y: number
price_change_3y: number
risk_level: 'low' | 'medium' | 'high'
risk_reason: string
popularity_rank?: number
type?: string
}
// ============================================================================
// MAIN PAGE
// ============================================================================
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
// Data
const [tldData, setTldData] = useState([])
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('popularity')
const [sortDirection, setSortDirection] = useState('asc')
// Load Data
const loadData = useCallback(async () => {
setLoading(true)
try {
const response = await api.getTldOverview(100, 0, 'popularity')
const mapped: TLDData[] = (response.tlds || []).map((tld: any) => ({
tld: tld.tld,
min_price: tld.min_registration_price,
avg_price: tld.avg_registration_price,
max_price: tld.max_registration_price,
min_renewal_price: tld.min_renewal_price,
avg_renewal_price: tld.avg_renewal_price,
price_change_7d: tld.price_change_7d,
price_change_1y: tld.price_change_1y,
price_change_3y: tld.price_change_3y,
risk_level: tld.risk_level,
risk_reason: tld.risk_reason,
popularity_rank: tld.popularity_rank,
type: tld.type,
}))
setTldData(mapped)
setTotal(response.total || 0)
} catch (error) {
console.error('Failed to load TLD data:', error)
} finally {
setLoading(false)
}
}, [])
useEffect(() => { loadData() }, [loadData])
const handleRefresh = useCallback(async () => {
setRefreshing(true)
await loadData()
setRefreshing(false)
}, [loadData])
const handleSort = useCallback((field: SortField) => {
if (field === 'renewal' && !canSeeRenewal) return
if (field === 'change3y' && !canSee3yTrend) return
if (sortField === field) setSortDirection(d => d === 'asc' ? 'desc' : 'asc')
else {
setSortField(field)
setSortDirection(field === 'price' || field === 'renewal' || field === 'risk' ? 'asc' : 'desc')
}
}, [sortField, canSeeRenewal, canSee3yTrend])
// Transform & Filter
const filteredData = useMemo(() => {
let data = tldData
if (filterType === 'tech') data = data.filter(t => ['ai', 'io', 'app', 'dev', 'tech', 'cloud'].includes(t.tld))
if (filterType === 'geo') data = data.filter(t => ['us', 'uk', 'de', 'ch', 'fr', 'eu'].includes(t.tld))
if (filterType === 'budget') data = data.filter(t => t.min_price < 10)
if (searchQuery) {
data = data.filter(t => t.tld.toLowerCase().includes(searchQuery.toLowerCase()))
}
const mult = sortDirection === 'asc' ? 1 : -1
data.sort((a, b) => {
switch (sortField) {
case 'tld': return mult * a.tld.localeCompare(b.tld)
case 'price': return mult * (a.min_price - b.min_price)
case 'renewal': return mult * (a.min_renewal_price - b.min_renewal_price)
case 'change': return mult * ((a.price_change_1y || 0) - (b.price_change_1y || 0))
case 'change3y': return mult * ((a.price_change_3y || 0) - (b.price_change_3y || 0))
case 'risk':
const riskMap = { low: 1, medium: 2, high: 3 }
return mult * (riskMap[a.risk_level] - riskMap[b.risk_level])
case 'popularity': return mult * ((a.popularity_rank || 999) - (b.popularity_rank || 999))
default: return 0
}
})
return data
}, [tldData, filterType, searchQuery, sortField, sortDirection])
const stats = useMemo(() => {
const lowest = tldData.length > 0 ? Math.min(...tldData.map(t => t.min_price)) : 0
const hottest = tldData.reduce((prev, current) => (prev.price_change_1y > current.price_change_1y) ? prev : current, tldData[0] || {})
const traps = tldData.filter(t => t.risk_level === 'high').length
const avgRenewal = tldData.length > 0 ? tldData.reduce((sum, t) => sum + t.min_renewal_price, 0) / tldData.length : 0
return { lowest, hottest, traps, avgRenewal }
}, [tldData])
const formatPrice = (p: number) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(p)
return (
{/* Ambient Background Glow */}
{/* Header Section */}
Inflation Monitor & Pricing Analytics across 800+ TLDs.
{/* Quick Stats Pills */}
{userTier === 'tycoon' ? 'Tycoon Access' : userTier === 'trader' ? 'Trader Access' : 'Scout Access'}
{/* Metric Grid */}
{/* Control Bar */}
{/* Filter Pills */}
setFilterType('all')} label="All TLDs" />
setFilterType('tech')} label="Tech" icon={Zap} />
setFilterType('geo')} label="Geo / National" icon={Globe} />
setFilterType('budget')} label="Budget <$10" icon={DollarSign} />
{/* Refresh Button (Mobile) */}
{/* Search Filter */}
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"
/>
{/* DATA GRID */}
{/* Unified Table Header - Use a wrapper with min-width to force scrolling instead of breaking */}
{/* Force minimum width */}
{canSee3yTrend ? (
) : (
Trend (3y)
)}
Action
{/* Rows */}
{loading ? (
Analyzing registry data...
) : filteredData.length === 0 ? (
No TLDs found
Try adjusting your filters or search query.
) : (
{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 (
{/* TLD */}
.{tld.tld}
{/* Price */}
{formatPrice(tld.min_price)}
{/* Renewal (Trader+) */}
{canSeeRenewal ? (
<>
{formatPrice(tld.min_renewal_price)}
{isTrap && (
)}
>
) : (
)}
{/* Trend 1y */}
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 ? : trend < 0 ? : }
{Math.abs(trend)}%
{/* Trend 3y */}
{canSee3yTrend ? (
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 ? : trend3y < 0 ? : }
{Math.abs(trend3y)}%
) : (
—
)}
{/* Risk */}
{/* Action */}
)
})}
)}
{/* Upgrade CTA for Scout users */}
{userTier === 'scout' && (
Unlock Full TLD Intelligence
See renewal prices, identify renewal traps, and access detailed price history charts with Trader or Tycoon.
Upgrade Now
)}
)
}