fix: Smart 'Best' label + tooltips for TLD detail pages

BEST VALUE LOGIC:
- 'Best' badge only shown when:
  1. Cheapest registration price AND
  2. No renewal trap (renewal <= 1.5x registration)
- New 'Cheap Start' badge for cheapest with renewal trap
  Shows warning: 'Cheapest registration but high renewal costs'

TOOLTIPS ADDED:

Stats Cards:
- Buy (1y): 'Lowest first-year registration price...'
- Renew (1y): 'Annual renewal price after first year'
  or 'Warning: Renewal is Xx the registration price'
- 1y Change: 'Price change over the last 12 months'
- 3y Change: 'Price change over the last 3 years'

Registrar Table Headers:
- Register: 'First year registration price'
- Renew: 'Annual renewal price'
- Transfer: 'Transfer from another registrar'

Registrar Table Cells:
- Registration price: 'First year: $X.XX'
- Renewal price: 'Annual renewal: $X.XX' or trap warning
- Transfer price: 'Transfer from another registrar: $X.XX'
- AlertTriangle icon: 'Renewal trap: Xx registration price'
- Best badge: 'Best overall value: lowest registration...'
- Cheap Start badge: 'Cheapest registration but high renewal...'
- Visit link: 'Register at {registrar}'

Applied to both:
- /command/pricing/[tld] (Command Center)
- /tld-pricing/[tld] (Public)
This commit is contained in:
yves.gugger
2025-12-10 16:00:34 +01:00
parent d8736eac88
commit e0b53dd7fe
2 changed files with 221 additions and 123 deletions

View File

