'use client' import { useEffect, useState, useMemo, useCallback } from 'react' import { useStore } from '@/lib/store' import { api, PortfolioDomain, PortfolioSummary } from '@/lib/api' import { Sidebar } from '@/components/Sidebar' import { Toast, useToast } from '@/components/Toast' import { Plus, Trash2, RefreshCw, Loader2, Briefcase, X, Target, ExternalLink, Gavel, TrendingUp, Menu, Settings, ShieldCheck, LogOut, Crown, Tag, Zap, Eye, ChevronUp, ChevronDown, DollarSign, Calendar, Edit3, CheckCircle, AlertCircle, Copy, Check, ArrowUpRight, Navigation, Coins } from 'lucide-react' import clsx from 'clsx' import Link from 'next/link' import Image from 'next/image' // ============================================================================ // HELPERS // ============================================================================ function getDaysUntilRenewal(renewalDate: string | null): number | null { if (!renewalDate) return null const renDate = new Date(renewalDate) const now = new Date() const diffTime = renDate.getTime() - now.getTime() return Math.ceil(diffTime / (1000 * 60 * 60 * 24)) } function formatDate(date: string | null): string { if (!date) return '—' return new Date(date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) } function formatCurrency(value: number | null): string { if (value === null || value === undefined) return '—' return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(value) } function formatROI(roi: number | null): string { if (roi === null || roi === undefined) return '—' const sign = roi >= 0 ? '+' : '' return `${sign}${roi.toFixed(0)}%` } // ============================================================================ // MAIN PAGE // ============================================================================ export default function PortfolioPage() { const { subscription, user, logout, checkAuth } = useStore() const { toast, showToast, hideToast } = useToast() const [domains, setDomains] = useState([]) const [summary, setSummary] = useState(null) const [loading, setLoading] = useState(true) const [refreshingId, setRefreshingId] = useState(null) const [deletingId, setDeletingId] = useState(null) const [showAddModal, setShowAddModal] = useState(false) const [verifyingDomain, setVerifyingDomain] = useState(null) const [filter, setFilter] = useState<'all' | 'active' | 'sold'>('all') // Sorting const [sortField, setSortField] = useState<'domain' | 'value' | 'roi' | 'renewal'>('domain') const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc') // Mobile Menu & Navigation Drawer const [menuOpen, setMenuOpen] = useState(false) const [navDrawerOpen, setNavDrawerOpen] = useState(false) const tier = subscription?.tier || 'scout' const isScout = tier === 'scout' useEffect(() => { checkAuth() }, [checkAuth]) const loadData = useCallback(async () => { setLoading(true) try { const [domainsData, summaryData] = await Promise.all([ api.getPortfolio(), api.getPortfolioSummary() ]) setDomains(domainsData) setSummary(summaryData) } catch (err) { console.error('Failed to load portfolio:', err) } finally { setLoading(false) } }, []) useEffect(() => { loadData() }, [loadData]) // Stats const stats = useMemo(() => { const active = domains.filter(d => !d.is_sold).length const sold = domains.filter(d => d.is_sold).length const verified = domains.filter(d => d.is_dns_verified && !d.is_sold).length const renewingSoon = domains.filter(d => { if (d.is_sold || !d.renewal_date) return false const days = getDaysUntilRenewal(d.renewal_date) return days !== null && days <= 30 && days > 0 }).length return { total: domains.length, active, sold, verified, renewingSoon } }, [domains]) // Filtered & Sorted const filteredDomains = useMemo(() => { let filtered = domains.filter(d => { if (filter === 'active') return !d.is_sold if (filter === 'sold') return d.is_sold return true }) const mult = sortDirection === 'asc' ? 1 : -1 filtered.sort((a, b) => { switch (sortField) { case 'domain': return mult * a.domain.localeCompare(b.domain) case 'value': return mult * ((a.estimated_value || 0) - (b.estimated_value || 0)) case 'roi': return mult * ((a.roi || 0) - (b.roi || 0)) case 'renewal': const aDate = a.renewal_date ? new Date(a.renewal_date).getTime() : Infinity const bDate = b.renewal_date ? new Date(b.renewal_date).getTime() : Infinity return mult * (aDate - bDate) default: return 0 } }) return filtered }, [domains, filter, sortField, sortDirection]) const handleSort = useCallback((field: typeof sortField) => { if (sortField === field) { setSortDirection(d => d === 'asc' ? 'desc' : 'asc') } else { setSortField(field) setSortDirection('asc') } }, [sortField]) const handleRefreshValue = useCallback(async (id: number) => { setRefreshingId(id) try { const updated = await api.refreshDomainValue(id) setDomains(prev => prev.map(d => d.id === id ? updated : d)) showToast('Value updated', 'success') } catch { showToast('Update failed', 'error') } finally { setRefreshingId(null) } }, [showToast]) const handleDelete = useCallback(async (id: number, name: string) => { if (!confirm(`Remove ${name} from portfolio?`)) return setDeletingId(id) try { await api.deletePortfolioDomain(id) setDomains(prev => prev.filter(d => d.id !== id)) showToast('Domain removed', 'success') loadData() } catch { showToast('Failed', 'error') } finally { setDeletingId(null) } }, [showToast, loadData]) const tierName = subscription?.tier_name || subscription?.tier || 'Scout' const TierIcon = tierName === 'Tycoon' ? Crown : tierName === 'Trader' ? TrendingUp : Zap const mobileNavItems = [ { href: '/terminal/radar', label: 'Radar', icon: Target, active: false }, { href: '/terminal/market', label: 'Market', icon: Gavel, active: false }, { href: '/terminal/watchlist', label: 'Watch', icon: Eye, active: false }, { href: '/terminal/portfolio', label: 'Portfolio', icon: Briefcase, active: true }, ] const drawerNavSections = [ { title: 'Discover', items: [ { href: '/terminal/radar', label: 'Radar', icon: Target }, { href: '/terminal/market', label: 'Market', icon: Gavel }, { href: '/terminal/intel', label: 'Intel', icon: TrendingUp }, ]}, { title: 'Manage', items: [ { href: '/terminal/watchlist', label: 'Watchlist', icon: Eye }, { href: '/terminal/portfolio', label: 'Portfolio', icon: Briefcase, active: true }, { href: '/terminal/sniper', label: 'Sniper', icon: Target }, ]}, { title: 'Monetize', items: [ { href: '/terminal/yield', label: 'Yield', icon: Coins, isNew: true }, { href: '/terminal/listing', label: 'For Sale', icon: Tag }, ]} ] return (
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* MOBILE HEADER */} {/* ═══════════════════════════════════════════════════════════════════════ */}
Portfolio
{stats.total} domains
{/* Stats Grid */}
{stats.active}
Active
{formatCurrency(summary?.total_value || 0).replace('$', '').slice(0, 6)}
Value
= 0 ? "text-accent" : "text-rose-400")}> {formatROI(summary?.overall_roi || 0)}
ROI
{stats.verified}
Verified
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* DESKTOP HEADER */} {/* ═══════════════════════════════════════════════════════════════════════ */}
Domain Assets

