'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 { PremiumTable, Badge, StatCard, PageContainer, TableActionButton, SearchInput, TabBar, FilterBar, ActionButton, } from '@/components/PremiumTable' import { Toast, useToast } from '@/components/Toast' import { Plus, Trash2, RefreshCw, Loader2, Bell, BellOff, ExternalLink, Eye, Sparkles, ArrowUpRight, X, Activity, Shield, AlertTriangle, ShoppingCart, HelpCircle, } from 'lucide-react' import clsx from 'clsx' import Link from 'next/link' // Health status badge colors and icons const healthStatusConfig: Record = { healthy: { label: 'Healthy', color: 'text-accent', bgColor: 'bg-accent/10 border-accent/20', icon: Activity, description: 'Domain is active and well-maintained' }, weakening: { label: 'Weakening', color: 'text-amber-400', bgColor: 'bg-amber-400/10 border-amber-400/20', icon: AlertTriangle, description: 'Warning signs detected - owner may be losing interest' }, parked: { label: 'For Sale', color: 'text-orange-400', bgColor: 'bg-orange-400/10 border-orange-400/20', icon: ShoppingCart, description: 'Domain is parked and likely for sale' }, critical: { label: 'Critical', color: 'text-red-400', bgColor: 'bg-red-400/10 border-red-400/20', icon: AlertTriangle, description: 'Domain drop is imminent!' }, unknown: { label: 'Unknown', color: 'text-foreground-muted', bgColor: 'bg-foreground/5 border-border/30', icon: HelpCircle, description: 'Could not determine status' }, } type FilterStatus = 'all' | 'available' | 'watching' export default function WatchlistPage() { const { domains, addDomain, deleteDomain, refreshDomain, 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 [filterStatus, setFilterStatus] = useState('all') const [searchQuery, setSearchQuery] = useState('') // Health check state const [healthReports, setHealthReports] = useState>({}) const [loadingHealth, setLoadingHealth] = useState>({}) const [selectedHealthDomainId, setSelectedHealthDomainId] = useState(null) // Memoized stats - avoids recalculation on every render const stats = useMemo(() => ({ availableCount: domains?.filter(d => d.is_available).length || 0, watchingCount: domains?.filter(d => !d.is_available).length || 0, domainsUsed: domains?.length || 0, domainLimit: subscription?.domain_limit || 5, }), [domains, subscription?.domain_limit]) const canAddMore = stats.domainsUsed < stats.domainLimit // Memoized filtered domains const filteredDomains = useMemo(() => { if (!domains) return [] return domains.filter(domain => { if (searchQuery && !domain.name.toLowerCase().includes(searchQuery.toLowerCase())) { return false } if (filterStatus === 'available' && !domain.is_available) return false if (filterStatus === 'watching' && domain.is_available) return false return true }) }, [domains, searchQuery, filterStatus]) // Memoized tabs config const tabs = useMemo(() => [ { id: 'all', label: 'All', count: stats.domainsUsed }, { id: 'available', label: 'Available', count: stats.availableCount, color: 'accent' as const }, { id: 'watching', label: 'Monitoring', count: stats.watchingCount }, ], [stats]) // Callbacks - prevent recreation on every render 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) showToast(!currentState ? 'Notifications enabled' : 'Notifications disabled', 'success') } catch (err: any) { showToast(err.message || 'Failed to update', 'error') } finally { setTogglingNotifyId(null) } }, [showToast]) 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]) // Dynamic subtitle const subtitle = useMemo(() => { if (stats.domainsUsed === 0) return 'Start tracking domains to monitor their availability' return `Monitoring ${stats.domainsUsed} domain${stats.domainsUsed !== 1 ? 's' : ''} • ${stats.domainLimit === -1 ? 'Unlimited' : `${stats.domainLimit - stats.domainsUsed} slots left`}` }, [stats]) // Memoized columns config const columns = useMemo(() => [ { key: 'domain', header: 'Domain', render: (domain: any) => (
{domain.is_available && ( )}
{domain.name} {domain.is_available && ( AVAILABLE )}
), }, { key: 'status', header: 'Status', align: 'left' as const, hideOnMobile: true, render: (domain: any) => { const health = healthReports[domain.id] if (health) { const config = healthStatusConfig[health.status] const Icon = config.icon return (
{config.label}
) } return ( {domain.is_available ? 'Ready to pounce!' : 'Monitoring...'} ) }, }, { key: 'notifications', header: 'Alerts', align: 'center' as const, width: '80px', hideOnMobile: true, render: (domain: any) => ( ), }, { key: 'actions', header: '', align: 'right' as const, render: (domain: any) => (
handleHealthCheck(domain.id)} loading={loadingHealth[domain.id]} title="Health check (DNS, HTTP, SSL)" variant={healthReports[domain.id] ? 'accent' : 'default'} /> handleRefresh(domain.id)} loading={refreshingId === domain.id} title="Refresh availability" /> handleDelete(domain.id, domain.name)} variant="danger" loading={deletingId === domain.id} title="Remove" /> {domain.is_available && ( e.stopPropagation()} className="flex items-center gap-1.5 px-3 py-2 bg-accent text-background text-xs font-medium rounded-lg hover:bg-accent-hover transition-colors ml-1" > Register )}
), }, ], [healthReports, togglingNotifyId, loadingHealth, refreshingId, deletingId, handleToggleNotify, handleHealthCheck, handleRefresh, handleDelete]) return ( {toast && } {/* Stats Cards */}
{/* Add Domain Form */} handleAddDomain({} as React.FormEvent)} disabled={adding || !newDomain.trim() || !canAddMore} icon={adding ? Loader2 : Plus} > Add Domain {!canAddMore && (

You've reached your domain limit. Upgrade to track more.

Upgrade
)} {/* Filters */} setFilterStatus(id as FilterStatus)} /> {/* Domain Table */} d.id} emptyIcon={} emptyTitle={stats.domainsUsed === 0 ? "Your watchlist is empty" : "No domains match your filters"} emptyDescription={stats.domainsUsed === 0 ? "Add a domain above to start tracking" : "Try adjusting your filter criteria"} columns={columns} /> {/* Health Report Modal */} {selectedHealthDomainId && healthReports[selectedHealthDomainId] && ( setSelectedHealthDomainId(null)} /> )}
) } // Health Report Modal Component - memoized const HealthReportModal = memo(function HealthReportModal({ report, onClose }: { report: DomainHealthReport onClose: () => void }) { const config = healthStatusConfig[report.status] const Icon = config.icon return (
e.stopPropagation()} > {/* Header */}

