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'
|
'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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user