Portfolio {stats.total}

Manage your domain assets. Track value, verify ownership, and list for sale.

{formatCurrency(summary?.total_invested || 0)}
Invested
{formatCurrency(summary?.total_value || 0)}
Value
= 0 ? "text-accent" : "text-rose-400")}> {formatROI(summary?.overall_roi || 0)}
ROI
{stats.verified}
Verified
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* FILTERS + ADD BUTTON */} {/* ═══════════════════════════════════════════════════════════════════════ */}
{/* Filters */}
{[ { value: 'all', label: 'All', count: stats.total }, { value: 'active', label: 'Active', count: stats.active }, { value: 'sold', label: 'Sold', count: stats.sold }, ].map((item) => ( ))}
{/* Add Domain Button */}
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* DOMAIN LIST */} {/* ═══════════════════════════════════════════════════════════════════════ */}
{loading ? (
) : !filteredDomains.length ? (

No domains in your portfolio

Add your first domain to start tracking

) : (
{/* Desktop Table Header */}
Status
Actions
{filteredDomains.map((domain) => { const daysUntilRenewal = getDaysUntilRenewal(domain.renewal_date) const isRenewingSoon = daysUntilRenewal !== null && daysUntilRenewal <= 30 && daysUntilRenewal > 0 const roiPositive = (domain.roi || 0) >= 0 return (
{/* Mobile Row */}
{domain.is_sold ? : }
{domain.domain}
{domain.registrar && ( {domain.registrar} )} {domain.is_dns_verified && ( Verified )}
{formatCurrency(domain.estimated_value)}
{formatROI(domain.roi)}
{/* Mobile Actions */} {!domain.is_sold && (
{daysUntilRenewal !== null ? ( {daysUntilRenewal}d ) : '—'}
{!domain.is_dns_verified && ( )}
)}
{/* Desktop Row */}
{/* Domain */}
{domain.is_sold ? : }
{domain.domain}
{domain.registrar || 'Unknown'}
{/* Value */}
{formatCurrency(domain.estimated_value)}
{domain.purchase_price && (
Cost: {formatCurrency(domain.purchase_price)}
)}
{/* ROI */}
{formatROI(domain.roi)}
{/* Expires */}
{daysUntilRenewal !== null ? ( {daysUntilRenewal}d ) : ( )}
{/* Status */}
{domain.is_sold ? ( Sold ) : domain.is_dns_verified ? ( Verified ) : ( )}
{/* Actions */}
{!domain.is_sold && domain.is_dns_verified && ( Sell )}
) })}
)}
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* MOBILE BOTTOM NAVIGATION */} {/* ═══════════════════════════════════════════════════════════════════════ */} {/* Navigation Drawer */} {navDrawerOpen && (
setNavDrawerOpen(false)} />
Pounce Terminal
{drawerNavSections.map((section) => (
{section.title}
{section.items.map((item) => ( setNavDrawerOpen(false)} className={clsx( "flex items-center gap-3 px-3 py-2.5 transition-colors", item.active ? "bg-accent/10 text-accent" : "text-white/60 hover:text-white hover:bg-white/[0.02]" )} > {item.label} {item.isNew && ( New )} ))}
))}
setNavDrawerOpen(false)} className="flex items-center gap-3 px-3 py-2.5 text-white/60 hover:text-white transition-colors" > Settings
)}
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* ADD DOMAIN MODAL */} {/* ═══════════════════════════════════════════════════════════════════════ */} {showAddModal && ( setShowAddModal(false)} onSuccess={() => { setShowAddModal(false); loadData() }} showToast={showToast} /> )} {/* ═══════════════════════════════════════════════════════════════════════ */} {/* DNS VERIFICATION MODAL */} {/* ═══════════════════════════════════════════════════════════════════════ */} {verifyingDomain && ( setVerifyingDomain(null)} onVerified={() => { setVerifyingDomain(null); loadData() }} showToast={showToast} /> )} {toast && }
) } // ============================================================================ // ADD DOMAIN MODAL // ============================================================================ function AddDomainModal({ onClose, onSuccess, showToast }: { onClose: () => void onSuccess: () => void showToast: (msg: string, type: 'success' | 'error') => void }) { const [domain, setDomain] = useState('') const [purchasePrice, setPurchasePrice] = useState('') const [registrar, setRegistrar] = useState('') const [loading, setLoading] = useState(false) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!domain.trim()) return setLoading(true) try { await api.addPortfolioDomain({ domain: domain.trim().toLowerCase(), purchase_price: purchasePrice ? parseFloat(purchasePrice) : undefined, registrar: registrar || undefined, }) showToast('Domain added successfully', 'success') onSuccess() } catch (err: any) { showToast(err.message || 'Failed to add domain', 'error') } finally { setLoading(false) } } return (

