refactor: Simplify Portfolio page - remove unreliable data

SIMPLIFIED STATS:
- Removed: Total Invested, Est. Value, Profit/Loss
- Added: Expiring Soon (domains expiring in 30 days)
- Added: Need Attention (domains with health issues)
- Kept: Total Domains, Listed for Sale

SIMPLIFIED TABLE:
- Domain column: name + registrar
- Added column: simple date added
- Expires column: with color-coded expiry warnings (30d, expired)
- Health column: quick health status
- Actions: Three-dot dropdown menu

THREE-DOT MENU:
├── Health Check
├── Edit Details
├── ─────────────
├── List for Sale (accent color)
├── Visit Website
├── ─────────────
├── Record Sale
└── Remove (danger)

SIMPLIFIED SUBTITLE:
- Shows domain count + expiring soon count
- No more profit/loss display

This focuses on reliable, actionable data:
 Domain names (100% accurate)
 Expiry dates (user input)
 Health status (real-time check)
 Valuations (unreliable estimates)
 Profit/Loss (depends on estimates)
This commit is contained in:
yves.gugger
2025-12-10 15:27:50 +01:00
parent cb1f009dc3
commit 877e402df8

View File

@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'
import { useStore } from '@/lib/store'
import { api, PortfolioDomain, PortfolioSummary, DomainValuation, DomainHealthReport, HealthStatus } from '@/lib/api'
import { CommandCenterLayout } from '@/components/CommandCenterLayout'
import { PremiumTable, StatCard, PageContainer, TableActionButton } from '@/components/PremiumTable'
import { PremiumTable, StatCard, PageContainer } from '@/components/PremiumTable'
import { Toast, useToast } from '@/components/Toast'
import {
Plus,
@ -13,19 +13,17 @@ import {
DollarSign,
Calendar,
Building,
RefreshCw,
Loader2,
TrendingUp,
Sparkles,
ArrowUpRight,
X,
Briefcase,
PiggyBank,
ShoppingCart,
Activity,
Shield,
AlertTriangle,
Tag,
MoreVertical,
ExternalLink,
} from 'lucide-react'
// Health status configuration
@ -67,6 +65,9 @@ export default function PortfolioPage() {
const [healthReports, setHealthReports] = useState<Record<string, DomainHealthReport>>({})
const [loadingHealth, setLoadingHealth] = useState<Record<string, boolean>>({})
const [selectedHealthDomain, setSelectedHealthDomain] = useState<string | null>(null)
// Dropdown menu state
const [openMenuId, setOpenMenuId] = useState<number | null>(null)
const [addForm, setAddForm] = useState({
domain: '',
@ -261,10 +262,15 @@ export default function PortfolioPage() {
// Dynamic subtitle
const getSubtitle = () => {
if (loading) return 'Loading your portfolio...'
if (portfolio.length === 0) return 'Start tracking your domain investments'
const profit = summary?.total_profit || 0
if (profit > 0) return `${portfolio.length} domains • +$${profit.toLocaleString()} profit`
if (profit < 0) return `${portfolio.length} domains • -$${Math.abs(profit).toLocaleString()} loss`
if (portfolio.length === 0) return 'Start tracking your domains'
const expiringSoon = portfolio.filter(d => {
if (!d.renewal_date) return false
const days = Math.ceil((new Date(d.renewal_date).getTime() - Date.now()) / (1000 * 60 * 60 * 24))
return days <= 30 && days > 0
}).length
if (expiringSoon > 0) {
return `${portfolio.length} domain${portfolio.length !== 1 ? 's' : ''}${expiringSoon} expiring soon`
}
return `Managing ${portfolio.length} domain${portfolio.length !== 1 ? 's' : ''}`
}
@ -288,18 +294,25 @@ export default function PortfolioPage() {
{toast && <Toast message={toast.message} type={toast.type} onClose={hideToast} />}
<PageContainer>
{/* Summary Stats */}
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4">
{/* Summary Stats - Only reliable data */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard title="Total Domains" value={summary?.total_domains || 0} icon={Briefcase} />
<StatCard title="Total Invested" value={`$${(summary?.total_invested || 0).toLocaleString()}`} icon={DollarSign} />
<StatCard title="Est. Value" value={`$${(summary?.total_value || 0).toLocaleString()}`} icon={TrendingUp} />
<StatCard
title="Profit/Loss"
value={`${(summary?.total_profit || 0) >= 0 ? '+' : ''}$${(summary?.total_profit || 0).toLocaleString()}`}
icon={PiggyBank}
accent={(summary?.total_profit || 0) >= 0}
title="Expiring Soon"
value={portfolio.filter(d => {
if (!d.renewal_date) return false
const days = Math.ceil((new Date(d.renewal_date).getTime() - Date.now()) / (1000 * 60 * 60 * 24))
return days <= 30 && days > 0
}).length}
icon={Calendar}
accent
/>
<StatCard title="Sold" value={summary?.sold_domains || 0} icon={ShoppingCart} />
<StatCard
title="Need Attention"
value={Object.values(healthReports).filter(r => r.status !== 'healthy').length}
icon={AlertTriangle}
/>
<StatCard title="Listed for Sale" value={summary?.sold_domains || 0} icon={Tag} />
</div>
{!canAddMore && (
@ -340,48 +353,53 @@ export default function PortfolioPage() {
),
},
{
key: 'purchase',
header: 'Purchase',
key: 'added',
header: 'Added',
hideOnMobile: true,
hideOnTablet: true,
render: (domain) => (
<div>
{domain.purchase_price && (
<span className="font-medium text-foreground">${domain.purchase_price.toLocaleString()}</span>
)}
{domain.purchase_date && (
<p className="text-xs text-foreground-muted flex items-center gap-1 mt-0.5">
<Calendar className="w-3 h-3" /> {new Date(domain.purchase_date).toLocaleDateString()}
</p>
)}
</div>
),
},
{
key: 'valuation',
header: 'Est. Value',
align: 'right',
render: (domain) => (
domain.current_valuation ? (
<span className="font-semibold text-foreground">${domain.current_valuation.toLocaleString()}</span>
) : (
<span className="text-foreground-muted">—</span>
)
<span className="text-sm text-foreground-muted">
{domain.purchase_date
? new Date(domain.purchase_date).toLocaleDateString()
: new Date(domain.created_at).toLocaleDateString()
}
</span>
),
},
{
key: 'renewal',
header: 'Renewal',
header: 'Expires',
hideOnMobile: true,
hideOnTablet: true,
render: (domain) => (
domain.renewal_date ? (
<span className="text-sm text-foreground-muted">
{new Date(domain.renewal_date).toLocaleDateString()}
</span>
) : (
<span className="text-foreground-subtle">—</span>
render: (domain) => {
if (!domain.renewal_date) {
return <span className="text-foreground-subtle">—</span>
}
const days = Math.ceil((new Date(domain.renewal_date).getTime() - Date.now()) / (1000 * 60 * 60 * 24))
const isExpiringSoon = days <= 30 && days > 0
const isExpired = days <= 0
return (
<div className="flex items-center gap-2">
<span className={clsx(
"text-sm",
isExpired && "text-red-400",
isExpiringSoon && "text-amber-400",
!isExpired && !isExpiringSoon && "text-foreground-muted"
)}>
{new Date(domain.renewal_date).toLocaleDateString()}
</span>
{isExpiringSoon && (
<span className="px-1.5 py-0.5 text-[10px] font-medium bg-amber-400/10 text-amber-400 rounded">
{days}d
</span>
)}
{isExpired && (
<span className="px-1.5 py-0.5 text-[10px] font-medium bg-red-400/10 text-red-400 rounded">
EXPIRED
</span>
)}
</div>
)
),
},
},
{
key: 'health',
@ -424,51 +442,77 @@ export default function PortfolioPage() {
header: '',
align: 'right',
render: (domain) => (
<div className="flex items-center gap-1 justify-end">
<TableActionButton
icon={Shield}
onClick={() => handleHealthCheck(domain.domain)}
loading={loadingHealth[domain.domain]}
title="Health check (SSL, DNS, HTTP)"
variant={healthReports[domain.domain] ? 'accent' : 'default'}
/>
<TableActionButton
icon={Sparkles}
onClick={() => handleValuate(domain)}
title="Get valuation"
/>
<TableActionButton
icon={RefreshCw}
onClick={() => handleRefresh(domain)}
loading={refreshingId === domain.id}
title="Refresh valuation"
/>
<TableActionButton
icon={Edit2}
onClick={() => openEditModal(domain)}
title="Edit"
/>
<Link
href={`/command/listings?domain=${encodeURIComponent(domain.domain)}`}
onClick={(e) => e.stopPropagation()}
className="px-3 py-2 text-xs font-medium text-accent hover:bg-accent/10 rounded-lg transition-colors flex items-center gap-1"
>
<Tag className="w-3 h-3" />
List
</Link>
<div className="relative">
<button
onClick={(e) => { e.stopPropagation(); openSellModal(domain) }}
className="px-3 py-2 text-xs font-medium text-foreground-muted hover:bg-foreground/5 rounded-lg transition-colors"
title="Record a completed sale (for P&L tracking)"
onClick={(e) => {
e.stopPropagation()
setOpenMenuId(openMenuId === domain.id ? null : domain.id)
}}
className="p-2 text-foreground-muted hover:text-foreground hover:bg-foreground/5 rounded-lg transition-colors"
>
Sold?
<MoreVertical className="w-4 h-4" />
</button>
<TableActionButton
icon={Trash2}
onClick={() => handleDelete(domain)}
variant="danger"
title="Remove"
/>
{openMenuId === domain.id && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-40"
onClick={() => setOpenMenuId(null)}
/>
{/* Menu */}
<div className="absolute right-0 top-full mt-1 z-50 w-48 py-1 bg-background-secondary border border-border/50 rounded-xl shadow-xl">
<button
onClick={() => { handleHealthCheck(domain.domain); setOpenMenuId(null) }}
className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-foreground-muted hover:text-foreground hover:bg-foreground/5 transition-colors"
>
<Shield className="w-4 h-4" />
Health Check
</button>
<button
onClick={() => { openEditModal(domain); setOpenMenuId(null) }}
className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-foreground-muted hover:text-foreground hover:bg-foreground/5 transition-colors"
>
<Edit2 className="w-4 h-4" />
Edit Details
</button>
<div className="my-1 border-t border-border/30" />
<Link
href={`/command/listings?domain=${encodeURIComponent(domain.domain)}`}
onClick={() => setOpenMenuId(null)}
className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-accent hover:bg-accent/5 transition-colors"
>
<Tag className="w-4 h-4" />
List for Sale
</Link>
<a
href={`https://${domain.domain}`}
target="_blank"
rel="noopener noreferrer"
onClick={() => setOpenMenuId(null)}
className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-foreground-muted hover:text-foreground hover:bg-foreground/5 transition-colors"
>
<ExternalLink className="w-4 h-4" />
Visit Website
</a>
<div className="my-1 border-t border-border/30" />
<button
onClick={() => { openSellModal(domain); setOpenMenuId(null) }}
className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-foreground-muted hover:text-foreground hover:bg-foreground/5 transition-colors"
>
<DollarSign className="w-4 h-4" />
Record Sale
</button>
<button
onClick={() => { handleDelete(domain); setOpenMenuId(null) }}
className="w-full flex items-center gap-3 px-4 py-2.5 text-sm text-red-400 hover:bg-red-400/5 transition-colors"
>
<Trash2 className="w-4 h-4" />
Remove
</button>
</div>
</>
)}
</div>
),
},