'use client' import { useEffect, useState, useMemo, useCallback } from 'react' import { Header } from '@/components/Header' import { Footer } from '@/components/Footer' import { useStore } from '@/lib/store' import { api } from '@/lib/api' import { TrendingUp, TrendingDown, Minus, ArrowRight, BarChart3, ChevronUp, ChevronDown, ChevronsUpDown, Lock, ChevronRight, ChevronLeft, Search, X, } from 'lucide-react' import Link from 'next/link' import clsx from 'clsx' interface TldData { tld: string type: string description: string avg_registration_price: number min_registration_price: number max_registration_price: number min_renewal_price: number avg_renewal_price: number registrar_count: number trend: string price_change_7d: number price_change_1y: number price_change_3y: number risk_level: 'low' | 'medium' | 'high' risk_reason: string popularity_rank?: number } interface TrendingTld { tld: string reason: string price_change: number current_price: number } interface PaginationData { total: number limit: number offset: number has_more: boolean } type SortField = 'popularity' | 'tld' | 'avg_registration_price' | 'min_registration_price' type SortDirection = 'asc' | 'desc' // Mini sparkline chart component function MiniChart({ tld, isAuthenticated }: { tld: string, isAuthenticated: boolean }) { const [historyData, setHistoryData] = useState([]) const [loading, setLoading] = useState(true) useEffect(() => { if (isAuthenticated) { loadHistory() } else { setLoading(false) } }, [tld, isAuthenticated]) const loadHistory = async () => { try { const data = await api.getTldHistory(tld, 365) const history = data.history || [] const sampledData = history .filter((_: unknown, i: number) => i % Math.max(1, Math.floor(history.length / 12)) === 0) .slice(0, 12) .map((h: { price: number }) => h.price) setHistoryData(sampledData.length > 0 ? sampledData : []) } catch (error) { console.error('Failed to load history:', error) setHistoryData([]) } finally { setLoading(false) } } if (!isAuthenticated) { return (
Sign in
) } if (loading) { return
} if (historyData.length === 0) { return
No data
} const min = Math.min(...historyData) const max = Math.max(...historyData) const range = max - min || 1 const isIncreasing = historyData[historyData.length - 1] > historyData[0] const linePoints = historyData.map((value, i) => { const x = (i / (historyData.length - 1)) * 100 const y = 100 - ((value - min) / range) * 80 - 10 return `${x},${y}` }).join(' ') const areaPath = historyData.map((value, i) => { const x = (i / (historyData.length - 1)) * 100 const y = 100 - ((value - min) / range) * 80 - 10 return i === 0 ? `M${x},${y}` : `L${x},${y}` }).join(' ') + ' L100,100 L0,100 Z' const gradientId = `gradient-${tld}` return ( ) } function SortIcon({ field, currentField, direction }: { field: SortField, currentField: SortField, direction: SortDirection }) { if (field !== currentField) { return } return direction === 'asc' ? : } export default function TldPricingPage() { const { isAuthenticated, checkAuth, isLoading: authLoading } = useStore() const [tlds, setTlds] = useState([]) const [trending, setTrending] = useState([]) const [loading, setLoading] = useState(true) const [pagination, setPagination] = useState({ total: 0, limit: 25, offset: 0, has_more: false }) // Search & Sort state const [searchQuery, setSearchQuery] = useState('') const [debouncedSearch, setDebouncedSearch] = useState('') const [sortField, setSortField] = useState('popularity') const [sortDirection, setSortDirection] = useState('asc') // Debounce search useEffect(() => { const timer = setTimeout(() => { setDebouncedSearch(searchQuery) }, 300) return () => clearTimeout(timer) }, [searchQuery]) useEffect(() => { checkAuth() loadTrending() }, [checkAuth]) // Load TLDs with pagination, search, and sort useEffect(() => { loadTlds() }, [debouncedSearch, sortField, sortDirection, pagination.offset]) const loadTlds = async () => { setLoading(true) try { const sortBy = sortField === 'tld' ? 'name' : sortField === 'popularity' ? 'popularity' : sortField === 'avg_registration_price' ? (sortDirection === 'asc' ? 'price_asc' : 'price_desc') : (sortDirection === 'asc' ? 'price_asc' : 'price_desc') const data = await api.getTldOverview( pagination.limit, pagination.offset, sortBy, debouncedSearch || undefined ) setTlds(data?.tlds || []) setPagination(prev => ({ ...prev, total: data?.total || 0, has_more: data?.has_more || false, })) } catch (error) { console.error('Failed to load TLD data:', error) setTlds([]) } finally { setLoading(false) } } const loadTrending = async () => { try { const data = await api.getTrendingTlds() setTrending(data?.trending || []) } catch (error) { console.error('Failed to load trending:', error) } } const handleSort = (field: SortField) => { if (sortField === field) { setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc') } else { setSortField(field) setSortDirection('asc') } // Reset to first page on sort change setPagination(prev => ({ ...prev, offset: 0 })) } const handlePageChange = (newOffset: number) => { setPagination(prev => ({ ...prev, offset: newOffset })) // Scroll to top of table window.scrollTo({ top: 300, behavior: 'smooth' }) } const getTrendIcon = (trend: string) => { switch (trend) { case 'up': return case 'down': return default: return } } // Pagination calculations const currentPage = Math.floor(pagination.offset / pagination.limit) + 1 const totalPages = Math.ceil(pagination.total / pagination.limit) if (authLoading) { return (
) } return (
{/* Background Effects - matching landing page */}
{/* Header */}
Market Intel

{pagination.total}+ TLDs. Live Prices.

See what domains cost. Spot trends. Find opportunities.

{/* Login Banner for non-authenticated users */} {!isAuthenticated && (

See the full picture

Sign in for detailed pricing, charts, and trends.

Hunt Free
)} {/* Trending Section */} {trending.length > 0 && (

Moving Now

{trending.map((item) => (
.{item.tld} 0 ? "text-[#f97316] bg-[#f9731615]" : "text-accent bg-accent-muted" )}> {item.price_change > 0 ? '+' : ''}{item.price_change.toFixed(1)}%

{item.reason}

${item.current_price.toFixed(2)}/yr
))}
)} {/* Search Bar */}
{ setSearchQuery(e.target.value) setPagination(prev => ({ ...prev, offset: 0 })) }} className="w-full pl-12 pr-10 py-3 bg-background-secondary/50 border border-border rounded-xl text-body text-foreground placeholder:text-foreground-subtle focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent transition-all duration-300" /> {searchQuery && ( )}
{/* TLD Table */}
{loading ? ( // Loading skeleton Array.from({ length: 10 }).map((_, idx) => ( )) ) : tlds.length === 0 ? ( ) : ( tlds.map((tld, idx) => { // Show full data for authenticated users OR for the first row (idx 0 on first page) // This lets visitors see how good the data is for .com before signing up const showFullData = isAuthenticated || (pagination.offset === 0 && idx === 0) return ( ) }) )}
Type 12-Month Chart Renew 1y Trend Risk
{searchQuery ? `No TLDs found matching "${searchQuery}"` : 'No TLDs found'}
{pagination.offset + idx + 1} .{tld.tld} {!isAuthenticated && idx === 0 && pagination.offset === 0 && ( Preview )} {tld.type} {showFullData ? ( ${tld.avg_registration_price.toFixed(2)} ) : ( ••• )} {showFullData ? ( ${tld.min_registration_price.toFixed(2)} ) : ( ••• )} {showFullData ? (
${tld.min_renewal_price?.toFixed(2) || '—'} {tld.min_renewal_price && tld.min_renewal_price / tld.min_registration_price > 2 && ( ⚠️ )}
) : ( $XX.XX )}
{showFullData ? ( 0 ? "text-[#f97316]" : (tld.price_change_1y || 0) < 0 ? "text-accent" : "text-foreground-muted" )}> {(tld.price_change_1y || 0) > 0 ? '+' : ''}{(tld.price_change_1y || 0).toFixed(0)}% ) : ( +X% )} {showFullData ? ( {tld.risk_level === 'high' && '🔴'} {tld.risk_level === 'medium' && '🟡'} {tld.risk_level === 'low' && '🟢'} ) : ( 🟢 )} Details
{/* Pagination */} {!loading && pagination.total > pagination.limit && (

Showing {pagination.offset + 1}-{Math.min(pagination.offset + pagination.limit, pagination.total)} of {pagination.total} TLDs

{/* Previous Button */} {/* Page Numbers */}
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => { let pageNum: number if (totalPages <= 5) { pageNum = i + 1 } else if (currentPage <= 3) { pageNum = i + 1 } else if (currentPage >= totalPages - 2) { pageNum = totalPages - 4 + i } else { pageNum = currentPage - 2 + i } return ( ) })}
{/* Mobile Page Indicator */} Page {currentPage} of {totalPages} {/* Next Button */}
)}
{/* Stats */} {!loading && (

{searchQuery ? `Found ${pagination.total} TLDs matching "${searchQuery}"` : `${pagination.total} TLDs available • Sorted by ${sortField === 'popularity' ? 'popularity' : sortField === 'tld' ? 'name' : 'price'}` }

)}
) }