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:
@ -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>
|
||||
),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user