From 8201367da3658f448bdd4f77c9312dfcb3d3fffd Mon Sep 17 00:00:00 2001 From: Yves Gugger Date: Fri, 12 Dec 2025 21:41:46 +0100 Subject: [PATCH] Radar & Watchlist: cleaner UI, more horizontal padding, less top spacing, better readability --- frontend/src/app/terminal/radar/page.tsx | 447 +++++++---------- frontend/src/app/terminal/watchlist/page.tsx | 459 +++++++++--------- .../src/components/CommandCenterLayout.tsx | 2 +- 3 files changed, 413 insertions(+), 495 deletions(-) diff --git a/frontend/src/app/terminal/radar/page.tsx b/frontend/src/app/terminal/radar/page.tsx index b2e5d61..f2f3b36 100644 --- a/frontend/src/app/terminal/radar/page.tsx +++ b/frontend/src/app/terminal/radar/page.tsx @@ -18,7 +18,8 @@ import { Crosshair, Zap, Globe, - Target + Target, + TrendingUp } from 'lucide-react' import clsx from 'clsx' import Link from 'next/link' @@ -46,31 +47,6 @@ interface SearchResult { auctionData?: HotAuction } -// ============================================================================ -// LIVE TICKER -// ============================================================================ - -function LiveTicker({ items }: { items: { label: string; value: string; highlight?: boolean }[] }) { - return ( -
-
-
- -
- {[...items, ...items, ...items].map((item, i) => ( -
- {item.label} - {item.value} -
- ))} -
-
- ) -} - // ============================================================================ // MAIN PAGE // ============================================================================ @@ -93,7 +69,7 @@ export default function RadarPage() { const loadDashboardData = useCallback(async () => { try { const summary = await api.getDashboardSummary() - setHotAuctions((summary.market.ending_soon_preview || []).slice(0, 5)) + setHotAuctions((summary.market.ending_soon_preview || []).slice(0, 6)) setMarketStats({ totalAuctions: summary.market.total_auctions || 0, endingSoon: summary.market.ending_soon || 0, @@ -162,267 +138,210 @@ export default function RadarPage() { // Computed const availableDomains = domains?.filter(d => d.is_available) || [] const totalDomains = domains?.length || 0 - - const tickerItems = [ - { label: 'Status', value: 'ONLINE', highlight: true }, - { label: 'Tracking', value: totalDomains.toString() }, - { label: 'Available', value: availableDomains.length.toString(), highlight: availableDomains.length > 0 }, - { label: 'Auctions', value: marketStats.totalAuctions.toString() }, - ] return ( {toast && } {/* ═══════════════════════════════════════════════════════════════════════ */} - {/* HERO - Compact for Laptops */} + {/* HEADER ROW */} {/* ═══════════════════════════════════════════════════════════════════════ */} -
-
+
+
+
+ Intelligence Hub +
+
+ Tracking: {totalDomains} + Available: {availableDomains.length} + Auctions: {marketStats.totalAuctions.toLocaleString()} +
+
+ + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* MAIN CONTENT */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+ + {/* Left: Search + Actions */} +
- {/* Left: Typography */} -
-
-
- - Intelligence Hub - -
- -

- Global Recon. - Zero Blind Spots. + {/* Hero Title */} +
+

+ Domain Radar

- -

- Real-time monitoring across {marketStats.totalAuctions.toLocaleString()}+ auctions. - Your targets. Your intel. +

+ Search domains, track availability, discover opportunities.

