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 { useStore } from '@/lib/store'
import { api, PortfolioDomain, PortfolioSummary, DomainValuation, DomainHealthReport, HealthStatus } from '@/lib/api' import { api, PortfolioDomain, PortfolioSummary, DomainValuation, DomainHealthReport, HealthStatus } from '@/lib/api'
import { CommandCenterLayout } from '@/components/CommandCenterLayout' 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 { Toast, useToast } from '@/components/Toast'
import { import {
Plus, Plus,
@ -13,19 +13,17 @@ import {
DollarSign, DollarSign,
Calendar, Calendar,
Building, Building,
RefreshCw,
Loader2, Loader2,
TrendingUp,
Sparkles,
ArrowUpRight, ArrowUpRight,
X, X,
Briefcase, Briefcase,
PiggyBank,
ShoppingCart, ShoppingCart,
Activity, Activity,
Shield, Shield,
AlertTriangle, AlertTriangle,
Tag, Tag,
MoreVertical,
ExternalLink,
} from 'lucide-react' } from 'lucide-react'
// Health status configuration // Health status configuration
@ -68,6 +66,9 @@ export default function PortfolioPage() {
const [loadingHealth, setLoadingHealth] = useState<Record<string, boolean>>({}) const [loadingHealth, setLoadingHealth] = useState<Record<string, boolean>>({})
const [selectedHealthDomain, setSelectedHealthDomain] = useState<string | null>(null) const [selectedHealthDomain, setSelectedHealthDomain] = useState<string | null>(null)
// Dropdown menu state
const [openMenuId, setOpenMenuId] = useState<number | null>(null)
const [addForm, setAddForm] = useState({ const [addForm, setAddForm] = useState({
domain: '', domain: '',
purchase_price: '', purchase_price: '',
@ -261,10 +262,15 @@ export default function PortfolioPage() {
// Dynamic subtitle // Dynamic subtitle
const getSubtitle = () => { const getSubtitle = () => {
if (loading) return 'Loading your portfolio...' if (loading) return 'Loading your portfolio...'
if (portfolio.length === 0) return 'Start tracking your domain investments' if (portfolio.length === 0) return 'Start tracking your domains'
const profit = summary?.total_profit || 0 const expiringSoon = portfolio.filter(d => {
if (profit > 0) return `${portfolio.length} domains • +$${profit.toLocaleString()} profit` if (!d.renewal_date) return false
if (profit < 0) return `${portfolio.length} domains • -$${Math.abs(profit).toLocaleString()} loss` 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' : ''}` 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} />} {toast && <Toast message={toast.message} type={toast.type} onClose={hideToast} />}
<PageContainer> <PageContainer>
{/* Summary Stats */} {/* Summary Stats - Only reliable data */}
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4"> <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 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 <StatCard
title="Profit/Loss" title="Expiring Soon"
value={`${(summary?.total_profit || 0) >= 0 ? '+' : ''}$${(summary?.total_profit || 0).toLocaleString()}`} value={portfolio.filter(d => {
icon={PiggyBank} if (!d.renewal_date) return false
accent={(summary?.total_profit || 0) >= 0} 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> </div>
{!canAddMore && ( {!canAddMore && (
@ -340,48 +353,53 @@ export default function PortfolioPage() {
), ),
}, },
{ {
key: 'purchase', key: 'added',
header: 'Purchase', header: 'Added',
hideOnMobile: true, hideOnMobile: true,
hideOnTablet: true,
render: (domain) => ( render: (domain) => (
<div> <span className="text-sm text-foreground-muted">
{domain.purchase_price && ( {domain.purchase_date
<span className="font-medium text-foreground">${domain.purchase_price.toLocaleString()}</span> ? new Date(domain.purchase_date).toLocaleDateString()
)} : new Date(domain.created_at).toLocaleDateString()
{domain.purchase_date && ( }
<p className="text-xs text-foreground-muted flex items-center gap-1 mt-0.5"> </span>
<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>
)
), ),
}, },
{ {
key: 'renewal', key: 'renewal',
header: 'Renewal', header: 'Expires',
hideOnMobile: true, hideOnMobile: true,
hideOnTablet: true, render: (domain) => {
render: (domain) => ( if (!domain.renewal_date) {
domain.renewal_date ? ( return <span className="text-foreground-subtle">—</span>
<span className="text-sm text-foreground-muted"> }
{new Date(domain.renewal_date).toLocaleDateString()} const days = Math.ceil((new Date(domain.renewal_date).getTime() - Date.now()) / (1000 * 60 * 60 * 24))
</span> const isExpiringSoon = days <= 30 && days > 0
) : ( const isExpired = days <= 0
<span className="text-foreground-subtle">—</span> 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', key: 'health',
@ -424,51 +442,77 @@ export default function PortfolioPage() {
header: '', header: '',
align: 'right', align: 'right',
render: (domain) => ( render: (domain) => (
<div className="flex items-center gap-1 justify-end"> <div className="relative">
<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>
<button <button
onClick={(e) => { e.stopPropagation(); openSellModal(domain) }} onClick={(e) => {
className="px-3 py-2 text-xs font-medium text-foreground-muted hover:bg-foreground/5 rounded-lg transition-colors" e.stopPropagation()
title="Record a completed sale (for P&L tracking)" 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> </button>
<TableActionButton
icon={Trash2} {openMenuId === domain.id && (
onClick={() => handleDelete(domain)} <>
variant="danger" {/* Backdrop */}
title="Remove" <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> </div>
), ),
}, },