yves.gugger 74670cf9e5 fix: Correct portfolio import and TypeScript errors
- Fixed import: get_current_user from app.api.deps (not app.api.auth)
- Removed empty routes folder
- Fixed TldHistory interface (optional trend fields)
- Fixed DomainCheckResult interface (nullable fields)
- Fixed loadData function to compute values from available API data
2025-12-08 11:26:35 +01:00

1042 lines
42 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useEffect, useState, useMemo, useRef } from 'react'
import { useParams, useRouter } from 'next/navigation'
import { Header } from '@/components/Header'
import { Footer } from '@/components/Footer'
import { useStore } from '@/lib/store'
import { api } from '@/lib/api'
import {
ArrowLeft,
TrendingUp,
TrendingDown,
Minus,
Calendar,
Globe,
Building,
ExternalLink,
Bell,
Search,
ChevronRight,
Sparkles,
Check,
X,
Lock,
RefreshCw,
Clock,
Shield,
Zap,
} 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
trend?: string
trend_reason?: string
history: Array<{
date: string
price: number
}>
}
interface DomainCheckResult {
domain: string
is_available: boolean
status: string
registrar?: string | null
creation_date?: string | null
expiration_date?: string | null
}
// Registrar URLs with affiliate parameters
// Note: Replace REF_CODE with actual affiliate IDs when available
const REGISTRAR_URLS: Record<string, string> = {
'Namecheap': 'https://www.namecheap.com/domains/registration/results/?domain=',
'Porkbun': 'https://porkbun.com/checkout/search?q=',
'Cloudflare': 'https://www.cloudflare.com/products/registrar/',
'Google Domains': 'https://domains.google.com/registrar/search?searchTerm=',
'GoDaddy': 'https://www.godaddy.com/domainsearch/find?domainToCheck=',
'porkbun': 'https://porkbun.com/checkout/search?q=',
'Dynadot': 'https://www.dynadot.com/domain/search?domain=',
'Hover': 'https://www.hover.com/domains/results?q=',
}
// Related TLDs
const RELATED_TLDS: Record<string, string[]> = {
'com': ['net', 'org', 'co', 'io'],
'net': ['com', 'org', 'io', 'dev'],
'org': ['com', 'net', 'ngo', 'foundation'],
'io': ['dev', 'app', 'tech', 'ai'],
'ai': ['io', 'tech', 'dev', 'ml'],
'dev': ['io', 'app', 'tech', 'code'],
'app': ['dev', 'io', 'mobile', 'software'],
'co': ['com', 'io', 'biz', 'inc'],
'de': ['at', 'ch', 'eu', 'com'],
'ch': ['de', 'at', 'li', 'eu'],
}
type ChartPeriod = '1M' | '3M' | '1Y' | 'ALL'
// Shimmer component for unauthenticated users
function Shimmer({ className }: { className?: string }) {
return (
<div className={clsx(
"relative overflow-hidden rounded bg-foreground/5",
className
)}>
<div className="absolute inset-0 -translate-x-full animate-[shimmer_2s_infinite] bg-gradient-to-r from-transparent via-foreground/10 to-transparent" />
</div>
)
}
// Premium Chart Component
function PriceChart({
data,
isAuthenticated,
chartStats,
}: {
data: Array<{ date: string; price: number }>
isAuthenticated: boolean
chartStats: { high: number; low: number; avg: number }
}) {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)
const [tooltipPos, setTooltipPos] = useState({ x: 0, y: 0 })
const containerRef = useRef<HTMLDivElement>(null)
if (!isAuthenticated) {
return (
<div className="relative h-48 flex items-center justify-center">
<div className="absolute inset-0 bg-gradient-to-t from-background-secondary/50 to-transparent rounded-xl" />
<Shimmer className="absolute inset-4 h-40" />
<div className="relative z-10 flex flex-col items-center gap-3">
<Lock className="w-5 h-5 text-foreground-subtle" />
<span className="text-ui-sm text-foreground-muted">Sign in to view price history</span>
</div>
</div>
)
}
if (data.length === 0) {
return (
<div className="h-48 flex items-center justify-center text-foreground-subtle">
No price history available
</div>
)
}
const range = chartStats.high - chartStats.low || 1
const padding = range * 0.1
// Create smooth path
const points = data.map((point, i) => {
const x = (i / (data.length - 1)) * 100
const y = 100 - ((point.price - chartStats.low + padding) / (range + padding * 2)) * 100
return { x, y, ...point }
})
// Create SVG path for smooth curve
const linePath = points.reduce((path, point, i) => {
if (i === 0) return `M ${point.x} ${point.y}`
// Use bezier curves for smoothness
const prev = points[i - 1]
const cpx = (prev.x + point.x) / 2
return `${path} C ${cpx} ${prev.y}, ${cpx} ${point.y}, ${point.x} ${point.y}`
}, '')
// Area path
const areaPath = `${linePath} L 100 100 L 0 100 Z`
const handleMouseMove = (e: React.MouseEvent<SVGSVGElement>) => {
if (!containerRef.current) return
const rect = containerRef.current.getBoundingClientRect()
const x = e.clientX - rect.left
const percentage = x / rect.width
const index = Math.round(percentage * (data.length - 1))
if (index >= 0 && index < data.length) {
setHoveredIndex(index)
setTooltipPos({ x: e.clientX - rect.left, y: points[index].y * rect.height / 100 })
}
}
return (
<div ref={containerRef} className="relative h-48">
<svg
className="w-full h-full"
viewBox="0 0 100 100"
preserveAspectRatio="none"
onMouseMove={handleMouseMove}
onMouseLeave={() => setHoveredIndex(null)}
>
<defs>
<linearGradient id="chartGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="rgb(0, 212, 170)" stopOpacity="0.15" />
<stop offset="100%" stopColor="rgb(0, 212, 170)" stopOpacity="0" />
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="1" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
{/* Grid lines */}
{[20, 40, 60, 80].map(y => (
<line
key={y}
x1="0" y1={y} x2="100" y2={y}
stroke="currentColor"
strokeOpacity="0.05"
strokeWidth="0.2"
vectorEffect="non-scaling-stroke"
/>
))}
{/* Area fill */}
<path
d={areaPath}
fill="url(#chartGradient)"
/>
{/* Main line */}
<path
d={linePath}
fill="none"
stroke="rgb(0, 212, 170)"
strokeWidth="0.4"
strokeLinecap="round"
strokeLinejoin="round"
vectorEffect="non-scaling-stroke"
filter="url(#glow)"
/>
{/* Hover indicator */}
{hoveredIndex !== null && (
<g>
<line
x1={points[hoveredIndex].x}
y1="0"
x2={points[hoveredIndex].x}
y2="100"
stroke="rgb(0, 212, 170)"
strokeWidth="1"
strokeDasharray="4 4"
vectorEffect="non-scaling-stroke"
opacity="0.3"
/>
</g>
)}
</svg>
{/* Hover dot */}
{hoveredIndex !== null && containerRef.current && (
<div
className="absolute w-3 h-3 bg-accent rounded-full border-2 border-background shadow-lg pointer-events-none transform -translate-x-1/2 -translate-y-1/2 transition-all duration-75"
style={{
left: `${(points[hoveredIndex].x / 100) * containerRef.current.offsetWidth}px`,
top: `${(points[hoveredIndex].y / 100) * containerRef.current.offsetHeight}px`
}}
/>
)}
{/* Tooltip */}
{hoveredIndex !== null && (
<div
className="absolute z-20 px-3 py-2 bg-background border border-border rounded-lg shadow-xl pointer-events-none transform -translate-x-1/2"
style={{ left: tooltipPos.x, top: Math.max(tooltipPos.y - 60, 10) }}
>
<p className="text-ui-sm font-medium text-foreground tabular-nums">
${data[hoveredIndex].price.toFixed(2)}
</p>
<p className="text-ui-xs text-foreground-subtle">
{new Date(data[hoveredIndex].date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
})}
</p>
</div>
)}
</div>
)
}
// Domain Check Result Card (like landing page)
function DomainResultCard({
result,
tld,
cheapestPrice,
cheapestRegistrar,
onClose
}: {
result: DomainCheckResult
tld: string
cheapestPrice: number
cheapestRegistrar: string
onClose: () => void
}) {
const registrarUrl = REGISTRAR_URLS[cheapestRegistrar] || '#'
return (
<div className={clsx(
"mt-6 p-6 rounded-2xl border transition-all duration-500 animate-fade-in",
result.is_available
? "bg-gradient-to-br from-accent/10 via-accent/5 to-transparent border-accent/30"
: "bg-gradient-to-br from-orange-500/10 via-orange-500/5 to-transparent border-orange-500/30"
)}>
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<div className="flex items-center gap-3 mb-3">
<div className={clsx(
"w-10 h-10 rounded-xl flex items-center justify-center",
result.is_available ? "bg-accent/20" : "bg-orange-500/20"
)}>
{result.is_available ? (
<Check className="w-5 h-5 text-accent" />
) : (
<X className="w-5 h-5 text-orange-400" />
)}
</div>
<div>
<h3 className="font-mono text-lg text-foreground">{result.domain}</h3>
<p className={clsx(
"text-ui-sm",
result.is_available ? "text-accent" : "text-orange-400"
)}>
{result.is_available ? 'Available for registration' : 'Already registered'}
</p>
</div>
</div>
{result.is_available ? (
<div className="flex flex-wrap items-center gap-4 mt-4">
<div className="flex items-center gap-2">
<Zap className="w-4 h-4 text-accent" />
<span className="text-body-sm text-foreground">
Register from <span className="font-medium text-accent">${cheapestPrice.toFixed(2)}</span>/yr
</span>
</div>
<a
href={`${registrarUrl}${result.domain}`}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-5 py-2.5 bg-accent text-background text-ui-sm font-medium rounded-xl hover:bg-accent-hover transition-all"
>
Register at {cheapestRegistrar}
<ExternalLink className="w-4 h-4" />
</a>
</div>
) : (
<div className="flex flex-wrap items-center gap-4 mt-4 text-body-sm text-foreground-muted">
{result.registrar && (
<div className="flex items-center gap-2">
<Shield className="w-4 h-4 text-foreground-subtle" />
<span>Registrar: {result.registrar}</span>
</div>
)}
{result.expiration_date && (
<div className="flex items-center gap-2">
<Clock className="w-4 h-4 text-foreground-subtle" />
<span>Expires: {new Date(result.expiration_date).toLocaleDateString()}</span>
</div>
)}
</div>
)}
</div>
<button
onClick={onClose}
className="p-2 text-foreground-subtle hover:text-foreground hover:bg-background-secondary rounded-lg transition-colors"
>
<X className="w-4 h-4" />
</button>
</div>
</div>
)
}
export default function TldDetailPage() {
const params = useParams()
const router = useRouter()
const { isAuthenticated, checkAuth, isLoading: authLoading } = useStore()
const tld = params.tld as string
const [details, setDetails] = useState<TldDetails | null>(null)
const [history, setHistory] = useState<TldHistory | null>(null)
const [relatedTlds, setRelatedTlds] = useState<Array<{ tld: string; price: number }>>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [chartPeriod, setChartPeriod] = useState<ChartPeriod>('1Y')
const [domainSearch, setDomainSearch] = useState('')
const [checkingDomain, setCheckingDomain] = useState(false)
const [domainResult, setDomainResult] = useState<DomainCheckResult | null>(null)
const [alertEnabled, setAlertEnabled] = useState(false)
useEffect(() => {
checkAuth()
}, [checkAuth])
useEffect(() => {
if (tld) {
loadData()
loadRelatedTlds()
}
}, [tld])
const loadData = async () => {
try {
const [historyData, compareData] = await Promise.all([
api.getTldHistory(tld, 365),
api.getTldCompare(tld),
])
if (historyData && compareData) {
const registrars = compareData.registrars || []
const sortedRegistrars = [...registrars].sort((a, b) =>
a.registration_price - b.registration_price
)
const minPrice = sortedRegistrars[0]?.registration_price || historyData.current_price || 0
const maxPrice = sortedRegistrars[sortedRegistrars.length - 1]?.registration_price || historyData.current_price || 0
const avgPrice = registrars.length > 0
? registrars.reduce((sum, r) => sum + r.registration_price, 0) / registrars.length
: historyData.current_price || 0
// Determine trend from price change
const trend = historyData.price_change_30d > 5 ? 'up'
: historyData.price_change_30d < -5 ? 'down'
: 'stable'
setDetails({
tld: compareData.tld || tld,
type: 'generic',
description: `Domain extension .${tld}`,
registry: 'Various',
introduced: 0,
trend: trend,
trend_reason: trend === 'up' ? 'Price increase' : trend === 'down' ? 'Price decrease' : 'Stable pricing',
pricing: {
avg: avgPrice,
min: minPrice,
max: maxPrice,
},
registrars: sortedRegistrars,
cheapest_registrar: sortedRegistrars[0]?.name || 'N/A',
})
setHistory({
...historyData,
trend: trend,
trend_reason: trend === 'up' ? 'Price increase' : trend === 'down' ? 'Price decrease' : 'Stable pricing',
})
} else {
setError('Failed to load TLD data')
}
} catch (err) {
console.error('Error loading TLD data:', err)
setError('Failed to load TLD data')
} finally {
setLoading(false)
}
}
const loadRelatedTlds = async () => {
const related = RELATED_TLDS[tld.toLowerCase()] || ['com', 'net', 'org', 'io']
const relatedData: Array<{ tld: string; price: number }> = []
for (const relatedTld of related.slice(0, 4)) {
try {
const data = await api.getTldHistory(relatedTld, 30)
if (data) {
relatedData.push({ tld: relatedTld, price: data.current_price })
}
} catch {
// Skip failed
}
}
setRelatedTlds(relatedData)
}
const filteredHistory = useMemo(() => {
if (!history?.history) return []
const now = new Date()
let cutoffDays = 365
switch (chartPeriod) {
case '1M': cutoffDays = 30; break
case '3M': cutoffDays = 90; break
case '1Y': cutoffDays = 365; break
case 'ALL': cutoffDays = 9999; break
}
const cutoff = new Date(now.getTime() - cutoffDays * 24 * 60 * 60 * 1000)
return history.history.filter(h => new Date(h.date) >= cutoff)
}, [history, chartPeriod])
const chartStats = useMemo(() => {
if (filteredHistory.length === 0) return { high: 0, low: 0, avg: 0 }
const prices = filteredHistory.map(h => h.price)
return {
high: Math.max(...prices),
low: Math.min(...prices),
avg: prices.reduce((a, b) => a + b, 0) / prices.length,
}
}, [filteredHistory])
const handleDomainCheck = async () => {
if (!domainSearch.trim()) return
setCheckingDomain(true)
setDomainResult(null)
try {
const domain = domainSearch.includes('.') ? domainSearch : `${domainSearch}.${tld}`
const result = await api.checkDomain(domain, false)
setDomainResult({
domain,
is_available: result.is_available,
status: result.status,
registrar: result.registrar,
creation_date: result.creation_date,
expiration_date: result.expiration_date,
})
} catch (err) {
console.error('Domain check failed:', err)
} finally {
setCheckingDomain(false)
}
}
const getRegistrarUrl = (registrarName: string, domain?: string) => {
const baseUrl = REGISTRAR_URLS[registrarName]
if (!baseUrl) return '#'
if (domain) return `${baseUrl}${domain}`
return baseUrl
}
const savings = useMemo(() => {
if (!details || details.registrars.length < 2) return null
const cheapest = details.registrars[0].registration_price
const mostExpensive = details.registrars[details.registrars.length - 1].registration_price
return {
amount: mostExpensive - cheapest,
cheapestName: details.registrars[0].name,
expensiveName: details.registrars[details.registrars.length - 1].name,
}
}, [details])
const getTrendIcon = (trend: string) => {
switch (trend) {
case 'up': return <TrendingUp className="w-4 h-4" />
case 'down': return <TrendingDown className="w-4 h-4" />
default: return <Minus className="w-4 h-4" />
}
}
if (loading || authLoading) {
return (
<div className="min-h-screen bg-background">
<Header />
<main className="pt-28 pb-16 px-4 sm:px-6">
<div className="max-w-5xl mx-auto space-y-8">
<Shimmer className="h-8 w-32" />
<Shimmer className="h-20 w-48" />
<div className="grid sm:grid-cols-4 gap-4">
{[1, 2, 3, 4].map(i => <Shimmer key={i} className="h-24" />)}
</div>
<Shimmer className="h-64" />
</div>
</main>
</div>
)
}
if (error || !details) {
return (
<div className="min-h-screen bg-background flex flex-col">
<Header />
<main className="flex-1 flex items-center justify-center px-4">
<div className="text-center">
<div className="w-16 h-16 bg-background-secondary rounded-full flex items-center justify-center mx-auto mb-6">
<X className="w-8 h-8 text-foreground-subtle" />
</div>
<h1 className="text-heading-md text-foreground mb-2">TLD Not Found</h1>
<p className="text-body text-foreground-muted mb-8">{error || `The TLD .${tld} could not be found.`}</p>
<Link
href="/tld-pricing"
className="inline-flex items-center gap-2 px-6 py-3 bg-accent text-background rounded-xl font-medium hover:bg-accent-hover transition-all"
>
<ArrowLeft className="w-4 h-4" />
Back to TLD Overview
</Link>
</div>
</main>
<Footer />
</div>
)
}
return (
<div className="min-h-screen bg-background relative flex flex-col">
{/* Subtle ambient */}
<div className="fixed inset-0 pointer-events-none overflow-hidden">
<div className="absolute -top-40 -right-40 w-[600px] h-[600px] bg-accent/[0.02] rounded-full blur-3xl" />
</div>
<Header />
<main className="relative pt-24 sm:pt-28 pb-20 px-4 sm:px-6 flex-1">
<div className="max-w-5xl mx-auto">
{/* Breadcrumb */}
<nav className="flex items-center gap-2 text-ui-sm mb-8">
<Link href="/tld-pricing" className="text-foreground-subtle hover:text-foreground transition-colors">
TLD Pricing
</Link>
<ChevronRight className="w-3.5 h-3.5 text-foreground-subtle" />
<span className="text-foreground font-medium">.{details.tld}</span>
</nav>
{/* Hero */}
<div className="grid lg:grid-cols-[1fr,320px] gap-8 mb-12">
{/* Left: TLD Info */}
<div>
<div className="flex items-start gap-4 mb-4">
<h1 className="font-mono text-[4rem] sm:text-[5rem] lg:text-[6rem] leading-[0.85] tracking-tight text-foreground">
.{details.tld}
</h1>
<div className={clsx(
"mt-3 flex items-center gap-1.5 px-2.5 py-1 rounded-full text-ui-sm font-medium",
details.trend === 'up' ? "bg-orange-500/10 text-orange-400" :
details.trend === 'down' ? "bg-accent/10 text-accent" :
"bg-foreground/5 text-foreground-muted"
)}>
{getTrendIcon(details.trend)}
<span>{details.trend === 'up' ? 'Rising' : details.trend === 'down' ? 'Falling' : 'Stable'}</span>
</div>
</div>
<p className="text-body-lg text-foreground-muted mb-2">{details.description}</p>
<p className="text-body-sm text-foreground-subtle">{details.trend_reason}</p>
{/* Quick Stats - Only for authenticated */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mt-8">
<div className="p-4 bg-background-secondary/50 border border-border/50 rounded-xl">
<p className="text-ui-xs text-foreground-subtle uppercase tracking-wider mb-1">Average</p>
{isAuthenticated ? (
<p className="text-body-lg font-medium text-foreground tabular-nums">${details.pricing.avg.toFixed(2)}</p>
) : (
<Shimmer className="h-6 w-16 mt-1" />
)}
</div>
<div className="p-4 bg-background-secondary/50 border border-border/50 rounded-xl">
<p className="text-ui-xs text-foreground-subtle uppercase tracking-wider mb-1">Range</p>
{isAuthenticated ? (
<p className="text-body-lg font-medium text-foreground tabular-nums">
${details.pricing.min.toFixed(0)}${details.pricing.max.toFixed(0)}
</p>
) : (
<Shimmer className="h-6 w-20 mt-1" />
)}
</div>
<div className="p-4 bg-background-secondary/50 border border-border/50 rounded-xl">
<p className="text-ui-xs text-foreground-subtle uppercase tracking-wider mb-1">30d Change</p>
{isAuthenticated && history ? (
<p className={clsx(
"text-body-lg font-medium tabular-nums",
history.price_change_30d > 0 ? "text-orange-400" :
history.price_change_30d < 0 ? "text-accent" :
"text-foreground"
)}>
{history.price_change_30d > 0 ? '+' : ''}{history.price_change_30d.toFixed(1)}%
</p>
) : (
<Shimmer className="h-6 w-14 mt-1" />
)}
</div>
<div className="p-4 bg-background-secondary/50 border border-border/50 rounded-xl">
<p className="text-ui-xs text-foreground-subtle uppercase tracking-wider mb-1">Registrars</p>
{isAuthenticated ? (
<p className="text-body-lg font-medium text-foreground">{details.registrars.length}</p>
) : (
<Shimmer className="h-6 w-8 mt-1" />
)}
</div>
</div>
</div>
{/* Right: Price Card */}
<div className="lg:sticky lg:top-28 h-fit">
<div className="p-6 bg-background-secondary/80 backdrop-blur-sm border border-border rounded-2xl">
{isAuthenticated ? (
<>
<div className="flex items-baseline gap-1 mb-1">
<span className="text-[2.75rem] font-semibold text-foreground tracking-tight tabular-nums">
${details.pricing.min.toFixed(2)}
</span>
<span className="text-body text-foreground-subtle">/yr</span>
</div>
<p className="text-ui-sm text-foreground-subtle mb-6">
Cheapest at {details.cheapest_registrar}
</p>
<div className="space-y-3">
<a
href={getRegistrarUrl(details.cheapest_registrar, `example.${tld}`)}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-2 w-full py-3.5 bg-accent text-background font-medium rounded-xl hover:bg-accent-hover transition-all"
>
Register Domain
<ExternalLink className="w-4 h-4" />
</a>
<button
onClick={() => setAlertEnabled(!alertEnabled)}
className={clsx(
"flex items-center justify-center gap-2 w-full py-3.5 font-medium rounded-xl transition-all",
alertEnabled
? "bg-accent/10 text-accent border border-accent/30"
: "bg-background border border-border text-foreground hover:bg-background-secondary"
)}
>
<Bell className={clsx("w-4 h-4", alertEnabled && "fill-current")} />
{alertEnabled ? 'Price Alert Active' : 'Enable Price Alert'}
</button>
</div>
{savings && savings.amount > 0.5 && (
<div className="mt-5 pt-5 border-t border-border/50">
<div className="flex items-start gap-2.5">
<Sparkles className="w-4 h-4 text-accent mt-0.5 shrink-0" />
<p className="text-ui-sm text-foreground-muted leading-relaxed">
Save <span className="text-accent font-medium">${savings.amount.toFixed(2)}</span>/yr vs {savings.expensiveName}
</p>
</div>
</div>
)}
</>
) : (
<>
<Shimmer className="h-12 w-32 mb-2" />
<Shimmer className="h-4 w-28 mb-6" />
<Link
href="/register"
className="flex items-center justify-center gap-2 w-full py-3.5 bg-accent text-background font-medium rounded-xl hover:bg-accent-hover transition-all"
>
<Lock className="w-4 h-4" />
Sign in to View Prices
</Link>
</>
)}
</div>
</div>
</div>
{/* Price Chart */}
<section className="mb-12">
<div className="flex items-center justify-between mb-4">
<h2 className="text-body-lg font-medium text-foreground">Price History</h2>
{isAuthenticated && (
<div className="flex items-center gap-1 p-1 bg-background-secondary/50 border border-border/50 rounded-lg">
{(['1M', '3M', '1Y', 'ALL'] as ChartPeriod[]).map(period => (
<button
key={period}
onClick={() => setChartPeriod(period)}
className={clsx(
"px-3 py-1.5 text-ui-sm font-medium rounded-md transition-all",
chartPeriod === period
? "bg-foreground text-background"
: "text-foreground-muted hover:text-foreground"
)}
>
{period}
</button>
))}
</div>
)}
</div>
<div className="p-6 bg-background-secondary/30 border border-border/50 rounded-2xl">
<PriceChart
data={filteredHistory}
isAuthenticated={isAuthenticated}
chartStats={chartStats}
/>
{isAuthenticated && filteredHistory.length > 0 && (
<div className="flex items-center justify-between mt-4 pt-4 border-t border-border/30 text-ui-sm">
<span className="text-foreground-subtle">
{new Date(filteredHistory[0]?.date).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
</span>
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<span className="text-foreground-subtle">High</span>
<span className="text-foreground font-medium tabular-nums">${chartStats.high.toFixed(2)}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-foreground-subtle">Low</span>
<span className="text-accent font-medium tabular-nums">${chartStats.low.toFixed(2)}</span>
</div>
</div>
<span className="text-foreground-subtle">Today</span>
</div>
)}
</div>
</section>
{/* Domain Search */}
<section className="mb-12">
<h2 className="text-body-lg font-medium text-foreground mb-4">
Check .{details.tld} Availability
</h2>
<div className="p-6 bg-background-secondary/30 border border-border/50 rounded-2xl">
<div className="flex gap-3">
<div className="flex-1 relative">
<input
type="text"
value={domainSearch}
onChange={(e) => setDomainSearch(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleDomainCheck()}
placeholder="Enter domain name"
className="w-full px-4 py-3.5 pr-20 bg-background 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"
/>
<span className="absolute right-4 top-1/2 -translate-y-1/2 text-foreground-subtle font-mono text-body-sm">
.{tld}
</span>
</div>
<button
onClick={handleDomainCheck}
disabled={checkingDomain || !domainSearch.trim()}
className="px-6 py-3.5 bg-foreground text-background font-medium rounded-xl hover:bg-foreground/90 disabled:opacity-50 disabled:cursor-not-allowed transition-all flex items-center gap-2"
>
{checkingDomain ? (
<RefreshCw className="w-4 h-4 animate-spin" />
) : (
<Search className="w-4 h-4" />
)}
Check
</button>
</div>
{domainResult && (
<DomainResultCard
result={domainResult}
tld={tld}
cheapestPrice={details.pricing.min}
cheapestRegistrar={details.cheapest_registrar}
onClose={() => setDomainResult(null)}
/>
)}
</div>
</section>
{/* Registrar Comparison */}
<section className="mb-12">
<h2 className="text-body-lg font-medium text-foreground mb-4">Compare Registrars</h2>
{isAuthenticated ? (
<div className="bg-background-secondary/30 border border-border/50 rounded-2xl overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-border/50">
<th className="text-left text-ui-xs text-foreground-subtle font-medium uppercase tracking-wider px-5 py-4">
Registrar
</th>
<th className="text-right text-ui-xs text-foreground-subtle font-medium uppercase tracking-wider px-5 py-4">
Register
</th>
<th className="text-right text-ui-xs text-foreground-subtle font-medium uppercase tracking-wider px-5 py-4 hidden sm:table-cell">
Renew
</th>
<th className="text-right text-ui-xs text-foreground-subtle font-medium uppercase tracking-wider px-5 py-4 hidden sm:table-cell">
Transfer
</th>
<th className="px-5 py-4 w-24"></th>
</tr>
</thead>
<tbody className="divide-y divide-border/30">
{details.registrars.map((registrar, i) => (
<tr key={registrar.name} className={clsx(
"transition-colors group",
i === 0 && "bg-accent/[0.03]"
)}>
<td className="px-5 py-4">
<div className="flex items-center gap-2.5">
<span className="text-body-sm font-medium text-foreground">{registrar.name}</span>
{i === 0 && (
<span className="text-ui-xs text-accent bg-accent/10 px-2 py-0.5 rounded-full font-medium">
Best
</span>
)}
</div>
</td>
<td className="px-5 py-4 text-right">
<span className={clsx(
"text-body-sm font-medium tabular-nums",
i === 0 ? "text-accent" : "text-foreground"
)}>
${registrar.registration_price.toFixed(2)}
</span>
</td>
<td className="px-5 py-4 text-right hidden sm:table-cell">
<span className="text-body-sm text-foreground-muted tabular-nums">
${registrar.renewal_price.toFixed(2)}
</span>
{registrar.renewal_price > registrar.registration_price * 1.5 && (
<span className="ml-1.5 text-orange-400" title="High renewal"></span>
)}
</td>
<td className="px-5 py-4 text-right hidden sm:table-cell">
<span className="text-body-sm text-foreground-muted tabular-nums">
${registrar.transfer_price.toFixed(2)}
</span>
</td>
<td className="px-5 py-4">
<a
href={getRegistrarUrl(registrar.name, `example.${tld}`)}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-ui-sm text-foreground-muted hover:text-accent transition-colors opacity-0 group-hover:opacity-100"
>
Visit
<ExternalLink className="w-3.5 h-3.5" />
</a>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
) : (
<div className="relative p-8 bg-background-secondary/30 border border-border/50 rounded-2xl">
<div className="space-y-3">
{[1, 2, 3, 4].map(i => (
<div key={i} className="flex items-center justify-between">
<Shimmer className="h-5 w-24" />
<Shimmer className="h-5 w-16" />
<Shimmer className="h-5 w-16 hidden sm:block" />
<Shimmer className="h-5 w-16 hidden sm:block" />
</div>
))}
</div>
<div className="absolute inset-0 flex items-center justify-center bg-background/50 backdrop-blur-sm rounded-2xl">
<div className="text-center">
<Lock className="w-6 h-6 text-foreground-subtle mx-auto mb-2" />
<p className="text-body-sm text-foreground-muted mb-3">Sign in to compare registrar prices</p>
<Link
href="/register"
className="inline-flex items-center gap-2 px-4 py-2 bg-accent text-background text-ui-sm font-medium rounded-lg hover:bg-accent-hover transition-all"
>
Get Started Free
</Link>
</div>
</div>
</div>
)}
</section>
{/* TLD Info */}
<section className="mb-12">
<h2 className="text-body-lg font-medium text-foreground mb-4">About .{details.tld}</h2>
<div className="grid sm:grid-cols-3 gap-4">
<div className="p-5 bg-background-secondary/30 border border-border/50 rounded-xl">
<Building className="w-5 h-5 text-foreground-subtle mb-3" />
<p className="text-ui-xs text-foreground-subtle uppercase tracking-wider mb-1">Registry</p>
<p className="text-body font-medium text-foreground">{details.registry}</p>
</div>
<div className="p-5 bg-background-secondary/30 border border-border/50 rounded-xl">
<Calendar className="w-5 h-5 text-foreground-subtle mb-3" />
<p className="text-ui-xs text-foreground-subtle uppercase tracking-wider mb-1">Introduced</p>
<p className="text-body font-medium text-foreground">{details.introduced || 'Unknown'}</p>
</div>
<div className="p-5 bg-background-secondary/30 border border-border/50 rounded-xl">
<Globe className="w-5 h-5 text-foreground-subtle mb-3" />
<p className="text-ui-xs text-foreground-subtle uppercase tracking-wider mb-1">Type</p>
<p className="text-body font-medium text-foreground capitalize">{details.type}</p>
</div>
</div>
</section>
{/* Related TLDs */}
{relatedTlds.length > 0 && (
<section className="mb-12">
<h2 className="text-body-lg font-medium text-foreground mb-4">Similar Extensions</h2>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
{relatedTlds.map(related => (
<Link
key={related.tld}
href={`/tld-pricing/${related.tld}`}
className="group p-5 bg-background-secondary/30 border border-border/50 rounded-xl hover:border-accent/30 hover:bg-background-secondary/50 transition-all"
>
<p className="font-mono text-body-lg text-foreground group-hover:text-accent transition-colors mb-1">
.{related.tld}
</p>
{isAuthenticated ? (
<p className="text-ui-sm text-foreground-subtle tabular-nums">
from ${related.price.toFixed(2)}/yr
</p>
) : (
<Shimmer className="h-4 w-20" />
)}
</Link>
))}
</div>
</section>
)}
{/* CTA */}
<section className="p-8 sm:p-10 bg-gradient-to-br from-accent/10 via-accent/5 to-transparent border border-accent/20 rounded-2xl text-center">
<h3 className="text-heading-sm font-medium text-foreground mb-2">
Track .{details.tld} Domains
</h3>
<p className="text-body text-foreground-muted mb-8 max-w-lg mx-auto">
Monitor specific domains and get instant notifications when they become available.
</p>
<Link
href={isAuthenticated ? '/dashboard' : '/register'}
className="inline-flex items-center gap-2 px-8 py-4 bg-foreground text-background text-ui font-medium rounded-xl hover:bg-foreground/90 transition-all"
>
{isAuthenticated ? 'Go to Dashboard' : 'Start Monitoring Free'}
<ChevronRight className="w-4 h-4" />
</Link>
</section>
</div>
</main>
<Footer />
</div>
)
}