{report.domain}

{config.description}

{/* Score */}
Health Score
= 70 ? "bg-accent" : report.score >= 40 ? "bg-amber-400" : "bg-red-400" )} style={{ width: `${report.score}%` }} />
= 70 ? "text-accent" : report.score >= 40 ? "text-amber-400" : "text-red-400" )}> {report.score}/100
{/* Check Results */}
{/* DNS */} {report.dns && (

DNS Infrastructure

{report.dns.has_ns ? '✓' : '✗'} Nameservers
{report.dns.has_a ? '✓' : '✗'} A Record
{report.dns.has_mx ? '✓' : '—'} MX Record
{report.dns.is_parked && (

⚠ Parked at {report.dns.parking_provider || 'unknown provider'}

)}
)} {/* HTTP */} {report.http && (

Website Status

{report.http.is_reachable ? 'Reachable' : 'Unreachable'} {report.http.status_code && ( HTTP {report.http.status_code} )}
{report.http.is_parked && (

⚠ Parking page detected

)}
)} {/* SSL */} {report.ssl && (

SSL Certificate

{report.ssl.has_certificate ? (

{report.ssl.is_valid ? '✓ Valid certificate' : '✗ Certificate invalid/expired'}

{report.ssl.days_until_expiry !== undefined && (

30 ? "text-foreground-muted" : report.ssl.days_until_expiry > 7 ? "text-amber-400" : "text-red-400" )}> Expires in {report.ssl.days_until_expiry} days

)}
) : (

No SSL certificate

)}
)} {/* Signals & Recommendations */} {((report.signals?.length || 0) > 0 || (report.recommendations?.length || 0) > 0) && (
{(report.signals?.length || 0) > 0 && (

Signals

    {report.signals?.map((signal, i) => (
  • {signal}
  • ))}
)} {(report.recommendations?.length || 0) > 0 && (

Recommendations

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

Checked at {new Date(report.checked_at).toLocaleString()}

) })