'use client' import { useEffect, useState, useMemo, useCallback, memo } from 'react' import { useStore } from '@/lib/store' import { api, DomainHealthReport, HealthStatus } from '@/lib/api' import { TerminalLayout } from '@/components/TerminalLayout' import { Toast, useToast } from '@/components/Toast' import { Plus, Trash2, RefreshCw, Loader2, Bell, BellOff, Eye, Sparkles, ArrowUpRight, X, Activity, Shield, AlertTriangle, ShoppingCart, HelpCircle, Search, Globe, Clock, Calendar, ArrowRight, CheckCircle2, XCircle, Wifi, Lock, TrendingDown, Zap, Diamond } from 'lucide-react' import clsx from 'clsx' import Link from 'next/link' // ============================================================================ // SHARED COMPONENTS (Matched to Market/Intel/Radar) // ============================================================================ const Tooltip = memo(({ children, content }: { children: React.ReactNode; content: string }) => (
{children}
{content}
)) Tooltip.displayName = 'Tooltip' const StatCard = memo(({ label, value, subValue, icon: Icon, highlight, trend }: { label: string value: string | number subValue?: string icon: any highlight?: boolean trend?: 'up' | 'down' | 'neutral' | 'active' }) => (
{label}
{value} {subValue && {subValue}}
{highlight && (
● LIVE
)}
)) StatCard.displayName = 'StatCard' // Health status badge configuration const healthStatusConfig: Record = { healthy: { label: 'Online', color: 'text-emerald-400', bgColor: 'bg-emerald-500/10 border-emerald-500/20', icon: Activity, description: 'Domain is active and reachable', }, weakening: { label: 'Issues', color: 'text-amber-400', bgColor: 'bg-amber-500/10 border-amber-500/20', icon: AlertTriangle, description: 'Warning signs detected', }, parked: { label: 'Parked', color: 'text-blue-400', bgColor: 'bg-blue-500/10 border-blue-500/20', icon: ShoppingCart, description: 'Domain is parked/for sale', }, critical: { label: 'Critical', color: 'text-rose-400', bgColor: 'bg-rose-500/10 border-rose-500/20', icon: AlertTriangle, description: 'Domain may be dropping soon', }, unknown: { label: 'Unknown', color: 'text-zinc-400', bgColor: 'bg-zinc-800 border-zinc-700', icon: HelpCircle, description: 'Health check pending', }, } type FilterTab = 'all' | 'available' | 'expiring' | 'critical' // ============================================================================ // HELPER FUNCTIONS // ============================================================================ function getDaysUntilExpiry(expirationDate: string | null): number | null { if (!expirationDate) return null const expDate = new Date(expirationDate) const now = new Date() const diffTime = expDate.getTime() - now.getTime() return Math.ceil(diffTime / (1000 * 60 * 60 * 24)) } function formatExpiryDate(expirationDate: string | null): string { if (!expirationDate) return '—' return new Date(expirationDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) } function getTimeAgo(date: string | null): string { if (!date) return 'Never' const now = new Date() const past = new Date(date) const diffMs = now.getTime() - past.getTime() const diffMins = Math.floor(diffMs / 60000) const diffHours = Math.floor(diffMs / 3600000) const diffDays = Math.floor(diffMs / 86400000) if (diffMins < 1) return 'Just now' if (diffMins < 60) return `${diffMins}m ago` if (diffHours < 24) return `${diffHours}h ago` if (diffDays < 7) return `${diffDays}d ago` return formatExpiryDate(date) } // ============================================================================ // MAIN PAGE // ============================================================================ export default function WatchlistPage() { const { domains, addDomain, deleteDomain, refreshDomain, updateDomain, subscription } = useStore() const { toast, showToast, hideToast } = useToast() const [newDomain, setNewDomain] = useState('') const [adding, setAdding] = useState(false) const [refreshingId, setRefreshingId] = useState(null) const [deletingId, setDeletingId] = useState(null) const [togglingNotifyId, setTogglingNotifyId] = useState(null) const [filterTab, setFilterTab] = useState('all') const [searchQuery, setSearchQuery] = useState('') // Health check state const [healthReports, setHealthReports] = useState>({}) const [loadingHealth, setLoadingHealth] = useState>({}) const [selectedHealthDomainId, setSelectedHealthDomainId] = useState(null) // Memoized stats const stats = useMemo(() => { const available = domains?.filter(d => d.is_available) || [] const expiringSoon = domains?.filter(d => { if (d.is_available || !d.expiration_date) return false const days = getDaysUntilExpiry(d.expiration_date) return days !== null && days <= 30 && days > 0 }) || [] const critical = Object.values(healthReports).filter(h => h.status === 'critical').length return { total: domains?.length || 0, available: available.length, expiringSoon: expiringSoon.length, critical, limit: subscription?.domain_limit || 5, } }, [domains, subscription?.domain_limit, healthReports]) const canAddMore = stats.total < stats.limit || stats.limit === -1 // Memoized filtered domains const filteredDomains = useMemo(() => { if (!domains) return [] return domains.filter(domain => { // Search filter if (searchQuery && !domain.name.toLowerCase().includes(searchQuery.toLowerCase())) { return false } // Tab filter switch (filterTab) { case 'available': return domain.is_available case 'expiring': if (domain.is_available || !domain.expiration_date) return false const days = getDaysUntilExpiry(domain.expiration_date) return days !== null && days <= 30 && days > 0 case 'critical': const health = healthReports[domain.id] return health?.status === 'critical' || health?.status === 'weakening' default: return true } }).sort((a, b) => { // Sort available first, then by expiry date if (a.is_available && !b.is_available) return -1 if (!a.is_available && b.is_available) return 1 // Then by expiry (soonest first) const daysA = getDaysUntilExpiry(a.expiration_date) const daysB = getDaysUntilExpiry(b.expiration_date) if (daysA !== null && daysB !== null) return daysA - daysB if (daysA !== null) return -1 if (daysB !== null) return 1 return a.name.localeCompare(b.name) }) }, [domains, searchQuery, filterTab, healthReports]) // Callbacks const handleAddDomain = useCallback(async (e: React.FormEvent) => { e.preventDefault() if (!newDomain.trim()) return setAdding(true) try { await addDomain(newDomain.trim()) setNewDomain('') showToast(`Added ${newDomain.trim()} to watchlist`, 'success') } catch (err: any) { showToast(err.message || 'Failed to add domain', 'error') } finally { setAdding(false) } }, [newDomain, addDomain, showToast]) const handleRefresh = useCallback(async (id: number) => { setRefreshingId(id) try { await refreshDomain(id) showToast('Domain status refreshed', 'success') } catch (err: any) { showToast(err.message || 'Failed to refresh', 'error') } finally { setRefreshingId(null) } }, [refreshDomain, showToast]) const handleDelete = useCallback(async (id: number, name: string) => { if (!confirm(`Remove ${name} from your watchlist?`)) return setDeletingId(id) try { await deleteDomain(id) showToast(`Removed ${name} from watchlist`, 'success') } catch (err: any) { showToast(err.message || 'Failed to remove', 'error') } finally { setDeletingId(null) } }, [deleteDomain, showToast]) const handleToggleNotify = useCallback(async (id: number, currentState: boolean) => { setTogglingNotifyId(id) try { await api.updateDomainNotify(id, !currentState) // Instant optimistic update updateDomain(id, { notify_on_available: !currentState }) showToast(!currentState ? 'Notifications enabled' : 'Notifications disabled', 'success') } catch (err: any) { showToast(err.message || 'Failed to update', 'error') } finally { setTogglingNotifyId(null) } }, [showToast, updateDomain]) const handleHealthCheck = useCallback(async (domainId: number) => { if (loadingHealth[domainId]) return setLoadingHealth(prev => ({ ...prev, [domainId]: true })) try { const report = await api.getDomainHealth(domainId) setHealthReports(prev => ({ ...prev, [domainId]: report })) setSelectedHealthDomainId(domainId) } catch (err: any) { showToast(err.message || 'Health check failed', 'error') } finally { setLoadingHealth(prev => ({ ...prev, [domainId]: false })) } }, [loadingHealth, showToast]) // Load health data for all domains on mount useEffect(() => { const loadHealthData = async () => { if (!domains || domains.length === 0) return // Load health for registered domains only (not available ones) const registeredDomains = domains.filter(d => !d.is_available) for (const domain of registeredDomains.slice(0, 10)) { // Limit to first 10 to avoid overload try { const report = await api.getDomainHealth(domain.id) setHealthReports(prev => ({ ...prev, [domain.id]: report })) } catch { // Silently fail - health data is optional } await new Promise(r => setTimeout(r, 200)) // Small delay } } loadHealthData() }, [domains]) return (
{toast && } {/* Ambient Background Glow */}
{/* Header Section */}

