-
+
-
-
{domain.name}
-
-
- {domain.is_available ? 'Available!' : 'Taken'}
-
- {isExpiringSoon && (
-
- {days}d left
-
- )}
-
-
+
{domain.name}
-
-
+
+ {domain.is_available ? 'Available!' : 'Taken'}
+
{/* Info Row */}
-
- {domain.expiration_date && (
-
-
- {formatExpiryDate(domain.expiration_date)}
-
- )}
- {domain.registrar && (
-
-
- {domain.registrar}
-
- )}
-
-
- {/* Health Button */}
-
@@ -358,137 +417,128 @@ function MobileDomainCard({
}
// ============================================================================
-// MOBILE HEALTH DETAIL SHEET
+// MOBILE HEALTH MODAL
// ============================================================================
-function MobileHealthSheet({
- domain,
+function MobileHealthModal({
+ isOpen,
+ onClose,
+ domain,
health,
- isOpen,
- onClose,
onRefresh,
- isLoading
-}: {
- domain: any
- health: DomainHealthReport | null
+ loading
+}: {
isOpen: boolean
onClose: () => void
+ domain: any
+ health?: DomainHealthReport
onRefresh: () => void
- isLoading: boolean
+ loading: boolean
}) {
if (!isOpen || !domain) return null
- const config = healthConfig[health?.status || 'unknown']
+ 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 (
-
-
-
e.stopPropagation()}
- >
- {/* Handle */}
-
+
+
+ Close
+ Health Report
+
+ {loading ? : 'Refresh'}
+
+
- {/* Header */}
-
-
-
-
{domain.name}
-
- {config.label}
- {health?.score !== undefined && (
- Score: {health.score}/100
- )}
-
-
-
-
-
+
+ {/* 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 */}
-
- {health ? (
- <>
-
-
-
-
-
- {(health.http?.error === 'timeout' || health.ssl?.error?.includes('timeout')) && (
-
-
- ⚠️ Network issue: Server could not reach this domain
-
-
+
+ {checks.map((check, i) => (
+
+
+
+
+
+
{check.label}
+
{check.detail}
+
+ {check.ok ? (
+
+ ) : (
+
)}
- >
- ) : (
-
- No health data yet
- )}
+ ))}
- {/* Refresh Button */}
-
-
- {isLoading ? : }
- Run Health Check
-
-
+ {/* Warning */}
+ {health?.http?.error === 'timeout' && (
+
+
+
+
+
Network Issue
+
+ Could not reach domain from our servers. This may be a temporary issue or firewall restriction.
+
+
+
+
+ )}
)
}
-function HealthCheckRow({ label, ok, error }: { label: string; ok?: boolean; error?: string }) {
- return (
-
-
-
{label}
- {error &&
{error}
}
-
- {ok ? (
-
- ) : error ? (
-
- ) : (
-
- )}
-
- )
-}
-
// ============================================================================
// MAIN PAGE
// ============================================================================
export default function WatchlistPage() {
- const { domains, fetchDomains, addDomain, deleteDomain, refreshDomain, updateDomain } = useStore()
+ const { domains, addDomain, deleteDomain, refreshDomain, updateDomain } = useStore()
const { toast, showToast, hideToast } = useToast()
const [newDomain, setNewDomain] = useState('')
@@ -498,11 +548,11 @@ export default function WatchlistPage() {
const [togglingNotifyId, setTogglingNotifyId] = useState
(null)
const [healthReports, setHealthReports] = useState>({})
const [loadingHealth, setLoadingHealth] = useState>({})
+ const [selectedDomain, setSelectedDomain] = useState(null)
const [filter, setFilter] = useState<'all' | 'available' | 'expiring'>('all')
-
- // Mobile states
- const [showAddModal, setShowAddModal] = useState(false)
- const [selectedDomainId, setSelectedDomainId] = useState(null)
+ const [menuOpen, setMenuOpen] = useState(false)
+ const [addModalOpen, setAddModalOpen] = useState(false)
+ const [healthModalOpen, setHealthModalOpen] = useState(false)
const [isRefreshingAll, setIsRefreshingAll] = useState(false)
// Stats
@@ -533,40 +583,24 @@ export default function WatchlistPage() {
})
}, [domains, filter])
- // Load health on mount
- 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])
-
// Handlers
- const handleAdd = useCallback(async () => {
- if (!newDomain.trim()) return
+ const handleAdd = useCallback(async (domainName: string) => {
setAdding(true)
try {
- await addDomain(newDomain.trim().toLowerCase())
- showToast(`Added: ${newDomain.trim()}`, 'success')
- setNewDomain('')
- setShowAddModal(false)
+ await addDomain(domainName.toLowerCase())
+ showToast(`Added: ${domainName}`, 'success')
} catch (err: any) {
showToast(err.message || 'Failed', 'error')
} finally {
setAdding(false)
}
- }, [newDomain, addDomain, showToast])
+ }, [addDomain, showToast])
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')
@@ -577,11 +611,13 @@ export default function WatchlistPage() {
const handleRefreshAll = useCallback(async () => {
setIsRefreshingAll(true)
try {
- await fetchDomains()
- showToast('All domains refreshed', 'success')
- } catch { showToast('Failed', 'error') }
+ for (const domain of (domains || [])) {
+ await refreshDomain(domain.id)
+ }
+ showToast('All refreshed', 'success')
+ } catch {}
finally { setIsRefreshingAll(false) }
- }, [fetchDomains, showToast])
+ }, [domains, refreshDomain, showToast])
const handleDelete = useCallback(async (id: number, name: string) => {
setDeletingId(id)
@@ -597,7 +633,7 @@ export default function WatchlistPage() {
try {
await api.updateDomainNotify(id, !current)
updateDomain(id, { notify_on_available: !current })
- showToast(!current ? 'Alerts on' : 'Alerts off', 'success')
+ showToast(!current ? 'Alert on' : 'Alert off', 'success')
} catch { showToast('Failed', 'error') }
finally { setTogglingNotifyId(null) }
}, [updateDomain, showToast])
@@ -612,77 +648,124 @@ export default function WatchlistPage() {
finally { setLoadingHealth(prev => ({ ...prev, [id]: false })) }
}, [loadingHealth])
- const selectedDomain = selectedDomainId ? domains?.find(d => d.id === selectedDomainId) : null
- const selectedHealth = selectedDomainId ? healthReports[selectedDomainId] : null
+ // Load health
+ useEffect(() => {
+ const load = async () => {
+ if (!domains?.length) return
+ try {
+ const data = await api.getDomainsHealthCache()
+ if (data?.reports) setHealthReports(prev => ({ ...prev, ...data.reports }))
+ } catch {}
+ }
+ 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])
+
+ const selectedDomainData = selectedDomain ? domains?.find(d => d.id === selectedDomain) : null
+ const selectedHealth = selectedDomain ? healthReports[selectedDomain] : null
return (
<>
+ {/* Fullscreen Navigation */}
+ setMenuOpen(false)} />
+
{/* Mobile Header */}
setShowAddModal(true)}
+ onAddOpen={() => setAddModalOpen(true)}
+ onRefreshAll={handleRefreshAll}
isRefreshing={isRefreshingAll}
- onRefresh={handleRefreshAll}
+ availableCount={stats.available}
/>
{/* Mobile Add Modal */}
- setShowAddModal(false)}
- value={newDomain}
- setValue={setNewDomain}
- isAdding={adding}
+ setAddModalOpen(false)}
onAdd={handleAdd}
+ adding={adding}
/>
- {/* Mobile Health Sheet */}
- setHealthModalOpen(false)}
+ domain={selectedDomainData}
health={selectedHealth}
- isOpen={selectedDomainId !== null}
- onClose={() => setSelectedDomainId(null)}
- onRefresh={() => selectedDomainId && handleHealthCheck(selectedDomainId)}
- isLoading={selectedDomainId ? loadingHealth[selectedDomainId] : false}
+ onRefresh={() => selectedDomain && handleHealthCheck(selectedDomain)}
+ loading={selectedDomain ? loadingHealth[selectedDomain] || false : false}
/>
- {/* Mobile Content */}
-
+ {/* ═══════════════════════════════════════════════════════════════════════ */}
+ {/* MOBILE CONTENT */}
+ {/* ═══════════════════════════════════════════════════════════════════════ */}
+
{toast &&
}
-
- {/* Filters */}
-
+
+ {/* Stats */}
+
{[
- { value: 'all', label: 'All', count: stats.total },
- { value: 'available', label: 'Available', count: stats.available },
- { value: 'expiring', label: 'Expiring', count: stats.expiring },
+ { 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 },
].map((item) => (
setFilter(item.value as typeof filter)}
+ key={item.label}
+ onClick={() => setFilter(item.label.toLowerCase() as typeof filter)}
className={clsx(
- "px-4 py-2 text-[13px] font-semibold whitespace-nowrap rounded-lg transition-colors",
- filter === item.value
- ? "bg-accent text-black"
- : "bg-white/10 text-white/60"
+ "p-4 text-center rounded-xl border transition-all active:scale-95",
+ item.active
+ ? "bg-accent/10 border-accent/40"
+ : "bg-[#0c0c0c] border-white/[0.15]"
)}
>
- {item.label} ({item.count})
+
+ {item.value}
+
+ {item.label}
))}
- {/* Domain List */}
-
- {filteredDomains.length === 0 ? (
-
-
-
+ {/* Available Banner */}
+ {stats.available > 0 && (
+
+
+
+
-
No domains yet
-
setShowAddModal(true)}
- className="text-accent font-semibold text-[15px]"
- >
+
+
{stats.available} Domain{stats.available > 1 ? 's' : ''} Available!
+
Grab them before someone else does
+
+
+
+ )}
+
+ {/* Domains List */}
+
+ {filteredDomains.length === 0 ? (
+
+
+
+
+
No domains yet
+
setAddModalOpen(true)} className="text-accent font-semibold">
Add your first domain
@@ -692,13 +775,14 @@ export default function WatchlistPage() {
key={domain.id}
domain={domain}
health={healthReports[domain.id]}
+ loadingHealth={loadingHealth[domain.id] || false}
onRefresh={() => handleRefresh(domain.id)}
onDelete={() => handleDelete(domain.id, domain.name)}
onToggleNotify={() => handleToggleNotify(domain.id, domain.notify_on_available)}
- onViewHealth={() => setSelectedDomainId(domain.id)}
- isRefreshing={refreshingId === domain.id}
- isDeleting={deletingId === domain.id}
- isTogglingNotify={togglingNotifyId === domain.id}
+ onHealthClick={() => { setSelectedDomain(domain.id); setHealthModalOpen(true) }}
+ refreshing={refreshingId === domain.id}
+ deleting={deletingId === domain.id}
+ togglingNotify={togglingNotifyId === domain.id}
/>
))
)}
@@ -706,7 +790,7 @@ export default function WatchlistPage() {
{/* Mobile Bottom Nav */}
-
+
setMenuOpen(true)} />
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* DESKTOP LAYOUT */}
@@ -715,7 +799,7 @@ export default function WatchlistPage() {
{toast && }
- {/* Header */}
+ {/* HEADER */}
@@ -741,9 +825,9 @@ export default function WatchlistPage() {
- {/* Add Domain */}
+ {/* ADD DOMAIN */}