@ -406,28 +406,38 @@ export default function CommandTldDetailPage() {
{/* Stats Grid - All info from table */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard
title="Buy Price (1y)"
value={`$${details.pricing.min.toFixed(2)}`}
subtitle={`at ${details.cheapest_registrar}`}
icon={DollarSign}
/>
<StatCard
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="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="3y Change"
value={`${details.price_change_3y > 0 ? '+' : ''}${details.price_change_3y.toFixed(0)}%`}
icon={BarChart3}
/>
<div title="Lowest first-year registration price across all tracked registrars">
<StatCard
title="Buy Price (1y)"
value={`$${details.pricing.min.toFixed(2)}`}
subtitle={`at ${details.cheapest_registrar}`}
icon={DollarSign}
/>
</div>
<div title={renewalInfo?.isTrap
? `Warning: Renewal is ${renewalInfo.ratio.toFixed(1)}x the registration price`
: 'Annual renewal price after first year'}>
<StatCard
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}
/>
</div>
<div title="Price change over the last 12 months">
<StatCard
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}
/>
</div>
<div title="Price change over the last 3 years">
<StatCard
title="3y Change"
value={`${details.price_change_3y > 0 ? '+' : ''}${details.price_change_3y.toFixed(0)}%`}
icon={BarChart3}
/>
</div>
</div>
{/* Risk Level */}
@ -506,55 +516,95 @@ export default function CommandTldDetailPage() {
<thead>
<tr className="border-b border-border/30">
<th className="text-left pb-3 text-sm font-medium text-foreground-muted">Registrar</th>
<th className="text-right pb-3 text-sm font-medium text-foreground-muted">Register</th>
<th className="text-right pb-3 text-sm font-medium text-foreground-muted">Renew</th>
<th className="text-right pb-3 text-sm font-medium text-foreground-muted">Transfer</th>
<th className="text-right pb-3 text-sm font-medium text-foreground-muted" title="First year registration price">Register</th>
<th className="text-right pb-3 text-sm font-medium text-foreground-muted" title="Annual renewal price">Renew</th>
<th className="text-right pb-3 text-sm font-medium text-foreground-muted" title="Transfer from another registrar">Transfer</th>
<th className="text-right pb-3"></th>
</tr>
</thead>
<tbody className="divide-y divide-border/20">
{details.registrars.map((registrar, idx) => (
<tr key={registrar.name} className={clsx(idx === 0 && "bg-accent/5")}>
<td className="py-4">
<div className="flex items-center gap-2">
<span className="font-medium text-foreground">{registrar.name}</span>
{idx === 0 && (
<span className="px-2 py-0.5 text-xs bg-accent/10 text-accent rounded-full">Cheapest</span>
)}
</div>
</td>
<td className="py-4 text-right">
<span className={clsx(
"font-medium tabular-nums",
idx === 0 ? "text-accent" : "text-foreground"
)}>
${registrar.registration_price.toFixed(2)}
</span>
</td>
<td className="py-4 text-right">
<div className="flex items-center gap-1 justify-end">
<span className="text-foreground-muted tabular-nums">${registrar.renewal_price.toFixed(2)}</span>
{registrar.renewal_price / registrar.registration_price > 2 && (
<AlertTriangle className="w-3.5 h-3.5 text-amber-400" />
)}
</div>
</td>
<td className="py-4 text-right">
<span className="text-foreground-muted tabular-nums">${registrar.transfer_price.toFixed(2)}</span>
</td>
<td className="py-4 text-right">
<a
href={getRegistrarUrl(registrar.name)}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-sm text-accent hover:text-accent/80 transition-colors"
>
Visit
<ExternalLink className="w-3.5 h-3.5" />
</a>
</td>
</tr>
))}
{details.registrars.map((registrar, idx) => {
const hasRenewalTrap = registrar.renewal_price / registrar.registration_price > 1.5
const isBestValue = idx === 0 && !hasRenewalTrap
return (
<tr key={registrar.name} className={clsx(isBestValue && "bg-accent/5")}>
<td className="py-4">
<div className="flex items-center gap-2">
<span className="font-medium text-foreground">{registrar.name}</span>
{isBestValue && (
<span
className="px-2 py-0.5 text-xs bg-accent/10 text-accent rounded-full cursor-help"
title="Best overall value: lowest registration price without renewal trap"
>
Best
</span>
)}
{idx === 0 && hasRenewalTrap && (
<span
className="px-2 py-0.5 text-xs bg-amber-500/10 text-amber-400 rounded-full cursor-help"
title="Cheapest registration but high renewal costs"
>
Cheap Start
</span>
)}
</div>
</td>
<td className="py-4 text-right">
<span
className={clsx(
"font-medium tabular-nums cursor-help",
isBestValue ? "text-accent" : "text-foreground"
)}
title={`First year: $${registrar.registration_price.toFixed(2)}`}
>
${registrar.registration_price.toFixed(2)}
</span>
</td>
<td className="py-4 text-right">
<div className="flex items-center gap-1 justify-end">
<span
className={clsx(
"tabular-nums cursor-help",
hasRenewalTrap ? "text-amber-400" : "text-foreground-muted"
)}
title={hasRenewalTrap
? `Renewal is ${(registrar.renewal_price / registrar.registration_price).toFixed(1)}x the registration price`
: `Annual renewal: $${registrar.renewal_price.toFixed(2)}`}
>
${registrar.renewal_price.toFixed(2)}
</span>
{hasRenewalTrap && (
<AlertTriangle
className="w-3.5 h-3.5 text-amber-400 cursor-help"
title={`Renewal trap: ${(registrar.renewal_price / registrar.registration_price).toFixed(1)}x registration price`}
/>
)}
</div>
</td>
<td className="py-4 text-right">
<span
className="text-foreground-muted tabular-nums cursor-help"
title={`Transfer from another registrar: $${registrar.transfer_price.toFixed(2)}`}
>
${registrar.transfer_price.toFixed(2)}
</span>
</td>
<td className="py-4 text-right">
<a
href={getRegistrarUrl(registrar.name)}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-sm text-accent hover:text-accent/80 transition-colors"
title={`Register at ${registrar.name}`}
>
Visit
<ExternalLink className="w-3.5 h-3.5" />
</a>
</td>
</tr>
)
})}
</tbody>
</table>
</div>

View File

@ -690,7 +690,10 @@ export default function TldDetailPage() {
{/* 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">
<div
className="p-4 bg-background-secondary/50 border border-border/50 rounded-xl cursor-help"
title="Lowest first-year registration price across all tracked registrars"
>
<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.min.toFixed(2)}</p>
@ -698,7 +701,12 @@ export default function TldDetailPage() {
<Shimmer className="h-6 w-16 mt-1" />
)}
</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 cursor-help"
title={renewalInfo?.isTrap
? `Warning: Renewal is ${renewalInfo.ratio.toFixed(1)}x the registration price`
: 'Annual renewal price after first year'}
>
<p className="text-ui-xs text-foreground-subtle uppercase tracking-wider mb-1">Renew (1y)</p>
{isAuthenticated ? (
<div className="flex items-center gap-1">
@ -706,14 +714,17 @@ export default function TldDetailPage() {
${details.min_renewal_price.toFixed(2)}
</p>
{renewalInfo?.isTrap && (
<AlertTriangle className="w-4 h-4 text-amber-400" />
<AlertTriangle className="w-4 h-4 text-amber-400" title={`Renewal trap: ${renewalInfo.ratio.toFixed(1)}x registration`} />
)}
</div>
) : (
<Shimmer className="h-6 w-20 mt-1" />
)}
</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 cursor-help"
title="Price change over the last 12 months"
>
<p className="text-ui-xs text-foreground-subtle uppercase tracking-wider mb-1">1y Change</p>
{isAuthenticated ? (
<p className={clsx(
@ -728,7 +739,10 @@ export default function TldDetailPage() {
<Shimmer className="h-6 w-14 mt-1" />
)}
</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 cursor-help"
title="Price change over the last 3 years"
>
<p className="text-ui-xs text-foreground-subtle uppercase tracking-wider mb-1">3y Change</p>
{isAuthenticated ? (
<p className={clsx(
@ -969,68 +983,102 @@ export default function TldDetailPage() {
<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">
<th className="text-right text-ui-xs text-foreground-subtle font-medium uppercase tracking-wider px-5 py-4 cursor-help" title="First year registration price">
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">
<th className="text-right text-ui-xs text-foreground-subtle font-medium uppercase tracking-wider px-5 py-4 hidden sm:table-cell cursor-help" title="Annual renewal price">
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">
<th className="text-right text-ui-xs text-foreground-subtle font-medium uppercase tracking-wider px-5 py-4 hidden sm:table-cell cursor-help" title="Transfer from another registrar">
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>
{details.registrars.map((registrar, i) => {
const hasRenewalTrap = registrar.renewal_price / registrar.registration_price > 1.5
const isBestValue = i === 0 && !hasRenewalTrap
return (
<tr key={registrar.name} className={clsx(
"transition-colors group",
isBestValue && "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>
{isBestValue && (
<span
className="text-ui-xs text-accent bg-accent/10 px-2 py-0.5 rounded-full font-medium cursor-help"
title="Best overall value: lowest registration price without renewal trap"
>
Best
</span>
)}
{i === 0 && hasRenewalTrap && (
<span
className="text-ui-xs text-amber-400 bg-amber-500/10 px-2 py-0.5 rounded-full font-medium cursor-help"
title="Cheapest registration but high renewal costs"
>
Cheap Start
</span>
)}
</div>
</td>
<td className="px-5 py-4 text-right">
<span
className={clsx(
"text-body-sm font-medium tabular-nums cursor-help",
isBestValue ? "text-accent" : "text-foreground"
)}
title={`First year: $${registrar.registration_price.toFixed(2)}`}
>
${registrar.registration_price.toFixed(2)}
</span>
</td>
<td className="px-5 py-4 text-right hidden sm:table-cell">
<span
className={clsx(
"text-body-sm tabular-nums cursor-help",
hasRenewalTrap ? "text-amber-400" : "text-foreground-muted"
)}
title={hasRenewalTrap
? `Renewal is ${(registrar.renewal_price / registrar.registration_price).toFixed(1)}x the registration price`
: `Annual renewal: $${registrar.renewal_price.toFixed(2)}`}
>
${registrar.renewal_price.toFixed(2)}
</span>
{hasRenewalTrap && (
<AlertTriangle
className="inline-block ml-1.5 w-3.5 h-3.5 text-amber-400 cursor-help"
title={`Renewal trap: ${(registrar.renewal_price / registrar.registration_price).toFixed(1)}x registration price`}
/>
)}
</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 && (
<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">
<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>
))}
</td>
<td className="px-5 py-4 text-right hidden sm:table-cell">
<span
className="text-body-sm text-foreground-muted tabular-nums cursor-help"
title={`Transfer from another registrar: $${registrar.transfer_price.toFixed(2)}`}
>
${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"
title={`Register at ${registrar.name}`}
>
Visit
<ExternalLink className="w-3.5 h-3.5" />
</a>
</td>
</tr>
)
})}
</tbody>
</table>
</div>