Yves Gugger 48b33e9252 Improve UX: Add Footer everywhere, unified max-width, beautiful charts
- 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
2025-12-08 07:46:58 +01:00

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>
)
}