Watchlist

Monitor availability, health, and expiration dates for your tracked domains.

{/* Quick Stats Pills */}
{stats.available > 0 && (
{stats.available} Available!
)}
Auto-Monitoring
{/* Metric Grid */}
0} trend={stats.available > 0 ? 'up' : 'active'} /> 0 ? 'up' : 'neutral'} /> 0 ? 'down' : 'neutral'} /> 0 ? 'down' : 'neutral'} />
{/* Control Bar */}
{/* Filter Tabs */}
{([ { key: 'all', label: 'All', count: stats.total }, { key: 'available', label: 'Available', count: stats.available }, { key: 'expiring', label: 'Expiring', count: stats.expiringSoon }, { key: 'critical', label: 'Issues', count: stats.critical }, ] as const).map((tab) => ( ))}
{/* Add Domain Input */}
setNewDomain(e.target.value)} placeholder="Add domain (e.g. apple.com)..." className="w-full bg-black/50 border border-white/10 rounded-lg pl-10 pr-12 py-2 text-sm text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 focus:ring-1 focus:ring-emerald-500/50 transition-all" /> {/* Search Filter */}
setSearchQuery(e.target.value)} placeholder="Filter domains..." className="w-full bg-black/50 border border-white/10 rounded-lg pl-9 pr-4 py-2 text-sm text-white placeholder:text-zinc-600 focus:outline-none focus:border-white/20 transition-all" />
{/* Limit Warning */} {!canAddMore && (
Limit reached ({stats.total}/{stats.limit}). Upgrade to track more.
Upgrade
)} {/* Data Grid */}
{/* Table Header */}
Domain
Status
Health
Expires
Alerts
Actions
{filteredDomains.length === 0 ? (

{searchQuery ? "No matches found" : filterTab !== 'all' ? `No ${filterTab} domains` : "Watchlist is empty"}

{searchQuery ? "Try adjusting your filter." : "Start by adding domains you want to track."}

{!searchQuery && filterTab === 'all' && ( )}
) : (
{filteredDomains.map((domain) => { const health = healthReports[domain.id] const healthConfig = health ? healthStatusConfig[health.status] : healthStatusConfig.unknown const daysUntilExpiry = getDaysUntilExpiry(domain.expiration_date) const isExpiringSoon = daysUntilExpiry !== null && daysUntilExpiry <= 30 && daysUntilExpiry > 0 const isExpired = daysUntilExpiry !== null && daysUntilExpiry <= 0 return (
{/* Domain */}
{domain.is_available && (
)}
{domain.name} {domain.registrar && (

{domain.registrar}

)}
{/* Status */}
{domain.is_available ? ( Available ) : ( Registered )}
{/* Health */}
{domain.is_available ? ( ) : health ? ( ) : ( )}
{/* Expiry */}
{domain.is_available ? ( ) : domain.expiration_date ? (

{formatExpiryDate(domain.expiration_date)}

{daysUntilExpiry !== null && (

{isExpired ? 'EXPIRED' : `${daysUntilExpiry}d left`}

)}
) : ( Not public )}
{/* Alerts */}
{/* Actions */}
{domain.is_available && ( Buy )}
) })}
)}
{/* Subtle Footer Info */}
Checks: {subscription?.tier === 'tycoon' ? '10min' : subscription?.tier === 'trader' ? 'hourly' : 'daily'} {subscription?.tier !== 'tycoon' && ( Upgrade )}
{/* Health Report Modal */} {selectedHealthDomainId && healthReports[selectedHealthDomainId] && ( setSelectedHealthDomainId(null)} /> )}
) } // Health Report Modal Component const HealthReportModal = memo(function HealthReportModal({ report, onClose }: { report: DomainHealthReport onClose: () => void }) { const config = healthStatusConfig[report.status] const Icon = config.icon // Safely access nested properties const dns = report.dns || {} const http = report.http || {} const ssl = report.ssl || {} return (
e.stopPropagation()} > {/* Header */}

