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">
{!filteredDomains.length ? (
<div className="text-center py-16 border border-dashed border-white/[0.08]">
<Eye className="w-8 h-8 text-white/10 mx-auto mb-3" />
<p className="text-white/40 text-sm font-mono">No domains in your watchlist</p>
<p className="text-white/25 text-xs font-mono mt-1">Add a domain above to start monitoring</p>
<div className="text-center py-24 border border-dashed border-white/[0.08] bg-white/[0.01]">
<Eye className="w-16 h-16 text-white/5 mx-auto mb-6" />
<p className="text-white/50 text-sm font-mono uppercase tracking-widest font-bold">No domains in watchlist</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 className="space-y-px">
<div className="border border-white/[0.08] bg-[#020202] overflow-hidden">
{/* 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]">
<button onClick={() => handleSortWatch('domain')} className="flex items-center gap-1 hover:text-white/60 text-left">
Domain
{sortField === 'domain' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
<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-2 hover:text-white transition-colors text-left">
<span className={clsx(sortField === 'domain' && "text-accent font-bold")}>Domain</span>
{sortField === 'domain' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3 text-accent" /> : <ChevronDown className="w-3 h-3 text-accent" />)}
</button>
<button onClick={() => handleSortWatch('status')} className="flex items-center gap-1 justify-center hover:text-white/60">
Status
{sortField === 'status' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
<button onClick={() => handleSortWatch('status')} className="flex items-center gap-2 justify-center hover:text-white transition-colors">
<span className={clsx(sortField === 'status' && "text-accent font-bold")}>Status</span>
{sortField === 'status' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3 text-accent" /> : <ChevronDown className="w-3 h-3 text-accent" />)}
</button>
<button onClick={() => handleSortWatch('health')} className="flex items-center gap-1 justify-center hover:text-white/60">
Health
{sortField === 'health' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
<button onClick={() => handleSortWatch('health')} className="flex items-center gap-2 justify-center hover:text-white transition-colors">
<span className={clsx(sortField === 'health' && "text-accent font-bold")}>Health</span>
{sortField === 'health' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3 text-accent" /> : <ChevronDown className="w-3 h-3 text-accent" />)}
</button>
<button onClick={() => handleSortWatch('expiry')} className="flex items-center gap-1 justify-center hover:text-white/60">
Expiry
{sortField === 'expiry' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
<button onClick={() => handleSortWatch('expiry')} className="flex items-center gap-2 justify-center hover:text-white transition-colors">
<span className={clsx(sortField === 'expiry' && "text-accent font-bold")}>Expiry</span>
{sortField === 'expiry' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3 text-accent" /> : <ChevronDown className="w-3 h-3 text-accent" />)}
</button>
<div className="text-center">Alert</div>
<div className="text-right">Actions</div>
</div>
{/* Table Body */}
<div className="divide-y divide-white/[0.04]">
{filteredDomains.map((domain) => {
const health = healthReports[domain.id]
const healthStatus = health?.status || 'unknown'
@ -593,61 +598,35 @@ export default function WatchlistPage() {
className="bg-[#020202] hover:bg-white/[0.02] transition-all"
>
{/* Mobile Row */}
<div className={clsx(
"lg:hidden p-3 border border-white/[0.06]",
domain.is_available
? "bg-accent/[0.02] border-accent/20"
: "bg-[#020202]"
)}>
<div className="flex items-start justify-between gap-3 mb-3">
<div className="flex items-center gap-2.5 min-w-0 flex-1">
<div className="min-w-0 flex-1">
<button
onClick={() => openAnalyze(domain.name)}
className="text-sm font-bold text-white font-mono truncate text-left"
title="Analyze"
>
{domain.name}
</button>
<div className="text-[10px] font-mono text-white/30">
{domain.registrar || 'Unknown registrar'}
</div>
<div className={clsx("lg:hidden p-5", domain.is_available && "bg-accent/[0.02]")}>
<div className="flex items-start justify-between gap-4 mb-4">
<div className="min-w-0">
<button
onClick={() => openAnalyze(domain.name)}
className="text-lg font-bold text-white font-mono truncate block text-left hover:text-accent transition-colors"
>
{domain.name}
</button>
<div className="flex items-center gap-2 mt-2 text-[10px] font-mono text-white/30 uppercase tracking-wider">
<span className="bg-white/5 px-2 py-0.5 border border-white/5">{domain.registrar || 'Unknown'}</span>
{days !== null && days <= 30 && days > 0 && (
<span className="text-orange-400 font-bold">{days}d left</span>
)}
</div>
</div>
<div className="text-right shrink-0">
<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
? "text-accent bg-accent/10 border-accent/30"
: "text-white/40 bg-white/5 border-white/10"
? "text-accent bg-accent/5 border-accent/20"
: "text-white/30 bg-white/5 border-white/5"
)}>
{domain.is_available ? 'AVAIL' : 'TAKEN'}
{domain.is_available ? 'AVAIL' : 'TAKEN'}
</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>
{/* 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 */}
<div className="flex gap-2">
{domain.is_available ? (
@ -655,58 +634,48 @@ export default function WatchlistPage() {
href={`https://www.namecheap.com/domains/registration/results/?domain=${domain.name}`}
target="_blank"
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 Now
Buy
</a>
) : (
<button
onClick={() => handleToggleNotify(domain.id, domain.notify_on_available)}
disabled={togglingNotifyId === domain.id}
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
? "border-accent/30 bg-accent/10 text-accent"
: "border-white/10 bg-white/[0.02] text-white/40"
? "border-accent bg-accent/5 text-accent"
: "border-white/10 text-white/50 hover:bg-white/5"
)}
>
{togglingNotifyId === domain.id ? (
<Loader2 className="w-3 h-3 animate-spin" />
<Loader2 className="w-4 h-4 animate-spin" />
) : 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
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
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"
title="Analyze"
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"
>
<Shield className="w-4 h-4" />
<Shield className="w-5 h-5" />
</button>
<button
onClick={() => handleDelete(domain.id, domain.name)}
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 ? (
<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>
</div>
@ -714,39 +683,32 @@ export default function WatchlistPage() {
{/* Desktop Row */}
<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",
domain.is_available
? "bg-accent/[0.02] hover:bg-accent/[0.05] border-accent/20"
: "bg-[#020202] hover:bg-white/[0.02]"
"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 ? "bg-accent/[0.02]" : ""
)}>
{/* Domain */}
<div className="flex items-center gap-3 min-w-0 flex-1">
<div className="min-w-0 flex-1">
<button
onClick={() => openAnalyze(domain.name)}
className="text-sm font-bold text-white font-mono truncate group-hover:text-accent transition-colors text-left"
title="Analyze"
>
{domain.name}
</button>
<div className="text-[10px] font-mono text-white/30">
{domain.registrar || 'Unknown registrar'}
</div>
<div className="flex items-center gap-3 min-w-0">
<button
onClick={() => openAnalyze(domain.name)}
className="text-sm font-bold text-white font-mono truncate group-hover:text-accent transition-colors text-left"
title="Analyze"
>
{domain.name}
</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">
<span>{domain.registrar || 'Unknown'}</span>
</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>
{/* Status */}
<div className="flex justify-center">
<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
? "text-accent bg-accent/10 border-accent/30"
: "text-white/40 bg-white/5 border-white/10"
)}>
{domain.is_available ? 'AVAIL' : 'TAKEN'}
{domain.is_available ? 'AVAIL' : 'TAKEN'}
</span>
</div>
@ -755,10 +717,8 @@ export default function WatchlistPage() {
<button
onClick={() => { setSelectedDomain(domain.id); handleHealthCheck(domain.id) }}
className={clsx(
"flex items-center gap-1.5 px-2 py-1 text-[10px] font-mono uppercase border transition-colors hover:opacity-80",
config.color,
config.bg.replace('bg-', 'bg-'),
"border-white/10"
"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, "border-white/10"
)}
>
{loadingHealth[domain.id] ? (
@ -773,9 +733,9 @@ export default function WatchlistPage() {
</div>
{/* Expires */}
<div className="text-center text-xs font-mono">
<div className="text-center text-sm font-mono">
{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>
)}
@ -787,7 +747,7 @@ export default function WatchlistPage() {
onClick={() => handleToggleNotify(domain.id, domain.notify_on_available)}
disabled={togglingNotifyId === domain.id}
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
? "text-accent border-accent/30 bg-accent/10"
: "text-white/20 border-white/10 hover:text-white/40 hover:bg-white/5"
@ -804,16 +764,16 @@ export default function WatchlistPage() {
</div>
{/* 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 ? (
<a
href={`https://www.namecheap.com/domains/registration/results/?domain=${domain.name}`}
target="_blank"
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" />
Buy Now
<ShoppingCart className="w-4 h-4" />
Buy
</a>
) : (
<>
@ -821,16 +781,16 @@ export default function WatchlistPage() {
onClick={() => handleRefresh(domain.id)}
disabled={refreshingId === domain.id}
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
onClick={() => openAnalyze(domain.name)}
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>
</>
)}
@ -838,12 +798,12 @@ export default function WatchlistPage() {
onClick={() => handleDelete(domain.id, domain.name)}
disabled={deletingId === domain.id}
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 ? (
<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>
</div>
@ -851,6 +811,7 @@ export default function WatchlistPage() {
</div>
)
})}
</div>
</div>
)}
</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 (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 (Array.isArray(value)) return `${value.length} items`
return 'Details'
@ -521,7 +528,7 @@ export function AnalyzePanel() {
item.status === 'warn' ? "text-amber-400" :
item.status === 'fail' ? "text-rose-400" : "text-white/70"
)}>
{formatValue(item.value)}
{formatValue(item.value, item.key)}
</span>
</div>
)}