- Created reusable Footer component with social links - Replaced mock data with real API calls for TLD history - Added Trending TLDs section at top of pricing page - Mini-charts now display actual 12-month price history - Improved data authenticity throughout TLD pricing page
444 lines
18 KiB
TypeScript
444 lines
18 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState, useMemo } 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,
|
|
} 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
|
|
registrar_count: number
|
|
trend: string
|
|
}
|
|
|
|
interface TrendingTld {
|
|
tld: string
|
|
reason: string
|
|
price_change: number
|
|
current_price: number
|
|
}
|
|
|
|
interface TldHistoryData {
|
|
history: Array<{
|
|
date: string
|
|
price: number
|
|
}>
|
|
}
|
|
|
|
type SortField = 'tld' | 'avg_registration_price' | 'min_registration_price' | 'registrar_count'
|
|
type SortDirection = 'asc' | 'desc'
|
|
|
|
// Mini sparkline chart component with real data
|
|
function MiniChart({ tld, isAuthenticated }: { tld: string, isAuthenticated: boolean }) {
|
|
const [historyData, setHistoryData] = useState<number[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
if (isAuthenticated) {
|
|
loadHistory()
|
|
}
|
|
}, [tld, isAuthenticated])
|
|
|
|
const loadHistory = async () => {
|
|
try {
|
|
const data = await api.getTldHistory(tld, 365) // Get 1 year of data
|
|
// Sample down to 12 data points (monthly)
|
|
const history = data.history || []
|
|
const sampledData = history
|
|
.filter((_, i) => i % Math.floor(history.length / 12) === 0)
|
|
.slice(0, 12)
|
|
.map(h => h.price)
|
|
|
|
setHistoryData(sampledData.length > 0 ? sampledData : [])
|
|
} catch (error) {
|
|
console.error('Failed to load history:', error)
|
|
setHistoryData([])
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
if (!isAuthenticated || loading || historyData.length === 0) {
|
|
return (
|
|
<div className="flex items-center gap-2 text-ui-sm text-foreground-subtle">
|
|
<Lock className="w-3 h-3" />
|
|
Sign in
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const min = Math.min(...historyData)
|
|
const max = Math.max(...historyData)
|
|
const range = max - min || 1
|
|
|
|
const points = historyData.map((value, i) => {
|
|
const x = (i / (historyData.length - 1)) * 100
|
|
const y = 100 - ((value - min) / range) * 100
|
|
return `${x},${y}`
|
|
}).join(' ')
|
|
|
|
const isIncreasing = historyData[historyData.length - 1] > historyData[0]
|
|
|
|
return (
|
|
<svg className="w-24 h-8" viewBox="0 0 100 100" preserveAspectRatio="none">
|
|
<polyline
|
|
points={points}
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="3"
|
|
className={clsx(
|
|
"transition-colors",
|
|
isIncreasing ? "text-[#f97316]/60" : "text-accent/60"
|
|
)}
|
|
/>
|
|
</svg>
|
|
)
|
|
}
|
|
|
|
function SortIcon({ field, currentField, direction }: { field: SortField, currentField: SortField, direction: SortDirection }) {
|
|
if (field !== currentField) {
|
|
return <ChevronsUpDown className="w-4 h-4 text-foreground-subtle" />
|
|
}
|
|
return direction === 'asc'
|
|
? <ChevronUp className="w-4 h-4 text-accent" />
|
|
: <ChevronDown className="w-4 h-4 text-accent" />
|
|
}
|
|
|
|
function ShimmerBlock({ className }: { className?: string }) {
|
|
return (
|
|
<div className={clsx(
|
|
"relative overflow-hidden rounded bg-background-tertiary",
|
|
className
|
|
)}>
|
|
<div className="absolute inset-0 -translate-x-full animate-[shimmer_2s_infinite] bg-gradient-to-r from-transparent via-foreground/5 to-transparent" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default function TldPricingPage() {
|
|
const { isAuthenticated, checkAuth, isLoading: authLoading } = useStore()
|
|
const [tlds, setTlds] = useState<TldData[]>([])
|
|
const [trending, setTrending] = useState<TrendingTld[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [sortField, setSortField] = useState<SortField>('tld')
|
|
const [sortDirection, setSortDirection] = useState<SortDirection>('asc')
|
|
|
|
useEffect(() => {
|
|
checkAuth()
|
|
loadData()
|
|
}, [checkAuth])
|
|
|
|
const loadData = async () => {
|
|
try {
|
|
const [overviewData, trendingData] = await Promise.all([
|
|
api.getTldOverview(100),
|
|
api.getTrendingTlds(),
|
|
])
|
|
setTlds(overviewData?.tlds || [])
|
|
setTrending(trendingData?.trending || [])
|
|
} catch (error) {
|
|
console.error('Failed to load TLD data:', error)
|
|
setTlds([])
|
|
setTrending([])
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleSort = (field: SortField) => {
|
|
if (sortField === field) {
|
|
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc')
|
|
} else {
|
|
setSortField(field)
|
|
setSortDirection('asc')
|
|
}
|
|
}
|
|
|
|
const sortedTlds = useMemo(() => {
|
|
const sorted = [...tlds].sort((a, b) => {
|
|
let aVal: number | string = a[sortField]
|
|
let bVal: number | string = b[sortField]
|
|
|
|
if (typeof aVal === 'string') aVal = aVal.toLowerCase()
|
|
if (typeof bVal === 'string') bVal = bVal.toLowerCase()
|
|
|
|
if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1
|
|
if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1
|
|
return 0
|
|
})
|
|
return sorted
|
|
}, [tlds, sortField, sortDirection])
|
|
|
|
const getTrendIcon = (trend: string) => {
|
|
switch (trend) {
|
|
case 'up':
|
|
return <TrendingUp className="w-4 h-4 text-[#f97316]" />
|
|
case 'down':
|
|
return <TrendingDown className="w-4 h-4 text-accent" />
|
|
default:
|
|
return <Minus className="w-4 h-4 text-foreground-subtle" />
|
|
}
|
|
}
|
|
|
|
if (loading || authLoading) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center">
|
|
<div className="w-5 h-5 border-2 border-accent border-t-transparent rounded-full animate-spin" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-background relative flex flex-col">
|
|
{/* Ambient glow */}
|
|
<div className="fixed inset-0 pointer-events-none">
|
|
<div className="absolute top-0 left-1/4 w-[500px] h-[400px] bg-accent/[0.02] rounded-full blur-3xl" />
|
|
</div>
|
|
|
|
<Header />
|
|
|
|
<main className="relative pt-28 sm:pt-32 pb-16 sm:pb-20 px-4 sm:px-6 flex-1">
|
|
<div className="max-w-7xl mx-auto">
|
|
{/* Header */}
|
|
<div className="text-center mb-12 sm:mb-16 animate-fade-in">
|
|
<div className="inline-flex items-center gap-2 px-3 py-1.5 bg-background-secondary/80 backdrop-blur-sm border border-border rounded-full mb-6">
|
|
<BarChart3 className="w-4 h-4 text-accent" />
|
|
<span className="text-ui-sm text-foreground-muted">TLD Price Intelligence</span>
|
|
</div>
|
|
<h1 className="font-display text-[2rem] sm:text-[2.75rem] md:text-[3.5rem] lg:text-[4rem] leading-[1.1] tracking-[-0.03em] text-foreground mb-4">
|
|
Domain Extension Pricing
|
|
</h1>
|
|
<p className="text-body sm:text-body-lg text-foreground-muted max-w-2xl mx-auto">
|
|
Track price trends across all major TLDs. Compare prices and monitor trends over time.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Login Banner for non-authenticated users */}
|
|
{!isAuthenticated && (
|
|
<div className="mb-8 p-5 bg-accent-muted border border-accent/20 rounded-xl flex flex-col sm:flex-row items-center justify-between gap-4 animate-fade-in">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-accent/20 rounded-xl flex items-center justify-center">
|
|
<Lock className="w-5 h-5 text-accent" />
|
|
</div>
|
|
<div>
|
|
<p className="text-body-sm font-medium text-foreground">Unlock Full TLD Data</p>
|
|
<p className="text-ui-sm text-foreground-muted">
|
|
Sign in to see detailed pricing and trends.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<Link
|
|
href="/register"
|
|
className="shrink-0 px-5 py-2.5 bg-accent text-background text-ui font-medium rounded-lg
|
|
hover:bg-accent-hover transition-all duration-300"
|
|
>
|
|
Get Started Free
|
|
</Link>
|
|
</div>
|
|
)}
|
|
|
|
{/* Trending Section */}
|
|
{trending.length > 0 && (
|
|
<div className="mb-12 sm:mb-16 animate-slide-up">
|
|
<h2 className="text-body-lg sm:text-heading-sm font-medium text-foreground mb-4 sm:mb-6 flex items-center gap-2">
|
|
<TrendingUp className="w-5 h-5 text-accent" />
|
|
Trending Now
|
|
</h2>
|
|
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
|
|
{trending.map((item) => (
|
|
<Link
|
|
key={item.tld}
|
|
href={isAuthenticated ? `/tld-pricing/${item.tld}` : '/register'}
|
|
className="p-4 sm:p-5 bg-background-secondary/50 border border-border rounded-xl hover:border-border-hover hover:bg-background-secondary transition-all duration-300 text-left group"
|
|
>
|
|
<div className="flex items-center justify-between mb-3">
|
|
<span className="font-mono text-body-lg sm:text-heading-sm text-foreground">.{item.tld}</span>
|
|
{isAuthenticated ? (
|
|
<span className={clsx(
|
|
"text-ui-sm font-medium px-2 py-0.5 rounded-full",
|
|
item.price_change > 0
|
|
? "text-[#f97316] bg-[#f9731615]"
|
|
: "text-accent bg-accent-muted"
|
|
)}>
|
|
{item.price_change > 0 ? '+' : ''}{item.price_change.toFixed(1)}%
|
|
</span>
|
|
) : (
|
|
<ShimmerBlock className="h-5 w-14" />
|
|
)}
|
|
</div>
|
|
<p className="text-body-sm text-foreground-muted mb-2 line-clamp-2">
|
|
{isAuthenticated ? item.reason : 'Sign in to view trend details'}
|
|
</p>
|
|
<div className="flex items-center justify-between">
|
|
{isAuthenticated ? (
|
|
<span className="text-body-sm text-foreground-subtle">
|
|
${item.current_price.toFixed(2)}/yr
|
|
</span>
|
|
) : (
|
|
<ShimmerBlock className="h-5 w-20" />
|
|
)}
|
|
<ChevronRight className="w-4 h-4 text-foreground-subtle group-hover:text-accent transition-colors" />
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* TLD Table */}
|
|
<div className="bg-background-secondary/30 border border-border rounded-xl overflow-hidden animate-slide-up">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead>
|
|
<tr className="bg-background-secondary border-b border-border">
|
|
<th className="text-left px-4 sm:px-6 py-4">
|
|
<button
|
|
onClick={() => handleSort('tld')}
|
|
className="flex items-center gap-2 text-ui-sm text-foreground-subtle font-medium hover:text-foreground transition-colors"
|
|
>
|
|
TLD
|
|
<SortIcon field="tld" currentField={sortField} direction={sortDirection} />
|
|
</button>
|
|
</th>
|
|
<th className="text-left px-4 sm:px-6 py-4 hidden lg:table-cell">
|
|
<span className="text-ui-sm text-foreground-subtle font-medium">
|
|
Description
|
|
</span>
|
|
</th>
|
|
<th className="text-left px-4 sm:px-6 py-4 hidden md:table-cell">
|
|
<span className="text-ui-sm text-foreground-subtle font-medium">
|
|
12-Month Trend
|
|
</span>
|
|
</th>
|
|
<th className="text-right px-4 sm:px-6 py-4">
|
|
<button
|
|
onClick={() => handleSort('avg_registration_price')}
|
|
className="flex items-center gap-2 ml-auto text-ui-sm text-foreground-subtle font-medium hover:text-foreground transition-colors"
|
|
>
|
|
Avg. Price
|
|
<SortIcon field="avg_registration_price" currentField={sortField} direction={sortDirection} />
|
|
</button>
|
|
</th>
|
|
<th className="text-right px-4 sm:px-6 py-4 hidden sm:table-cell">
|
|
<button
|
|
onClick={() => handleSort('min_registration_price')}
|
|
className="flex items-center gap-2 ml-auto text-ui-sm text-foreground-subtle font-medium hover:text-foreground transition-colors"
|
|
>
|
|
From
|
|
<SortIcon field="min_registration_price" currentField={sortField} direction={sortDirection} />
|
|
</button>
|
|
</th>
|
|
<th className="text-center px-4 sm:px-6 py-4 hidden xl:table-cell">
|
|
<button
|
|
onClick={() => handleSort('registrar_count')}
|
|
className="flex items-center gap-2 mx-auto text-ui-sm text-foreground-subtle font-medium hover:text-foreground transition-colors"
|
|
>
|
|
Registrars
|
|
<SortIcon field="registrar_count" currentField={sortField} direction={sortDirection} />
|
|
</button>
|
|
</th>
|
|
<th className="text-center px-4 sm:px-6 py-4 hidden sm:table-cell">
|
|
<span className="text-ui-sm text-foreground-subtle font-medium">Trend</span>
|
|
</th>
|
|
<th className="px-4 sm:px-6 py-4"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-border">
|
|
{sortedTlds.map((tld, idx) => (
|
|
<tr
|
|
key={tld.tld}
|
|
className="hover:bg-background-secondary/50 transition-colors group"
|
|
>
|
|
<td className="px-4 sm:px-6 py-4">
|
|
<span className="font-mono text-body-sm sm:text-body font-medium text-foreground">
|
|
.{tld.tld}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 sm:px-6 py-4 hidden lg:table-cell">
|
|
<span className="text-body-sm text-foreground-muted line-clamp-1">
|
|
{tld.description}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 sm:px-6 py-4 hidden md:table-cell">
|
|
<MiniChart tld={tld.tld} isAuthenticated={isAuthenticated} />
|
|
</td>
|
|
<td className="px-4 sm:px-6 py-4 text-right">
|
|
{isAuthenticated ? (
|
|
<span className="text-body-sm font-medium text-foreground">
|
|
${tld.avg_registration_price.toFixed(2)}
|
|
</span>
|
|
) : (
|
|
<span className="text-body-sm text-foreground-subtle">•••</span>
|
|
)}
|
|
</td>
|
|
<td className="px-4 sm:px-6 py-4 text-right hidden sm:table-cell">
|
|
{isAuthenticated ? (
|
|
<span className="text-body-sm text-accent">
|
|
${tld.min_registration_price.toFixed(2)}
|
|
</span>
|
|
) : (
|
|
<span className="text-body-sm text-foreground-subtle">•••</span>
|
|
)}
|
|
</td>
|
|
<td className="px-4 sm:px-6 py-4 text-center hidden xl:table-cell">
|
|
{isAuthenticated ? (
|
|
<span className="text-body-sm text-foreground-muted">
|
|
{tld.registrar_count}
|
|
</span>
|
|
) : (
|
|
<span className="text-body-sm text-foreground-subtle">•</span>
|
|
)}
|
|
</td>
|
|
<td className="px-4 sm:px-6 py-4 text-center hidden sm:table-cell">
|
|
{isAuthenticated ? getTrendIcon(tld.trend) : <Minus className="w-4 h-4 text-foreground-subtle mx-auto" />}
|
|
</td>
|
|
<td className="px-4 sm:px-6 py-4">
|
|
<Link
|
|
href={isAuthenticated ? `/tld-pricing/${tld.tld}` : '/register'}
|
|
className="flex items-center gap-1 text-ui-sm text-accent hover:text-accent-hover transition-colors opacity-0 group-hover:opacity-100"
|
|
>
|
|
Details
|
|
<ArrowRight className="w-3 h-3" />
|
|
</Link>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats */}
|
|
<div className="mt-6 flex justify-center">
|
|
<p className="text-ui-sm text-foreground-subtle">
|
|
Showing {sortedTlds.length} TLDs
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<Footer />
|
|
</div>
|
|
)
|
|
}
|