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 */} {/* 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 <div title="Lowest first-year registration price across all tracked registrars">
title="Buy Price (1y)" <StatCard
value={`$${details.pricing.min.toFixed(2)}`} title="Buy Price (1y)"
subtitle={`at ${details.cheapest_registrar}`} value={`$${details.pricing.min.toFixed(2)}`}
icon={DollarSign} subtitle={`at ${details.cheapest_registrar}`}
/> icon={DollarSign}
<StatCard />
title="Renewal (1y)" </div>
value={details.min_renewal_price ? `$${details.min_renewal_price.toFixed(2)}` : '—'} <div title={renewalInfo?.isTrap
subtitle={renewalInfo?.isTrap ? `${renewalInfo.ratio.toFixed(1)}x registration` : 'per year'} ? `Warning: Renewal is ${renewalInfo.ratio.toFixed(1)}x the registration price`
icon={RefreshCw} : 'Annual renewal price after first year'}>
/> <StatCard
<StatCard title="Renewal (1y)"
title="1y Change" value={details.min_renewal_price ? `$${details.min_renewal_price.toFixed(2)}` : '—'}
value={`${details.price_change_1y > 0 ? '+' : ''}${details.price_change_1y.toFixed(0)}%`} subtitle={renewalInfo?.isTrap ? `${renewalInfo.ratio.toFixed(1)}x registration` : 'per year'}
icon={details.price_change_1y > 0 ? TrendingUp : details.price_change_1y < 0 ? TrendingDown : Minus} icon={RefreshCw}
/> />
<StatCard </div>
title="3y Change" <div title="Price change over the last 12 months">
value={`${details.price_change_3y > 0 ? '+' : ''}${details.price_change_3y.toFixed(0)}%`} <StatCard
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}
/>
</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> </div>
{/* Risk Level */} {/* Risk Level */}
@ -506,55 +516,95 @@ 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
<td className="py-4"> const isBestValue = idx === 0 && !hasRenewalTrap
<div className="flex items-center gap-2">
<span className="font-medium text-foreground">{registrar.name}</span> return (
{idx === 0 && ( <tr key={registrar.name} className={clsx(isBestValue && "bg-accent/5")}>
<span className="px-2 py-0.5 text-xs bg-accent/10 text-accent rounded-full">Cheapest</span> <td className="py-4">
)} <div className="flex items-center gap-2">
</div> <span className="font-medium text-foreground">{registrar.name}</span>
</td> {isBestValue && (
<td className="py-4 text-right"> <span
<span className={clsx( className="px-2 py-0.5 text-xs bg-accent/10 text-accent rounded-full cursor-help"
"font-medium tabular-nums", title="Best overall value: lowest registration price without renewal trap"
idx === 0 ? "text-accent" : "text-foreground" >
)}> Best
${registrar.registration_price.toFixed(2)} </span>
</span> )}
</td> {idx === 0 && hasRenewalTrap && (
<td className="py-4 text-right"> <span
<div className="flex items-center gap-1 justify-end"> className="px-2 py-0.5 text-xs bg-amber-500/10 text-amber-400 rounded-full cursor-help"
<span className="text-foreground-muted tabular-nums">${registrar.renewal_price.toFixed(2)}</span> title="Cheapest registration but high renewal costs"
{registrar.renewal_price / registrar.registration_price > 2 && ( >
<AlertTriangle className="w-3.5 h-3.5 text-amber-400" /> Cheap Start
)} </span>
</div> )}
</td> </div>
<td className="py-4 text-right"> </td>
<span className="text-foreground-muted tabular-nums">${registrar.transfer_price.toFixed(2)}</span> <td className="py-4 text-right">
</td> <span
<td className="py-4 text-right"> className={clsx(
<a "font-medium tabular-nums cursor-help",
href={getRegistrarUrl(registrar.name)} isBestValue ? "text-accent" : "text-foreground"
target="_blank" )}
rel="noopener noreferrer" title={`First year: $${registrar.registration_price.toFixed(2)}`}
className="inline-flex items-center gap-1 text-sm text-accent hover:text-accent/80 transition-colors" >
> ${registrar.registration_price.toFixed(2)}
Visit </span>
<ExternalLink className="w-3.5 h-3.5" /> </td>
</a> <td className="py-4 text-right">
</td> <div className="flex items-center gap-1 justify-end">
</tr> <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> </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,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"> <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) => {
<tr key={registrar.name} className={clsx( const hasRenewalTrap = registrar.renewal_price / registrar.registration_price > 1.5
"transition-colors group", const isBestValue = i === 0 && !hasRenewalTrap
i === 0 && "bg-accent/[0.03]"
)}> return (
<td className="px-5 py-4"> <tr key={registrar.name} className={clsx(
<div className="flex items-center gap-2.5"> "transition-colors group",
<span className="text-body-sm font-medium text-foreground">{registrar.name}</span> isBestValue && "bg-accent/[0.03]"
{i === 0 && ( )}>
<span className="text-ui-xs text-accent bg-accent/10 px-2 py-0.5 rounded-full font-medium"> <td className="px-5 py-4">
Best <div className="flex items-center gap-2.5">
</span> <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> <td className="px-5 py-4 text-right hidden sm:table-cell">
<td className="px-5 py-4 text-right"> <span
<span className={clsx( className="text-body-sm text-foreground-muted tabular-nums cursor-help"
"text-body-sm font-medium tabular-nums", title={`Transfer from another registrar: $${registrar.transfer_price.toFixed(2)}`}
i === 0 ? "text-accent" : "text-foreground" >
)}> ${registrar.transfer_price.toFixed(2)}
${registrar.registration_price.toFixed(2)} </span>
</span> </td>
</td> <td className="px-5 py-4">
<td className="px-5 py-4 text-right hidden sm:table-cell"> <a
<span className="text-body-sm text-foreground-muted tabular-nums"> href={getRegistrarUrl(registrar.name, `example.${tld}`)}
${registrar.renewal_price.toFixed(2)} target="_blank"
</span> rel="noopener noreferrer"
{registrar.renewal_price > registrar.registration_price * 1.5 && ( className="flex items-center gap-1.5 text-ui-sm text-foreground-muted hover:text-accent transition-colors opacity-0 group-hover:opacity-100"
<AlertTriangle className="inline-block ml-1.5 w-3.5 h-3.5 text-amber-400" /> title={`Register at ${registrar.name}`}
)} >
</td> Visit
<td className="px-5 py-4 text-right hidden sm:table-cell"> <ExternalLink className="w-3.5 h-3.5" />
<span className="text-body-sm text-foreground-muted tabular-nums"> </a>
${registrar.transfer_price.toFixed(2)} </td>
</span> </tr>
</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>
))}
</tbody> </tbody>
</table> </table>
</div> </div>