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:
@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState, useMemo, useRef } from 'react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { CommandCenterLayout } from '@/components/CommandCenterLayout'
|
||||
import { PageContainer, StatCard } from '@/components/PremiumTable'
|
||||
import { useStore } from '@/lib/store'
|
||||
@ -15,19 +15,15 @@ import {
|
||||
Globe,
|
||||
Building,
|
||||
ExternalLink,
|
||||
Bell,
|
||||
Search,
|
||||
ChevronRight,
|
||||
Sparkles,
|
||||
Check,
|
||||
X,
|
||||
RefreshCw,
|
||||
Clock,
|
||||
Shield,
|
||||
Zap,
|
||||
AlertTriangle,
|
||||
DollarSign,
|
||||
BarChart3,
|
||||
Shield,
|
||||
} from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import clsx from 'clsx'
|
||||
@ -52,6 +48,12 @@ interface TldDetails {
|
||||
transfer_price: number
|
||||
}>
|
||||
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 {
|
||||
@ -90,7 +92,7 @@ const REGISTRAR_URLS: Record<string, string> = {
|
||||
|
||||
type ChartPeriod = '1M' | '3M' | '1Y' | 'ALL'
|
||||
|
||||
// Premium Chart Component
|
||||
// Premium Chart Component with real data
|
||||
function PriceChart({
|
||||
data,
|
||||
chartStats,
|
||||
@ -187,8 +189,7 @@ function PriceChart({
|
||||
|
||||
export default function CommandTldDetailPage() {
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
const { subscription, fetchSubscription } = useStore()
|
||||
const { fetchSubscription } = useStore()
|
||||
const tld = params.tld as string
|
||||
|
||||
const [details, setDetails] = useState<TldDetails | null>(null)
|
||||
@ -199,43 +200,20 @@ export default function CommandTldDetailPage() {
|
||||
const [domainSearch, setDomainSearch] = useState('')
|
||||
const [checkingDomain, setCheckingDomain] = useState(false)
|
||||
const [domainResult, setDomainResult] = useState<DomainCheckResult | null>(null)
|
||||
const [alertEnabled, setAlertEnabled] = useState(false)
|
||||
const [alertLoading, setAlertLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetchSubscription()
|
||||
if (tld) {
|
||||
loadData()
|
||||
loadAlertStatus()
|
||||
}
|
||||
}, [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 () => {
|
||||
try {
|
||||
const [historyData, compareData] = await Promise.all([
|
||||
const [historyData, compareData, overviewData] = await Promise.all([
|
||||
api.getTldHistory(tld, 365),
|
||||
api.getTldCompare(tld),
|
||||
api.getTldOverview(1, 0, 'popularity', tld), // Get the specific TLD data
|
||||
])
|
||||
|
||||
if (historyData && compareData) {
|
||||
@ -243,6 +221,9 @@ export default function CommandTldDetailPage() {
|
||||
a.registration_price - b.registration_price
|
||||
)
|
||||
|
||||
// Get additional data from overview API (1y, 3y change, risk)
|
||||
const tldFromOverview = overviewData?.tlds?.[0]
|
||||
|
||||
setDetails({
|
||||
tld: compareData.tld || tld,
|
||||
type: compareData.type || 'generic',
|
||||
@ -258,6 +239,12 @@ export default function CommandTldDetailPage() {
|
||||
},
|
||||
registrars: sortedRegistrars,
|
||||
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)
|
||||
} else {
|
||||
@ -344,12 +331,27 @@ export default function CommandTldDetailPage() {
|
||||
|
||||
const renewalInfo = getRenewalInfo()
|
||||
|
||||
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" />
|
||||
}
|
||||
// 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>
|
||||
)
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
@ -391,21 +393,6 @@ export default function CommandTldDetailPage() {
|
||||
<CommandCenterLayout
|
||||
title={`.${details.tld}`}
|
||||
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>
|
||||
{/* Breadcrumb */}
|
||||
@ -417,34 +404,42 @@ export default function CommandTldDetailPage() {
|
||||
<span className="text-foreground font-medium">.{details.tld}</span>
|
||||
</nav>
|
||||
|
||||
{/* Stats Grid */}
|
||||
{/* Stats Grid - All info from table */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<StatCard
|
||||
title="Best Price"
|
||||
title="Buy Price (1y)"
|
||||
value={`$${details.pricing.min.toFixed(2)}`}
|
||||
subtitle={`at ${details.cheapest_registrar}`}
|
||||
icon={DollarSign}
|
||||
accent
|
||||
/>
|
||||
<StatCard
|
||||
title="Renewal"
|
||||
value={renewalInfo ? `$${renewalInfo.renewal.toFixed(2)}` : '—'}
|
||||
subtitle={renewalInfo?.isTrap ? `⚠️ ${renewalInfo.ratio.toFixed(1)}x registration` : 'per year'}
|
||||
title="Renewal (1y)"
|
||||
value={details.min_renewal_price ? `$${details.min_renewal_price.toFixed(2)}` : '—'}
|
||||
subtitle={renewalInfo?.isTrap ? `${renewalInfo.ratio.toFixed(1)}x registration` : 'per year'}
|
||||
icon={RefreshCw}
|
||||
/>
|
||||
<StatCard
|
||||
title="7d Change"
|
||||
value={history ? `${history.price_change_7d > 0 ? '+' : ''}${history.price_change_7d.toFixed(1)}%` : '—'}
|
||||
icon={BarChart3}
|
||||
title="1y Change"
|
||||
value={`${details.price_change_1y > 0 ? '+' : ''}${details.price_change_1y.toFixed(0)}%`}
|
||||
icon={details.price_change_1y > 0 ? TrendingUp : details.price_change_1y < 0 ? TrendingDown : Minus}
|
||||
/>
|
||||
<StatCard
|
||||
title="Trend"
|
||||
value={details.trend === 'up' ? 'Rising' : details.trend === 'down' ? 'Falling' : 'Stable'}
|
||||
subtitle={details.trend_reason}
|
||||
icon={details.trend === 'up' ? TrendingUp : details.trend === 'down' ? TrendingDown : Minus}
|
||||
title="3y Change"
|
||||
value={`${details.price_change_3y > 0 ? '+' : ''}${details.price_change_3y.toFixed(0)}%`}
|
||||
icon={BarChart3}
|
||||
/>
|
||||
</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 */}
|
||||
{renewalInfo?.isTrap && (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState, useMemo, useRef } from 'react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Header } from '@/components/Header'
|
||||
import { Footer } from '@/components/Footer'
|
||||
import { useStore } from '@/lib/store'
|
||||
@ -15,10 +15,8 @@ import {
|
||||
Globe,
|
||||
Building,
|
||||
ExternalLink,
|
||||
Bell,
|
||||
Search,
|
||||
ChevronRight,
|
||||
Sparkles,
|
||||
Check,
|
||||
X,
|
||||
Lock,
|
||||
@ -26,6 +24,7 @@ import {
|
||||
Clock,
|
||||
Shield,
|
||||
Zap,
|
||||
AlertTriangle,
|
||||
} from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import clsx from 'clsx'
|
||||
@ -50,6 +49,12 @@ interface TldDetails {
|
||||
transfer_price: number
|
||||
}>
|
||||
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 {
|
||||
@ -79,8 +84,7 @@ interface DomainCheckResult {
|
||||
expiration_date?: string | null
|
||||
}
|
||||
|
||||
// Registrar URLs with affiliate parameters
|
||||
// Note: Replace REF_CODE with actual affiliate IDs when available
|
||||
// Registrar URLs
|
||||
const REGISTRAR_URLS: Record<string, string> = {
|
||||
'Namecheap': 'https://www.namecheap.com/domains/registration/results/?domain=',
|
||||
'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({
|
||||
data,
|
||||
isAuthenticated,
|
||||
@ -294,7 +298,7 @@ function PriceChart({
|
||||
)
|
||||
}
|
||||
|
||||
// Domain Check Result Card (like landing page)
|
||||
// Domain Check Result Card
|
||||
function DomainResultCard({
|
||||
result,
|
||||
tld,
|
||||
@ -390,7 +394,6 @@ function DomainResultCard({
|
||||
|
||||
export default function TldDetailPage() {
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
const { isAuthenticated, checkAuth, isLoading: authLoading, subscription, fetchSubscription } = useStore()
|
||||
const tld = params.tld as string
|
||||
|
||||
@ -406,8 +409,6 @@ export default function TldDetailPage() {
|
||||
const [domainSearch, setDomainSearch] = useState('')
|
||||
const [checkingDomain, setCheckingDomain] = useState(false)
|
||||
const [domainResult, setDomainResult] = useState<DomainCheckResult | null>(null)
|
||||
const [alertEnabled, setAlertEnabled] = useState(false)
|
||||
const [alertLoading, setAlertLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
checkAuth()
|
||||
@ -418,53 +419,25 @@ export default function TldDetailPage() {
|
||||
if (tld) {
|
||||
loadData()
|
||||
loadRelatedTlds()
|
||||
loadAlertStatus()
|
||||
}
|
||||
}, [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 () => {
|
||||
try {
|
||||
const [historyData, compareData] = await Promise.all([
|
||||
const [historyData, compareData, overviewData] = await Promise.all([
|
||||
api.getTldHistory(tld, 365),
|
||||
api.getTldCompare(tld),
|
||||
api.getTldOverview(1, 0, 'popularity', tld),
|
||||
])
|
||||
|
||||
if (historyData && compareData) {
|
||||
// Sort registrars by price for display
|
||||
const sortedRegistrars = [...(compareData.registrars || [])].sort((a, b) =>
|
||||
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({
|
||||
tld: compareData.tld || tld,
|
||||
type: compareData.type || 'generic',
|
||||
@ -474,13 +447,18 @@ export default function TldDetailPage() {
|
||||
trend: historyData.trend || 'stable',
|
||||
trend_reason: historyData.trend_reason || 'Price tracking available',
|
||||
pricing: {
|
||||
// Use price_range from API for consistency with overview
|
||||
avg: compareData.price_range?.avg || historyData.current_price || 0,
|
||||
min: compareData.price_range?.min || historyData.current_price || 0,
|
||||
max: compareData.price_range?.max || historyData.current_price || 0,
|
||||
},
|
||||
registrars: sortedRegistrars,
|
||||
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)
|
||||
} else {
|
||||
@ -580,6 +558,42 @@ export default function TldDetailPage() {
|
||||
}
|
||||
}, [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) => {
|
||||
switch (trend) {
|
||||
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-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="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 ? (
|
||||
<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" />
|
||||
)}
|
||||
</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>
|
||||
<p className="text-ui-xs text-foreground-subtle uppercase tracking-wider mb-1">Renew (1y)</p>
|
||||
{isAuthenticated ? (
|
||||
<p className="text-body-lg font-medium text-foreground tabular-nums">
|
||||
${details.pricing.min.toFixed(0)}–${details.pricing.max.toFixed(0)}
|
||||
</p>
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="text-body-lg font-medium text-foreground tabular-nums">
|
||||
${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" />
|
||||
)}
|
||||
</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="text-ui-xs text-foreground-subtle uppercase tracking-wider mb-1">1y Change</p>
|
||||
{isAuthenticated ? (
|
||||
<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" :
|
||||
details.price_change_1y > 0 ? "text-orange-400" :
|
||||
details.price_change_1y < 0 ? "text-accent" :
|
||||
"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>
|
||||
) : (
|
||||
<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>
|
||||
<p className="text-ui-xs text-foreground-subtle uppercase tracking-wider mb-1">3y Change</p>
|
||||
{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>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* Right: Price Card */}
|
||||
@ -745,35 +782,12 @@ export default function TldDetailPage() {
|
||||
Register Domain
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</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>
|
||||
|
||||
{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" />
|
||||
<Zap 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>
|
||||
@ -798,6 +812,20 @@ export default function TldDetailPage() {
|
||||
</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 */}
|
||||
<section className="mb-12">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
@ -982,7 +1010,7 @@ export default function TldDetailPage() {
|
||||
${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>
|
||||
<AlertTriangle className="inline-block ml-1.5 w-3.5 h-3.5 text-amber-400" />
|
||||
)}
|
||||
</td>
|
||||
<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.
|
||||
</p>
|
||||
<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"
|
||||
>
|
||||
{isAuthenticated ? 'Go to Dashboard' : 'Start Monitoring Free'}
|
||||
{isAuthenticated ? 'Go to Command Center' : 'Start Monitoring Free'}
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Link>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user