- - {/* Stats Row */} -
-
-
{totalDomains}
-
Tracking
-
-
-
{availableDomains.length}
-
Available
-
-
-
{marketStats.endingSoon}
-
Ending Soon
-
-
- {/* Right: Search Terminal */} + {/* Search Box */}
-
- -
- {/* Tech Corners */} -
-
-
-
+
+
+
{'>'}
+ setSearchQuery(e.target.value)} + onFocus={() => setSearchFocused(true)} + onBlur={() => setSearchFocused(false)} + placeholder="Search domain..." + className="w-full bg-transparent px-4 py-5 text-lg text-white placeholder:text-white/20 outline-none" + /> + {searchQuery && ( + + )} +
-
- {/* Header */} -
- - - Target Acquisition - -
-
-
-
-
-
- - {/* Input */} -
-
{'>'}
- setSearchQuery(e.target.value)} - onFocus={() => setSearchFocused(true)} - onBlur={() => setSearchFocused(false)} - placeholder="ENTER_TARGET..." - className="w-full bg-black/50 px-8 py-4 text-lg lg:text-xl text-white placeholder:text-white/15 font-mono uppercase tracking-tight outline-none" - /> - {searchQuery && ( - + {/* Search Results */} + {searchResult && ( +
+ {searchResult.loading ? ( +
+ + Checking availability... +
+ ) : ( +
+
+
+
+ {searchResult.domain} +
+ + {searchResult.is_available ? 'Available' : 'Taken'} + +
+ + {searchResult.is_available && ( +
+ + + Register + +
+ )} + + {searchResult.registrar && ( +
+ Registrar: {searchResult.registrar} +
+ )} +
)}
- - {/* Results */} - {searchResult && ( -
- {searchResult.loading ? ( -
- - Scanning... -
- ) : ( -
-
-
- {searchResult.is_available ? ( -
- ) : ( -
- )} - {searchResult.domain} -
- - {searchResult.is_available ? 'AVAILABLE' : 'TAKEN'} - -
- - {searchResult.is_available && ( -
- - - GET - -
- )} -
- )} -
- )} - - {/* Footer */} -
- SECURE - V2.1 -
-
+ )}
-
-

- - {/* 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} - - - ))} -
- - {/* Status */} -
-
-
- System Online -
+
+ {[ + { label: 'Watchlist', href: '/terminal/watchlist', icon: Eye, count: totalDomains }, + { label: 'Market', href: '/terminal/market', icon: Gavel, count: marketStats.endingSoon }, + { label: 'Intel', href: '/terminal/intel', icon: Globe }, + ].map((item) => ( + +
+ + {item.count !== undefined && ( + {item.count} + )} +
+ {item.label} + + ))} +
+
+ + {/* Right: Live Auctions */} +
+
+
+ + Live Auctions
+ + View All → +
+
+ {loadingData ? ( +
+ +
+ ) : hotAuctions.length > 0 ? ( + hotAuctions.map((auction, i) => ( + +
+
+ {auction.domain} +
+
+ {auction.platform.substring(0, 3)} + · + {auction.time_remaining} +
+
+
+ ${auction.current_bid.toLocaleString()} +
+
+ )) + ) : ( +
No active auctions
+ )} +
+ + {/* Stats Footer */} +
+
+ Ending Soon + {marketStats.endingSoon} +
+
-
- - +
) } \ No newline at end of file diff --git a/frontend/src/app/terminal/watchlist/page.tsx b/frontend/src/app/terminal/watchlist/page.tsx index d2c9ce7..67cdef6 100755 --- a/frontend/src/app/terminal/watchlist/page.tsx +++ b/frontend/src/app/terminal/watchlist/page.tsx @@ -61,11 +61,11 @@ function getTimeAgo(date: string | null): string { } const healthConfig: Record = { - healthy: { label: 'ONLINE', 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: 'CRIT', color: 'text-rose-400', bg: 'bg-rose-500/10 border-rose-500/20' }, - unknown: { label: '???', color: 'text-white/40', bg: 'bg-white/5 border-white/10' }, + 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' }, } // ============================================================================ @@ -121,10 +121,10 @@ export default function WatchlistPage() { setAdding(true) try { await addDomain(newDomain.trim()) - showToast(`Target locked: ${newDomain.trim()}`, 'success') + showToast(`Added: ${newDomain.trim()}`, 'success') setNewDomain('') } catch (err: any) { - showToast(err.message || 'Failed', 'error') + showToast(err.message || 'Failed to add domain', 'error') } finally { setAdding(false) } @@ -134,18 +134,18 @@ export default function WatchlistPage() { setRefreshingId(id) try { await refreshDomain(id) - showToast('Intel updated', 'success') - } catch { showToast('Update failed', 'error') } + showToast('Domain refreshed', 'success') + } catch { showToast('Refresh failed', 'error') } finally { setRefreshingId(null) } }, [refreshDomain, showToast]) const handleDelete = useCallback(async (id: number, name: string) => { - if (!confirm(`Drop target: ${name}?`)) return + if (!confirm(`Remove ${name} from watchlist?`)) return setDeletingId(id) try { await deleteDomain(id) - showToast('Target dropped', 'success') - } catch { showToast('Failed', 'error') } + showToast('Domain removed', 'success') + } catch { showToast('Failed to remove', 'error') } finally { setDeletingId(null) } }, [deleteDomain, showToast]) @@ -154,8 +154,8 @@ export default function WatchlistPage() { try { await api.updateDomainNotify(id, !current) updateDomain(id, { notify_on_available: !current }) - showToast(!current ? 'Alerts armed' : 'Alerts disarmed', 'success') - } catch { showToast('Failed', 'error') } + showToast(!current ? 'Notifications enabled' : 'Notifications disabled', 'success') + } catch { showToast('Failed to update', 'error') } finally { setTogglingNotifyId(null) } }, [updateDomain, showToast]) @@ -190,68 +190,59 @@ export default function WatchlistPage() { {toast && } {/* ═══════════════════════════════════════════════════════════════════════ */} - {/* HEADER - Compact */} + {/* HEADER */} {/* ═══════════════════════════════════════════════════════════════════════ */} -
-
- - {/* Left */} -
-
- - Surveillance -
- -

- Watchlist - {stats.total} -

+
+
+

+ Watchlist +

+

+ Monitor {stats.total} domain{stats.total !== 1 ? 's' : ''} for availability changes +

+
+ +
+
+
+ Available: + {stats.available}
- - {/* Right: Stats */} -
-
-
{stats.available}
-
Available
-
-
-
{stats.expiring}
-
Expiring
-
+
+
+ Expiring: + {stats.expiring}
-
+
{/* ═══════════════════════════════════════════════════════════════════════ */} - {/* ADD DOMAIN */} + {/* ADD + FILTER */} {/* ═══════════════════════════════════════════════════════════════════════ */} -
-
-
-
{'>'}
+
+ {/* Add Form */} + +
setNewDomain(e.target.value)} - placeholder="ADD_TARGET..." - className="flex-1 bg-transparent px-3 py-3 text-sm text-white placeholder:text-white/20 font-mono uppercase outline-none" + placeholder="Add domain to watch..." + className="flex-1 bg-transparent px-4 py-3 text-sm text-white placeholder:text-white/25 outline-none" />
-
- - {/* ═══════════════════════════════════════════════════════════════════════ */} - {/* FILTERS */} - {/* ═══════════════════════════════════════════════════════════════════════ */} -
-
+ + {/* Filters */} +
{[ { value: 'all', label: 'All', count: stats.total }, { value: 'available', label: 'Available', count: stats.available }, @@ -261,250 +252,258 @@ export default function WatchlistPage() { key={item.value} onClick={() => setFilter(item.value as typeof filter)} className={clsx( - "px-4 py-2 text-[10px] font-mono uppercase tracking-wider transition-colors", + "px-4 py-2 text-sm transition-colors", filter === item.value - ? "bg-white/10 text-white" - : "text-white/30 hover:text-white/50" + ? "bg-white/10 text-white font-medium" + : "text-white/40 hover:text-white/60" )} > - {item.label} ({item.count}) + {item.label} ({item.count}) ))}
-
+ {/* ═══════════════════════════════════════════════════════════════════════ */} {/* TABLE */} {/* ═══════════════════════════════════════════════════════════════════════ */} -
+
{!filteredDomains.length ? (
-
- +
+
-

No targets

+

No domains in your watchlist

+

Add a domain above to start monitoring

) : ( -
+ <> {/* Table Header */} -
+
Domain
Status
Health
Expires
-
Alert
+
Notify
Actions
{/* Rows */} - {filteredDomains.map((domain) => { - const health = healthReports[domain.id] - const healthStatus = health?.status || 'unknown' - const config = healthConfig[healthStatus] - const days = getDaysUntilExpiry(domain.expiration_date) - - return ( -
- {/* Mobile */} -
-
-
+
+ {filteredDomains.map((domain) => { + const health = healthReports[domain.id] + const healthStatus = health?.status || 'unknown' + const config = healthConfig[healthStatus] + const days = getDaysUntilExpiry(domain.expiration_date) + + return ( +
+ {/* Mobile */} +
+
+
+
+ {domain.name} +
+ + {domain.is_available ? 'Available' : 'Taken'} + +
+ +
+ {formatExpiryDate(domain.expiration_date)} +
+ + +
+
+
+ + {/* Desktop */} +
+ {/* Domain */} +
- {domain.name} + {domain.name} + + +
- - {domain.is_available ? 'OPEN' : 'TAKEN'} - -
- -
- {formatExpiryDate(domain.expiration_date)} -
- - + + {/* Status */} +
+ + {domain.is_available ? 'Available' : 'Taken'} +
-
-
- - {/* Desktop */} -
- {/* Domain */} -
-
- {domain.name} - - - -
- - {/* Status */} -
- - {domain.is_available ? 'AVAIL' : 'TAKEN'} - -
- - {/* Health */} - - - {/* Expires */} -
- {days !== null && days <= 30 && days > 0 ? ( - {days}d - ) : ( - formatExpiryDate(domain.expiration_date) - )} -
- - {/* Alert */} - - - {/* Actions */} -
+ + {/* Health */} - + + {/* Expires */} +
+ {days !== null && days <= 30 && days > 0 ? ( + {days} days + ) : ( + formatExpiryDate(domain.expiration_date) + )} +
+ + {/* Notify */} + + + {/* Actions */} +
+ + +
-
- ) - })} -
+ ) + })} +
+ )} -
+ {/* ═══════════════════════════════════════════════════════════════════════ */} {/* HEALTH MODAL */} {/* ═══════════════════════════════════════════════════════════════════════ */} {selectedDomainData && ( -
setSelectedDomain(null)}> -
e.stopPropagation()}> - {/* Corner Decorations */} -
-
-
-
- -
- {/* Header */} -
-
- - Health Intel -
- +
setSelectedDomain(null)} + > +
e.stopPropagation()} + > + {/* Header */} +
+
+ + Health Report
- + +
+ + {/* Content */} +
{/* Domain */}
-

