Radar & Watchlist: cleaner UI, more horizontal padding, less top spacing, better readability
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled

This commit is contained in:
2025-12-12 21:41:46 +01:00
parent 8347611ad2
commit 8201367da3
3 changed files with 413 additions and 495 deletions

View File

@ -18,7 +18,8 @@ import {
Crosshair, Crosshair,
Zap, Zap,
Globe, Globe,
Target Target,
TrendingUp
} from 'lucide-react' } from 'lucide-react'
import clsx from 'clsx' import clsx from 'clsx'
import Link from 'next/link' import Link from 'next/link'
@ -46,31 +47,6 @@ interface SearchResult {
auctionData?: HotAuction auctionData?: HotAuction
} }
// ============================================================================
// LIVE TICKER
// ============================================================================
function LiveTicker({ items }: { items: { label: string; value: string; highlight?: boolean }[] }) {
return (
<div className="relative border-y border-white/[0.08] bg-black/40 overflow-hidden">
<div className="absolute left-0 top-0 bottom-0 w-16 bg-gradient-to-r from-[#020202] to-transparent z-10" />
<div className="absolute right-0 top-0 bottom-0 w-16 bg-gradient-to-l from-[#020202] to-transparent z-10" />
<div className="flex animate-[ticker_30s_linear_infinite] py-2.5" style={{ width: 'max-content' }}>
{[...items, ...items, ...items].map((item, i) => (
<div key={i} className="flex items-center gap-3 px-6 border-r border-white/[0.08]">
<span className="text-[9px] font-mono uppercase tracking-widest text-white/30">{item.label}</span>
<span className={clsx(
"text-xs font-mono font-medium",
item.highlight ? "text-accent" : "text-white/70"
)}>{item.value}</span>
</div>
))}
</div>
</div>
)
}
// ============================================================================ // ============================================================================
// MAIN PAGE // MAIN PAGE
// ============================================================================ // ============================================================================
@ -93,7 +69,7 @@ export default function RadarPage() {
const loadDashboardData = useCallback(async () => { const loadDashboardData = useCallback(async () => {
try { try {
const summary = await api.getDashboardSummary() const summary = await api.getDashboardSummary()
setHotAuctions((summary.market.ending_soon_preview || []).slice(0, 5)) setHotAuctions((summary.market.ending_soon_preview || []).slice(0, 6))
setMarketStats({ setMarketStats({
totalAuctions: summary.market.total_auctions || 0, totalAuctions: summary.market.total_auctions || 0,
endingSoon: summary.market.ending_soon || 0, endingSoon: summary.market.ending_soon || 0,
@ -162,267 +138,210 @@ export default function RadarPage() {
// Computed // Computed
const availableDomains = domains?.filter(d => d.is_available) || [] const availableDomains = domains?.filter(d => d.is_available) || []
const totalDomains = domains?.length || 0 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 ( return (
<CommandCenterLayout minimal> <CommandCenterLayout minimal>
{toast && <Toast message={toast.message} type={toast.type} onClose={hideToast} />} {toast && <Toast message={toast.message} type={toast.type} onClose={hideToast} />}
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* ═══════════════════════════════════════════════════════════════════════ */}
{/* HERO - Compact for Laptops */} {/* HEADER ROW */}
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* ═══════════════════════════════════════════════════════════════════════ */}
<section className="py-12 lg:py-16"> <div className="flex items-center justify-between pb-6 border-b border-white/[0.06]">
<div className="grid lg:grid-cols-2 gap-10 lg:gap-16 items-center"> <div className="flex items-center gap-3">
<div className="w-2 h-2 bg-accent rounded-full animate-pulse" />
<span className="text-xs font-mono uppercase tracking-wider text-white/50">Intelligence Hub</span>
</div>
<div className="flex items-center gap-6 text-xs font-mono">
<span className="text-white/30">Tracking: <span className="text-white">{totalDomains}</span></span>
<span className="text-white/30">Available: <span className="text-accent">{availableDomains.length}</span></span>
<span className="text-white/30">Auctions: <span className="text-white">{marketStats.totalAuctions.toLocaleString()}</span></span>
</div>
</div>
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* MAIN CONTENT */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
<div className="grid lg:grid-cols-[1fr_380px] gap-8 pt-8">
{/* Left: Search + Actions */}
<div className="space-y-8">
{/* Left: Typography */} {/* Hero Title */}
<div className="space-y-6"> <div>
<div className="inline-flex items-center gap-3"> <h1 className="text-3xl lg:text-4xl font-bold text-white tracking-tight">
<div className="w-1.5 h-1.5 bg-accent rounded-full animate-pulse shadow-[0_0_10px_rgba(16,185,129,0.8)]" /> Domain Radar
<span className="text-[10px] font-mono uppercase tracking-[0.2em] text-accent">
Intelligence Hub
</span>
</div>
<h1 className="font-display text-[2.5rem] sm:text-[3rem] lg:text-[3.5rem] leading-[0.95] tracking-[-0.03em]">
<span className="block text-white">Global Recon.</span>
<span className="block text-white/30">Zero Blind Spots.</span>
</h1> </h1>
<p className="text-base text-white/40 mt-2">
<p className="text-sm lg:text-base text-white/50 max-w-md font-light leading-relaxed"> Search domains, track availability, discover opportunities.
Real-time monitoring across {marketStats.totalAuctions.toLocaleString()}+ auctions.
<span className="text-white/70"> Your targets. Your intel.</span>
</p> </p>
{/* Stats Row */}
<div className="flex gap-8 lg:gap-10 pt-6 border-t border-white/[0.08]">
<div>
<div className="text-2xl lg:text-3xl font-display text-white">{totalDomains}</div>
<div className="text-[9px] uppercase tracking-widest text-white/30 font-mono mt-1">Tracking</div>
</div>
<div>
<div className="text-2xl lg:text-3xl font-display text-accent">{availableDomains.length}</div>
<div className="text-[9px] uppercase tracking-widest text-white/30 font-mono mt-1">Available</div>
</div>
<div>
<div className="text-2xl lg:text-3xl font-display text-white">{marketStats.endingSoon}</div>
<div className="text-[9px] uppercase tracking-widest text-white/30 font-mono mt-1">Ending Soon</div>
</div>
</div>
</div> </div>
{/* Right: Search Terminal */} {/* Search Box */}
<div className="relative"> <div className="relative">
<div className="absolute -inset-2 bg-gradient-to-tr from-accent/10 via-transparent to-accent/5 blur-2xl opacity-40" /> <div className={clsx(
"bg-[#0A0A0A] border transition-all duration-200",
<div className="relative bg-[#0A0A0A] border border-white/15 p-1.5"> searchFocused ? "border-accent/50" : "border-white/10"
{/* Tech Corners */} )}>
<div className="absolute -top-px -left-px w-4 h-4 border-t border-l border-accent/60" /> <div className="flex items-center">
<div className="absolute -top-px -right-px w-4 h-4 border-t border-r border-accent/60" /> <div className="pl-5 text-accent font-mono text-lg">{'>'}</div>
<div className="absolute -bottom-px -left-px w-4 h-4 border-b border-l border-accent/60" /> <input
<div className="absolute -bottom-px -right-px w-4 h-4 border-b border-r border-accent/60" /> ref={searchInputRef}
type="text"
value={searchQuery}
onChange={(e) => 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 && (
<button
onClick={() => { setSearchQuery(''); setSearchResult(null) }}
className="pr-5 text-white/30 hover:text-white transition-colors"
>
<XCircle className="w-5 h-5" />
</button>
)}
</div>
<div className="bg-[#050505] p-6 lg:p-8 relative"> {/* Search Results */}
{/* Header */} {searchResult && (
<div className="flex items-center justify-between mb-6"> <div className="border-t border-white/[0.06] p-5">
<span className="text-[9px] font-mono uppercase tracking-[0.15em] text-accent flex items-center gap-2"> {searchResult.loading ? (
<Crosshair className="w-3 h-3" /> <div className="flex items-center gap-3">
Target Acquisition <Loader2 className="w-4 h-4 animate-spin text-accent" />
</span> <span className="text-sm text-white/50">Checking availability...</span>
<div className="flex gap-1"> </div>
<div className="w-1 h-1 bg-accent/50" /> ) : (
<div className="w-1 h-1 bg-accent/30" /> <div className="space-y-4">
<div className="w-1 h-1 bg-accent/10" /> <div className="flex items-center justify-between">
</div> <div className="flex items-center gap-3">
</div> <div className={clsx(
"w-2.5 h-2.5",
{/* Input */} searchResult.is_available ? "bg-accent" : "bg-white/20"
<div className={clsx( )} />
"relative border transition-all duration-300", <span className="text-lg font-medium text-white">{searchResult.domain}</span>
searchFocused ? "border-accent/50 shadow-[0_0_20px_-5px_rgba(16,185,129,0.2)]" : "border-white/10" </div>
)}> <span className={clsx(
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-accent font-mono text-sm">{'>'}</div> "text-xs font-mono uppercase tracking-wider px-3 py-1 border",
<input searchResult.is_available
ref={searchInputRef} ? "text-accent border-accent/30 bg-accent/5"
type="text" : "text-white/40 border-white/10"
value={searchQuery} )}>
onChange={(e) => setSearchQuery(e.target.value)} {searchResult.is_available ? 'Available' : 'Taken'}
onFocus={() => setSearchFocused(true)} </span>
onBlur={() => setSearchFocused(false)} </div>
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" {searchResult.is_available && (
/> <div className="flex gap-3 pt-2">
{searchQuery && ( <button
<button onClick={handleAddToWatchlist}
onClick={() => { setSearchQuery(''); setSearchResult(null) }} disabled={addingToWatchlist}
className="absolute right-3 top-1/2 -translate-y-1/2 text-white/20 hover:text-white" className="flex-1 py-3 border border-white/15 text-white/70 text-sm font-medium hover:bg-white/5 transition-colors flex items-center justify-center gap-2"
> >
<XCircle className="w-4 h-4" /> {addingToWatchlist ? <Loader2 className="w-4 h-4 animate-spin" /> : <Eye className="w-4 h-4" />}
</button> Add to Watchlist
</button>
<a
href={`https://www.namecheap.com/domains/registration/results/?domain=${searchResult.domain}`}
target="_blank"
className="flex-1 py-3 bg-accent text-black text-sm font-bold hover:bg-white transition-colors flex items-center justify-center gap-2"
>
Register <ArrowRight className="w-4 h-4" />
</a>
</div>
)}
{searchResult.registrar && (
<div className="text-xs text-white/30 pt-2">
Registrar: {searchResult.registrar}
</div>
)}
</div>
)} )}
</div> </div>
)}
{/* Results */}
{searchResult && (
<div className="mt-5 border-t border-white/[0.08] pt-5">
{searchResult.loading ? (
<div className="flex items-center gap-3 text-accent">
<Loader2 className="w-3 h-3 animate-spin" />
<span className="text-xs font-mono uppercase tracking-widest">Scanning...</span>
</div>
) : (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{searchResult.is_available ? (
<div className="w-2 h-2 bg-accent shadow-[0_0_8px_rgba(16,185,129,0.8)]" />
) : (
<div className="w-2 h-2 bg-white/20" />
)}
<span className="text-base font-mono text-white">{searchResult.domain}</span>
</div>
<span className={clsx(
"text-[9px] font-mono uppercase tracking-widest px-2 py-0.5 border",
searchResult.is_available
? "text-accent border-accent/30 bg-accent/5"
: "text-white/30 border-white/10"
)}>
{searchResult.is_available ? 'AVAILABLE' : 'TAKEN'}
</span>
</div>
{searchResult.is_available && (
<div className="flex gap-2 pt-2">
<button
onClick={handleAddToWatchlist}
disabled={addingToWatchlist}
className="flex-1 py-2.5 border border-white/15 text-white/70 font-mono text-[10px] uppercase tracking-widest hover:bg-white/5 transition-colors"
>
{addingToWatchlist ? 'TRACKING...' : '+ TRACK'}
</button>
<a
href={`https://www.namecheap.com/domains/registration/results/?domain=${searchResult.domain}`}
target="_blank"
className="flex-1 py-2.5 bg-accent text-black font-mono text-[10px] font-bold uppercase tracking-widest hover:bg-white transition-colors flex items-center justify-center gap-1.5"
>
GET <ArrowRight className="w-3 h-3" />
</a>
</div>
)}
</div>
)}
</div>
)}
{/* Footer */}
<div className="mt-6 pt-4 border-t border-white/[0.05] flex justify-between items-center text-[9px] text-white/15 font-mono">
<span>SECURE</span>
<span>V2.1</span>
</div>
</div>
</div> </div>
</div> </div>
</div>
</section>
{/* Ticker */}
<LiveTicker items={tickerItems} />
{/* ═══════════════════════════════════════════════════════════════════════ */}
{/* CONTENT GRID */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
<section className="py-12 lg:py-16">
<div className="grid lg:grid-cols-3 gap-px bg-white/[0.08] border border-white/[0.08]">
{/* Hot Auctions - 2 cols */}
<div className="lg:col-span-2 bg-[#020202] p-6 lg:p-8">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-2">
<Gavel className="w-4 h-4 text-accent" />
<span className="text-xs font-bold text-white uppercase tracking-wider">Live Auctions</span>
</div>
<Link href="/terminal/market" className="text-[9px] font-mono uppercase text-white/30 hover:text-white transition-colors">
View All
</Link>
</div>
{loadingData ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="w-5 h-5 text-accent animate-spin" />
</div>
) : hotAuctions.length > 0 ? (
<div className="space-y-px">
{hotAuctions.map((auction, i) => (
<a
key={i}
href={auction.affiliate_url || '#'}
target="_blank"
className="flex items-center justify-between p-3 bg-white/[0.02] hover:bg-white/[0.04] transition-colors group"
>
<div className="flex items-center gap-3">
<span className="text-[9px] font-mono text-white/20 w-5">{auction.platform.substring(0, 2).toUpperCase()}</span>
<div>
<div className="font-mono text-sm text-white group-hover:text-accent transition-colors">{auction.domain}</div>
<div className="text-[9px] text-white/25 font-mono uppercase">{auction.time_remaining}</div>
</div>
</div>
<div className="font-mono text-sm text-accent">${auction.current_bid.toLocaleString()}</div>
</a>
))}
</div>
) : (
<div className="text-center py-8 text-white/15 font-mono text-xs uppercase">No active auctions</div>
)}
</div>
{/* Quick Links */} {/* Quick Links */}
<div className="bg-[#020202] p-6 lg:p-8"> <div className="grid grid-cols-3 gap-3">
<div className="flex items-center gap-2 mb-6"> {[
<Zap className="w-4 h-4 text-white/50" /> { label: 'Watchlist', href: '/terminal/watchlist', icon: Eye, count: totalDomains },
<span className="text-xs font-bold text-white uppercase tracking-wider">Quick Access</span> { label: 'Market', href: '/terminal/market', icon: Gavel, count: marketStats.endingSoon },
</div> { label: 'Intel', href: '/terminal/intel', icon: Globe },
].map((item) => (
<div className="space-y-2"> <Link
{[ key={item.href}
{ label: 'Watchlist', href: '/terminal/watchlist', icon: Eye }, href={item.href}
{ label: 'Market', href: '/terminal/market', icon: Gavel }, className="group flex flex-col p-4 bg-white/[0.02] border border-white/[0.06] hover:border-accent/30 hover:bg-accent/5 transition-all"
{ label: 'Intel', href: '/terminal/intel', icon: Globe }, >
].map((item) => ( <div className="flex items-center justify-between mb-3">
<Link <item.icon className="w-5 h-5 text-white/30 group-hover:text-accent transition-colors" />
key={item.href} {item.count !== undefined && (
href={item.href} <span className="text-xs font-mono text-white/40">{item.count}</span>
className="flex items-center gap-3 p-3 border border-white/[0.05] hover:border-accent/30 hover:bg-accent/5 transition-all group" )}
> </div>
<item.icon className="w-4 h-4 text-white/30 group-hover:text-accent transition-colors" /> <span className="text-sm font-medium text-white/70 group-hover:text-white transition-colors">{item.label}</span>
<span className="text-sm text-white/70 group-hover:text-white transition-colors flex-1">{item.label}</span> </Link>
<ArrowRight className="w-3 h-3 text-white/15 group-hover:text-accent group-hover:translate-x-0.5 transition-all" /> ))}
</Link> </div>
))} </div>
</div>
{/* Right: Live Auctions */}
{/* Status */} <div className="bg-[#0A0A0A] border border-white/[0.06]">
<div className="mt-6 pt-4 border-t border-white/[0.05]"> <div className="flex items-center justify-between p-5 border-b border-white/[0.06]">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-1 h-1 bg-accent rounded-full animate-pulse" /> <Gavel className="w-4 h-4 text-accent" />
<span className="text-[9px] font-mono text-white/30 uppercase">System Online</span> <span className="text-sm font-semibold text-white">Live Auctions</span>
</div>
</div> </div>
<Link href="/terminal/market" className="text-xs text-white/30 hover:text-accent transition-colors">
View All
</Link>
</div> </div>
<div className="divide-y divide-white/[0.04]">
{loadingData ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-5 h-5 text-accent animate-spin" />
</div>
) : hotAuctions.length > 0 ? (
hotAuctions.map((auction, i) => (
<a
key={i}
href={auction.affiliate_url || '#'}
target="_blank"
className="flex items-center justify-between p-4 hover:bg-white/[0.02] transition-colors group"
>
<div className="min-w-0">
<div className="text-sm font-medium text-white truncate group-hover:text-accent transition-colors">
{auction.domain}
</div>
<div className="text-xs text-white/30 mt-0.5 flex items-center gap-2">
<span className="uppercase">{auction.platform.substring(0, 3)}</span>
<span>·</span>
<span>{auction.time_remaining}</span>
</div>
</div>
<div className="text-sm font-mono text-accent font-medium">
${auction.current_bid.toLocaleString()}
</div>
</a>
))
) : (
<div className="py-12 text-center text-sm text-white/20">No active auctions</div>
)}
</div>
{/* Stats Footer */}
<div className="p-4 border-t border-white/[0.06] bg-black/30">
<div className="flex items-center justify-between text-xs">
<span className="text-white/30">Ending Soon</span>
<span className="text-accent font-mono">{marketStats.endingSoon}</span>
</div>
</div>
</div> </div>
</section> </div>
<style jsx global>{`
@keyframes ticker {
0% { transform: translateX(0); }
100% { transform: translateX(-33.33%); }
}
`}</style>
</CommandCenterLayout> </CommandCenterLayout>
) )
} }

View File

@ -61,11 +61,11 @@ function getTimeAgo(date: string | null): string {
} }
const healthConfig: Record<HealthStatus, { label: string; color: string; bg: string }> = { const healthConfig: Record<HealthStatus, { label: string; color: string; bg: string }> = {
healthy: { label: 'ONLINE', color: 'text-accent', bg: 'bg-accent/10 border-accent/20' }, 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' }, 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' }, 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' }, critical: { label: 'Critical', 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' }, unknown: { label: 'Unknown', color: 'text-white/40', bg: 'bg-white/5 border-white/10' },
} }
// ============================================================================ // ============================================================================
@ -121,10 +121,10 @@ export default function WatchlistPage() {
setAdding(true) setAdding(true)
try { try {
await addDomain(newDomain.trim()) await addDomain(newDomain.trim())
showToast(`Target locked: ${newDomain.trim()}`, 'success') showToast(`Added: ${newDomain.trim()}`, 'success')
setNewDomain('') setNewDomain('')
} catch (err: any) { } catch (err: any) {
showToast(err.message || 'Failed', 'error') showToast(err.message || 'Failed to add domain', 'error')
} finally { } finally {
setAdding(false) setAdding(false)
} }
@ -134,18 +134,18 @@ export default function WatchlistPage() {
setRefreshingId(id) setRefreshingId(id)
try { try {
await refreshDomain(id) await refreshDomain(id)
showToast('Intel updated', 'success') showToast('Domain refreshed', 'success')
} catch { showToast('Update failed', 'error') } } catch { showToast('Refresh failed', 'error') }
finally { setRefreshingId(null) } finally { setRefreshingId(null) }
}, [refreshDomain, showToast]) }, [refreshDomain, showToast])
const handleDelete = useCallback(async (id: number, name: string) => { const handleDelete = useCallback(async (id: number, name: string) => {
if (!confirm(`Drop target: ${name}?`)) return if (!confirm(`Remove ${name} from watchlist?`)) return
setDeletingId(id) setDeletingId(id)
try { try {
await deleteDomain(id) await deleteDomain(id)
showToast('Target dropped', 'success') showToast('Domain removed', 'success')
} catch { showToast('Failed', 'error') } } catch { showToast('Failed to remove', 'error') }
finally { setDeletingId(null) } finally { setDeletingId(null) }
}, [deleteDomain, showToast]) }, [deleteDomain, showToast])
@ -154,8 +154,8 @@ export default function WatchlistPage() {
try { try {
await api.updateDomainNotify(id, !current) await api.updateDomainNotify(id, !current)
updateDomain(id, { notify_on_available: !current }) updateDomain(id, { notify_on_available: !current })
showToast(!current ? 'Alerts armed' : 'Alerts disarmed', 'success') showToast(!current ? 'Notifications enabled' : 'Notifications disabled', 'success')
} catch { showToast('Failed', 'error') } } catch { showToast('Failed to update', 'error') }
finally { setTogglingNotifyId(null) } finally { setTogglingNotifyId(null) }
}, [updateDomain, showToast]) }, [updateDomain, showToast])
@ -190,68 +190,59 @@ export default function WatchlistPage() {
{toast && <Toast message={toast.message} type={toast.type} onClose={hideToast} />} {toast && <Toast message={toast.message} type={toast.type} onClose={hideToast} />}
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* ═══════════════════════════════════════════════════════════════════════ */}
{/* HEADER - Compact */} {/* HEADER */}
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* ═══════════════════════════════════════════════════════════════════════ */}
<section className="pt-10 lg:pt-12 pb-8"> <div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 pb-6 border-b border-white/[0.06]">
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-6"> <div>
<h1 className="text-2xl lg:text-3xl font-bold text-white tracking-tight">
{/* Left */} Watchlist
<div className="space-y-3"> </h1>
<div className="inline-flex items-center gap-2"> <p className="text-sm text-white/40 mt-1">
<Target className="w-4 h-4 text-accent" /> Monitor {stats.total} domain{stats.total !== 1 ? 's' : ''} for availability changes
<span className="text-[9px] font-mono uppercase tracking-[0.2em] text-accent">Surveillance</span> </p>
</div> </div>
<h1 className="font-display text-[2.5rem] lg:text-[3rem] leading-[0.95] tracking-[-0.03em]"> <div className="flex items-center gap-6 text-sm">
<span className="text-white">Watchlist</span> <div className="flex items-center gap-2">
<span className="text-white/30 ml-3">{stats.total}</span> <div className="w-2 h-2 bg-accent rounded-full" />
</h1> <span className="text-white/50">Available:</span>
<span className="font-medium text-accent">{stats.available}</span>
</div> </div>
<div className="flex items-center gap-2">
{/* Right: Stats */} <div className="w-2 h-2 bg-amber-400 rounded-full" />
<div className="flex gap-6 lg:gap-8"> <span className="text-white/50">Expiring:</span>
<div className="text-right"> <span className="font-medium text-amber-400">{stats.expiring}</span>
<div className="text-2xl font-display text-accent">{stats.available}</div>
<div className="text-[8px] uppercase tracking-widest text-white/30 font-mono">Available</div>
</div>
<div className="text-right">
<div className="text-2xl font-display text-amber-400">{stats.expiring}</div>
<div className="text-[8px] uppercase tracking-widest text-white/30 font-mono">Expiring</div>
</div>
</div> </div>
</div> </div>
</section> </div>
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* ═══════════════════════════════════════════════════════════════════════ */}
{/* ADD DOMAIN */} {/* ADD + FILTER */}
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* ═══════════════════════════════════════════════════════════════════════ */}
<section className="pb-8"> <div className="flex flex-col lg:flex-row lg:items-center gap-4 py-6">
<form onSubmit={handleAdd} className="relative max-w-xl"> {/* Add Form */}
<div className="flex items-center bg-[#050505] border border-white/10 focus-within:border-accent/40 transition-colors"> <form onSubmit={handleAdd} className="flex-1 max-w-lg">
<div className="pl-4 text-accent font-mono">{'>'}</div> <div className="flex items-center bg-[#0A0A0A] border border-white/10 focus-within:border-accent/40 transition-colors">
<input <input
type="text" type="text"
value={newDomain} value={newDomain}
onChange={(e) => setNewDomain(e.target.value)} onChange={(e) => setNewDomain(e.target.value)}
placeholder="ADD_TARGET..." placeholder="Add domain to watch..."
className="flex-1 bg-transparent px-3 py-3 text-sm text-white placeholder:text-white/20 font-mono uppercase outline-none" className="flex-1 bg-transparent px-4 py-3 text-sm text-white placeholder:text-white/25 outline-none"
/> />
<button <button
type="submit" type="submit"
disabled={adding || !newDomain.trim()} disabled={adding || !newDomain.trim()}
className="h-full px-5 py-3 bg-accent text-black font-mono text-[10px] font-bold uppercase tracking-wider hover:bg-white transition-colors disabled:opacity-30" className="px-5 py-3 bg-accent text-black text-sm font-semibold hover:bg-white transition-colors disabled:opacity-30 flex items-center gap-2"
> >
{adding ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Plus className="w-3.5 h-3.5" />} {adding ? <Loader2 className="w-4 h-4 animate-spin" /> : <Plus className="w-4 h-4" />}
Add
</button> </button>
</div> </div>
</form> </form>
</section>
{/* Filters */}
{/* ═══════════════════════════════════════════════════════════════════════ */} <div className="flex items-center gap-1 bg-white/[0.02] p-1">
{/* FILTERS */}
{/* ═══════════════════════════════════════════════════════════════════════ */}
<section className="pb-6 border-b border-white/[0.08]">
<div className="flex items-center gap-1">
{[ {[
{ value: 'all', label: 'All', count: stats.total }, { value: 'all', label: 'All', count: stats.total },
{ value: 'available', label: 'Available', count: stats.available }, { value: 'available', label: 'Available', count: stats.available },
@ -261,250 +252,258 @@ export default function WatchlistPage() {
key={item.value} key={item.value}
onClick={() => setFilter(item.value as typeof filter)} onClick={() => setFilter(item.value as typeof filter)}
className={clsx( 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 filter === item.value
? "bg-white/10 text-white" ? "bg-white/10 text-white font-medium"
: "text-white/30 hover:text-white/50" : "text-white/40 hover:text-white/60"
)} )}
> >
{item.label} ({item.count}) {item.label} <span className="text-white/30">({item.count})</span>
</button> </button>
))} ))}
</div> </div>
</section> </div>
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* ═══════════════════════════════════════════════════════════════════════ */}
{/* TABLE */} {/* TABLE */}
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* ═══════════════════════════════════════════════════════════════════════ */}
<section className="py-6"> <div className="bg-[#0A0A0A] border border-white/[0.06]">
{!filteredDomains.length ? ( {!filteredDomains.length ? (
<div className="text-center py-16"> <div className="text-center py-16">
<div className="w-12 h-12 mx-auto border border-white/10 flex items-center justify-center mb-4"> <div className="w-14 h-14 mx-auto bg-white/[0.02] border border-white/[0.06] flex items-center justify-center mb-4">
<Crosshair className="w-5 h-5 text-white/20" /> <Eye className="w-6 h-6 text-white/20" />
</div> </div>
<p className="text-white/30 font-mono text-sm uppercase">No targets</p> <p className="text-white/40 text-sm">No domains in your watchlist</p>
<p className="text-white/20 text-xs mt-1">Add a domain above to start monitoring</p>
</div> </div>
) : ( ) : (
<div className="space-y-px"> <>
{/* Table Header */} {/* Table Header */}
<div className="hidden lg:grid grid-cols-[1fr_100px_100px_100px_80px_100px] gap-4 px-4 py-2 text-[9px] font-mono uppercase tracking-widest text-white/30 border-b border-white/[0.05]"> <div className="hidden lg:grid grid-cols-[1fr_100px_100px_120px_80px_100px] gap-4 px-5 py-3 text-xs font-medium uppercase tracking-wider text-white/30 border-b border-white/[0.06] bg-black/30">
<div>Domain</div> <div>Domain</div>
<div>Status</div> <div>Status</div>
<div>Health</div> <div>Health</div>
<div>Expires</div> <div>Expires</div>
<div>Alert</div> <div>Notify</div>
<div className="text-right">Actions</div> <div className="text-right">Actions</div>
</div> </div>
{/* Rows */} {/* Rows */}
{filteredDomains.map((domain) => { <div className="divide-y divide-white/[0.04]">
const health = healthReports[domain.id] {filteredDomains.map((domain) => {
const healthStatus = health?.status || 'unknown' const health = healthReports[domain.id]
const config = healthConfig[healthStatus] const healthStatus = health?.status || 'unknown'
const days = getDaysUntilExpiry(domain.expiration_date) const config = healthConfig[healthStatus]
const days = getDaysUntilExpiry(domain.expiration_date)
return (
<div return (
key={domain.id} <div key={domain.id} className="group hover:bg-white/[0.02] transition-colors">
className="group bg-white/[0.01] hover:bg-white/[0.03] border border-white/[0.05] hover:border-white/[0.08] transition-all" {/* Mobile */}
> <div className="lg:hidden p-4 space-y-3">
{/* Mobile */} <div className="flex items-center justify-between">
<div className="lg:hidden p-4"> <div className="flex items-center gap-3">
<div className="flex items-center justify-between mb-3"> <div className={clsx(
<div className="flex items-center gap-3"> "w-2 h-2 rounded-full",
domain.is_available ? "bg-accent" : "bg-white/20"
)} />
<span className="font-medium text-white">{domain.name}</span>
</div>
<span className={clsx(
"text-xs px-2 py-0.5 border",
domain.is_available ? "text-accent border-accent/30 bg-accent/5" : "text-white/40 border-white/10"
)}>
{domain.is_available ? 'Available' : 'Taken'}
</span>
</div>
<div className="flex items-center justify-between text-xs text-white/40">
<span>{formatExpiryDate(domain.expiration_date)}</span>
<div className="flex gap-2">
<button onClick={() => handleRefresh(domain.id)} className="p-2 hover:bg-white/5 rounded">
<RefreshCw className={clsx("w-4 h-4", refreshingId === domain.id && "animate-spin")} />
</button>
<button onClick={() => handleDelete(domain.id, domain.name)} className="p-2 hover:bg-rose-500/10 hover:text-rose-400 rounded">
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
</div>
{/* Desktop */}
<div className="hidden lg:grid grid-cols-[1fr_100px_100px_120px_80px_100px] gap-4 items-center px-5 py-4">
{/* Domain */}
<div className="flex items-center gap-3 min-w-0">
<div className={clsx( <div className={clsx(
"w-2 h-2", "w-2 h-2 rounded-full shrink-0",
domain.is_available ? "bg-accent shadow-[0_0_8px_rgba(16,185,129,0.8)]" : "bg-white/15" domain.is_available ? "bg-accent" : "bg-white/20"
)} /> )} />
<span className="font-mono text-sm text-white">{domain.name}</span> <span className="font-medium text-white truncate">{domain.name}</span>
<a
href={`https://${domain.name}`}
target="_blank"
className="opacity-0 group-hover:opacity-50 hover:!opacity-100 transition-opacity"
>
<ExternalLink className="w-3.5 h-3.5" />
</a>
</div> </div>
<span className={clsx(
"text-[9px] font-mono uppercase px-2 py-0.5 border", {/* Status */}
domain.is_available ? "text-accent border-accent/30" : "text-white/30 border-white/10" <div>
)}> <span className={clsx(
{domain.is_available ? 'OPEN' : 'TAKEN'} "text-xs px-2 py-0.5 border",
</span> domain.is_available ? "text-accent border-accent/30 bg-accent/5" : "text-white/40 border-white/10"
</div> )}>
{domain.is_available ? 'Available' : 'Taken'}
<div className="flex items-center justify-between text-[10px] font-mono text-white/30"> </span>
<span>{formatExpiryDate(domain.expiration_date)}</span>
<div className="flex gap-2">
<button onClick={() => handleRefresh(domain.id)} className="p-1.5 hover:bg-white/5">
<RefreshCw className={clsx("w-3.5 h-3.5", refreshingId === domain.id && "animate-spin")} />
</button>
<button onClick={() => handleDelete(domain.id, domain.name)} className="p-1.5 hover:bg-rose-500/10 hover:text-rose-400">
<Trash2 className="w-3.5 h-3.5" />
</button>
</div> </div>
</div>
</div> {/* Health */}
{/* Desktop */}
<div className="hidden lg:grid grid-cols-[1fr_100px_100px_100px_80px_100px] gap-4 items-center px-4 py-3">
{/* Domain */}
<div className="flex items-center gap-3 min-w-0">
<div className={clsx(
"w-1.5 h-1.5 shrink-0",
domain.is_available ? "bg-accent shadow-[0_0_6px_rgba(16,185,129,0.8)]" : "bg-white/15"
)} />
<span className="font-mono text-sm text-white truncate">{domain.name}</span>
<a href={`https://${domain.name}`} target="_blank" className="opacity-0 group-hover:opacity-50 hover:!opacity-100 transition-opacity">
<ExternalLink className="w-3 h-3" />
</a>
</div>
{/* Status */}
<div>
<span className={clsx(
"text-[9px] font-mono uppercase px-2 py-0.5 border",
domain.is_available ? "text-accent border-accent/30 bg-accent/5" : "text-white/30 border-white/10"
)}>
{domain.is_available ? 'AVAIL' : 'TAKEN'}
</span>
</div>
{/* Health */}
<button
onClick={() => { setSelectedDomain(domain.id); handleHealthCheck(domain.id) }}
className="flex items-center gap-1.5 group/health"
>
{loadingHealth[domain.id] ? (
<Loader2 className="w-3 h-3 animate-spin text-white/30" />
) : (
<>
<Activity className={clsx("w-3 h-3", config.color)} />
<span className={clsx("text-[9px] font-mono uppercase", config.color)}>{config.label}</span>
</>
)}
</button>
{/* Expires */}
<div className="text-[10px] font-mono text-white/40">
{days !== null && days <= 30 && days > 0 ? (
<span className="text-amber-400">{days}d</span>
) : (
formatExpiryDate(domain.expiration_date)
)}
</div>
{/* Alert */}
<button
onClick={() => handleToggleNotify(domain.id, domain.notify_on_available)}
disabled={togglingNotifyId === domain.id}
className={clsx(
"w-6 h-6 flex items-center justify-center transition-colors",
domain.notify_on_available ? "text-accent" : "text-white/20 hover:text-white/40"
)}
>
{togglingNotifyId === domain.id ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" />
) : domain.notify_on_available ? (
<Bell className="w-3.5 h-3.5" />
) : (
<BellOff className="w-3.5 h-3.5" />
)}
</button>
{/* Actions */}
<div className="flex items-center justify-end gap-1">
<button <button
onClick={() => handleRefresh(domain.id)} onClick={() => { setSelectedDomain(domain.id); handleHealthCheck(domain.id) }}
disabled={refreshingId === domain.id} className="flex items-center gap-2 hover:opacity-80 transition-opacity"
className="w-7 h-7 flex items-center justify-center text-white/20 hover:text-white hover:bg-white/5 transition-all"
> >
<RefreshCw className={clsx("w-3.5 h-3.5", refreshingId === domain.id && "animate-spin")} /> {loadingHealth[domain.id] ? (
</button> <Loader2 className="w-3.5 h-3.5 animate-spin text-white/30" />
<button
onClick={() => handleDelete(domain.id, domain.name)}
disabled={deletingId === domain.id}
className="w-7 h-7 flex items-center justify-center text-white/20 hover:text-rose-400 hover:bg-rose-500/10 transition-all"
>
{deletingId === domain.id ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" />
) : ( ) : (
<Trash2 className="w-3.5 h-3.5" /> <>
<Activity className={clsx("w-3.5 h-3.5", config.color)} />
<span className={clsx("text-xs", config.color)}>{config.label}</span>
</>
)} )}
</button> </button>
{/* Expires */}
<div className="text-sm text-white/50">
{days !== null && days <= 30 && days > 0 ? (
<span className="text-amber-400 font-medium">{days} days</span>
) : (
formatExpiryDate(domain.expiration_date)
)}
</div>
{/* Notify */}
<button
onClick={() => handleToggleNotify(domain.id, domain.notify_on_available)}
disabled={togglingNotifyId === domain.id}
className={clsx(
"w-8 h-8 flex items-center justify-center rounded hover:bg-white/5 transition-all",
domain.notify_on_available ? "text-accent" : "text-white/25"
)}
>
{togglingNotifyId === domain.id ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : domain.notify_on_available ? (
<Bell className="w-4 h-4" />
) : (
<BellOff className="w-4 h-4" />
)}
</button>
{/* Actions */}
<div className="flex items-center justify-end gap-1">
<button
onClick={() => handleRefresh(domain.id)}
disabled={refreshingId === domain.id}
className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-white hover:bg-white/5 rounded transition-all"
>
<RefreshCw className={clsx("w-4 h-4", refreshingId === domain.id && "animate-spin")} />
</button>
<button
onClick={() => handleDelete(domain.id, domain.name)}
disabled={deletingId === domain.id}
className="w-8 h-8 flex items-center justify-center text-white/30 hover:text-rose-400 hover:bg-rose-500/10 rounded transition-all"
>
{deletingId === domain.id ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Trash2 className="w-4 h-4" />
)}
</button>
</div>
</div> </div>
</div> </div>
</div> )
) })}
})} </div>
</div> </>
)} )}
</section> </div>
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* ═══════════════════════════════════════════════════════════════════════ */}
{/* HEALTH MODAL */} {/* HEALTH MODAL */}
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* ═══════════════════════════════════════════════════════════════════════ */}
{selectedDomainData && ( {selectedDomainData && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm" onClick={() => setSelectedDomain(null)}> <div
<div className="w-full max-w-md bg-[#0A0A0A] border border-white/15 p-1.5" onClick={(e) => e.stopPropagation()}> className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
{/* Corner Decorations */} onClick={() => setSelectedDomain(null)}
<div className="absolute -top-px -left-px w-4 h-4 border-t border-l border-accent/60" /> >
<div className="absolute -top-px -right-px w-4 h-4 border-t border-r border-accent/60" /> <div
<div className="absolute -bottom-px -left-px w-4 h-4 border-b border-l border-accent/60" /> className="w-full max-w-md bg-[#0A0A0A] border border-white/15 shadow-2xl"
<div className="absolute -bottom-px -right-px w-4 h-4 border-b border-r border-accent/60" /> onClick={(e) => e.stopPropagation()}
>
<div className="bg-[#050505] p-6 relative"> {/* Header */}
{/* Header */} <div className="flex items-center justify-between px-6 py-4 border-b border-white/[0.06]">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center gap-3">
<div className="flex items-center gap-3"> <Activity className="w-5 h-5 text-accent" />
<Activity className="w-4 h-4 text-accent" /> <span className="font-semibold text-white">Health Report</span>
<span className="text-[10px] font-mono uppercase tracking-widest text-accent">Health Intel</span>
</div>
<button onClick={() => setSelectedDomain(null)} className="text-white/30 hover:text-white">
<X className="w-4 h-4" />
</button>
</div> </div>
<button onClick={() => setSelectedDomain(null)} className="text-white/30 hover:text-white p-1">
<X className="w-5 h-5" />
</button>
</div>
{/* Content */}
<div className="p-6">
{/* Domain */} {/* Domain */}
<div className="mb-6"> <div className="mb-6">
<h3 className="font-mono text-lg text-white">{selectedDomainData.name}</h3> <h3 className="text-lg font-medium text-white">{selectedDomainData.name}</h3>
<div className="flex items-center gap-2 mt-2"> <div className="flex items-center gap-2 mt-2">
<div className={clsx( <span className={clsx(
"text-[9px] font-mono uppercase px-2 py-0.5 border", "text-xs px-2.5 py-1 border",
healthConfig[selectedHealth?.status || 'unknown'].bg, healthConfig[selectedHealth?.status || 'unknown'].bg,
healthConfig[selectedHealth?.status || 'unknown'].color healthConfig[selectedHealth?.status || 'unknown'].color
)}> )}>
{healthConfig[selectedHealth?.status || 'unknown'].label} {healthConfig[selectedHealth?.status || 'unknown'].label}
</div> </span>
{selectedHealth?.score !== undefined && (
<span className="text-xs text-white/40">Score: {selectedHealth.score}/100</span>
)}
</div> </div>
</div> </div>
{/* Checks */} {/* Checks */}
{selectedHealth && ( {selectedHealth && (
<div className="space-y-2"> <div className="space-y-3">
{[ {[
{ label: 'DNS', value: selectedHealth.dns?.has_a }, { label: 'DNS Resolution', value: selectedHealth.dns?.has_a },
{ label: 'HTTP', value: selectedHealth.http?.is_reachable }, { label: 'HTTP Reachable', value: selectedHealth.http?.is_reachable },
{ label: 'SSL', value: selectedHealth.ssl?.has_certificate }, { label: 'SSL Certificate', value: selectedHealth.ssl?.has_certificate },
{ label: 'Parked', value: !selectedHealth.dns?.is_parked && !selectedHealth.http?.is_parked }, { label: 'Not Parked', value: !selectedHealth.dns?.is_parked && !selectedHealth.http?.is_parked },
].map((check) => ( ].map((check) => (
<div key={check.label} className="flex items-center justify-between py-2 border-b border-white/[0.05]"> <div key={check.label} className="flex items-center justify-between py-2">
<span className="text-xs font-mono text-white/50 uppercase">{check.label}</span> <span className="text-sm text-white/60">{check.label}</span>
{check.value ? ( {check.value ? (
<CheckCircle2 className="w-4 h-4 text-accent" /> <CheckCircle2 className="w-5 h-5 text-accent" />
) : ( ) : (
<XCircle className="w-4 h-4 text-rose-400" /> <XCircle className="w-5 h-5 text-rose-400" />
)} )}
</div> </div>
))} ))}
</div> </div>
)} )}
{/* Refresh */} {/* Refresh Button */}
<button <button
onClick={() => handleHealthCheck(selectedDomainData.id)} onClick={() => handleHealthCheck(selectedDomainData.id)}
disabled={loadingHealth[selectedDomainData.id]} disabled={loadingHealth[selectedDomainData.id]}
className="w-full mt-6 py-3 border border-white/10 text-white/50 font-mono text-[10px] uppercase tracking-wider hover:bg-white/5 hover:text-white transition-all flex items-center justify-center gap-2" className="w-full mt-6 py-3 bg-white/5 border border-white/10 text-white/70 text-sm font-medium hover:bg-white/10 hover:text-white transition-all flex items-center justify-center gap-2"
> >
{loadingHealth[selectedDomainData.id] ? ( {loadingHealth[selectedDomainData.id] ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" /> <Loader2 className="w-4 h-4 animate-spin" />
) : ( ) : (
<> <>
<RefreshCw className="w-3.5 h-3.5" /> <RefreshCw className="w-4 h-4" />
Refresh Refresh Health Check
</> </>
)} )}
</button> </button>

View File

@ -235,7 +235,7 @@ export function CommandCenterLayout({
<main className="relative"> <main className="relative">
<div className={clsx( <div className={clsx(
"mx-auto", "mx-auto",
minimal ? "max-w-[1600px] px-6" : "max-w-7xl px-4 sm:px-6 lg:px-8 py-6 sm:py-8" minimal ? "max-w-[1400px] px-6 sm:px-10 lg:px-16 py-4" : "max-w-7xl px-4 sm:px-6 lg:px-8 py-6 sm:py-8"
)}> )}>
{children} {children}
</div> </div>