From cfbc84387ab58cffdff9d818c79b0d87e4039248 Mon Sep 17 00:00:00 2001 From: Yves Gugger Date: Thu, 11 Dec 2025 08:01:24 +0100 Subject: [PATCH] feat: INTEL - Complete Redesign (Award-Winning Style) Changes: - Rebuilt INTEL (Analytics) page to match Market/Radar style - Features: - 'Emerald Glow' background effect - High-end Stat Cards grid - Integrated header with 'Pill' style filters - Advanced Data Table with: - Renewal Trap warnings (Amber alert if >1.5x reg price) - Trend indicators (Sparklines/Arrows) - Risk Level meters (Visual bars) - Mobile Optimization: - Elegant Card layout for small screens - Touch-friendly controls --- frontend/src/app/terminal/intel/page.tsx | 699 ++++++++++++----------- 1 file changed, 377 insertions(+), 322 deletions(-) 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 ( +
+ {children} +
+ {content} +
+
+
+ ) +} + +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 ( - - {isNeutral ? ( - - ) : isPositive ? ( - - ) : ( - - )} - - ) -}) - -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 */} +
+
+
+
+
+
+
+
Provider
+
+
+ {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 */} +
+ {tld.cheapest_registrar ? ( + + {tld.cheapest_registrar} + + ) : ( + - + )} +
+
+ ) + })} +
+
+ + {/* 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 && ( +
+ Best price at + + {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)} - - -
- )} - +
) }