{report.domain}

{config.description}

{/* Score */}
Health Score = 70 ? "text-emerald-400" : report.score >= 40 ? "text-amber-400" : "text-rose-400" )}> {report.score}/100
= 70 ? "bg-emerald-500 shadow-[0_0_10px_rgba(16,185,129,0.5)]" : report.score >= 40 ? "bg-amber-500 shadow-[0_0_10px_rgba(245,158,11,0.5)]" : "bg-rose-500 shadow-[0_0_10px_rgba(244,63,94,0.5)]" )} style={{ width: `${report.score}%` }} />
{/* Check Results */}
{/* Infrastructure */}

Infrastructure

{/* DNS */}
DNS Status
{dns.has_ns ? '● Active' : '○ No Records'}
{dns.nameservers && dns.nameservers.length > 0 && (

{dns.nameservers[0]}

)}
{/* Web Server */}
Web Server
{http.is_reachable ? `● HTTP ${http.status_code || 200}` : http.error ? `○ ${http.error}` : '○ Unreachable' }
{http.content_length !== undefined && http.content_length > 0 && (

{(http.content_length / 1024).toFixed(1)} KB

)}
{/* A Record */}
A Record
{dns.has_a ? '● Configured' : '○ Not set'}
{/* MX Record */}
Mail (MX)
{dns.has_mx ? '● Configured' : '○ Not set'}
{/* Security */}

Security

SSL Certificate {ssl.has_certificate && ssl.is_valid ? 'Secure' : ssl.has_certificate ? 'Invalid' : 'None'}
{ssl.issuer && (
Issuer {ssl.issuer}
)} {ssl.days_until_expiry !== undefined && ssl.days_until_expiry !== null && (
Expires in {ssl.days_until_expiry} days
)} {ssl.error && (

{ssl.error}

)}
{/* Parking Detection */} {(dns.is_parked || http.is_parked) && (

Parking Detected

This domain appears to be parked or for sale. {dns.parking_provider && ( Provider: {dns.parking_provider} )}

{http.parking_keywords && http.parking_keywords.length > 0 && (

Keywords: {http.parking_keywords.slice(0, 3).join(', ')}

)}
)} {/* Signals */} {report.signals && report.signals.length > 0 && (

Signals

    {report.signals.map((signal, i) => (
  • {signal}
  • ))}
)} {/* Recommendations */} {report.recommendations && report.recommendations.length > 0 && (

Recommendations

    {report.recommendations.map((rec, i) => (
  • {rec}
  • ))}
)}
{/* Footer */}

LAST CHECK: {new Date(report.checked_at).toLocaleString().toUpperCase()}

) })