diff --git a/frontend/src/app/terminal/intel/page.tsx b/frontend/src/app/terminal/intel/page.tsx
index 6ea5ae8..70303bb 100755
--- a/frontend/src/app/terminal/intel/page.tsx
+++ b/frontend/src/app/terminal/intel/page.tsx
@@ -1,34 +1,135 @@
'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 {
- PremiumTable,
- StatCard,
- PageContainer,
- SearchInput,
- TabBar,
- FilterBar,
- SelectDropdown,
- ActionButton,
-} from '@/components/PremiumTable'
-import {
+ ExternalLink,
+ Loader2,
TrendingUp,
- ChevronRight,
+ TrendingDown,
Globe,
DollarSign,
- RefreshCw,
AlertTriangle,
- Cpu,
- MapPin,
- Coins,
- Crown,
+ RefreshCw,
+ Search,
+ Filter,
+ ChevronDown,
+ ChevronUp,
Info,
- Loader2,
+ ArrowRight,
+ BarChart3,
+ PieChart
} from 'lucide-react'
import clsx from 'clsx'
+import Link from 'next/link'
+
+// ============================================================================
+// SHARED COMPONENTS (Matching Market/Radar Style)
+// ============================================================================
+
+function Tooltip({ children, content }: { children: React.ReactNode; content: string }) {
+ return (
+
+ )
+}
+
+function StatCard({
+ label,
+ value,
+ subValue,
+ icon: Icon,
+ trend
+}: {
+ label: string
+ value: string | number
+ subValue?: string
+ icon: any
+ trend?: 'up' | 'down' | 'neutral' | 'active'
+}) {
+ return (
+
+
+
+
{label}
+
+ {value}
+ {subValue && {subValue}}
+
+
+
+
+
+
+ )
+}
+
+function FilterToggle({ active, onClick, label }: { active: boolean; onClick: () => void; label: string }) {
+ return (
+
+ )
+}
+
+function SortableHeader({
+ label, field, currentSort, currentDirection, onSort, align = 'left', tooltip
+}: {
+ label: string; field: SortField; currentSort: SortField; currentDirection: SortDirection; onSort: (field: SortField) => void; align?: 'left'|'center'|'right'; tooltip?: string
+}) {
+ const isActive = currentSort === field
+ return (
+
+
+ {tooltip && (
+
+
+
+ )}
+
+ )
+}
+
+// ============================================================================
+// TYPES
+// ============================================================================
interface TLDData {
tld: string
@@ -41,93 +142,42 @@ interface TLDData {
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
}
-// Category definitions
-const CATEGORIES = [
- { id: 'all', label: 'All', icon: Globe },
- { id: 'tech', label: 'Tech', icon: Cpu },
- { id: 'geo', label: 'Geo', icon: MapPin },
- { id: 'budget', label: 'Budget', icon: Coins },
- { id: 'premium', label: 'Premium', icon: Crown },
-]
+type SortField = 'tld' | 'price' | 'change' | 'risk' | 'popularity'
+type SortDirection = 'asc' | 'desc'
-const CATEGORY_FILTERS: Record boolean> = {
- all: () => true,
- tech: (tld) => ['ai', 'io', 'app', 'dev', 'tech', 'code', 'cloud', 'data', 'api', 'software'].includes(tld.tld),
- geo: (tld) => ['ch', 'de', 'uk', 'us', 'fr', 'it', 'es', 'nl', 'at', 'eu', 'co', 'ca', 'au', 'nz', 'jp', 'cn', 'in', 'br', 'mx', 'nyc', 'london', 'paris', 'berlin', 'tokyo', 'swiss'].includes(tld.tld),
- budget: (tld) => tld.min_price < 5,
- premium: (tld) => tld.min_price >= 50,
-}
+// ============================================================================
+// MAIN PAGE
+// ============================================================================
-const SORT_OPTIONS = [
- { value: 'popularity', label: 'By Popularity' },
- { value: 'price_asc', label: 'Price: Low → High' },
- { value: 'price_desc', label: 'Price: High → Low' },
- { value: 'change', label: 'By Price Change' },
- { value: 'risk', label: 'By Risk Level' },
-]
-
-// Memoized Sparkline
-const Sparkline = memo(function Sparkline({ trend }: { trend: number }) {
- const isPositive = trend > 0
- const isNeutral = trend === 0
-
- return (
-
- )
-})
-
-export default function TLDPricingPage() {
+export default function IntelPage() {
const { subscription } = useStore()
+ // Data
const [tldData, setTldData] = useState([])
const [loading, setLoading] = useState(true)
const [refreshing, setRefreshing] = useState(false)
- const [searchQuery, setSearchQuery] = useState('')
- const [sortBy, setSortBy] = useState('popularity')
- const [category, setCategory] = useState('all')
- const [page, setPage] = useState(0)
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')
- const loadTLDData = useCallback(async () => {
+ // Load Data
+ const loadData = useCallback(async () => {
setLoading(true)
try {
- const response = await api.getTldOverview(
- 50,
- page * 50,
- sortBy === 'risk' || sortBy === 'change' ? 'popularity' : sortBy as any,
- )
- const mapped: TLDData[] = (response.tlds || []).map((tld) => ({
+ 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,
@@ -149,261 +199,266 @@ export default function TLDPricingPage() {
} finally {
setLoading(false)
}
- }, [page, sortBy])
+ }, [])
- useEffect(() => {
- loadTLDData()
- }, [loadTLDData])
+ useEffect(() => { loadData() }, [loadData])
const handleRefresh = useCallback(async () => {
setRefreshing(true)
- await loadTLDData()
+ await loadData()
setRefreshing(false)
- }, [loadTLDData])
+ }, [loadData])
- // Memoized filtered and sorted data
- const sortedData = useMemo(() => {
- let data = tldData.filter(CATEGORY_FILTERS[category] || (() => true))
-
+ const handleSort = useCallback((field: SortField) => {
+ if (sortField === field) setSortDirection(d => d === 'asc' ? 'desc' : 'asc')
+ else {
+ setSortField(field)
+ setSortDirection(field === 'price' || field === 'risk' ? 'asc' : 'desc')
+ }
+ }, [sortField])
+
+ // Transform & Filter
+ const filteredData = useMemo(() => {
+ let data = tldData
+
+ // Category Filter
+ 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)
+
+ // Search
if (searchQuery) {
- const q = searchQuery.toLowerCase()
- data = data.filter(tld => tld.tld.toLowerCase().includes(q))
+ data = data.filter(t => t.tld.toLowerCase().includes(searchQuery.toLowerCase()))
}
-
- if (sortBy === 'risk') {
- const riskOrder = { high: 0, medium: 1, low: 2 }
- data = [...data].sort((a, b) => riskOrder[a.risk_level] - riskOrder[b.risk_level])
- }
-
- return data
- }, [tldData, category, searchQuery, sortBy])
- // Memoized stats
+ // Sort
+ 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 'change': return mult * ((a.price_change_1y || 0) - (b.price_change_1y || 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])
+
+ // Stats
const stats = useMemo(() => {
- const lowestPrice = tldData.length > 0
- ? tldData.reduce((min, tld) => Math.min(min, tld.min_price), Infinity)
- : 0.99
- const hottestTld = tldData.find(tld => (tld.price_change_7d || 0) > 5)?.tld || 'ai'
- const trapCount = tldData.filter(tld => tld.risk_level === 'high' || tld.risk_level === 'medium').length
- return { lowestPrice, hottestTld, trapCount }
+ const lowest = tldData.length > 0 ? Math.min(...tldData.map(t => t.min_price)) : 0
+ const hottest = tldData.reduce((prev, current) => (prev.price_change_7d > current.price_change_7d) ? prev : current, tldData[0] || {})
+ const traps = tldData.filter(t => t.risk_level === 'high').length
+ return { lowest, hottest, traps }
}, [tldData])
- const subtitle = useMemo(() => {
- if (loading && total === 0) return 'Loading TLD pricing data...'
- if (total === 0) return 'No TLD data available'
- return `Tracking ${total.toLocaleString()} TLDs • Updated daily`
- }, [loading, total])
-
- // Memoized columns
- const columns = useMemo(() => [
- {
- key: 'tld',
- header: 'TLD',
- width: '100px',
- render: (tld: TLDData) => (
-
- .{tld.tld}
-
- ),
- },
- {
- key: 'trend',
- header: 'Trend',
- width: '80px',
- hideOnMobile: true,
- render: (tld: TLDData) => ,
- },
- {
- key: 'buy_price',
- header: 'Buy (1y)',
- align: 'right' as const,
- width: '100px',
- render: (tld: TLDData) => (
- ${tld.min_price.toFixed(2)}
- ),
- },
- {
- key: 'renew_price',
- header: 'Renew (1y)',
- align: 'right' as const,
- width: '120px',
- render: (tld: TLDData) => {
- const ratio = tld.min_renewal_price / tld.min_price
- return (
-
-
${tld.min_renewal_price.toFixed(2)}
- {ratio > 2 && (
-
-
-
- )}
-
- )
- },
- },
- {
- key: 'change_1y',
- header: '1y',
- align: 'right' as const,
- width: '80px',
- hideOnMobile: true,
- render: (tld: TLDData) => {
- const change = tld.price_change_1y || 0
- return (
- 0 ? "text-orange-400" : change < 0 ? "text-accent" : "text-foreground-muted")}>
- {change > 0 ? '+' : ''}{change.toFixed(0)}%
-
- )
- },
- },
- {
- key: 'change_3y',
- header: '3y',
- align: 'right' as const,
- width: '80px',
- hideOnMobile: true,
- render: (tld: TLDData) => {
- const change = tld.price_change_3y || 0
- return (
- 0 ? "text-orange-400" : change < 0 ? "text-accent" : "text-foreground-muted")}>
- {change > 0 ? '+' : ''}{change.toFixed(0)}%
-
- )
- },
- },
- {
- key: 'cheapest',
- header: 'Cheapest At',
- align: 'left' as const,
- width: '140px',
- hideOnMobile: true,
- render: (tld: TLDData) => (
- tld.cheapest_registrar ? (
- e.stopPropagation()}
- className="text-xs text-accent hover:text-accent/80 hover:underline transition-colors"
- >
- {tld.cheapest_registrar}
-
- ) : (
- —
- )
- ),
- },
- {
- key: 'risk',
- header: 'Risk',
- align: 'center' as const,
- width: '120px',
- render: (tld: TLDData) => (
-
-
- {tld.risk_reason}
-
- ),
- },
- {
- key: 'actions',
- header: '',
- align: 'right' as const,
- width: '50px',
- render: () => ,
- },
- ], [])
+ const formatPrice = (p: number) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(p)
return (
-
- {refreshing ? '' : 'Refresh'}
-
- }
+
-
- {/* Stats Overview */}
-
-
0 ? total.toLocaleString() : '—'} subtitle="updated daily" icon={Globe} />
- 0 ? `$${stats.lowestPrice.toFixed(2)}` : '—'} icon={DollarSign} />
- 0 ? `.${stats.hottestTld}` : '—'} subtitle="rising prices" icon={TrendingUp} />
-
+
+ {/* Glow Effect */}
+
- {/* Category Tabs */}
-
({ id: c.id, label: c.label, icon: c.icon }))}
- activeTab={category}
- onChange={setCategory}
- />
+
+
+ {/* METRICS */}
+
+
+
+ 0 ? '+' : ''}${stats.hottest?.price_change_7d}% (7d)`} icon={TrendingUp} trend="active" />
+
+
- {/* Filters */}
-
-
-
-
+ {/* CONTROLS */}
+
+
+ {/* Search */}
+
+
+ setSearchQuery(e.target.value)}
+ placeholder="Search TLDs (e.g. .io)..."
+ className="w-full pl-10 pr-4 py-2.5 bg-zinc-900 border border-white/10 rounded-xl
+ text-sm text-white placeholder:text-zinc-600
+ focus:outline-none focus:border-white/20 focus:ring-1 focus:ring-white/20 transition-all"
+ />
+
+
+ {/* Filters */}
+
+ setFilterType('all')} label="All TLDs" />
+ setFilterType('tech')} label="Tech" />
+ setFilterType('geo')} label="Geo / National" />
+ setFilterType('budget')} label="Budget <$10" />
+
- {/* Legend */}
-
-
-
-
Tip: Renewal traps show ⚠️ when renewal price is >2x registration
+
+
+
+
+
+
+ {/* DATA GRID */}
+
+ {loading ? (
+
+
+
Analyzing registry data...
+
+ ) : filteredData.length === 0 ? (
+
+
+
+
+
No TLDs found
+
Try adjusting your filters
+
+ ) : (
+ <>
+ {/* DESKTOP TABLE */}
+
+
+
+ {filteredData.map((tld) => {
+ const isTrap = tld.min_renewal_price > tld.min_price * 1.5
+ const trend = tld.price_change_1y || 0
+
+ return (
+
+ {/* TLD */}
+
+ .{tld.tld}
+
+
+ {/* Price */}
+
+ {formatPrice(tld.min_price)}
+
+
+ {/* Renewal */}
+
+
+ {formatPrice(tld.min_renewal_price)}
+
+ {isTrap && (
+
+
+
+ )}
+
+
+ {/* Trend */}
+
+
5 ? "bg-orange-500/10 text-orange-400" :
+ trend < -5 ? "bg-emerald-500/10 text-emerald-400" :
+ "text-zinc-500"
+ )}>
+ {trend > 0 ? : trend < 0 ? : null}
+ {Math.abs(trend)}%
+
+
+
+ {/* Risk */}
+
+
+ {/* Provider */}
+
+
+ )
+ })}
+
+
+
+ {/* MOBILE CARDS */}
+
+ {filteredData.map((tld) => {
+ const isTrap = tld.min_renewal_price > tld.min_price * 1.5
+ return (
+
+
+
.{tld.tld}
+
+ {tld.risk_level} Risk
+
+
+
+
+
+
Register
+
{formatPrice(tld.min_price)}
+
+
+
Renew
+
+ {formatPrice(tld.min_renewal_price)}
+
+
+
+
+ {tld.cheapest_registrar && (
+
+ )}
+
+ )
+ })}
+
+ >
+ )}
-
- {/* TLD Table */}
-
tld.tld}
- loading={loading}
- onRowClick={(tld) => window.location.href = `/terminal/intel/${tld.tld}`}
- emptyIcon={}
- emptyTitle="No TLDs found"
- emptyDescription={searchQuery ? `No TLDs matching "${searchQuery}"` : "Check back later for TLD data"}
- columns={columns}
- />
-
- {/* Pagination */}
- {total > 50 && (
-
-
-
- Page {page + 1} of {Math.ceil(total / 50)}
-
-
-
- )}
-
+
)
}