refactor: TLD Detail pages - remove alerts, add all table data

REMOVED:
- Alert/Notify button from both pages
- Bell icon and handleToggleAlert functions
- alertEnabled, alertLoading state

ADDED (matching table columns):
- Buy Price (1y) - was already there
- Renewal (1y) with trap warning
- 1y Change with color coding
- 3y Change with color coding
- Risk Assessment with badge (dot + reason text)

COMMAND CENTER (/command/pricing/[tld]):
- StatCards: Buy, Renew, 1y Change, 3y Change
- Risk Assessment section with badge
- Renewal Trap warning when ratio > 2x
- Real chart data from history.history API

PUBLIC (/tld-pricing/[tld]):
- Same stats in Quick Stats grid
- Risk Assessment for authenticated users
- Shimmer placeholders for non-authenticated
- Real chart data from history.history API

Both pages now show ALL info from the overview table:
 TLD name
 Trend (chart + badge)
 Buy (1y)
 Renew (1y) with trap
 1y Change
 3y Change
 Risk Level + Reason
This commit is contained in:
yves.gugger
2025-12-10 15:55:38 +01:00
parent 64785e95ce
commit d8736eac88
2 changed files with 177 additions and 155 deletions

View File

@ -1,7 +1,7 @@
'use client' 'use client'
import { useEffect, useState, useMemo, useRef } from 'react' import { useEffect, useState, useMemo, useRef } from 'react'
import { useParams, useRouter } from 'next/navigation' import { useParams } from 'next/navigation'
import { CommandCenterLayout } from '@/components/CommandCenterLayout' import { CommandCenterLayout } from '@/components/CommandCenterLayout'
import { PageContainer, StatCard } from '@/components/PremiumTable' import { PageContainer, StatCard } from '@/components/PremiumTable'
import { useStore } from '@/lib/store' import { useStore } from '@/lib/store'
@ -15,19 +15,15 @@ import {
Globe, Globe,
Building, Building,
ExternalLink, ExternalLink,
Bell,
Search, Search,
ChevronRight, ChevronRight,
Sparkles,
Check, Check,
X, X,
RefreshCw, RefreshCw,
Clock,
Shield,
Zap,
AlertTriangle, AlertTriangle,
DollarSign, DollarSign,
BarChart3, BarChart3,
Shield,
} from 'lucide-react' } from 'lucide-react'
import Link from 'next/link' import Link from 'next/link'
import clsx from 'clsx' import clsx from 'clsx'
@ -52,6 +48,12 @@ interface TldDetails {
transfer_price: number transfer_price: number
}> }>
cheapest_registrar: string cheapest_registrar: string
// New fields from table
min_renewal_price: number
price_change_1y: number
price_change_3y: number
risk_level: 'low' | 'medium' | 'high'
risk_reason: string
} }
interface TldHistory { interface TldHistory {
@ -90,7 +92,7 @@ const REGISTRAR_URLS: Record<string, string> = {
type ChartPeriod = '1M' | '3M' | '1Y' | 'ALL' type ChartPeriod = '1M' | '3M' | '1Y' | 'ALL'
// Premium Chart Component // Premium Chart Component with real data
function PriceChart({ function PriceChart({
data, data,
chartStats, chartStats,
@ -187,8 +189,7 @@ function PriceChart({
export default function CommandTldDetailPage() { export default function CommandTldDetailPage() {
const params = useParams() const params = useParams()
const router = useRouter() const { fetchSubscription } = useStore()
const { subscription, fetchSubscription } = useStore()
const tld = params.tld as string const tld = params.tld as string
const [details, setDetails] = useState<TldDetails | null>(null) const [details, setDetails] = useState<TldDetails | null>(null)
@ -199,43 +200,20 @@ export default function CommandTldDetailPage() {
const [domainSearch, setDomainSearch] = useState('') const [domainSearch, setDomainSearch] = useState('')
const [checkingDomain, setCheckingDomain] = useState(false) const [checkingDomain, setCheckingDomain] = useState(false)
const [domainResult, setDomainResult] = useState<DomainCheckResult | null>(null) const [domainResult, setDomainResult] = useState<DomainCheckResult | null>(null)
const [alertEnabled, setAlertEnabled] = useState(false)
const [alertLoading, setAlertLoading] = useState(false)
useEffect(() => { useEffect(() => {
fetchSubscription() fetchSubscription()
if (tld) { if (tld) {
loadData() loadData()
loadAlertStatus()
} }
}, [tld, fetchSubscription]) }, [tld, fetchSubscription])
const loadAlertStatus = async () => {
try {
const status = await api.getPriceAlertStatus(tld)
setAlertEnabled(status.has_alert && status.is_active)
} catch (err) {
// Ignore
}
}
const handleToggleAlert = async () => {
setAlertLoading(true)
try {
const result = await api.togglePriceAlert(tld)
setAlertEnabled(result.is_active)
} catch (err) {
console.error('Failed to toggle alert:', err)
} finally {
setAlertLoading(false)
}
}
const loadData = async () => { const loadData = async () => {
try { try {
const [historyData, compareData] = await Promise.all([ const [historyData, compareData, overviewData] = await Promise.all([
api.getTldHistory(tld, 365), api.getTldHistory(tld, 365),
api.getTldCompare(tld), api.getTldCompare(tld),
api.getTldOverview(1, 0, 'popularity', tld), // Get the specific TLD data
]) ])
if (historyData && compareData) { if (historyData && compareData) {
@ -243,6 +221,9 @@ export default function CommandTldDetailPage() {
a.registration_price - b.registration_price a.registration_price - b.registration_price
) )
// Get additional data from overview API (1y, 3y change, risk)
const tldFromOverview = overviewData?.tlds?.[0]
setDetails({ setDetails({
tld: compareData.tld || tld, tld: compareData.tld || tld,
type: compareData.type || 'generic', type: compareData.type || 'generic',
@ -258,6 +239,12 @@ export default function CommandTldDetailPage() {
}, },
registrars: sortedRegistrars, registrars: sortedRegistrars,
cheapest_registrar: compareData.cheapest_registrar || sortedRegistrars[0]?.name || 'N/A', cheapest_registrar: compareData.cheapest_registrar || sortedRegistrars[0]?.name || 'N/A',
// New fields from overview
min_renewal_price: tldFromOverview?.min_renewal_price || sortedRegistrars[0]?.renewal_price || 0,
price_change_1y: tldFromOverview?.price_change_1y || 0,
price_change_3y: tldFromOverview?.price_change_3y || 0,
risk_level: tldFromOverview?.risk_level || 'low',
risk_reason: tldFromOverview?.risk_reason || 'Stable',
}) })
setHistory(historyData) setHistory(historyData)
} else { } else {
@ -344,12 +331,27 @@ export default function CommandTldDetailPage() {
const renewalInfo = getRenewalInfo() const renewalInfo = getRenewalInfo()
const getTrendIcon = (trend: string) => { // Risk badge component
switch (trend) { const getRiskBadge = () => {
case 'up': return <TrendingUp className="w-4 h-4" /> if (!details) return null
case 'down': return <TrendingDown className="w-4 h-4" /> const level = details.risk_level
default: return <Minus className="w-4 h-4" /> const reason = details.risk_reason
} return (
<span className={clsx(
"inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-sm font-medium",
level === 'high' && "bg-red-500/10 text-red-400",
level === 'medium' && "bg-amber-500/10 text-amber-400",
level === 'low' && "bg-accent/10 text-accent"
)}>
<span className={clsx(
"w-2.5 h-2.5 rounded-full",
level === 'high' && "bg-red-400",
level === 'medium' && "bg-amber-400",
level === 'low' && "bg-accent"
)} />
{reason}
</span>
)
} }
if (loading) { if (loading) {
@ -391,21 +393,6 @@ export default function CommandTldDetailPage() {
<CommandCenterLayout <CommandCenterLayout
title={`.${details.tld}`} title={`.${details.tld}`}
subtitle={details.description} subtitle={details.description}
actions={
<button
onClick={handleToggleAlert}
disabled={alertLoading}
className={clsx(
"flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all",
alertEnabled
? "bg-accent/10 text-accent border border-accent/30"
: "bg-foreground/5 text-foreground-muted hover:text-foreground hover:bg-foreground/10"
)}
>
<Bell className={clsx("w-4 h-4", alertEnabled && "fill-current")} />
{alertEnabled ? 'Alert On' : 'Set Alert'}
</button>
}
> >
<PageContainer> <PageContainer>
{/* Breadcrumb */} {/* Breadcrumb */}
@ -417,34 +404,42 @@ export default function CommandTldDetailPage() {
<span className="text-foreground font-medium">.{details.tld}</span> <span className="text-foreground font-medium">.{details.tld}</span>
</nav> </nav>
{/* Stats Grid */} {/* Stats Grid - All info from table */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard <StatCard
title="Best Price" title="Buy Price (1y)"
value={`$${details.pricing.min.toFixed(2)}`} value={`$${details.pricing.min.toFixed(2)}`}
subtitle={`at ${details.cheapest_registrar}`} subtitle={`at ${details.cheapest_registrar}`}
icon={DollarSign} icon={DollarSign}
accent
/> />
<StatCard <StatCard
title="Renewal" title="Renewal (1y)"
value={renewalInfo ? `$${renewalInfo.renewal.toFixed(2)}` : '—'} value={details.min_renewal_price ? `$${details.min_renewal_price.toFixed(2)}` : '—'}
subtitle={renewalInfo?.isTrap ? `⚠️ ${renewalInfo.ratio.toFixed(1)}x registration` : 'per year'} subtitle={renewalInfo?.isTrap ? `${renewalInfo.ratio.toFixed(1)}x registration` : 'per year'}
icon={RefreshCw} icon={RefreshCw}
/> />
<StatCard <StatCard
title="7d Change" title="1y Change"
value={history ? `${history.price_change_7d > 0 ? '+' : ''}${history.price_change_7d.toFixed(1)}%` : '—'} value={`${details.price_change_1y > 0 ? '+' : ''}${details.price_change_1y.toFixed(0)}%`}
icon={BarChart3} icon={details.price_change_1y > 0 ? TrendingUp : details.price_change_1y < 0 ? TrendingDown : Minus}
/> />
<StatCard <StatCard
title="Trend" title="3y Change"
value={details.trend === 'up' ? 'Rising' : details.trend === 'down' ? 'Falling' : 'Stable'} value={`${details.price_change_3y > 0 ? '+' : ''}${details.price_change_3y.toFixed(0)}%`}
subtitle={details.trend_reason} icon={BarChart3}
icon={details.trend === 'up' ? TrendingUp : details.trend === 'down' ? TrendingDown : Minus}
/> />
</div> </div>
{/* Risk Level */}
<div className="flex items-center gap-4 p-4 bg-background-secondary/30 border border-border/50 rounded-xl">
<Shield className="w-5 h-5 text-foreground-muted" />
<div className="flex-1">
<p className="text-sm font-medium text-foreground">Risk Assessment</p>
<p className="text-xs text-foreground-muted">Based on renewal ratio, price volatility, and market trends</p>
</div>
{getRiskBadge()}
</div>
{/* Renewal Trap Warning */} {/* Renewal Trap Warning */}
{renewalInfo?.isTrap && ( {renewalInfo?.isTrap && (
<div className="p-4 bg-amber-500/10 border border-amber-500/20 rounded-xl flex items-start gap-3"> <div className="p-4 bg-amber-500/10 border border-amber-500/20 rounded-xl flex items-start gap-3">
@ -675,4 +670,3 @@ export default function CommandTldDetailPage() {
</CommandCenterLayout> </CommandCenterLayout>
) )
} }

View File

@ -1,7 +1,7 @@
'use client' 'use client'
import { useEffect, useState, useMemo, useRef } from 'react' import { useEffect, useState, useMemo, useRef } from 'react'
import { useParams, useRouter } from 'next/navigation' import { useParams } from 'next/navigation'
import { Header } from '@/components/Header' import { Header } from '@/components/Header'
import { Footer } from '@/components/Footer' import { Footer } from '@/components/Footer'
import { useStore } from '@/lib/store' import { useStore } from '@/lib/store'
@ -15,10 +15,8 @@ import {
Globe, Globe,
Building, Building,
ExternalLink, ExternalLink,
Bell,
Search, Search,
ChevronRight, ChevronRight,
Sparkles,
Check, Check,
X, X,
Lock, Lock,
@ -26,6 +24,7 @@ import {
Clock, Clock,
Shield, Shield,
Zap, Zap,
AlertTriangle,
} from 'lucide-react' } from 'lucide-react'
import Link from 'next/link' import Link from 'next/link'
import clsx from 'clsx' import clsx from 'clsx'
@ -50,6 +49,12 @@ interface TldDetails {
transfer_price: number transfer_price: number
}> }>
cheapest_registrar: string cheapest_registrar: string
// New fields from table
min_renewal_price: number
price_change_1y: number
price_change_3y: number
risk_level: 'low' | 'medium' | 'high'
risk_reason: string
} }
interface TldHistory { interface TldHistory {
@ -79,8 +84,7 @@ interface DomainCheckResult {
expiration_date?: string | null expiration_date?: string | null
} }
// Registrar URLs with affiliate parameters // Registrar URLs
// Note: Replace REF_CODE with actual affiliate IDs when available
const REGISTRAR_URLS: Record<string, string> = { const REGISTRAR_URLS: Record<string, string> = {
'Namecheap': 'https://www.namecheap.com/domains/registration/results/?domain=', 'Namecheap': 'https://www.namecheap.com/domains/registration/results/?domain=',
'Porkbun': 'https://porkbun.com/checkout/search?q=', 'Porkbun': 'https://porkbun.com/checkout/search?q=',
@ -120,7 +124,7 @@ function Shimmer({ className }: { className?: string }) {
) )
} }
// Premium Chart Component // Premium Chart Component with real data
function PriceChart({ function PriceChart({
data, data,
isAuthenticated, isAuthenticated,
@ -294,7 +298,7 @@ function PriceChart({
) )
} }
// Domain Check Result Card (like landing page) // Domain Check Result Card
function DomainResultCard({ function DomainResultCard({
result, result,
tld, tld,
@ -390,7 +394,6 @@ function DomainResultCard({
export default function TldDetailPage() { export default function TldDetailPage() {
const params = useParams() const params = useParams()
const router = useRouter()
const { isAuthenticated, checkAuth, isLoading: authLoading, subscription, fetchSubscription } = useStore() const { isAuthenticated, checkAuth, isLoading: authLoading, subscription, fetchSubscription } = useStore()
const tld = params.tld as string const tld = params.tld as string
@ -406,8 +409,6 @@ export default function TldDetailPage() {
const [domainSearch, setDomainSearch] = useState('') const [domainSearch, setDomainSearch] = useState('')
const [checkingDomain, setCheckingDomain] = useState(false) const [checkingDomain, setCheckingDomain] = useState(false)
const [domainResult, setDomainResult] = useState<DomainCheckResult | null>(null) const [domainResult, setDomainResult] = useState<DomainCheckResult | null>(null)
const [alertEnabled, setAlertEnabled] = useState(false)
const [alertLoading, setAlertLoading] = useState(false)
useEffect(() => { useEffect(() => {
checkAuth() checkAuth()
@ -418,53 +419,25 @@ export default function TldDetailPage() {
if (tld) { if (tld) {
loadData() loadData()
loadRelatedTlds() loadRelatedTlds()
loadAlertStatus()
} }
}, [tld]) }, [tld])
// Load alert status for this TLD
const loadAlertStatus = async () => {
try {
const status = await api.getPriceAlertStatus(tld)
setAlertEnabled(status.has_alert && status.is_active)
} catch (err) {
// Ignore - user may not be logged in
}
}
// Toggle price alert
const handleToggleAlert = async () => {
if (!isAuthenticated) {
// Redirect to login
window.location.href = `/login?redirect=/tld-pricing/${tld}`
return
}
setAlertLoading(true)
try {
const result = await api.togglePriceAlert(tld)
setAlertEnabled(result.is_active)
} catch (err: any) {
console.error('Failed to toggle alert:', err)
} finally {
setAlertLoading(false)
}
}
const loadData = async () => { const loadData = async () => {
try { try {
const [historyData, compareData] = await Promise.all([ const [historyData, compareData, overviewData] = await Promise.all([
api.getTldHistory(tld, 365), api.getTldHistory(tld, 365),
api.getTldCompare(tld), api.getTldCompare(tld),
api.getTldOverview(1, 0, 'popularity', tld),
]) ])
if (historyData && compareData) { if (historyData && compareData) {
// Sort registrars by price for display
const sortedRegistrars = [...(compareData.registrars || [])].sort((a, b) => const sortedRegistrars = [...(compareData.registrars || [])].sort((a, b) =>
a.registration_price - b.registration_price a.registration_price - b.registration_price
) )
// Use API data directly for consistency with overview table // Get additional data from overview API
const tldFromOverview = overviewData?.tlds?.[0]
setDetails({ setDetails({
tld: compareData.tld || tld, tld: compareData.tld || tld,
type: compareData.type || 'generic', type: compareData.type || 'generic',
@ -474,13 +447,18 @@ export default function TldDetailPage() {
trend: historyData.trend || 'stable', trend: historyData.trend || 'stable',
trend_reason: historyData.trend_reason || 'Price tracking available', trend_reason: historyData.trend_reason || 'Price tracking available',
pricing: { pricing: {
// Use price_range from API for consistency with overview
avg: compareData.price_range?.avg || historyData.current_price || 0, avg: compareData.price_range?.avg || historyData.current_price || 0,
min: compareData.price_range?.min || historyData.current_price || 0, min: compareData.price_range?.min || historyData.current_price || 0,
max: compareData.price_range?.max || historyData.current_price || 0, max: compareData.price_range?.max || historyData.current_price || 0,
}, },
registrars: sortedRegistrars, registrars: sortedRegistrars,
cheapest_registrar: compareData.cheapest_registrar || sortedRegistrars[0]?.name || 'N/A', cheapest_registrar: compareData.cheapest_registrar || sortedRegistrars[0]?.name || 'N/A',
// New fields from overview
min_renewal_price: tldFromOverview?.min_renewal_price || sortedRegistrars[0]?.renewal_price || 0,
price_change_1y: tldFromOverview?.price_change_1y || 0,
price_change_3y: tldFromOverview?.price_change_3y || 0,
risk_level: tldFromOverview?.risk_level || 'low',
risk_reason: tldFromOverview?.risk_reason || 'Stable',
}) })
setHistory(historyData) setHistory(historyData)
} else { } else {
@ -580,6 +558,42 @@ export default function TldDetailPage() {
} }
}, [details]) }, [details])
// Renewal trap info
const renewalInfo = useMemo(() => {
if (!details?.registrars?.length) return null
const cheapest = details.registrars[0]
const ratio = cheapest.renewal_price / cheapest.registration_price
return {
registration: cheapest.registration_price,
renewal: cheapest.renewal_price,
ratio,
isTrap: ratio > 2,
}
}, [details])
// Risk badge component
const getRiskBadge = () => {
if (!details) return null
const level = details.risk_level
const reason = details.risk_reason
return (
<span className={clsx(
"inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-sm font-medium",
level === 'high' && "bg-red-500/10 text-red-400",
level === 'medium' && "bg-amber-500/10 text-amber-400",
level === 'low' && "bg-accent/10 text-accent"
)}>
<span className={clsx(
"w-2.5 h-2.5 rounded-full",
level === 'high' && "bg-red-400",
level === 'medium' && "bg-amber-400",
level === 'low' && "bg-accent"
)} />
{reason}
</span>
)
}
const getTrendIcon = (trend: string) => { const getTrendIcon = (trend: string) => {
switch (trend) { switch (trend) {
case 'up': return <TrendingUp className="w-4 h-4" /> case 'up': return <TrendingUp className="w-4 h-4" />
@ -674,50 +688,73 @@ export default function TldDetailPage() {
<p className="text-body-lg text-foreground-muted mb-2">{details.description}</p> <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> <p className="text-body-sm text-foreground-subtle">{details.trend_reason}</p>
{/* Quick Stats - Only for authenticated */} {/* Quick Stats - All data from table */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mt-8"> <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"> <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> <p className="text-ui-xs text-foreground-subtle uppercase tracking-wider mb-1">Buy (1y)</p>
{isAuthenticated ? ( {isAuthenticated ? (
<p className="text-body-lg font-medium text-foreground tabular-nums">${details.pricing.avg.toFixed(2)}</p> <p className="text-body-lg font-medium text-foreground tabular-nums">${details.pricing.min.toFixed(2)}</p>
) : ( ) : (
<Shimmer className="h-6 w-16 mt-1" /> <Shimmer className="h-6 w-16 mt-1" />
)} )}
</div> </div>
<div className="p-4 bg-background-secondary/50 border border-border/50 rounded-xl"> <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> <p className="text-ui-xs text-foreground-subtle uppercase tracking-wider mb-1">Renew (1y)</p>
{isAuthenticated ? ( {isAuthenticated ? (
<p className="text-body-lg font-medium text-foreground tabular-nums"> <div className="flex items-center gap-1">
${details.pricing.min.toFixed(0)}${details.pricing.max.toFixed(0)} <p className="text-body-lg font-medium text-foreground tabular-nums">
</p> ${details.min_renewal_price.toFixed(2)}
</p>
{renewalInfo?.isTrap && (
<AlertTriangle className="w-4 h-4 text-amber-400" />
)}
</div>
) : ( ) : (
<Shimmer className="h-6 w-20 mt-1" /> <Shimmer className="h-6 w-20 mt-1" />
)} )}
</div> </div>
<div className="p-4 bg-background-secondary/50 border border-border/50 rounded-xl"> <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> <p className="text-ui-xs text-foreground-subtle uppercase tracking-wider mb-1">1y Change</p>
{isAuthenticated && history ? ( {isAuthenticated ? (
<p className={clsx( <p className={clsx(
"text-body-lg font-medium tabular-nums", "text-body-lg font-medium tabular-nums",
history.price_change_30d > 0 ? "text-orange-400" : details.price_change_1y > 0 ? "text-orange-400" :
history.price_change_30d < 0 ? "text-accent" : details.price_change_1y < 0 ? "text-accent" :
"text-foreground" "text-foreground"
)}> )}>
{history.price_change_30d > 0 ? '+' : ''}{history.price_change_30d.toFixed(1)}% {details.price_change_1y > 0 ? '+' : ''}{details.price_change_1y.toFixed(0)}%
</p> </p>
) : ( ) : (
<Shimmer className="h-6 w-14 mt-1" /> <Shimmer className="h-6 w-14 mt-1" />
)} )}
</div> </div>
<div className="p-4 bg-background-secondary/50 border border-border/50 rounded-xl"> <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> <p className="text-ui-xs text-foreground-subtle uppercase tracking-wider mb-1">3y Change</p>
{isAuthenticated ? ( {isAuthenticated ? (
<p className="text-body-lg font-medium text-foreground">{details.registrars.length}</p> <p className={clsx(
"text-body-lg font-medium tabular-nums",
details.price_change_3y > 0 ? "text-orange-400" :
details.price_change_3y < 0 ? "text-accent" :
"text-foreground"
)}>
{details.price_change_3y > 0 ? '+' : ''}{details.price_change_3y.toFixed(0)}%
</p>
) : ( ) : (
<Shimmer className="h-6 w-8 mt-1" /> <Shimmer className="h-6 w-14 mt-1" />
)} )}
</div> </div>
</div> </div>
{/* Risk Assessment */}
{isAuthenticated && (
<div className="flex items-center gap-4 mt-4 p-4 bg-background-secondary/30 border border-border/50 rounded-xl">
<Shield className="w-5 h-5 text-foreground-muted" />
<div className="flex-1">
<p className="text-sm font-medium text-foreground">Risk Assessment</p>
</div>
{getRiskBadge()}
</div>
)}
</div> </div>
{/* Right: Price Card */} {/* Right: Price Card */}
@ -745,35 +782,12 @@ export default function TldDetailPage() {
Register Domain Register Domain
<ExternalLink className="w-4 h-4" /> <ExternalLink className="w-4 h-4" />
</a> </a>
<button
onClick={handleToggleAlert}
disabled={alertLoading}
className={clsx(
"flex items-center justify-center gap-2 w-full py-3.5 font-medium rounded-xl transition-all disabled:opacity-50",
alertEnabled
? "bg-accent/10 text-accent border border-accent/30"
: "bg-background border border-border text-foreground hover:bg-background-secondary"
)}
>
{alertLoading ? (
<RefreshCw className="w-4 h-4 animate-spin" />
) : (
<Bell className={clsx("w-4 h-4", alertEnabled && "fill-current")} />
)}
{alertLoading
? 'Updating...'
: alertEnabled
? 'Price Alert Active'
: 'Enable Price Alert'
}
</button>
</div> </div>
{savings && savings.amount > 0.5 && ( {savings && savings.amount > 0.5 && (
<div className="mt-5 pt-5 border-t border-border/50"> <div className="mt-5 pt-5 border-t border-border/50">
<div className="flex items-start gap-2.5"> <div className="flex items-start gap-2.5">
<Sparkles className="w-4 h-4 text-accent mt-0.5 shrink-0" /> <Zap className="w-4 h-4 text-accent mt-0.5 shrink-0" />
<p className="text-ui-sm text-foreground-muted leading-relaxed"> <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} Save <span className="text-accent font-medium">${savings.amount.toFixed(2)}</span>/yr vs {savings.expensiveName}
</p> </p>
@ -798,6 +812,20 @@ export default function TldDetailPage() {
</div> </div>
</div> </div>
{/* Renewal Trap Warning */}
{isAuthenticated && renewalInfo?.isTrap && (
<div className="mb-8 p-4 bg-amber-500/10 border border-amber-500/20 rounded-xl flex items-start gap-3">
<AlertTriangle className="w-5 h-5 text-amber-400 shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-amber-400">Renewal Trap Detected</p>
<p className="text-sm text-foreground-muted mt-1">
The renewal price (${renewalInfo.renewal.toFixed(2)}) is {renewalInfo.ratio.toFixed(1)}x higher than the registration price (${renewalInfo.registration.toFixed(2)}).
Consider the total cost of ownership before registering.
</p>
</div>
</div>
)}
{/* Price Chart */} {/* Price Chart */}
<section className="mb-12"> <section className="mb-12">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
@ -982,7 +1010,7 @@ export default function TldDetailPage() {
${registrar.renewal_price.toFixed(2)} ${registrar.renewal_price.toFixed(2)}
</span> </span>
{registrar.renewal_price > registrar.registration_price * 1.5 && ( {registrar.renewal_price > registrar.registration_price * 1.5 && (
<span className="ml-1.5 text-orange-400" title="High renewal"></span> <AlertTriangle className="inline-block ml-1.5 w-3.5 h-3.5 text-amber-400" />
)} )}
</td> </td>
<td className="px-5 py-4 text-right hidden sm:table-cell"> <td className="px-5 py-4 text-right hidden sm:table-cell">
@ -1093,10 +1121,10 @@ export default function TldDetailPage() {
Monitor specific domains and get instant notifications when they become available. Monitor specific domains and get instant notifications when they become available.
</p> </p>
<Link <Link
href={isAuthenticated ? '/dashboard' : '/register'} href={isAuthenticated ? '/command' : '/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" 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'} {isAuthenticated ? 'Go to Command Center' : 'Start Monitoring Free'}
<ChevronRight className="w-4 h-4" /> <ChevronRight className="w-4 h-4" />
</Link> </Link>
</section> </section>