+ {/* Header Bar */}
+
- {/* Right: Search Terminal */}
-
-
+
+ {/* Input */}
+
+ setSearchQuery(e.target.value)}
+ onFocus={() => setSearchFocused(true)}
+ onBlur={() => setSearchFocused(false)}
+ placeholder="example.com"
+ className="w-full bg-transparent px-4 py-4 text-lg text-white placeholder:text-white/20 outline-none"
+ />
+ {searchQuery && (
+
+ )}
+
-
- {/* Header Bar */}
-
-
-
- Domain Search
-
-
-
-
-
- {/* Input */}
-
- setSearchQuery(e.target.value)}
- onFocus={() => setSearchFocused(true)}
- onBlur={() => setSearchFocused(false)}
- placeholder="example.com"
- className="w-full bg-transparent px-4 py-4 text-lg text-white placeholder:text-white/20 outline-none"
- />
- {searchQuery && (
-
- )}
-
-
- {/* Results */}
- {searchResult && (
-
- {searchResult.loading ? (
-
-
- Checking availability...
-
- ) : (
-
- {/* Status Header */}
-
-
- {searchResult.is_available ? (
-
- ) : (
-
- )}
- {searchResult.domain}
-
-
- {searchResult.is_available ? 'Available' : 'Taken'}
-
-
-
- {/* Registrar Info for taken domains */}
- {!searchResult.is_available && searchResult.registrar && (
-
- Registered with {searchResult.registrar}
-
+ {/* Results */}
+ {searchResult && (
+
+ {searchResult.loading ? (
+
+
+ Checking availability...
+
+ ) : (
+
+
+
+ {searchResult.is_available ? (
+
+ ) : (
+
)}
-
- {/* Actions */}
-
-
- {searchResult.is_available && (
-
- Register Now
-
- )}
-
+
{searchResult.domain}
+
+ {searchResult.is_available ? 'Available' : 'Taken'}
+
+
+
+ {!searchResult.is_available && searchResult.registrar && (
+
Registered with {searchResult.registrar}
)}
+
+
+
+ {searchResult.is_available && (
+
+ Register Now
+
+ )}
+
)}
-
- {/* Hint */}
- {!searchResult && (
-
- Enter a domain name to check availability
-
- )}
-
-
-
-
-
- {/* Ticker */}
-
-
- {/* CONTENT GRID */}
-
-
-
- {/* Hot Auctions - 2 cols */}
-
-
-
-
- Live Auctions
-
-
- View all →
-
-
+ )}
- {loadingData ? (
-
-
-
- ) : hotAuctions.length > 0 ? (
-
- ) : (
-
No active auctions
+ {!searchResult && (
+
Enter a domain name to check availability
)}
-
- {/* Quick Links */}
-
-
-
- Quick Access
-
-
-
- {[
- { label: 'Watchlist', href: '/terminal/watchlist', icon: Eye },
- { label: 'Market', href: '/terminal/market', icon: Gavel },
- { label: 'Intel', href: '/terminal/intel', icon: Globe },
- ].map((item) => (
-
-
-
{item.label}
-
-
- ))}
-
-
- {/* Status */}
-
-
-
-
-
-
+
+
+
+
+ {/* Ticker */}
+
+
+ {/* CONTENT GRID */}
+
+
+
+ {/* Hot Auctions - 2 cols */}
+
+
+
+
+ Live Auctions
+
+
+ View all →
+
+
+
+ {loadingData ? (
+
+
+
+ ) : hotAuctions.length > 0 ? (
+
+ ) : (
+
No active auctions
+ )}
+
+
+ {/* Quick Links */}
+
+
+
+ Quick Access
+
+
+
+ {[
+ { label: 'Watchlist', href: '/terminal/watchlist', icon: Eye },
+ { label: 'Market', href: '/terminal/market', icon: Gavel },
+ { label: 'Intel', href: '/terminal/intel', icon: Globe },
+ ].map((item) => (
+
+
+
{item.label}
+
+
+ ))}
+
+
+
+
+
+
+
+
+ )
+
+ // ============================================================================
+ // RENDER
+ // ============================================================================
+
+ return (
+ <>
+ {/* Mobile View */}
+
+
+
+
+ {/* Desktop View */}
+
+
+
+
+ {/* Global Styles for Safe Areas */}
+
>
diff --git a/frontend/src/app/terminal/watchlist/page.tsx b/frontend/src/app/terminal/watchlist/page.tsx
index bdf6f91..214fe18 100755
--- a/frontend/src/app/terminal/watchlist/page.tsx
+++ b/frontend/src/app/terminal/watchlist/page.tsx
@@ -1,6 +1,6 @@
'use client'
-import { useEffect, useState, useMemo, useCallback, useRef } from 'react'
+import { useEffect, useState, useMemo, useCallback } from 'react'
import { useStore } from '@/lib/store'
import { api, DomainHealthReport, HealthStatus } from '@/lib/api'
import { CommandCenterLayout } from '@/components/CommandCenterLayout'
@@ -24,21 +24,7 @@ import {
ExternalLink,
Calendar,
Shield,
- Crosshair,
- Menu,
- Search,
- Gavel,
- BarChart3,
- Settings,
- Tag,
- Coins,
- LogOut,
- User,
- ChevronRight,
- Clock,
- Wifi,
- WifiOff,
- Lock
+ Crosshair
} from 'lucide-react'
import clsx from 'clsx'
import Link from 'next/link'
@@ -57,7 +43,7 @@ function getDaysUntilExpiry(expirationDate: string | null): number | null {
function formatExpiryDate(expirationDate: string | null): string {
if (!expirationDate) return '—'
- return new Date(expirationDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
+ return new Date(expirationDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
}
function getTimeAgo(date: string | null): string {
@@ -75,462 +61,11 @@ function getTimeAgo(date: string | null): string {
}
const healthConfig: Record
= {
- healthy: { label: 'Healthy', color: 'text-accent', bg: 'bg-accent/10' },
- weakening: { label: 'Weak', color: 'text-amber-400', bg: 'bg-amber-500/10' },
- parked: { label: 'Parked', color: 'text-blue-400', bg: 'bg-blue-500/10' },
- critical: { label: 'Critical', color: 'text-rose-400', bg: 'bg-rose-500/10' },
- unknown: { label: 'Unknown', color: 'text-white/40', bg: 'bg-white/5' },
-}
-
-// ============================================================================
-// FULLSCREEN NAVIGATION (reuse)
-// ============================================================================
-
-function FullscreenNav({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
- const { user, logout, subscription } = useStore()
-
- const mainLinks = [
- { label: 'Radar', href: '/terminal/radar', icon: Crosshair, desc: 'Search & overview' },
- { label: 'Market', href: '/terminal/market', icon: Gavel, desc: 'Live auctions' },
- { label: 'Watchlist', href: '/terminal/watchlist', icon: Eye, desc: 'Track domains' },
- { label: 'Intel', href: '/terminal/intel', icon: BarChart3, desc: 'TLD pricing' },
- ]
-
- const secondaryLinks = [
- { label: 'Sniper', href: '/terminal/sniper', icon: Target },
- { label: 'Yield', href: '/terminal/yield', icon: Coins },
- { label: 'For Sale', href: '/terminal/listing', icon: Tag },
- { label: 'Settings', href: '/terminal/settings', icon: Settings },
- ]
-
- if (!isOpen) return null
-
- return (
-
-
-
-
- {mainLinks.map((item) => (
-
-
-
-
-
-
{item.label}
-
{item.desc}
-
-
-
- ))}
-
-
-
-
More
-
- {secondaryLinks.map((item) => (
-
-
- {item.label}
-
- ))}
-
-
-
-
-
-
-
-
-
-
{user?.name || user?.email?.split('@')[0]}
-
{subscription?.tier || 'Scout'}
-
-
-
-
-
- )
-}
-
-// ============================================================================
-// MOBILE BOTTOM NAV
-// ============================================================================
-
-function MobileBottomNav({ active, onMenuOpen }: { active: string; onMenuOpen: () => void }) {
- const navItems = [
- { id: 'radar', label: 'Radar', icon: Crosshair, href: '/terminal/radar' },
- { id: 'market', label: 'Market', icon: Gavel, href: '/terminal/market' },
- { id: 'watchlist', label: 'Watch', icon: Eye, href: '/terminal/watchlist' },
- ]
-
- return (
-
- )
-}
-
-// ============================================================================
-// MOBILE HEADER
-// ============================================================================
-
-function MobileHeader({
- onAddOpen,
- onRefreshAll,
- isRefreshing,
- availableCount
-}: {
- onAddOpen: () => void
- onRefreshAll: () => void
- isRefreshing: boolean
- availableCount: number
-}) {
- return (
-
-
-
-
-
- {availableCount > 0 && (
-
- {availableCount}
-
- )}
-
-
-
Watchlist
- Tracking domains
-
-
-
-
-
-
- )
-}
-
-// ============================================================================
-// MOBILE ADD MODAL
-// ============================================================================
-
-function MobileAddModal({ isOpen, onClose, onAdd, adding }: {
- isOpen: boolean
- onClose: () => void
- onAdd: (domain: string) => Promise
- adding: boolean
-}) {
- const [value, setValue] = useState('')
- const inputRef = useRef(null)
-
- useEffect(() => {
- if (isOpen) setTimeout(() => inputRef.current?.focus(), 100)
- }, [isOpen])
-
- const handleSubmit = async () => {
- if (!value.trim()) return
- await onAdd(value.trim())
- setValue('')
- onClose()
- }
-
- if (!isOpen) return null
-
- return (
-
-
-
- Add Domain
-
-
-
-
-
-
- setValue(e.target.value)}
- onKeyDown={(e) => e.key === 'Enter' && handleSubmit()}
- placeholder="example.com"
- className="w-full h-14 bg-white/10 border border-white/[0.2] pl-12 pr-4 text-white text-lg placeholder:text-white/40 outline-none focus:border-accent/50 rounded-xl"
- autoComplete="off"
- autoCorrect="off"
- autoCapitalize="none"
- />
-
-
-
- Enter a domain name to start tracking
-
-
-
- )
-}
-
-// ============================================================================
-// MOBILE DOMAIN CARD
-// ============================================================================
-
-function MobileDomainCard({
- domain,
- health,
- loadingHealth,
- onRefresh,
- onDelete,
- onToggleNotify,
- onHealthClick,
- refreshing,
- deleting,
- togglingNotify
-}: {
- domain: any
- health?: DomainHealthReport
- loadingHealth: boolean
- onRefresh: () => void
- onDelete: () => void
- onToggleNotify: () => void
- onHealthClick: () => void
- refreshing: boolean
- deleting: boolean
- togglingNotify: boolean
-}) {
- const config = healthConfig[health?.status || 'unknown']
- const days = getDaysUntilExpiry(domain.expiration_date)
- const isExpiringSoon = days !== null && days <= 30 && days > 0
-
- return (
-
- {/* Main Content */}
-
-
-
-
- {domain.is_available ? 'Available!' : 'Taken'}
-
-
-
- {/* Info Row */}
-
- {/* Health */}
-
-
-
•
-
- {/* Expiry */}
-
-
-
- {formatExpiryDate(domain.expiration_date)}
-
-
-
-
-
- {/* Actions */}
-
-
-
-
-
-
-
-
-
-
-
-
- )
-}
-
-// ============================================================================
-// MOBILE HEALTH MODAL
-// ============================================================================
-
-function MobileHealthModal({
- isOpen,
- onClose,
- domain,
- health,
- onRefresh,
- loading
-}: {
- isOpen: boolean
- onClose: () => void
- domain: any
- health?: DomainHealthReport
- onRefresh: () => void
- loading: boolean
-}) {
- if (!isOpen || !domain) return null
-
- const checks = [
- {
- label: 'DNS Resolution',
- ok: health?.dns?.has_a || health?.dns?.has_ns,
- icon: Globe,
- detail: health?.dns?.error || (health?.dns?.has_a ? 'A record found' : 'No A record')
- },
- {
- label: 'HTTP Reachable',
- ok: health?.http?.is_reachable,
- icon: Wifi,
- detail: health?.http?.error || (health?.http?.status_code ? `Status ${health.http.status_code}` : 'Not checked')
- },
- {
- label: 'SSL Certificate',
- ok: health?.ssl?.has_certificate,
- icon: Lock,
- detail: health?.ssl?.error || (health?.ssl?.has_certificate ? 'Valid SSL' : 'No SSL')
- },
- {
- label: 'Not Parked',
- ok: !(health?.dns?.is_parked || health?.http?.is_parked),
- icon: Shield,
- detail: (health?.dns?.is_parked || health?.http?.is_parked) ? 'Domain is parked' : 'Active site'
- },
- ]
-
- const score = health?.score ?? 0
-
- return (
-
-
-
- Health Report
-
-
-
-
- {/* Domain Header */}
-
-
{domain.name}
-
= 75 ? "bg-accent/15 text-accent" :
- score >= 50 ? "bg-amber-500/15 text-amber-400" :
- "bg-rose-500/15 text-rose-400"
- )}>
-
- Score: {score}/100
-
-
-
- {/* Checks */}
-
- {checks.map((check, i) => (
-
-
-
-
-
-
{check.label}
-
{check.detail}
-
- {check.ok ? (
-
- ) : (
-
- )}
-
- ))}
-
-
- {/* Warning */}
- {health?.http?.error === 'timeout' && (
-
-
-
-
-
Network Issue
-
- Could not reach domain from our servers. This may be a temporary issue or firewall restriction.
-
-
-
-
- )}
-
-
- )
+ healthy: { label: 'Healthy', color: 'text-accent', bg: 'bg-accent/10 border-accent/20' },
+ weakening: { label: 'Weak', color: 'text-amber-400', bg: 'bg-amber-500/10 border-amber-500/20' },
+ parked: { label: 'Parked', color: 'text-blue-400', bg: 'bg-blue-500/10 border-blue-500/20' },
+ critical: { label: 'Critical', color: 'text-rose-400', bg: 'bg-rose-500/10 border-rose-500/20' },
+ unknown: { label: 'Unknown', color: 'text-white/40', bg: 'bg-white/5 border-white/10' },
}
// ============================================================================
@@ -550,10 +85,6 @@ export default function WatchlistPage() {
const [loadingHealth, setLoadingHealth] = useState>({})
const [selectedDomain, setSelectedDomain] = useState(null)
const [filter, setFilter] = useState<'all' | 'available' | 'expiring'>('all')
- const [menuOpen, setMenuOpen] = useState(false)
- const [addModalOpen, setAddModalOpen] = useState(false)
- const [healthModalOpen, setHealthModalOpen] = useState(false)
- const [isRefreshingAll, setIsRefreshingAll] = useState(false)
// Stats
const stats = useMemo(() => {
@@ -584,46 +115,57 @@ export default function WatchlistPage() {
}, [domains, filter])
// Handlers
- const handleAdd = useCallback(async (domainName: string) => {
+ const handleAdd = useCallback(async (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!newDomain.trim()) return
+ const domainName = newDomain.trim().toLowerCase()
setAdding(true)
try {
- await addDomain(domainName.toLowerCase())
+ await addDomain(domainName)
showToast(`Added: ${domainName}`, 'success')
+ setNewDomain('')
} catch (err: any) {
showToast(err.message || 'Failed', 'error')
} finally {
setAdding(false)
}
- }, [addDomain, showToast])
+ }, [newDomain, addDomain, showToast])
+
+ // Auto-trigger health check for newly added domains
+ useEffect(() => {
+ if (!domains?.length) return
+
+ domains.forEach(domain => {
+ // If no health report exists and not currently loading, trigger check
+ if (!healthReports[domain.id] && !loadingHealth[domain.id]) {
+ setLoadingHealth(prev => ({ ...prev, [domain.id]: true }))
+ api.getDomainHealth(domain.id, { refresh: false })
+ .then(report => {
+ setHealthReports(prev => ({ ...prev, [domain.id]: report }))
+ })
+ .catch(() => {})
+ .finally(() => {
+ setLoadingHealth(prev => ({ ...prev, [domain.id]: false }))
+ })
+ }
+ })
+ }, [domains])
const handleRefresh = useCallback(async (id: number) => {
setRefreshingId(id)
try {
await refreshDomain(id)
- // Also refresh health
- const report = await api.getDomainHealth(id, { refresh: true })
- setHealthReports(prev => ({ ...prev, [id]: report }))
- showToast('Updated', 'success')
- } catch { showToast('Failed', 'error') }
+ showToast('Intel updated', 'success')
+ } catch { showToast('Update failed', 'error') }
finally { setRefreshingId(null) }
}, [refreshDomain, showToast])
- const handleRefreshAll = useCallback(async () => {
- setIsRefreshingAll(true)
- try {
- for (const domain of (domains || [])) {
- await refreshDomain(domain.id)
- }
- showToast('All refreshed', 'success')
- } catch {}
- finally { setIsRefreshingAll(false) }
- }, [domains, refreshDomain, showToast])
-
const handleDelete = useCallback(async (id: number, name: string) => {
+ if (!confirm(`Drop target: ${name}?`)) return
setDeletingId(id)
try {
await deleteDomain(id)
- showToast('Removed', 'success')
+ showToast('Target dropped', 'success')
} catch { showToast('Failed', 'error') }
finally { setDeletingId(null) }
}, [deleteDomain, showToast])
@@ -633,7 +175,7 @@ export default function WatchlistPage() {
try {
await api.updateDomainNotify(id, !current)
updateDomain(id, { notify_on_available: !current })
- showToast(!current ? 'Alert on' : 'Alert off', 'success')
+ showToast(!current ? 'Alerts armed' : 'Alerts disarmed', 'success')
} catch { showToast('Failed', 'error') }
finally { setTogglingNotifyId(null) }
}, [updateDomain, showToast])
@@ -660,298 +202,428 @@ export default function WatchlistPage() {
load()
}, [domains])
- // Auto-trigger health for new domains
- useEffect(() => {
- if (!domains?.length) return
- domains.forEach(domain => {
- if (!healthReports[domain.id] && !loadingHealth[domain.id]) {
- setLoadingHealth(prev => ({ ...prev, [domain.id]: true }))
- api.getDomainHealth(domain.id, { refresh: false })
- .then(report => setHealthReports(prev => ({ ...prev, [domain.id]: report })))
- .catch(() => {})
- .finally(() => setLoadingHealth(prev => ({ ...prev, [domain.id]: false })))
- }
- })
- }, [domains])
-
+ // Selected domain for detail view
const selectedDomainData = selectedDomain ? domains?.find(d => d.id === selectedDomain) : null
const selectedHealth = selectedDomain ? healthReports[selectedDomain] : null
return (
- <>
- {/* Fullscreen Navigation */}
- setMenuOpen(false)} />
-
- {/* Mobile Header */}
- setAddModalOpen(true)}
- onRefreshAll={handleRefreshAll}
- isRefreshing={isRefreshingAll}
- availableCount={stats.available}
- />
-
- {/* Mobile Add Modal */}
- setAddModalOpen(false)}
- onAdd={handleAdd}
- adding={adding}
- />
-
- {/* Mobile Health Modal */}
- setHealthModalOpen(false)}
- domain={selectedDomainData}
- health={selectedHealth ?? undefined}
- onRefresh={() => selectedDomain && handleHealthCheck(selectedDomain)}
- loading={selectedDomain ? loadingHealth[selectedDomain] || false : false}
- />
+
+ {toast && }
+
+ {/* ═══════════════════════════════════════════════════════════════════════ */}
+ {/* HEADER - Compact */}
+ {/* ═══════════════════════════════════════════════════════════════════════ */}
+
+
+
+ {/* Left */}
+
+
+
+ Domain Surveillance
+
+
+
+ Watchlist
+ {stats.total}
+
+
+
+ {/* Right: Stats */}
+
+
+
{stats.available}
+
Available
+
+
+
{stats.expiring}
+
Expiring
+
+
+
+
{/* ═══════════════════════════════════════════════════════════════════════ */}
- {/* MOBILE CONTENT */}
+ {/* ADD DOMAIN */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
-
- {toast &&
}
-
- {/* Stats */}
-
+
+
+ {/* ═══════════════════════════════════════════════════════════════════════ */}
+ {/* FILTERS */}
+ {/* ═══════════════════════════════════════════════════════════════════════ */}
+
+
{[
- { label: 'Total', value: stats.total, active: filter === 'all' },
- { label: 'Available', value: stats.available, active: filter === 'available', highlight: stats.available > 0 },
- { label: 'Expiring', value: stats.expiring, active: filter === 'expiring', warning: stats.expiring > 0 },
+ { value: 'all', label: 'All', count: stats.total },
+ { value: 'available', label: 'Available', count: stats.available },
+ { value: 'expiring', label: 'Expiring', count: stats.expiring },
].map((item) => (
))}
+
- {/* Available Banner */}
- {stats.available > 0 && (
-
-
-
-
-
-
-
{stats.available} Domain{stats.available > 1 ? 's' : ''} Available!
-
Grab them before someone else does
-
+ {/* ═══════════════════════════════════════════════════════════════════════ */}
+ {/* TABLE */}
+ {/* ═══════════════════════════════════════════════════════════════════════ */}
+
+ {!filteredDomains.length ? (
+
+
+
+
No domains in your watchlist
+
Add a domain above to start monitoring
- )}
-
- {/* Domains List */}
-
- {filteredDomains.length === 0 ? (
-
-
-
-
-
No domains yet
-
+ ) : (
+
+ {/* Table Header */}
+
+
Domain
+
Status
+
Health
+
Expires
+
Alert
+
Actions
- ) : (
- filteredDomains.map((domain) => (
-
handleRefresh(domain.id)}
- onDelete={() => handleDelete(domain.id, domain.name)}
- onToggleNotify={() => handleToggleNotify(domain.id, domain.notify_on_available)}
- onHealthClick={() => { setSelectedDomain(domain.id); setHealthModalOpen(true) }}
- refreshing={refreshingId === domain.id}
- deleting={deletingId === domain.id}
- togglingNotify={togglingNotifyId === domain.id}
- />
- ))
- )}
-
-
-
- {/* Mobile Bottom Nav */}
-
setMenuOpen(true)} />
-
- {/* ═══════════════════════════════════════════════════════════════════════ */}
- {/* DESKTOP LAYOUT */}
- {/* ═══════════════════════════════════════════════════════════════════════ */}
-
-
- {toast && }
-
- {/* HEADER */}
-
-
-
-
-
- Domain Surveillance
-
-
- Watchlist
- {stats.total}
-
-
-
-
-
{stats.available}
-
Available
-
-
-
{stats.expiring}
-
Expiring
-
-
-
-
-
- {/* ADD DOMAIN */}
-