Add Domain

setDomain(e.target.value)} placeholder="example.com" className="w-full px-4 py-3 bg-white/[0.02] border border-white/[0.08] text-white text-sm font-mono placeholder:text-white/20 focus:border-accent focus:outline-none" autoFocus />
setPurchasePrice(e.target.value)} placeholder="100" className="w-full px-4 py-3 bg-white/[0.02] border border-white/[0.08] text-white text-sm font-mono placeholder:text-white/20 focus:border-accent focus:outline-none" />
setRegistrar(e.target.value)} placeholder="Namecheap, GoDaddy, etc." className="w-full px-4 py-3 bg-white/[0.02] border border-white/[0.08] text-white text-sm font-mono placeholder:text-white/20 focus:border-accent focus:outline-none" />
) } // ============================================================================ // DNS VERIFICATION MODAL // ============================================================================ function DNSVerificationModal({ domain, onClose, onVerified, showToast }: { domain: PortfolioDomain onClose: () => void onVerified: () => void showToast: (msg: string, type: 'success' | 'error') => void }) { const [loading, setLoading] = useState(true) const [checking, setChecking] = useState(false) const [verificationData, setVerificationData] = useState<{ verification_code: string dns_record_name: string dns_record_value: string instructions: string } | null>(null) const [copied, setCopied] = useState(null) useEffect(() => { loadVerificationData() }, [domain.id]) const loadVerificationData = async () => { try { const data = await api.startPortfolioDnsVerification(domain.id) setVerificationData(data) } catch (err: any) { showToast(err.message || 'Failed to start verification', 'error') onClose() } finally { setLoading(false) } } const handleCheck = async () => { setChecking(true) try { const result = await api.checkPortfolioDnsVerification(domain.id) if (result.verified) { showToast('Domain verified successfully!', 'success') onVerified() } else { showToast(result.message || 'Verification pending', 'error') } } catch (err: any) { showToast(err.message || 'Verification failed', 'error') } finally { setChecking(false) } } const copyToClipboard = async (text: string, field: string) => { try { await navigator.clipboard.writeText(text) setCopied(field) setTimeout(() => setCopied(null), 2000) } catch {} } return (

Verify Ownership

Add a DNS TXT record to prove you own {domain.domain}

{loading ? (
) : verificationData ? (
{verificationData.dns_record_name}
{verificationData.verification_code}
💡 DNS changes can take 1-5 minutes to propagate. If verification fails, wait a moment and try again.
) : (
Failed to load verification data
)}
) }