fix: Yield slider fixed, pricing features aligned with docs
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled

This commit is contained in:
2025-12-13 18:16:17 +01:00
parent e3250baaf7
commit ddeb25446e
3 changed files with 99 additions and 82 deletions

View File

@ -22,10 +22,11 @@ const tiers = [
{ text: 'Market Overview', highlight: false, available: true }, { text: 'Market Overview', highlight: false, available: true },
{ text: 'Basic Search', highlight: false, available: true }, { text: 'Basic Search', highlight: false, available: true },
{ text: '5 Watchlist Domains', highlight: false, available: true }, { text: '5 Watchlist Domains', highlight: false, available: true },
{ text: 'Market Feed', highlight: false, available: true, sublabel: '🌪️ Raw' }, { text: 'Market Feed', highlight: false, available: true, sublabel: 'Raw' },
{ text: 'Alert Speed', highlight: false, available: true, sublabel: '🐢 Daily' }, { text: 'Alert Speed', highlight: false, available: true, sublabel: 'Daily' },
{ text: 'Pounce Score', highlight: false, available: false }, { text: 'Pounce Score', highlight: false, available: false },
{ text: '2 Sniper Alerts', highlight: false, available: true }, { text: '2 Sniper Alerts', highlight: false, available: true },
{ text: 'Marketplace', highlight: false, available: true, sublabel: 'Buy Only' },
], ],
cta: 'Start Free', cta: 'Start Free',
highlighted: false, highlighted: false,
@ -41,13 +42,13 @@ const tiers = [
description: 'The smart investor\'s choice.', description: 'The smart investor\'s choice.',
features: [ features: [
{ text: '50 Watchlist Domains', highlight: true, available: true }, { text: '50 Watchlist Domains', highlight: true, available: true },
{ text: 'Market Feed', highlight: true, available: true, sublabel: 'Curated' }, { text: 'Market Feed', highlight: true, available: true, sublabel: 'Curated' },
{ text: 'Alert Speed', highlight: true, available: true, sublabel: '🐇 Hourly' }, { text: 'Alert Speed', highlight: true, available: true, sublabel: 'Hourly' },
{ text: 'Renewal Price Intel', highlight: true, available: true }, { text: 'TLD Intel', highlight: true, available: true, sublabel: 'Renewal Prices' },
{ text: 'Pounce Score', highlight: true, available: true }, { text: 'Pounce Score', highlight: true, available: true },
{ text: '5 Listings', highlight: true, available: true, sublabel: '0% Fee' }, { text: '5 Listings', highlight: true, available: true, sublabel: '0% Fee' },
{ text: '10 Sniper Alerts', highlight: true, available: true }, { text: '10 Sniper Alerts', highlight: true, available: true },
{ text: 'Portfolio (25 domains)', highlight: true, available: true }, { text: 'Portfolio', highlight: true, available: true, sublabel: '25 Domains' },
], ],
cta: 'Upgrade to Trader', cta: 'Upgrade to Trader',
highlighted: true, highlighted: true,
@ -63,13 +64,13 @@ const tiers = [
description: 'For serious domain investors.', description: 'For serious domain investors.',
features: [ features: [
{ text: '500 Watchlist Domains', highlight: true, available: true }, { text: '500 Watchlist Domains', highlight: true, available: true },
{ text: 'Market Feed', highlight: true, available: true, sublabel: 'Priority' }, { text: 'Market Feed', highlight: true, available: true, sublabel: 'Priority' },
{ text: 'Alert Speed', highlight: true, available: true, sublabel: '⚡ 10 min' }, { text: 'Alert Speed', highlight: true, available: true, sublabel: 'Real-Time' },
{ text: 'Unlimited Portfolio', highlight: true, available: true }, { text: 'TLD Intel', highlight: true, available: true, sublabel: 'Full History' },
{ text: 'Score + SEO Data', highlight: true, available: true }, { text: 'Score + SEO Data', highlight: true, available: true },
{ text: '50 Listings', highlight: true, available: true, sublabel: 'Featured' }, { text: '50 Listings', highlight: true, available: true, sublabel: 'Featured' },
{ text: '50 Sniper Alerts', highlight: true, available: true }, { text: '50 Sniper Alerts', highlight: true, available: true },
{ text: 'Full Price History', highlight: true, available: true }, { text: 'Unlimited Portfolio', highlight: true, available: true },
], ],
cta: 'Go Tycoon', cta: 'Go Tycoon',
highlighted: false, highlighted: false,
@ -79,10 +80,10 @@ const tiers = [
] ]
const comparisonFeatures = [ const comparisonFeatures = [
{ name: 'Market Feed', scout: 'Raw', trader: 'Curated', tycoon: 'Priority' }, { name: 'Market Feed', scout: 'Raw (Unfiltered)', trader: 'Curated (Spam-Free)', tycoon: 'Curated + Priority' },
{ name: 'Alert Speed', scout: 'Daily', trader: 'Hourly', tycoon: '10 min' }, { name: 'Alert Speed', scout: 'Daily', trader: 'Hourly', tycoon: 'Real-Time (10 min)' },
{ name: 'Watchlist', scout: '5 Domains', trader: '50 Domains', tycoon: '500 Domains' }, { name: 'Watchlist', scout: '5 Domains', trader: '50 Domains', tycoon: '500 Domains' },
{ name: 'Listings', scout: '', trader: '5 (0% Fee)', tycoon: '50 Featured' }, { name: 'Marketplace', scout: 'Buy Only', trader: 'Sell (0% Fee)', tycoon: 'Sell + Featured' },
{ name: 'TLD Intel', scout: 'Public Trends', trader: 'Renewal Prices', tycoon: 'Full History' }, { name: 'TLD Intel', scout: 'Public Trends', trader: 'Renewal Prices', tycoon: 'Full History' },
{ name: 'Valuation', scout: 'Locked', trader: 'Pounce Score', tycoon: 'Score + SEO' }, { name: 'Valuation', scout: 'Locked', trader: 'Pounce Score', tycoon: 'Score + SEO' },
{ name: 'Sniper Alerts', scout: '2', trader: '10', tycoon: '50' }, { name: 'Sniper Alerts', scout: '2', trader: '10', tycoon: '50' },

View File

@ -298,15 +298,8 @@ export default function PortfolioPage() {
{/* ADD DOMAIN + FILTERS */} {/* ADD DOMAIN + FILTERS */}
<section className="px-4 lg:px-10 py-4 border-b border-white/[0.08]"> <section className="px-4 lg:px-10 py-4 border-b border-white/[0.08]">
<div className="flex items-center justify-between gap-4 mb-4"> <div className="flex items-center justify-between gap-4">
<button {/* Filters - LEFT */}
onClick={() => setShowAddModal(true)}
className="flex items-center gap-2 px-4 py-2.5 bg-accent text-black text-xs font-bold uppercase tracking-wider hover:bg-white transition-colors"
>
<Plus className="w-4 h-4" />Add Domain
</button>
{/* Filters */}
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{[ {[
{ value: 'all', label: 'All', count: stats.total }, { value: 'all', label: 'All', count: stats.total },
@ -327,6 +320,14 @@ export default function PortfolioPage() {
</button> </button>
))} ))}
</div> </div>
{/* Add Domain Button - RIGHT */}
<button
onClick={() => setShowAddModal(true)}
className="flex items-center gap-2 px-4 py-2.5 bg-accent text-black text-xs font-bold uppercase tracking-wider hover:bg-white transition-colors"
>
<Plus className="w-4 h-4" />Add Domain
</button>
</div> </div>
</section> </section>
@ -351,7 +352,7 @@ export default function PortfolioPage() {
) : ( ) : (
<div className="space-y-px bg-white/[0.04] border border-white/[0.08]"> <div className="space-y-px bg-white/[0.04] border border-white/[0.08]">
{/* Desktop Table Header */} {/* Desktop Table Header */}
<div className="hidden lg:grid grid-cols-[1fr_100px_100px_100px_80px_100px] gap-4 px-3 py-2 text-[10px] font-mono text-white/40 uppercase tracking-wider border-b border-white/[0.08]"> <div className="hidden lg:grid grid-cols-[1fr_90px_90px_80px_70px_160px] gap-4 px-4 py-2.5 text-[10px] font-mono text-white/40 uppercase tracking-wider border-b border-white/[0.08]">
<button onClick={() => handleSort('domain')} className="flex items-center gap-1 hover:text-white/60 text-left"> <button onClick={() => handleSort('domain')} className="flex items-center gap-1 hover:text-white/60 text-left">
Domain Domain
{sortField === 'domain' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)} {sortField === 'domain' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
@ -366,7 +367,7 @@ export default function PortfolioPage() {
{sortField === 'roi' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)} {sortField === 'roi' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button> </button>
<button onClick={() => handleSort('renewal')} className="flex items-center gap-1 justify-center hover:text-white/60"> <button onClick={() => handleSort('renewal')} className="flex items-center gap-1 justify-center hover:text-white/60">
Renewal Expires
{sortField === 'renewal' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)} {sortField === 'renewal' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button> </button>
<div className="text-right">Actions</div> <div className="text-right">Actions</div>
@ -442,44 +443,47 @@ export default function PortfolioPage() {
</div> </div>
{/* Desktop Row */} {/* Desktop Row */}
<div className="hidden lg:grid grid-cols-[1fr_100px_100px_100px_80px_100px] gap-4 items-center p-3"> <div className="hidden lg:grid grid-cols-[1fr_90px_90px_80px_70px_160px] gap-4 items-center px-4 py-3 hover:bg-white/[0.02] transition-colors">
{/* Domain Info */}
<div className="flex items-center gap-3 min-w-0 flex-1"> <div className="flex items-center gap-3 min-w-0 flex-1">
<div className={clsx( <div className={clsx(
"w-8 h-8 flex items-center justify-center border shrink-0", "w-9 h-9 flex items-center justify-center border shrink-0",
domain.is_sold ? "bg-white/[0.02] border-white/[0.06]" : domain.is_sold ? "bg-white/[0.02] border-white/[0.06]" :
domain.is_dns_verified ? "bg-accent/10 border-accent/20" : "bg-blue-400/10 border-blue-400/20" domain.is_dns_verified ? "bg-accent/10 border-accent/20" : "bg-blue-400/10 border-blue-400/20"
)}> )}>
{domain.is_sold ? <CheckCircle className="w-4 h-4 text-white/30" /> : {domain.is_sold ? <CheckCircle className="w-4 h-4 text-white/30" /> :
domain.is_dns_verified ? <ShieldCheck className="w-4 h-4 text-accent" /> : <ShieldAlert className="w-4 h-4 text-blue-400" />} domain.is_dns_verified ? <ShieldCheck className="w-4 h-4 text-accent" /> : <ShieldAlert className="w-4 h-4 text-blue-400" />}
</div> </div>
<div className="min-w-0"> <div className="min-w-0 flex-1">
<div className="text-sm font-bold text-white font-mono truncate group-hover:text-accent transition-colors">{domain.domain}</div> <div className="flex items-center gap-2">
<div className="flex items-center gap-2 text-[10px] font-mono text-white/30"> <span className="text-sm font-bold text-white font-mono truncate">{domain.domain}</span>
<span>{domain.registrar || 'Unknown'}</span> <a href={`https://${domain.domain}`} target="_blank" rel="noopener noreferrer" className="opacity-0 group-hover:opacity-40 hover:!opacity-100 transition-opacity">
{domain.is_sold && <span className="px-1 py-0.5 bg-white/5 text-white/40">SOLD</span>} <ExternalLink className="w-3 h-3 text-white" />
{!domain.is_sold && domain.is_dns_verified && <span className="px-1 py-0.5 bg-accent/10 text-accent">VERIFIED</span>} </a>
{!domain.is_sold && !domain.is_dns_verified && <span className="px-1 py-0.5 bg-blue-400/10 text-blue-400">UNVERIFIED</span>} </div>
<div className="flex items-center gap-1.5 mt-0.5">
<span className="text-[10px] font-mono text-white/30">{domain.registrar || '—'}</span>
{domain.is_sold && <span className="text-[9px] font-mono px-1.5 py-0.5 bg-white/5 text-white/40 uppercase">Sold</span>}
{!domain.is_sold && domain.is_dns_verified && <span className="text-[9px] font-mono px-1.5 py-0.5 bg-accent/10 text-accent uppercase">Verified</span>}
{!domain.is_sold && !domain.is_dns_verified && <span className="text-[9px] font-mono px-1.5 py-0.5 bg-blue-400/10 text-blue-400 uppercase">Unverified</span>}
</div> </div>
</div> </div>
<a href={`https://${domain.domain}`} target="_blank" className="opacity-0 group-hover:opacity-50 hover:!opacity-100 transition-opacity ml-2">
<ExternalLink className="w-3.5 h-3.5 text-white/40" />
</a>
</div> </div>
{/* Purchase */} {/* Purchase Price */}
<div className="text-right text-xs font-mono text-white/50"> <div className="text-right text-xs font-mono text-white/40 tabular-nums">
{formatCurrency(domain.purchase_price)} {formatCurrency(domain.purchase_price)}
</div> </div>
{/* Value */} {/* Estimated Value */}
<div className="text-right text-sm font-bold font-mono text-accent"> <div className="text-right text-sm font-bold font-mono text-accent tabular-nums">
{formatCurrency(domain.estimated_value)} {formatCurrency(domain.estimated_value)}
</div> </div>
{/* ROI */} {/* ROI Badge */}
<div className="text-center"> <div className="flex justify-center">
<span className={clsx( <span className={clsx(
"text-xs font-mono font-bold px-2 py-0.5 flex items-center justify-center gap-1", "inline-flex items-center gap-0.5 text-[11px] font-mono font-bold px-1.5 py-0.5 tabular-nums",
roiPositive ? "text-accent bg-accent/10" : "text-rose-400 bg-rose-400/10" roiPositive ? "text-accent bg-accent/10" : "text-rose-400 bg-rose-400/10"
)}> )}>
{roiPositive ? <TrendingUp className="w-3 h-3" /> : <TrendingDown className="w-3 h-3" />} {roiPositive ? <TrendingUp className="w-3 h-3" /> : <TrendingDown className="w-3 h-3" />}
@ -487,26 +491,26 @@ export default function PortfolioPage() {
</span> </span>
</div> </div>
{/* Renewal */} {/* Renewal/Expiry */}
<div className="text-center text-xs font-mono"> <div className="text-center text-xs font-mono tabular-nums">
{domain.is_sold ? ( {domain.is_sold ? (
<span className="text-white/30"></span> <span className="text-white/20"></span>
) : isRenewingSoon ? ( ) : isRenewingSoon ? (
<span className="text-orange-400 font-bold">{daysUntilRenewal}d</span> <span className="text-orange-400 font-bold">{daysUntilRenewal}d</span>
) : ( ) : (
<span className="text-white/50">{formatDate(domain.renewal_date)}</span> <span className="text-white/40">{formatDate(domain.renewal_date)}</span>
)} )}
</div> </div>
{/* Actions */} {/* Actions - Better organized */}
<div className="flex items-center gap-1 justify-end opacity-50 group-hover:opacity-100 transition-opacity"> <div className="flex items-center gap-1.5 justify-end">
{/* Verification Status & Actions */} {/* Primary Action - Verify or Sell */}
{!domain.is_sold && ( {!domain.is_sold && (
domain.is_dns_verified ? ( domain.is_dns_verified ? (
canListForSale && ( canListForSale && (
<Link <Link
href={`/terminal/listing?domain=${encodeURIComponent(domain.domain)}`} href={`/terminal/listing?domain=${encodeURIComponent(domain.domain)}`}
className="h-7 px-2 flex items-center gap-1 text-amber-400 text-[9px] font-bold uppercase border border-amber-400/20 bg-amber-400/10 hover:bg-amber-400/20 transition-all" className="h-7 px-2.5 flex items-center gap-1.5 text-amber-400 text-[10px] font-bold uppercase tracking-wide border border-amber-400/30 bg-amber-400/10 hover:bg-amber-400/20 transition-all"
> >
<Tag className="w-3 h-3" />Sell <Tag className="w-3 h-3" />Sell
</Link> </Link>
@ -514,32 +518,39 @@ export default function PortfolioPage() {
) : ( ) : (
<button <button
onClick={() => setVerifyingDomain(domain)} onClick={() => setVerifyingDomain(domain)}
className="h-7 px-2 flex items-center gap-1 text-blue-400 text-[9px] font-bold uppercase border border-blue-400/20 bg-blue-400/10 hover:bg-blue-400/20 transition-all" className="h-7 px-2.5 flex items-center gap-1.5 text-blue-400 text-[10px] font-bold uppercase tracking-wide border border-blue-400/30 bg-blue-400/10 hover:bg-blue-400/20 transition-all"
> >
<ShieldAlert className="w-3 h-3" />Verify <ShieldAlert className="w-3 h-3" />Verify
</button> </button>
) )
)} )}
<button
onClick={() => setSelectedDomain(domain)} {/* Secondary Actions - Icon Buttons */}
className="w-7 h-7 flex items-center justify-center text-white/20 hover:text-white border border-white/10 hover:bg-white/5 transition-all" <div className="flex items-center gap-0.5 ml-1">
> <button
<Edit3 className="w-3.5 h-3.5" /> onClick={() => setSelectedDomain(domain)}
</button> title="Edit Details"
<button className="w-7 h-7 flex items-center justify-center text-white/30 hover:text-white border border-transparent hover:border-white/10 hover:bg-white/5 transition-all rounded-sm"
onClick={() => handleRefreshValue(domain.id)} >
disabled={refreshingId === domain.id} <Edit3 className="w-3.5 h-3.5" />
className="w-7 h-7 flex items-center justify-center text-white/20 hover:text-white border border-white/10 hover:bg-white/5 transition-all" </button>
> <button
<RefreshCw className={clsx("w-3.5 h-3.5", refreshingId === domain.id && "animate-spin")} /> onClick={() => handleRefreshValue(domain.id)}
</button> disabled={refreshingId === domain.id}
<button title="Refresh Valuation"
onClick={() => handleDelete(domain.id, domain.domain)} className="w-7 h-7 flex items-center justify-center text-white/30 hover:text-accent border border-transparent hover:border-accent/20 hover:bg-accent/5 transition-all rounded-sm disabled:opacity-30"
disabled={deletingId === domain.id} >
className="w-7 h-7 flex items-center justify-center text-white/20 hover:text-rose-400 border border-white/10 hover:border-rose-400/20 hover:bg-rose-500/10 transition-all" <RefreshCw className={clsx("w-3.5 h-3.5", refreshingId === domain.id && "animate-spin text-accent")} />
> </button>
{deletingId === domain.id ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Trash2 className="w-3.5 h-3.5" />} <button
</button> onClick={() => handleDelete(domain.id, domain.domain)}
disabled={deletingId === domain.id}
title="Delete Domain"
className="w-7 h-7 flex items-center justify-center text-white/30 hover:text-rose-400 border border-transparent hover:border-rose-400/20 hover:bg-rose-500/10 transition-all rounded-sm disabled:opacity-30"
>
{deletingId === domain.id ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Trash2 className="w-3.5 h-3.5" />}
</button>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -78,7 +78,7 @@ function YieldSimulator() {
<span className="text-white/60">Monthly Traffic</span> <span className="text-white/60">Monthly Traffic</span>
<span className="text-white">{traffic.toLocaleString()} Visits</span> <span className="text-white">{traffic.toLocaleString()} Visits</span>
</div> </div>
<div className="relative h-4 flex items-center"> <div className="relative h-6 flex items-center">
<input <input
type="range" type="range"
min="100" min="100"
@ -86,17 +86,22 @@ function YieldSimulator() {
step="100" step="100"
value={traffic} value={traffic}
onChange={(e) => setTraffic(parseInt(e.target.value))} onChange={(e) => setTraffic(parseInt(e.target.value))}
className="absolute w-full h-1 bg-white/10 rounded-full appearance-none cursor-pointer z-20 className="w-full h-2 bg-white/10 appearance-none cursor-pointer
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:h-4
[&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-accent [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-white
[&::-webkit-slider-thumb]:shadow-[0_0_10px_rgba(255,255,255,0.8)]" [&::-webkit-slider-thumb]:cursor-pointer
/> [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:h-4
<div className="absolute top-1/2 left-0 right-0 h-px bg-white/10 z-10" /> [&::-moz-range-thumb]:bg-accent [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-white
<div [&::-moz-range-thumb]:cursor-pointer [&::-moz-range-thumb]:rounded-none"
className="absolute top-1/2 left-0 h-px bg-white/50 z-10" style={{
style={{ width: `${((traffic - 100) / (10000 - 100)) * 100}%` }} background: `linear-gradient(to right, rgb(16 185 129) 0%, rgb(16 185 129) ${((traffic - 100) / (5000 - 100)) * 100}%, rgba(255,255,255,0.1) ${((traffic - 100) / (5000 - 100)) * 100}%, rgba(255,255,255,0.1) 100%)`
}}
/> />
</div> </div>
<div className="flex justify-between text-[10px] font-mono text-white/30 mt-1">
<span>100</span>
<span>5,000</span>
</div>
</div> </div>
{/* Input 2: Vertical Selector (Grid) */} {/* Input 2: Vertical Selector (Grid) */}