- Added Footer component to all pages (home, dashboard, TLD pages) - Changed max-width from 6xl to 7xl for consistent layout - Enhanced mini-charts with gradients, area fills, and data points - Better visual hierarchy and spacing - Consistent design across all pages
408 lines
17 KiB
TypeScript
408 lines
17 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState } from 'react'
|
|
import { useParams } from 'next/navigation'
|
|
import { Header } from '@/components/Header'
|
|
import { Footer } from '@/components/Footer'
|
|
import { api } from '@/lib/api'
|
|
import {
|
|
ArrowLeft,
|
|
TrendingUp,
|
|
TrendingDown,
|
|
Minus,
|
|
Calendar,
|
|
Globe,
|
|
Building,
|
|
DollarSign,
|
|
ArrowRight,
|
|
ExternalLink,
|
|
} from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import clsx from 'clsx'
|
|
|
|
interface TldDetails {
|
|
tld: string
|
|
type: string
|
|
description: string
|
|
registry: string
|
|
introduced: number
|
|
trend: string
|
|
trend_reason: string
|
|
pricing: {
|
|
avg: number
|
|
min: number
|
|
max: number
|
|
}
|
|
registrars: Array<{
|
|
name: string
|
|
registration_price: number
|
|
renewal_price: number
|
|
transfer_price: number
|
|
}>
|
|
cheapest_registrar: string
|
|
}
|
|
|
|
interface TldHistory {
|
|
tld: string
|
|
current_price: number
|
|
price_change_7d: number
|
|
price_change_30d: number
|
|
price_change_90d: number
|
|
history: Array<{
|
|
date: string
|
|
price: number
|
|
}>
|
|
}
|
|
|
|
export default function TldDetailPage() {
|
|
const params = useParams()
|
|
const tld = params.tld as string
|
|
|
|
const [details, setDetails] = useState<TldDetails | null>(null)
|
|
const [history, setHistory] = useState<TldHistory | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
if (tld) {
|
|
loadData()
|
|
}
|
|
}, [tld])
|
|
|
|
const loadData = async () => {
|
|
try {
|
|
const [historyData, compareData] = await Promise.all([
|
|
api.getTldHistory(tld, 90),
|
|
api.getTldCompare(tld),
|
|
])
|
|
|
|
// Build details from API data
|
|
if (historyData && compareData) {
|
|
const registrars = compareData.registrars || []
|
|
const prices = registrars.map(r => r.registration_price)
|
|
const avgPrice = prices.length > 0 ? prices.reduce((a, b) => a + b, 0) / prices.length : 0
|
|
|
|
setDetails({
|
|
tld: tld,
|
|
type: 'generic',
|
|
description: `Domain extension .${tld}`,
|
|
registry: 'Various',
|
|
introduced: 2020,
|
|
trend: historyData.price_change_30d > 0 ? 'up' : historyData.price_change_30d < 0 ? 'down' : 'stable',
|
|
trend_reason: 'Price tracking available',
|
|
pricing: {
|
|
avg: avgPrice,
|
|
min: Math.min(...prices),
|
|
max: Math.max(...prices),
|
|
},
|
|
registrars: registrars.sort((a, b) => a.registration_price - b.registration_price),
|
|
cheapest_registrar: registrars[0]?.name || 'N/A',
|
|
})
|
|
setHistory(historyData)
|
|
} else {
|
|
setError('Failed to load TLD data')
|
|
}
|
|
} catch (err) {
|
|
setError('Failed to load TLD data')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const getTrendIcon = (trend: string) => {
|
|
switch (trend) {
|
|
case 'up':
|
|
return <TrendingUp className="w-5 h-5 text-warning" />
|
|
case 'down':
|
|
return <TrendingDown className="w-5 h-5 text-accent" />
|
|
default:
|
|
return <Minus className="w-5 h-5 text-foreground-subtle" />
|
|
}
|
|
}
|
|
|
|
if (loading) {
|
|
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>
|
|
)
|
|
}
|
|
|
|
if (error || !details) {
|
|
return (
|
|
<div className="min-h-screen bg-background">
|
|
<Header />
|
|
<main className="pt-28 pb-16 px-4 sm:px-6">
|
|
<div className="max-w-4xl mx-auto text-center">
|
|
<p className="text-body-lg text-foreground-muted mb-4">{error || 'TLD not found'}</p>
|
|
<Link
|
|
href="/tld-pricing"
|
|
className="inline-flex items-center gap-2 text-accent hover:text-accent-hover"
|
|
>
|
|
<ArrowLeft className="w-4 h-4" />
|
|
Back to TLD Overview
|
|
</Link>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-background relative">
|
|
{/* Ambient glow */}
|
|
<div className="fixed inset-0 pointer-events-none">
|
|
<div className="absolute top-0 left-1/3 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">
|
|
<div className="max-w-4xl mx-auto">
|
|
{/* Back Link */}
|
|
<Link
|
|
href="/tld-pricing"
|
|
className="inline-flex items-center gap-2 text-body-sm text-foreground-muted hover:text-foreground mb-8 transition-colors"
|
|
>
|
|
<ArrowLeft className="w-4 h-4" />
|
|
All TLDs
|
|
</Link>
|
|
|
|
{/* Header */}
|
|
<div className="mb-10 sm:mb-12 animate-fade-in">
|
|
<div className="flex items-start justify-between gap-4 mb-4">
|
|
<div>
|
|
<h1 className="font-mono text-[3rem] sm:text-[4rem] md:text-[5rem] leading-none text-foreground mb-2">
|
|
.{details.tld}
|
|
</h1>
|
|
<p className="text-body-lg text-foreground-muted">{details.description}</p>
|
|
</div>
|
|
<div className="flex items-center gap-2 p-3 bg-background-secondary border border-border rounded-xl">
|
|
{getTrendIcon(details.trend)}
|
|
<span className={clsx(
|
|
"text-body-sm font-medium",
|
|
details.trend === 'up' ? "text-warning" :
|
|
details.trend === 'down' ? "text-accent" :
|
|
"text-foreground-muted"
|
|
)}>
|
|
{details.trend === 'up' ? 'Rising' : details.trend === 'down' ? 'Falling' : 'Stable'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<p className="text-body-sm text-foreground-subtle">{details.trend_reason}</p>
|
|
</div>
|
|
|
|
{/* Price Stats */}
|
|
<div className="grid sm:grid-cols-3 gap-4 mb-10 sm:mb-12 animate-slide-up">
|
|
<div className="p-5 sm:p-6 bg-background-secondary border border-border rounded-xl">
|
|
<p className="text-ui-sm text-foreground-subtle mb-2">Average Price</p>
|
|
<p className="text-heading-md sm:text-heading-lg font-medium text-foreground">
|
|
${details.pricing.avg.toFixed(2)}<span className="text-body-sm text-foreground-subtle">/yr</span>
|
|
</p>
|
|
</div>
|
|
<div className="p-5 sm:p-6 bg-background-secondary border border-border rounded-xl">
|
|
<p className="text-ui-sm text-foreground-subtle mb-2">Cheapest</p>
|
|
<p className="text-heading-md sm:text-heading-lg font-medium text-accent">
|
|
${details.pricing.min.toFixed(2)}<span className="text-body-sm text-foreground-subtle">/yr</span>
|
|
</p>
|
|
<p className="text-ui-sm text-foreground-subtle mt-1">at {details.cheapest_registrar}</p>
|
|
</div>
|
|
<div className="p-5 sm:p-6 bg-background-secondary border border-border rounded-xl">
|
|
<p className="text-ui-sm text-foreground-subtle mb-2">Price Range</p>
|
|
<p className="text-heading-md sm:text-heading-lg font-medium text-foreground">
|
|
${details.pricing.min.toFixed(2)} - ${details.pricing.max.toFixed(2)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Price Changes */}
|
|
{history && (
|
|
<div className="mb-10 sm:mb-12 animate-slide-up delay-100">
|
|
<h2 className="text-body-lg font-medium text-foreground mb-4">Price Changes</h2>
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div className="p-4 bg-background-secondary/50 border border-border rounded-xl text-center">
|
|
<p className="text-ui-sm text-foreground-subtle mb-1">7 Days</p>
|
|
<p className={clsx(
|
|
"text-body-lg font-medium",
|
|
history.price_change_7d > 0 ? "text-warning" :
|
|
history.price_change_7d < 0 ? "text-accent" :
|
|
"text-foreground-muted"
|
|
)}>
|
|
{history.price_change_7d > 0 ? '+' : ''}{history.price_change_7d.toFixed(2)}%
|
|
</p>
|
|
</div>
|
|
<div className="p-4 bg-background-secondary/50 border border-border rounded-xl text-center">
|
|
<p className="text-ui-sm text-foreground-subtle mb-1">30 Days</p>
|
|
<p className={clsx(
|
|
"text-body-lg font-medium",
|
|
history.price_change_30d > 0 ? "text-warning" :
|
|
history.price_change_30d < 0 ? "text-accent" :
|
|
"text-foreground-muted"
|
|
)}>
|
|
{history.price_change_30d > 0 ? '+' : ''}{history.price_change_30d.toFixed(2)}%
|
|
</p>
|
|
</div>
|
|
<div className="p-4 bg-background-secondary/50 border border-border rounded-xl text-center">
|
|
<p className="text-ui-sm text-foreground-subtle mb-1">90 Days</p>
|
|
<p className={clsx(
|
|
"text-body-lg font-medium",
|
|
history.price_change_90d > 0 ? "text-warning" :
|
|
history.price_change_90d < 0 ? "text-accent" :
|
|
"text-foreground-muted"
|
|
)}>
|
|
{history.price_change_90d > 0 ? '+' : ''}{history.price_change_90d.toFixed(2)}%
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Price Chart */}
|
|
{history && history.history.length > 0 && (
|
|
<div className="mb-10 sm:mb-12 animate-slide-up delay-150">
|
|
<h2 className="text-body-lg font-medium text-foreground mb-4">90-Day Price History</h2>
|
|
<div className="p-6 bg-background-secondary border border-border rounded-xl">
|
|
<div className="h-40 flex items-end gap-0.5">
|
|
{history.history.map((point, i) => {
|
|
const prices = history.history.map(p => p.price)
|
|
const minPrice = Math.min(...prices)
|
|
const maxPrice = Math.max(...prices)
|
|
const range = maxPrice - minPrice || 1
|
|
const height = ((point.price - minPrice) / range) * 100
|
|
const isLast = i === history.history.length - 1
|
|
|
|
return (
|
|
<div
|
|
key={i}
|
|
className={clsx(
|
|
"flex-1 rounded-t transition-all hover:opacity-80",
|
|
isLast ? "bg-accent" : "bg-accent/40"
|
|
)}
|
|
style={{ height: `${Math.max(height, 5)}%` }}
|
|
title={`${point.date}: $${point.price.toFixed(2)}`}
|
|
/>
|
|
)
|
|
})}
|
|
</div>
|
|
<div className="flex justify-between mt-3 text-ui-sm text-foreground-subtle">
|
|
<span>{history.history[0]?.date}</span>
|
|
<span>Today</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* TLD Info */}
|
|
<div className="mb-10 sm:mb-12 animate-slide-up delay-200">
|
|
<h2 className="text-body-lg font-medium text-foreground mb-4">TLD Information</h2>
|
|
<div className="grid sm:grid-cols-2 gap-4">
|
|
<div className="flex items-center gap-4 p-4 bg-background-secondary/50 border border-border rounded-xl">
|
|
<Globe className="w-5 h-5 text-foreground-subtle" />
|
|
<div>
|
|
<p className="text-ui-sm text-foreground-subtle">Type</p>
|
|
<p className="text-body-sm text-foreground capitalize">{details.type}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-4 p-4 bg-background-secondary/50 border border-border rounded-xl">
|
|
<Building className="w-5 h-5 text-foreground-subtle" />
|
|
<div>
|
|
<p className="text-ui-sm text-foreground-subtle">Registry</p>
|
|
<p className="text-body-sm text-foreground">{details.registry}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-4 p-4 bg-background-secondary/50 border border-border rounded-xl">
|
|
<Calendar className="w-5 h-5 text-foreground-subtle" />
|
|
<div>
|
|
<p className="text-ui-sm text-foreground-subtle">Introduced</p>
|
|
<p className="text-body-sm text-foreground">{details.introduced}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-4 p-4 bg-background-secondary/50 border border-border rounded-xl">
|
|
<DollarSign className="w-5 h-5 text-foreground-subtle" />
|
|
<div>
|
|
<p className="text-ui-sm text-foreground-subtle">Registrars</p>
|
|
<p className="text-body-sm text-foreground">{details.registrars.length} available</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Registrar Comparison */}
|
|
<div className="mb-10 sm:mb-12 animate-slide-up delay-250">
|
|
<h2 className="text-body-lg font-medium text-foreground mb-4">Registrar Comparison</h2>
|
|
<div className="border border-border rounded-xl overflow-hidden">
|
|
<table className="w-full">
|
|
<thead>
|
|
<tr className="bg-background-secondary/50 border-b border-border">
|
|
<th className="text-left text-ui-sm text-foreground-subtle font-medium px-4 py-3">Registrar</th>
|
|
<th className="text-right text-ui-sm text-foreground-subtle font-medium px-4 py-3">Register</th>
|
|
<th className="text-right text-ui-sm text-foreground-subtle font-medium px-4 py-3 hidden sm:table-cell">Renew</th>
|
|
<th className="text-right text-ui-sm text-foreground-subtle font-medium px-4 py-3 hidden sm:table-cell">Transfer</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-border">
|
|
{details.registrars.map((registrar, i) => (
|
|
<tr key={registrar.name} className={clsx(
|
|
"transition-colors",
|
|
i === 0 ? "bg-accent/5" : "hover:bg-background-secondary/30"
|
|
)}>
|
|
<td className="px-4 py-3">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-body-sm text-foreground">{registrar.name}</span>
|
|
{i === 0 && (
|
|
<span className="text-ui-xs text-accent bg-accent-muted px-1.5 py-0.5 rounded">
|
|
Cheapest
|
|
</span>
|
|
)}
|
|
</div>
|
|
</td>
|
|
<td className="px-4 py-3 text-right">
|
|
<span className={clsx(
|
|
"text-body-sm font-medium",
|
|
i === 0 ? "text-accent" : "text-foreground"
|
|
)}>
|
|
${registrar.registration_price.toFixed(2)}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3 text-right hidden sm:table-cell">
|
|
<span className="text-body-sm text-foreground-muted">
|
|
${registrar.renewal_price.toFixed(2)}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3 text-right hidden sm:table-cell">
|
|
<span className="text-body-sm text-foreground-muted">
|
|
${registrar.transfer_price.toFixed(2)}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* CTA */}
|
|
<div className="p-6 sm:p-8 bg-background-secondary border border-border rounded-xl text-center animate-slide-up delay-300">
|
|
<h3 className="text-body-lg font-medium text-foreground mb-2">
|
|
Monitor .{details.tld} Domains
|
|
</h3>
|
|
<p className="text-body-sm text-foreground-muted mb-6">
|
|
Track availability and get notified when your target domains become available.
|
|
</p>
|
|
<Link
|
|
href="/register"
|
|
className="inline-flex items-center gap-2 px-6 py-3 bg-foreground text-background text-ui font-medium rounded-xl hover:bg-foreground/90 transition-all"
|
|
>
|
|
Start Monitoring
|
|
<ArrowRight className="w-4 h-4" />
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<Footer />
|
|
</div>
|
|
)
|
|
}
|
|
|