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
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:
@ -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>
|
||||
<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="inline-flex items-center gap-2 px-4 py-2 bg-accent text-black text-xs font-bold uppercase"
|
||||
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>
|
||||
) : (
|
||||
<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">
|
||||
<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-[9px] font-mono text-amber-400 hover:text-amber-300 underline"
|
||||
className="text-amber-400 hover:text-amber-300"
|
||||
>
|
||||
Verify
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</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 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 className="text-center">
|
||||
<div className="text-sm font-mono text-white/50">{formatShortDate(domain.purchase_date)}</div>
|
||||
</div>
|
||||
|
||||
{/* Expires */}
|
||||
<div className="text-right">
|
||||
<div className="text-center">
|
||||
{domain.is_sold ? (
|
||||
<div className="text-xs font-mono text-white/30">Sold</div>
|
||||
<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"
|
||||
<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` : '—'}
|
||||
</div>
|
||||
<div className="text-[10px] font-mono text-white/30">{formatShortDate(domain.renewal_date)}</div>
|
||||
</>
|
||||
</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,45 +1462,32 @@ 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"
|
||||
>
|
||||
{deletingId === domain.id ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -1633,6 +1617,7 @@ export default function PortfolioPage() {
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user