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,29 +406,39 @@ export default function CommandTldDetailPage() {
{/* Stats Grid - All info from table */} {/* 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">
<div title="Lowest first-year registration price across all tracked registrars">
<StatCard <StatCard
title="Buy Price (1y)" 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}
/> />
</div>
<div title={renewalInfo?.isTrap
? `Warning: Renewal is ${renewalInfo.ratio.toFixed(1)}x the registration price`
: 'Annual renewal price after first year'}>
<StatCard <StatCard
title="Renewal (1y)" title="Renewal (1y)"
value={details.min_renewal_price ? `$${details.min_renewal_price.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}
/> />
</div>
<div title="Price change over the last 12 months">
<StatCard <StatCard
title="1y Change" title="1y Change"
value={`${details.price_change_1y > 0 ? '+' : ''}${details.price_change_1y.toFixed(0)}%`} 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} 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 <StatCard
title="3y Change" title="3y Change"
value={`${details.price_change_3y > 0 ? '+' : ''}${details.price_change_3y.toFixed(0)}%`} value={`${details.price_change_3y > 0 ? '+' : ''}${details.price_change_3y.toFixed(0)}%`}
icon={BarChart3} icon={BarChart3}
/> />
</div> </div>
</div>
{/* Risk Level */} {/* Risk Level */}
<div className="flex items-center gap-4 p-4 bg-background-secondary/30 border border-border/50 rounded-xl"> <div className="flex items-center gap-4 p-4 bg-background-secondary/30 border border-border/50 rounded-xl">
@ -506,41 +516,79 @@ export default function CommandTldDetailPage() {
<thead> <thead>
<tr className="border-b border-border/30"> <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-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" title="First year registration price">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" title="Annual renewal price">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="Transfer from another registrar">Transfer</th>
<th className="text-right pb-3"></th> <th className="text-right pb-3"></th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-border/20"> <tbody className="divide-y divide-border/20">
{details.registrars.map((registrar, idx) => ( {details.registrars.map((registrar, idx) => {
<tr key={registrar.name} className={clsx(idx === 0 && "bg-accent/5")}> 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"> <td className="py-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="font-medium text-foreground">{registrar.name}</span> <span className="font-medium text-foreground">{registrar.name}</span>
{idx === 0 && ( {isBestValue && (
<span className="px-2 py-0.5 text-xs bg-accent/10 text-accent rounded-full">Cheapest</span> <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> </div>
</td> </td>
<td className="py-4 text-right"> <td className="py-4 text-right">
<span className={clsx( <span
"font-medium tabular-nums", className={clsx(
idx === 0 ? "text-accent" : "text-foreground" "font-medium tabular-nums cursor-help",
)}> isBestValue ? "text-accent" : "text-foreground"
)}
title={`First year: $${registrar.registration_price.toFixed(2)}`}
>
${registrar.registration_price.toFixed(2)} ${registrar.registration_price.toFixed(2)}
</span> </span>
</td> </td>
<td className="py-4 text-right"> <td className="py-4 text-right">
<div className="flex items-center gap-1 justify-end"> <div className="flex items-center gap-1 justify-end">
<span className="text-foreground-muted tabular-nums">${registrar.renewal_price.toFixed(2)}</span> <span
{registrar.renewal_price / registrar.registration_price > 2 && ( className={clsx(
<AlertTriangle className="w-3.5 h-3.5 text-amber-400" /> "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> </div>
</td> </td>
<td className="py-4 text-right"> <td className="py-4 text-right">
<span className="text-foreground-muted tabular-nums">${registrar.transfer_price.toFixed(2)}</span> <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>
<td className="py-4 text-right"> <td className="py-4 text-right">
<a <a
@ -548,13 +596,15 @@ export default function CommandTldDetailPage() {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-sm text-accent hover:text-accent/80 transition-colors" className="inline-flex items-center gap-1 text-sm text-accent hover:text-accent/80 transition-colors"
title={`Register at ${registrar.name}`}
> >
Visit Visit
<ExternalLink className="w-3.5 h-3.5" /> <ExternalLink className="w-3.5 h-3.5" />
</a> </a>
</td> </td>
</tr> </tr>
))} )
})}
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -690,7 +690,10 @@ export default function TldDetailPage() {
{/* Quick Stats - All data from table */} {/* 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 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> <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.min.toFixed(2)}</p> <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" /> <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 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> <p className="text-ui-xs text-foreground-subtle uppercase tracking-wider mb-1">Renew (1y)</p>
{isAuthenticated ? ( {isAuthenticated ? (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
@ -706,14 +714,17 @@ export default function TldDetailPage() {
${details.min_renewal_price.toFixed(2)} ${details.min_renewal_price.toFixed(2)}
</p> </p>
{renewalInfo?.isTrap && ( {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> </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 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> <p className="text-ui-xs text-foreground-subtle uppercase tracking-wider mb-1">1y Change</p>
{isAuthenticated ? ( {isAuthenticated ? (
<p className={clsx( <p className={clsx(
@ -728,7 +739,10 @@ export default function TldDetailPage() {
<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 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> <p className="text-ui-xs text-foreground-subtle uppercase tracking-wider mb-1">3y Change</p>
{isAuthenticated ? ( {isAuthenticated ? (
<p className={clsx( <p className={clsx(
@ -969,52 +983,84 @@ export default function TldDetailPage() {
<th className="text-left text-ui-xs text-foreground-subtle font-medium uppercase tracking-wider px-5 py-4"> <th className="text-left text-ui-xs text-foreground-subtle font-medium uppercase tracking-wider px-5 py-4">
Registrar Registrar
</th> </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 Register
</th> </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 Renew
</th> </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 Transfer
</th> </th>
<th className="px-5 py-4 w-24"></th> <th className="px-5 py-4 w-24"></th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-border/30"> <tbody className="divide-y divide-border/30">
{details.registrars.map((registrar, i) => ( {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( <tr key={registrar.name} className={clsx(
"transition-colors group", "transition-colors group",
i === 0 && "bg-accent/[0.03]" isBestValue && "bg-accent/[0.03]"
)}> )}>
<td className="px-5 py-4"> <td className="px-5 py-4">
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<span className="text-body-sm font-medium text-foreground">{registrar.name}</span> <span className="text-body-sm font-medium text-foreground">{registrar.name}</span>
{i === 0 && ( {isBestValue && (
<span className="text-ui-xs text-accent bg-accent/10 px-2 py-0.5 rounded-full font-medium"> <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 Best
</span> </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> </div>
</td> </td>
<td className="px-5 py-4 text-right"> <td className="px-5 py-4 text-right">
<span className={clsx( <span
"text-body-sm font-medium tabular-nums", className={clsx(
i === 0 ? "text-accent" : "text-foreground" "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)} ${registrar.registration_price.toFixed(2)}
</span> </span>
</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">
<span className="text-body-sm text-foreground-muted tabular-nums"> <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)} ${registrar.renewal_price.toFixed(2)}
</span> </span>
{registrar.renewal_price > registrar.registration_price * 1.5 && ( {hasRenewalTrap && (
<AlertTriangle className="inline-block ml-1.5 w-3.5 h-3.5 text-amber-400" /> <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`}
/>
)} )}
</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">
<span className="text-body-sm text-foreground-muted tabular-nums"> <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)} ${registrar.transfer_price.toFixed(2)}
</span> </span>
</td> </td>
@ -1024,13 +1070,15 @@ export default function TldDetailPage() {
target="_blank" target="_blank"
rel="noopener noreferrer" 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" 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 Visit
<ExternalLink className="w-3.5 h-3.5" /> <ExternalLink className="w-3.5 h-3.5" />
</a> </a>
</td> </td>
</tr> </tr>
))} )
})}
</tbody> </tbody>
</table> </table>
</div> </div>