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