style: Portfolio table styling like Watchlist/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

- Same border wrapper with bg-[#020202]
- Same grid gap (gap-6) and padding (px-6 py-4)
- Same header styling with accent highlight on active sort
- Same action button styling (w-10 h-10 with borders)
- Same hover opacity transitions
- Same empty state styling
- Table body with divide-y divider
This commit is contained in:
2025-12-18 14:04:20 +01:00
parent c85f5773fa
commit c41f870040

View File

@ -1330,42 +1330,52 @@ export default function PortfolioPage() {
<Loader2 className="w-6 h-6 text-accent animate-spin" />
</div>
) : !filteredDomains.length ? (
<div className="text-center py-16 border border-dashed border-white/[0.08]">
<Briefcase className="w-10 h-10 text-white/10 mx-auto mb-4" />
<p className="text-white/40 text-sm font-mono mb-1">No domains found</p>
<p className="text-white/25 text-xs font-mono mb-4">Add your first domain to start tracking</p>
<button
onClick={() => setShowAddModal(true)}
className="inline-flex items-center gap-2 px-4 py-2 bg-accent text-black text-xs font-bold uppercase"
>
<div className="text-center py-24 border border-dashed border-white/[0.08] bg-white/[0.01]">
<Briefcase 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 found</p>
<p className="text-white/20 text-xs font-mono mt-3 uppercase tracking-wider max-w-sm mx-auto leading-relaxed">
Add your first domain to start tracking your portfolio
</p>
<button
onClick={() => setShowAddModal(true)}
className="mt-6 inline-flex items-center gap-2 px-5 py-3 bg-accent text-black text-xs font-black uppercase tracking-widest hover:bg-white transition-colors"
>
<Plus className="w-4 h-4" />Add Domain
</button>
</div>
</button>
</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_1fr_100px_100px_100px_100px_90px_60px_140px] gap-3 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={() => handleSort('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_140px_90px_90px_100px_90px_80px_60px_160px] 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={() => handleSort('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>
<div>Registrar / Tags</div>
<button onClick={() => handleSort('purchased')} className="flex items-center gap-1 justify-end hover:text-white/60">
Purchased {sortField === 'purchased' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
<div>Registrar</div>
<button onClick={() => handleSort('purchased')} className="flex items-center gap-2 justify-center hover:text-white transition-colors">
<span className={clsx(sortField === 'purchased' && "text-accent font-bold")}>Purchased</span>
{sortField === 'purchased' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3 text-accent" /> : <ChevronDown className="w-3 h-3 text-accent" />)}
</button>
<button onClick={() => handleSort('renewal')} className="flex items-center gap-1 justify-end hover:text-white/60">
Expires {sortField === 'renewal' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
<button onClick={() => handleSort('renewal')} className="flex items-center gap-2 justify-center hover:text-white transition-colors">
<span className={clsx(sortField === 'renewal' && "text-accent font-bold")}>Expires</span>
{sortField === 'renewal' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3 text-accent" /> : <ChevronDown className="w-3 h-3 text-accent" />)}
</button>
<button onClick={() => handleSort('value')} className="flex items-center gap-1 justify-end hover:text-white/60">
Value {sortField === 'value' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
<button onClick={() => handleSort('value')} className="flex items-center gap-2 justify-end hover:text-white transition-colors">
<span className={clsx(sortField === 'value' && "text-accent font-bold")}>Value</span>
{sortField === 'value' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3 text-accent" /> : <ChevronDown className="w-3 h-3 text-accent" />)}
</button>
<button onClick={() => handleSort('roi')} className="flex items-center gap-1 justify-end hover:text-white/60">
ROI {sortField === 'roi' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
<button onClick={() => handleSort('roi')} className="flex items-center gap-2 justify-end hover:text-white transition-colors">
<span className={clsx(sortField === 'roi' && "text-accent font-bold")}>ROI</span>
{sortField === 'roi' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3 text-accent" /> : <ChevronDown className="w-3 h-3 text-accent" />)}
</button>
<div className="text-center">Yield</div>
<div className="text-center">Health</div>
<div className="text-right">Actions</div>
</div>
{/* Table Body */}
<div className="divide-y divide-white/[0.04]">
{filteredDomains.map((domain) => {
const daysUntilRenewal = getDaysUntil(domain.renewal_date)
const isExpiringSoon = daysUntilRenewal !== null && daysUntilRenewal <= 30 && daysUntilRenewal > 0
@ -1378,80 +1388,67 @@ export default function PortfolioPage() {
<div
key={domain.id}
className={clsx(
"border border-white/[0.06] bg-[#020202] hover:bg-white/[0.02] transition-all",
"group transition-all",
domain.is_sold && "opacity-60"
)}
>
{/* DESKTOP ROW */}
<div className="hidden lg:grid grid-cols-[1.5fr_1fr_100px_100px_100px_100px_90px_60px_140px] gap-3 px-4 py-3 items-center">
<div className="hidden lg:grid grid-cols-[1fr_140px_90px_90px_100px_90px_80px_60px_160px] gap-6 px-6 py-4 items-center group-hover:bg-white/[0.02]">
{/* Domain */}
<div className="flex items-center gap-3 min-w-0">
<div className="min-w-0">
<div className="text-sm font-bold text-white font-mono truncate">{domain.domain}</div>
<div className="flex items-center gap-1.5 mt-0.5">
{renderStatusBadges(domain)}
{!domain.is_dns_verified && !domain.is_sold && (
<button
onClick={() => setVerifyingDomain(domain)}
className="text-[9px] font-mono text-amber-400 hover:text-amber-300 underline"
>
Verify
</button>
)}
</div>
<button
onClick={() => openAnalyze(domain.domain)}
className="text-sm font-bold text-white font-mono truncate group-hover:text-accent transition-colors text-left"
>
{domain.domain}
</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">
{renderStatusBadges(domain)}
{!domain.is_dns_verified && !domain.is_sold && (
<button
onClick={() => setVerifyingDomain(domain)}
className="text-amber-400 hover:text-amber-300"
>
Verify
</button>
)}
</div>
</div>
{/* Registrar & Tags */}
{/* Registrar */}
<div className="min-w-0">
<div className="text-xs font-mono text-white/60 truncate">{domain.registrar || ''}</div>
{tags.length > 0 && (
<div className="flex items-center gap-1 mt-1 flex-wrap">
{tags.slice(0, 2).map(tag => (
<span key={tag} className="px-1.5 py-0.5 text-[8px] font-mono text-white/40 bg-white/5 border border-white/10">
{tag}
</span>
))}
{tags.length > 2 && (
<span className="text-[8px] font-mono text-white/30">+{tags.length - 2}</span>
)}
</div>
)}
</div>
<div className="text-sm font-mono text-white/60 truncate">{domain.registrar || ''}</div>
</div>
{/* Purchased */}
<div className="text-right">
<div className="text-xs font-mono text-white/60">{formatShortDate(domain.purchase_date)}</div>
<div className="text-[10px] font-mono text-white/30">{formatCurrency(domain.purchase_price)}</div>
</div>
<div className="text-center">
<div className="text-sm font-mono text-white/50">{formatShortDate(domain.purchase_date)}</div>
</div>
{/* Expires */}
<div className="text-right">
{domain.is_sold ? (
<div className="text-xs font-mono text-white/30">Sold</div>
<div className="text-center">
{domain.is_sold ? (
<span className="text-[10px] font-mono text-white/30 uppercase">Sold</span>
) : (
<>
<div className={clsx(
"text-xs font-mono",
isExpired ? "text-rose-400" : isExpiringSoon ? "text-orange-400" : "text-white/60"
)}>
{daysUntilRenewal !== null ? `${daysUntilRenewal}d` : ''}
</div>
<div className="text-[10px] font-mono text-white/30">{formatShortDate(domain.renewal_date)}</div>
</>
)}
<span className={clsx(
"text-sm font-mono",
isExpired ? "text-rose-400 font-bold" : isExpiringSoon ? "text-orange-400 font-bold" : "text-white/50"
)}>
{daysUntilRenewal !== null ? `${daysUntilRenewal}d` : ''}
</span>
)}
</div>
{/* Value */}
<div className="text-right">
<div className="text-sm font-bold text-accent font-mono">{formatCurrency(domain.estimated_value)}</div>
<span className="text-sm font-bold text-accent font-mono">{formatCurrency(domain.estimated_value)}</span>
</div>
{/* ROI */}
<div className="text-right">
<div className={clsx("text-sm font-bold font-mono", roiPositive ? "text-accent" : "text-rose-400")}>
<span className={clsx("text-sm font-bold font-mono", roiPositive ? "text-accent" : "text-rose-400")}>
{formatROI(domain.roi)}
</div>
</span>
</div>
{/* Yield */}
@ -1465,48 +1462,35 @@ export default function PortfolioPage() {
</div>
{/* Actions */}
<div className="flex items-center justify-end gap-1">
<div className="flex items-center justify-end gap-2 opacity-40 group-hover:opacity-100 transition-all">
<button
onClick={() => openAnalyze(domain.domain)}
title="Analyze"
className="p-2 text-white/30 hover:text-accent transition-colors"
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-4 h-4" />
</button>
<button
onClick={() => setEditingDomain(domain)}
title="Edit domain details"
className="p-2 text-white/30 hover:text-accent transition-colors"
title="Edit"
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"
>
<Edit3 className="w-4 h-4" />
</button>
<button
onClick={() => handleRefreshValue(domain.id)}
disabled={refreshingId === domain.id}
title="Refresh value estimate"
className={clsx("p-2 text-white/30 hover:text-accent transition-colors", refreshingId === domain.id && "animate-spin")}
>
<RefreshCw className="w-4 h-4" />
</button>
{domain.is_dns_verified && !domain.is_sold && !listedDomains.has(domain.domain.toLowerCase()) && (
<Link
href="/terminal/listing"
title="List for sale"
className="p-2 text-white/30 hover:text-blue-400 transition-colors"
>
<Tag className="w-4 h-4" />
</Link>
)}
<button
onClick={() => handleDelete(domain.id, domain.domain)}
disabled={deletingId === domain.id}
title="Remove from portfolio"
className="p-2 text-white/30 hover:text-rose-400 transition-colors"
title="Remove"
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"
>
<Trash2 className="w-4 h-4" />
{deletingId === domain.id ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Trash2 className="w-4 h-4" />
)}
</button>
</div>
</div>
</div>
{/* MOBILE ROW */}
<div className="lg:hidden">
@ -1632,8 +1616,9 @@ export default function PortfolioPage() {
</div>
)
})}
</div>
)}
</div>
</div>
)}
</>
)}
</section>