fix: Currency display in AnalyzePanel + Watchlist styling like Auctions
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

1. AnalyzePanel currency fix:
   - formatValue() now formats USD values with $ symbol
   - Affects: cheapest_registration, cheapest_renewal, cheapest_transfer, etc.

2. Watchlist styling (match Auctions):
   - Same grid layout (1fr_100px_100px_100px_80px_180px)
   - Same padding (px-6 py-4)
   - Same table structure with border wrapper
   - Same action button sizes (w-10 h-10)
   - Same hover opacity transitions
   - Same empty state styling
   - Same mobile layout (h-12 buttons)
This commit is contained in:
2025-12-18 13:52:39 +01:00
parent 9bdb673220
commit c85f5773fa
2 changed files with 97 additions and 129 deletions

View File

@ -552,35 +552,40 @@ export default function WatchlistPage() {
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* ═══════════════════════════════════════════════════════════════════════ */}
<section className="px-4 lg:px-10 py-4 pb-28 lg:pb-10"> <section className="px-4 lg:px-10 py-4 pb-28 lg:pb-10">
{!filteredDomains.length ? ( {!filteredDomains.length ? (
<div className="text-center py-16 border border-dashed border-white/[0.08]"> <div className="text-center py-24 border border-dashed border-white/[0.08] bg-white/[0.01]">
<Eye className="w-8 h-8 text-white/10 mx-auto mb-3" /> <Eye className="w-16 h-16 text-white/5 mx-auto mb-6" />
<p className="text-white/40 text-sm font-mono">No domains in your watchlist</p> <p className="text-white/50 text-sm font-mono uppercase tracking-widest font-bold">No domains in watchlist</p>
<p className="text-white/25 text-xs font-mono mt-1">Add a domain above to start monitoring</p> <p className="text-white/20 text-xs font-mono mt-3 uppercase tracking-wider max-w-sm mx-auto leading-relaxed">
Add a domain to start monitoring availability and expiry
</p>
</div> </div>
) : ( ) : (
<div className="space-y-px"> <div className="border border-white/[0.08] bg-[#020202] overflow-hidden">
{/* Desktop Table Header */} {/* Desktop Table Header */}
<div className="hidden lg:grid grid-cols-[1.5fr_100px_100px_100px_80px_160px] gap-4 px-4 py-2.5 text-[10px] font-mono text-white/40 uppercase tracking-wider border-b border-white/[0.08] bg-white/[0.02]"> <div className="hidden lg:grid grid-cols-[1fr_100px_100px_100px_80px_180px] gap-6 px-6 py-4 text-[10px] font-mono text-white/40 uppercase tracking-[0.15em] border-b border-white/[0.08] bg-white/[0.02]">
<button onClick={() => handleSortWatch('domain')} className="flex items-center gap-1 hover:text-white/60 text-left"> <button onClick={() => handleSortWatch('domain')} className="flex items-center gap-2 hover:text-white transition-colors text-left">
Domain <span className={clsx(sortField === 'domain' && "text-accent font-bold")}>Domain</span>
{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 text-accent" /> : <ChevronDown className="w-3 h-3 text-accent" />)}
</button> </button>
<button onClick={() => handleSortWatch('status')} className="flex items-center gap-1 justify-center hover:text-white/60"> <button onClick={() => handleSortWatch('status')} className="flex items-center gap-2 justify-center hover:text-white transition-colors">
Status <span className={clsx(sortField === 'status' && "text-accent font-bold")}>Status</span>
{sortField === 'status' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)} {sortField === 'status' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3 text-accent" /> : <ChevronDown className="w-3 h-3 text-accent" />)}
</button> </button>
<button onClick={() => handleSortWatch('health')} className="flex items-center gap-1 justify-center hover:text-white/60"> <button onClick={() => handleSortWatch('health')} className="flex items-center gap-2 justify-center hover:text-white transition-colors">
Health <span className={clsx(sortField === 'health' && "text-accent font-bold")}>Health</span>
{sortField === 'health' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)} {sortField === 'health' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3 text-accent" /> : <ChevronDown className="w-3 h-3 text-accent" />)}
</button> </button>
<button onClick={() => handleSortWatch('expiry')} className="flex items-center gap-1 justify-center hover:text-white/60"> <button onClick={() => handleSortWatch('expiry')} className="flex items-center gap-2 justify-center hover:text-white transition-colors">
Expiry <span className={clsx(sortField === 'expiry' && "text-accent font-bold")}>Expiry</span>
{sortField === 'expiry' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)} {sortField === 'expiry' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3 text-accent" /> : <ChevronDown className="w-3 h-3 text-accent" />)}
</button> </button>
<div className="text-center">Alert</div> <div className="text-center">Alert</div>
<div className="text-right">Actions</div> <div className="text-right">Actions</div>
</div> </div>
{/* Table Body */}
<div className="divide-y divide-white/[0.04]">
{filteredDomains.map((domain) => { {filteredDomains.map((domain) => {
const health = healthReports[domain.id] const health = healthReports[domain.id]
const healthStatus = health?.status || 'unknown' const healthStatus = health?.status || 'unknown'
@ -593,61 +598,35 @@ export default function WatchlistPage() {
className="bg-[#020202] hover:bg-white/[0.02] transition-all" className="bg-[#020202] hover:bg-white/[0.02] transition-all"
> >
{/* Mobile Row */} {/* Mobile Row */}
<div className={clsx( <div className={clsx("lg:hidden p-5", domain.is_available && "bg-accent/[0.02]")}>
"lg:hidden p-3 border border-white/[0.06]", <div className="flex items-start justify-between gap-4 mb-4">
domain.is_available <div className="min-w-0">
? "bg-accent/[0.02] border-accent/20" <button
: "bg-[#020202]" onClick={() => openAnalyze(domain.name)}
)}> className="text-lg font-bold text-white font-mono truncate block text-left hover:text-accent transition-colors"
<div className="flex items-start justify-between gap-3 mb-3"> >
<div className="flex items-center gap-2.5 min-w-0 flex-1"> {domain.name}
<div className="min-w-0 flex-1"> </button>
<button <div className="flex items-center gap-2 mt-2 text-[10px] font-mono text-white/30 uppercase tracking-wider">
onClick={() => openAnalyze(domain.name)} <span className="bg-white/5 px-2 py-0.5 border border-white/5">{domain.registrar || 'Unknown'}</span>
className="text-sm font-bold text-white font-mono truncate text-left" {days !== null && days <= 30 && days > 0 && (
title="Analyze" <span className="text-orange-400 font-bold">{days}d left</span>
> )}
{domain.name}
</button>
<div className="text-[10px] font-mono text-white/30">
{domain.registrar || 'Unknown registrar'}
</div>
</div> </div>
</div> </div>
<div className="text-right shrink-0"> <div className="text-right shrink-0">
<div className={clsx( <div className={clsx(
"text-[10px] font-bold uppercase tracking-wider px-2 py-0.5 mb-1 border", "text-[10px] font-mono px-2 py-0.5 mt-1 inline-block border",
domain.is_available domain.is_available
? "text-accent bg-accent/10 border-accent/30" ? "text-accent bg-accent/5 border-accent/20"
: "text-white/40 bg-white/5 border-white/10" : "text-white/30 bg-white/5 border-white/5"
)}> )}>
{domain.is_available ? 'AVAIL' : 'TAKEN'} {domain.is_available ? 'AVAIL' : 'TAKEN'}
</div> </div>
<button
onClick={() => { setSelectedDomain(domain.id); handleHealthCheck(domain.id) }}
className="flex items-center gap-1 justify-end"
>
{loadingHealth[domain.id] ? (
<Loader2 className="w-3 h-3 animate-spin text-white/30" />
) : (
<>
<Activity className={clsx("w-3 h-3", config.color)} />
<span className={clsx("text-[9px] font-mono", config.color)}>{config.label}</span>
</>
)}
</button>
</div> </div>
</div> </div>
{/* Expiry Info */}
{days !== null && days <= 30 && days > 0 && !domain.is_available && (
<div className="mb-3 text-[10px] font-mono text-orange-400 flex items-center gap-1">
<AlertTriangle className="w-3 h-3" />
Expires in {days} days
</div>
)}
{/* Actions */} {/* Actions */}
<div className="flex gap-2"> <div className="flex gap-2">
{domain.is_available ? ( {domain.is_available ? (
@ -655,58 +634,48 @@ export default function WatchlistPage() {
href={`https://www.namecheap.com/domains/registration/results/?domain=${domain.name}`} href={`https://www.namecheap.com/domains/registration/results/?domain=${domain.name}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex-1 py-3 bg-accent text-black text-[11px] font-bold uppercase tracking-wider flex items-center justify-center gap-2" className="flex-1 h-12 text-xs font-black uppercase tracking-widest flex items-center justify-center gap-2 bg-accent text-black hover:bg-white transition-all"
> >
<ShoppingCart className="w-4 h-4" /> Buy
Buy Now
</a> </a>
) : ( ) : (
<button <button
onClick={() => handleToggleNotify(domain.id, domain.notify_on_available)} onClick={() => handleToggleNotify(domain.id, domain.notify_on_available)}
disabled={togglingNotifyId === domain.id} disabled={togglingNotifyId === domain.id}
className={clsx( className={clsx(
"flex-1 py-2.5 text-[10px] font-bold uppercase tracking-wider border flex items-center justify-center gap-1.5 transition-all", "flex-1 h-12 text-xs font-bold uppercase tracking-widest border flex items-center justify-center gap-2 transition-all",
domain.notify_on_available domain.notify_on_available
? "border-accent/30 bg-accent/10 text-accent" ? "border-accent bg-accent/5 text-accent"
: "border-white/10 bg-white/[0.02] text-white/40" : "border-white/10 text-white/50 hover:bg-white/5"
)} )}
> >
{togglingNotifyId === domain.id ? ( {togglingNotifyId === domain.id ? (
<Loader2 className="w-3 h-3 animate-spin" /> <Loader2 className="w-4 h-4 animate-spin" />
) : domain.notify_on_available ? ( ) : domain.notify_on_available ? (
<Bell className="w-3.5 h-3.5" /> <Bell className="w-4 h-4" />
) : ( ) : (
<BellOff className="w-3.5 h-3.5" /> <BellOff className="w-4 h-4" />
)} )}
{domain.notify_on_available ? 'Alert ON' : 'Set Alert'} {domain.notify_on_available ? 'Alert ON' : 'Alert'}
</button> </button>
)} )}
<button
onClick={() => handleRefresh(domain.id)}
disabled={refreshingId === domain.id}
className="px-3 py-2 border border-white/10 text-white/40 hover:bg-white/5"
>
<RefreshCw className={clsx("w-4 h-4", refreshingId === domain.id && "animate-spin")} />
</button>
<button <button
onClick={() => openAnalyze(domain.name)} onClick={() => openAnalyze(domain.name)}
className="px-3 py-2 border border-white/10 text-white/40 hover:text-accent hover:border-accent/20 hover:bg-accent/10" className="w-14 h-12 border border-white/10 text-white/50 flex items-center justify-center hover:text-accent hover:border-accent/30 hover:bg-accent/5 transition-all"
title="Analyze"
> >
<Shield className="w-4 h-4" /> <Shield className="w-5 h-5" />
</button> </button>
<button <button
onClick={() => handleDelete(domain.id, domain.name)} onClick={() => handleDelete(domain.id, domain.name)}
disabled={deletingId === domain.id} disabled={deletingId === domain.id}
className="px-3 py-2 border border-white/10 text-white/40 hover:text-rose-400 hover:border-rose-400/20 hover:bg-rose-400/5" className="w-14 h-12 border border-white/10 text-white/50 flex items-center justify-center hover:text-rose-400 hover:border-rose-400/30 hover:bg-rose-400/5 transition-all"
> >
{deletingId === domain.id ? ( {deletingId === domain.id ? (
<Loader2 className="w-4 h-4 animate-spin" /> <Loader2 className="w-5 h-5 animate-spin" />
) : ( ) : (
<Trash2 className="w-4 h-4" /> <Trash2 className="w-5 h-5" />
)} )}
</button> </button>
</div> </div>
@ -714,39 +683,32 @@ export default function WatchlistPage() {
{/* Desktop Row */} {/* Desktop Row */}
<div className={clsx( <div className={clsx(
"hidden lg:grid grid-cols-[1.5fr_100px_100px_100px_80px_160px] gap-4 items-center p-4 group border border-white/[0.06] transition-all", "hidden lg:grid grid-cols-[1fr_100px_100px_100px_80px_180px] gap-6 items-center px-6 py-4 group transition-all",
domain.is_available domain.is_available ? "bg-accent/[0.02]" : ""
? "bg-accent/[0.02] hover:bg-accent/[0.05] border-accent/20"
: "bg-[#020202] hover:bg-white/[0.02]"
)}> )}>
{/* Domain */} {/* Domain */}
<div className="flex items-center gap-3 min-w-0 flex-1"> <div className="flex items-center gap-3 min-w-0">
<div className="min-w-0 flex-1"> <button
<button onClick={() => openAnalyze(domain.name)}
onClick={() => openAnalyze(domain.name)} className="text-sm font-bold text-white font-mono truncate group-hover:text-accent transition-colors text-left"
className="text-sm font-bold text-white font-mono truncate group-hover:text-accent transition-colors text-left" title="Analyze"
title="Analyze" >
> {domain.name}
{domain.name} </button>
</button> <div className="flex items-center gap-2 text-[9px] font-mono text-white/20 uppercase tracking-wider opacity-0 group-hover:opacity-100 transition-opacity">
<div className="text-[10px] font-mono text-white/30"> <span>{domain.registrar || 'Unknown'}</span>
{domain.registrar || 'Unknown registrar'}
</div>
</div> </div>
<a href={`https://${domain.name}`} target="_blank" className="opacity-0 group-hover:opacity-50 hover:!opacity-100 transition-opacity">
<ExternalLink className="w-3.5 h-3.5 text-white/40" />
</a>
</div> </div>
{/* Status */} {/* Status */}
<div className="flex justify-center"> <div className="flex justify-center">
<span className={clsx( <span className={clsx(
"text-[10px] font-mono font-bold uppercase px-2.5 py-1 border", "text-[10px] font-mono font-bold uppercase px-2.5 py-1.5 border",
domain.is_available domain.is_available
? "text-accent bg-accent/10 border-accent/30" ? "text-accent bg-accent/10 border-accent/30"
: "text-white/40 bg-white/5 border-white/10" : "text-white/40 bg-white/5 border-white/10"
)}> )}>
{domain.is_available ? 'AVAIL' : 'TAKEN'} {domain.is_available ? 'AVAIL' : 'TAKEN'}
</span> </span>
</div> </div>
@ -755,10 +717,8 @@ export default function WatchlistPage() {
<button <button
onClick={() => { setSelectedDomain(domain.id); handleHealthCheck(domain.id) }} onClick={() => { setSelectedDomain(domain.id); handleHealthCheck(domain.id) }}
className={clsx( className={clsx(
"flex items-center gap-1.5 px-2 py-1 text-[10px] font-mono uppercase border transition-colors hover:opacity-80", "flex items-center gap-1.5 px-2.5 py-1.5 text-[10px] font-mono uppercase border transition-colors hover:opacity-80",
config.color, config.color, "border-white/10"
config.bg.replace('bg-', 'bg-'),
"border-white/10"
)} )}
> >
{loadingHealth[domain.id] ? ( {loadingHealth[domain.id] ? (
@ -773,9 +733,9 @@ export default function WatchlistPage() {
</div> </div>
{/* Expires */} {/* Expires */}
<div className="text-center text-xs font-mono"> <div className="text-center text-sm font-mono">
{days !== null && days <= 30 && days > 0 ? ( {days !== null && days <= 30 && days > 0 ? (
<span className="text-orange-400 font-bold">{days}d left</span> <span className="text-orange-400 font-bold">{days}d</span>
) : ( ) : (
<span className="text-white/50">{formatExpiryDate(domain.expiration_date)}</span> <span className="text-white/50">{formatExpiryDate(domain.expiration_date)}</span>
)} )}
@ -787,7 +747,7 @@ export default function WatchlistPage() {
onClick={() => handleToggleNotify(domain.id, domain.notify_on_available)} onClick={() => handleToggleNotify(domain.id, domain.notify_on_available)}
disabled={togglingNotifyId === domain.id} disabled={togglingNotifyId === domain.id}
className={clsx( className={clsx(
"w-9 h-9 flex items-center justify-center border transition-colors", "w-10 h-10 flex items-center justify-center border transition-all",
domain.notify_on_available domain.notify_on_available
? "text-accent border-accent/30 bg-accent/10" ? "text-accent border-accent/30 bg-accent/10"
: "text-white/20 border-white/10 hover:text-white/40 hover:bg-white/5" : "text-white/20 border-white/10 hover:text-white/40 hover:bg-white/5"
@ -804,16 +764,16 @@ export default function WatchlistPage() {
</div> </div>
{/* Actions */} {/* Actions */}
<div className="flex items-center justify-end gap-1.5"> <div className="flex items-center justify-end gap-2 opacity-40 group-hover:opacity-100 transition-all">
{domain.is_available ? ( {domain.is_available ? (
<a <a
href={`https://www.namecheap.com/domains/registration/results/?domain=${domain.name}`} href={`https://www.namecheap.com/domains/registration/results/?domain=${domain.name}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="h-9 px-4 bg-accent text-black text-[10px] font-bold uppercase tracking-wider flex items-center gap-2 hover:bg-white transition-colors" className="h-10 px-5 bg-accent text-black text-[10px] font-black uppercase tracking-widest flex items-center gap-2 hover:bg-white transition-colors"
> >
<ShoppingCart className="w-3.5 h-3.5" /> <ShoppingCart className="w-4 h-4" />
Buy Now Buy
</a> </a>
) : ( ) : (
<> <>
@ -821,16 +781,16 @@ export default function WatchlistPage() {
onClick={() => handleRefresh(domain.id)} onClick={() => handleRefresh(domain.id)}
disabled={refreshingId === domain.id} disabled={refreshingId === domain.id}
title="Refresh" title="Refresh"
className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-white border border-white/10 hover:bg-white/5 transition-all" className="w-10 h-10 flex items-center justify-center text-white/40 hover:text-white border border-white/10 hover:bg-white/5 transition-all"
> >
<RefreshCw className={clsx("w-3.5 h-3.5", refreshingId === domain.id && "animate-spin")} /> <RefreshCw className={clsx("w-4 h-4", refreshingId === domain.id && "animate-spin")} />
</button> </button>
<button <button
onClick={() => openAnalyze(domain.name)} onClick={() => openAnalyze(domain.name)}
title="Analyze" title="Analyze"
className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-accent border border-white/10 hover:bg-accent/10 hover:border-accent/20 transition-all" className="w-10 h-10 flex items-center justify-center text-white/40 hover:text-accent border border-white/10 hover:bg-accent/10 hover:border-accent/20 transition-all"
> >
<Shield className="w-3.5 h-3.5" /> <Shield className="w-4 h-4" />
</button> </button>
</> </>
)} )}
@ -838,12 +798,12 @@ export default function WatchlistPage() {
onClick={() => handleDelete(domain.id, domain.name)} onClick={() => handleDelete(domain.id, domain.name)}
disabled={deletingId === domain.id} disabled={deletingId === domain.id}
title="Remove" title="Remove"
className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-rose-400 border border-white/10 hover:border-rose-400/20 hover:bg-rose-500/10 transition-all" className="w-10 h-10 flex items-center justify-center text-white/40 hover:text-rose-400 border border-white/10 hover:border-rose-400/20 hover:bg-rose-500/10 transition-all"
> >
{deletingId === domain.id ? ( {deletingId === domain.id ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" /> <Loader2 className="w-4 h-4 animate-spin" />
) : ( ) : (
<Trash2 className="w-3.5 h-3.5" /> <Trash2 className="w-4 h-4" />
)} )}
</button> </button>
</div> </div>
@ -851,6 +811,7 @@ export default function WatchlistPage() {
</div> </div>
) )
})} })}
</div>
</div> </div>
)} )}
</section> </section>

View File

@ -106,10 +106,17 @@ async function copyToClipboard(text: string) {
} }
} }
function formatValue(value: unknown): string { function formatValue(value: unknown, key?: string): string {
if (value === null || value === undefined) return '—' if (value === null || value === undefined) return '—'
if (typeof value === 'string') return value if (typeof value === 'string') return value
if (typeof value === 'number') return String(value) if (typeof value === 'number') {
// Format USD values with currency symbol
const usdKeys = ['cheapest_registration', 'cheapest_renewal', 'cheapest_transfer', 'renewal_burn', 'estimated_value', 'cpc']
if (key && usdKeys.some(k => key.toLowerCase().includes(k.replace('_', '')))) {
return `$${value.toFixed(2)}`
}
return String(value)
}
if (typeof value === 'boolean') return value ? 'Yes' : 'No' if (typeof value === 'boolean') return value ? 'Yes' : 'No'
if (Array.isArray(value)) return `${value.length} items` if (Array.isArray(value)) return `${value.length} items`
return 'Details' return 'Details'
@ -521,7 +528,7 @@ export function AnalyzePanel() {
item.status === 'warn' ? "text-amber-400" : item.status === 'warn' ? "text-amber-400" :
item.status === 'fail' ? "text-rose-400" : "text-white/70" item.status === 'fail' ? "text-rose-400" : "text-white/70"
)}> )}>
{formatValue(item.value)} {formatValue(item.value, item.key)}
</span> </span>
</div> </div>
)} )}