{selectedDomainData.name}

+

{selectedDomainData.name}

-
{healthConfig[selectedHealth?.status || 'unknown'].label} -
+ + {selectedHealth?.score !== undefined && ( + Score: {selectedHealth.score}/100 + )}
{/* Checks */} {selectedHealth && ( -
+
{[ - { label: 'DNS', value: selectedHealth.dns?.has_a }, - { label: 'HTTP', value: selectedHealth.http?.is_reachable }, - { label: 'SSL', value: selectedHealth.ssl?.has_certificate }, - { label: 'Parked', value: !selectedHealth.dns?.is_parked && !selectedHealth.http?.is_parked }, + { label: 'DNS Resolution', value: selectedHealth.dns?.has_a }, + { label: 'HTTP Reachable', value: selectedHealth.http?.is_reachable }, + { label: 'SSL Certificate', value: selectedHealth.ssl?.has_certificate }, + { label: 'Not Parked', value: !selectedHealth.dns?.is_parked && !selectedHealth.http?.is_parked }, ].map((check) => ( -
- {check.label} +
+ {check.label} {check.value ? ( - + ) : ( - + )}
))}
)} - {/* Refresh */} + {/* Refresh Button */} diff --git a/frontend/src/components/CommandCenterLayout.tsx b/frontend/src/components/CommandCenterLayout.tsx index d544ae0..9ac3371 100755 --- a/frontend/src/components/CommandCenterLayout.tsx +++ b/frontend/src/components/CommandCenterLayout.tsx @@ -235,7 +235,7 @@ export function CommandCenterLayout({
{children}