fix: Filter expired auctions on Acquire page, add live updates
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

This commit is contained in:
2025-12-14 21:15:21 +01:00
parent 71a94a765a
commit 3995c2d675
2 changed files with 134 additions and 171 deletions

View File

@ -187,11 +187,29 @@ export default function AcquirePage() {
}
}
// Trigger re-render every 30s to update live times
const [tick, setTick] = useState(0)
useEffect(() => {
const interval = setInterval(() => setTick(t => t + 1), 30000)
return () => clearInterval(interval)
}, [])
const displayAuctions = useMemo(() => {
const current = getCurrentAuctions()
if (isAuthenticated) return current
return current.filter(isVanityDomain)
}, [activeTab, allAuctions, endingSoon, hotAuctions, isAuthenticated])
const nowMs = Date.now()
// Filter out expired auctions
const activeAuctions = current.filter(auction => {
if (!auction.end_time) return true
const endMs = Date.parse(auction.end_time)
if (Number.isNaN(endMs)) return true
return endMs > (nowMs - 2000) // 2s grace
})
if (isAuthenticated) return activeAuctions
return activeAuctions.filter(isVanityDomain)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeTab, allAuctions, endingSoon, hotAuctions, isAuthenticated, tick])
const filteredAuctions = displayAuctions.filter(auction => {
if (searchQuery && !auction.domain.toLowerCase().includes(searchQuery.toLowerCase())) return false

View File

@ -139,6 +139,7 @@ export default function PortfolioPage() {
const [healthReports, setHealthReports] = useState<Record<number, DomainHealthReport>>({})
const [loadingHealth, setLoadingHealth] = useState<Record<number, boolean>>({})
const [togglingAlerts, setTogglingAlerts] = useState<Record<number, boolean>>({})
const [alertsEnabled, setAlertsEnabled] = useState<Record<number, boolean>>({})
const [showHealthDetail, setShowHealthDetail] = useState<number | null>(null)
const [showYieldModal, setShowYieldModal] = useState<PortfolioDomain | null>(null)
@ -219,18 +220,20 @@ export default function PortfolioPage() {
// ALERT HANDLERS
// ═══════════════════════════════════════════════════════════════════════════
const handleToggleEmailAlert = useCallback(async (domainId: number, currentValue: boolean) => {
const handleToggleEmailAlert = useCallback(async (domainId: number, _currentValue: boolean) => {
const currentEnabled = alertsEnabled[domainId] || false
setTogglingAlerts(prev => ({ ...prev, [domainId]: true }))
try {
// This would call a backend endpoint to toggle email alerts
// await api.updatePortfolioDomainAlerts(domainId, { email_alerts: !currentValue })
showToast(!currentValue ? 'Email alerts enabled' : 'Email alerts disabled', 'success')
// await api.updatePortfolioDomainAlerts(domainId, { email_alerts: !currentEnabled })
setAlertsEnabled(prev => ({ ...prev, [domainId]: !currentEnabled }))
showToast(!currentEnabled ? 'Alerts enabled' : 'Alerts disabled', 'success')
} catch {
showToast('Failed to update alert settings', 'error')
} finally {
setTogglingAlerts(prev => ({ ...prev, [domainId]: false }))
}
}, [showToast])
}, [showToast, alertsEnabled])
const handleToggleSmsAlert = useCallback(async (domainId: number, currentValue: boolean) => {
if (!canUseSmsAlerts) {
@ -517,8 +520,8 @@ export default function PortfolioPage() {
</div>
) : (
<div className="space-y-px bg-white/[0.04] border border-white/[0.08]">
{/* Desktop Table Header - Extended with Health & Alerts */}
<div className="hidden lg:grid grid-cols-[1fr_70px_80px_60px_60px_60px_50px_140px] gap-3 px-4 py-2.5 text-[10px] font-mono text-white/40 uppercase tracking-wider border-b border-white/[0.08]">
{/* Desktop Table Header - Matches Watchlist Style */}
<div className="hidden lg:grid grid-cols-[1fr_80px_90px_90px_60px_100px] gap-4 px-3 py-2 text-[10px] font-mono text-white/40 uppercase tracking-wider border-b border-white/[0.08]">
<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" />)}
@ -531,16 +534,11 @@ export default function PortfolioPage() {
Value
{sortField === 'value' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<button onClick={() => handleSort('roi')} className="flex items-center gap-1 justify-center hover:text-white/60">
ROI
{sortField === 'roi' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<button onClick={() => handleSort('renewal')} className="flex items-center gap-1 justify-center hover:text-white/60">
Expires
Expiry
{sortField === 'renewal' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<div className="text-center" title="Email & SMS Alerts">Alerts</div>
<div className="text-center" title="Yield Status (Coming Soon)">Yield</div>
<div className="text-center">Alert</div>
<div className="text-right">Actions</div>
</div>
@ -678,205 +676,152 @@ export default function PortfolioPage() {
</div>
</div>
{/* Desktop Row - Extended with Health, Alerts, Yield */}
<div className="hidden lg:grid grid-cols-[1fr_70px_80px_60px_60px_60px_50px_140px] gap-3 items-center px-4 py-3 hover:bg-white/[0.02] transition-colors">
{/* Desktop Row - Matches Watchlist Style */}
<div className="hidden lg:grid grid-cols-[1fr_80px_90px_90px_60px_100px] gap-4 items-center p-3 group">
{/* Domain Info */}
<div className="flex items-center gap-3 min-w-0 flex-1">
<div className={clsx(
"w-9 h-9 flex items-center justify-center border shrink-0",
"w-8 h-8 flex items-center justify-center border shrink-0",
domain.is_sold ? "bg-white/[0.02] border-white/[0.06]" :
domain.is_dns_verified ? "bg-accent/10 border-accent/20" : "bg-blue-400/10 border-blue-400/20"
)}>
{domain.is_sold ? <CheckCircle className="w-4 h-4 text-white/30" /> :
domain.is_dns_verified ? <ShieldCheck className="w-4 h-4 text-accent" /> : <ShieldAlert className="w-4 h-4 text-blue-400" />}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="text-sm font-bold text-white font-mono truncate">{domain.domain}</span>
<a href={`https://${domain.domain}`} target="_blank" rel="noopener noreferrer" className="opacity-0 group-hover:opacity-40 hover:!opacity-100 transition-opacity">
<ExternalLink className="w-3 h-3 text-white" />
</a>
</div>
<div className="flex items-center gap-1.5 mt-0.5">
<span className="text-[10px] font-mono text-white/30">{domain.registrar || '—'}</span>
{domain.is_sold && <span className="text-[9px] font-mono px-1.5 py-0.5 bg-white/5 text-white/40 uppercase">Sold</span>}
{!domain.is_sold && domain.is_dns_verified && <span className="text-[9px] font-mono px-1.5 py-0.5 bg-accent/10 text-accent uppercase">Verified</span>}
{!domain.is_sold && !domain.is_dns_verified && <span className="text-[9px] font-mono px-1.5 py-0.5 bg-blue-400/10 text-blue-400 uppercase">Unverified</span>}
<div className="min-w-0">
<div className="text-sm font-bold text-white font-mono truncate group-hover:text-accent transition-colors">{domain.domain}</div>
<div className="text-[10px] font-mono text-white/30">
{domain.registrar || 'Unknown'}
{domain.is_sold && <span className="ml-2 text-white/20"> Sold</span>}
{!domain.is_sold && domain.is_dns_verified && <span className="ml-2 text-accent"> Verified</span>}
</div>
</div>
<a href={`https://${domain.domain}`} target="_blank" rel="noopener noreferrer" className="opacity-0 group-hover:opacity-50 hover:!opacity-100 transition-opacity ml-2">
<ExternalLink className="w-3.5 h-3.5 text-white/40" />
</a>
</div>
{/* Health - Like Watchlist */}
<button
onClick={() => {
if (domain.is_dns_verified && !domain.is_sold) {
const health = healthReports[domain.id]
if (health) setShowHealthDetail(domain.id)
else handleRefreshHealth(domain.id, domain.domain)
}
}}
disabled={domain.is_sold || !domain.is_dns_verified}
className="w-20 flex items-center gap-1.5 hover:opacity-80 transition-opacity shrink-0"
>
{domain.is_sold ? (
<span className="text-white/20 text-xs"></span>
) : !domain.is_dns_verified ? (
<span className="text-white/20 text-[10px] font-mono">Verify</span>
) : loadingHealth[domain.id] ? (
<Loader2 className="w-3.5 h-3.5 animate-spin text-white/30" />
) : (() => {
const health = healthReports[domain.id]
if (!health) return <Activity className="w-3.5 h-3.5 text-white/30" />
const config = healthConfig[health.status]
return (
<>
<Activity className={clsx("w-3.5 h-3.5", config.color)} />
<span className={clsx("text-xs font-mono", config.color)}>{config.label}</span>
</>
)
})()}
</button>
{/* Value + ROI combined */}
<div className="w-24 text-right shrink-0">
<div className="text-sm font-bold font-mono text-accent tabular-nums">{formatCurrency(domain.estimated_value)}</div>
<div className={clsx("text-[10px] font-mono tabular-nums", roiPositive ? "text-accent/60" : "text-rose-400/60")}>
{roiPositive ? '+' : ''}{formatROI(domain.roi)}
</div>
</div>
{/* Health Status - NEW */}
<div className="flex justify-center">
{domain.is_sold ? (
<span className="text-white/20"></span>
) : domain.is_dns_verified ? (
(() => {
const health = healthReports[domain.id]
const isLoading = loadingHealth[domain.id]
if (isLoading) {
return <Loader2 className="w-4 h-4 text-white/30 animate-spin" />
}
if (!health) {
return (
<button
onClick={() => handleRefreshHealth(domain.id, domain.domain)}
className="text-white/30 hover:text-white"
title="Run health check"
>
<Activity className="w-4 h-4" />
</button>
)
}
const config = healthConfig[health.status]
return (
<button
onClick={() => setShowHealthDetail(domain.id)}
className={clsx("flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-mono border", config.bg, config.color)}
title={`Score: ${health.score}/100`}
>
{health.status === 'healthy' ? <Wifi className="w-3 h-3" /> :
health.status === 'critical' ? <WifiOff className="w-3 h-3" /> :
<AlertCircle className="w-3 h-3" />}
{health.score}
</button>
)
})()
) : (
<span className="text-white/20 text-[10px] font-mono">Verify first</span>
)}
</div>
{/* Estimated Value */}
<div className="text-right text-sm font-bold font-mono text-accent tabular-nums">
{formatCurrency(domain.estimated_value)}
</div>
{/* ROI Badge */}
<div className="flex justify-center">
<span className={clsx(
"inline-flex items-center gap-0.5 text-[10px] font-mono font-bold px-1 py-0.5 tabular-nums",
roiPositive ? "text-accent bg-accent/10" : "text-rose-400 bg-rose-400/10"
)}>
{roiPositive ? <TrendingUp className="w-3 h-3" /> : <TrendingDown className="w-3 h-3" />}
{formatROI(domain.roi)}
</span>
</div>
{/* Renewal/Expiry */}
<div className="text-center text-[10px] font-mono tabular-nums">
{/* Expiry - Like Watchlist */}
<div className="w-24 text-xs font-mono text-white/50 shrink-0">
{domain.is_sold ? (
<span className="text-white/20"></span>
) : isRenewingSoon ? (
<span className="text-orange-400 font-bold">{daysUntilRenewal}d</span>
) : daysUntilRenewal ? (
<span className="text-white/40">{daysUntilRenewal}d</span>
<span>{daysUntilRenewal}d</span>
) : (
<span className="text-white/20"></span>
formatDate(domain.renewal_date)
)}
</div>
{/* Alerts - Email & SMS - NEW */}
<div className="flex justify-center gap-1">
{domain.is_sold ? (
<span className="text-white/20"></span>
) : (
<>
<button
onClick={() => handleToggleEmailAlert(domain.id, false)}
disabled={togglingAlerts[domain.id]}
className="w-6 h-6 flex items-center justify-center text-white/30 hover:text-accent border border-transparent hover:border-accent/20 transition-all"
title="Email alerts"
>
<Mail className="w-3 h-3" />
</button>
<button
onClick={() => handleToggleSmsAlert(domain.id, false)}
disabled={togglingAlerts[domain.id] || !canUseSmsAlerts}
className={clsx(
"w-6 h-6 flex items-center justify-center border border-transparent transition-all",
canUseSmsAlerts
? "text-white/30 hover:text-accent hover:border-accent/20"
: "text-white/10 cursor-not-allowed"
)}
title={canUseSmsAlerts ? "SMS alerts" : "SMS alerts require Tycoon"}
>
{canUseSmsAlerts ? <Smartphone className="w-3 h-3" /> : <Lock className="w-3 h-3" />}
</button>
</>
{/* Alert Toggle - Like Watchlist Bell */}
<button
onClick={() => handleToggleEmailAlert(domain.id, false)}
disabled={togglingAlerts[domain.id] || domain.is_sold}
className={clsx(
"w-8 h-8 flex items-center justify-center border transition-colors shrink-0",
domain.is_sold
? "text-white/10 border-white/5"
: alertsEnabled[domain.id]
? "text-accent border-accent/20 bg-accent/10"
: "text-white/20 border-white/10 hover:text-white/40"
)}
</div>
{/* Yield Status - Phase 2 - NEW */}
<div className="flex justify-center">
{domain.is_sold ? (
<span className="text-white/20"></span>
) : !canUseYield ? (
<Link href="/pricing" className="text-white/20 hover:text-white/40" title="Upgrade to use Yield">
<Lock className="w-3 h-3" />
</Link>
) : domain.is_dns_verified ? (
<button
onClick={() => setShowYieldModal(domain)}
className="px-1.5 py-0.5 text-[9px] font-mono uppercase border border-white/10 text-white/40 hover:border-accent/20 hover:text-accent transition-all"
title="Activate Yield (Coming Soon)"
>
Activate
</button>
>
{togglingAlerts[domain.id] ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" />
) : alertsEnabled[domain.id] ? (
<Bell className="w-3.5 h-3.5" />
) : (
<span className="text-white/20 text-[9px]"></span>
<BellOff className="w-3.5 h-3.5" />
)}
</div>
</button>
{/* Actions - Better organized */}
<div className="flex items-center gap-1.5 justify-end">
{/* Actions - Like Watchlist */}
<div className="flex items-center gap-1 shrink-0 opacity-50 group-hover:opacity-100 transition-opacity">
{/* Primary Action - Verify or Sell */}
{!domain.is_sold && (
domain.is_dns_verified ? (
canListForSale && (
<Link
<Link
href={`/terminal/listing?domain=${encodeURIComponent(domain.domain)}`}
className="h-7 px-2.5 flex items-center gap-1.5 text-amber-400 text-[10px] font-bold uppercase tracking-wide border border-amber-400/30 bg-amber-400/10 hover:bg-amber-400/20 transition-all"
>
<Tag className="w-3 h-3" />Sell
</Link>
className="h-7 px-3 bg-amber-400/10 text-amber-400 text-xs font-bold flex items-center gap-1.5 border border-amber-400/20 hover:bg-amber-400/20 transition-colors"
>
Sell
<Tag className="w-3 h-3" />
</Link>
)
) : (
<button
onClick={() => setVerifyingDomain(domain)}
className="h-7 px-2.5 flex items-center gap-1.5 text-blue-400 text-[10px] font-bold uppercase tracking-wide border border-blue-400/30 bg-blue-400/10 hover:bg-blue-400/20 transition-all"
className="h-7 px-3 bg-blue-400/10 text-blue-400 text-xs font-bold flex items-center gap-1.5 border border-blue-400/20 hover:bg-blue-400/20 transition-colors"
>
<ShieldAlert className="w-3 h-3" />Verify
Verify
</button>
)
)}
{/* Secondary Actions - Icon Buttons */}
<div className="flex items-center gap-0.5 ml-1">
<button
onClick={() => setSelectedDomain(domain)}
title="Edit Details"
className="w-7 h-7 flex items-center justify-center text-white/30 hover:text-white border border-transparent hover:border-white/10 hover:bg-white/5 transition-all rounded-sm"
onClick={() => setSelectedDomain(domain)}
className="w-7 h-7 flex items-center justify-center text-white/20 hover:text-white border border-white/10 hover:bg-white/5 transition-all"
>
<Edit3 className="w-3.5 h-3.5" />
<Edit3 className="w-3.5 h-3.5" />
</button>
<button
onClick={() => handleRefreshValue(domain.id)}
disabled={refreshingId === domain.id}
title="Refresh Valuation"
className="w-7 h-7 flex items-center justify-center text-white/30 hover:text-accent border border-transparent hover:border-accent/20 hover:bg-accent/5 transition-all rounded-sm disabled:opacity-30"
>
<RefreshCw className={clsx("w-3.5 h-3.5", refreshingId === domain.id && "animate-spin text-accent")} />
</button>
<button
onClick={() => handleDelete(domain.id, domain.domain)}
disabled={deletingId === domain.id}
title="Delete Domain"
className="w-7 h-7 flex items-center justify-center text-white/30 hover:text-rose-400 border border-transparent hover:border-rose-400/20 hover:bg-rose-500/10 transition-all rounded-sm disabled:opacity-30"
>
{deletingId === domain.id ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Trash2 className="w-3.5 h-3.5" />}
onClick={() => handleRefreshValue(domain.id)}
disabled={refreshingId === domain.id}
className="w-7 h-7 flex items-center justify-center text-white/20 hover:text-white border border-white/10 hover:bg-white/5 transition-all"
>
<RefreshCw className={clsx("w-3.5 h-3.5", refreshingId === domain.id && "animate-spin")} />
</button>
<button
onClick={() => handleDelete(domain.id, domain.domain)}
disabled={deletingId === domain.id}
className="w-7 h-7 flex items-center justify-center text-white/20 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-3.5 h-3.5 animate-spin" />
) : (
<Trash2 className="w-3.5 h-3.5" />
)}
</button>
</div>
</div>
</div>
</div>