Radar & Watchlist: smaller fonts, no header, laptop optimized
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
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:
@ -8,22 +8,17 @@ import { Toast, useToast } from '@/components/Toast'
|
||||
import {
|
||||
Eye,
|
||||
Gavel,
|
||||
Clock,
|
||||
ExternalLink,
|
||||
Plus,
|
||||
Activity,
|
||||
Search,
|
||||
ArrowRight,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Loader2,
|
||||
Crosshair,
|
||||
Radar,
|
||||
Zap,
|
||||
Globe,
|
||||
Target,
|
||||
Cpu,
|
||||
Radio
|
||||
Target
|
||||
} from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
import Link from 'next/link'
|
||||
@ -40,13 +35,6 @@ interface HotAuction {
|
||||
affiliate_url?: string
|
||||
}
|
||||
|
||||
interface TrendingTld {
|
||||
tld: string
|
||||
current_price: number
|
||||
price_change: number
|
||||
reason: string
|
||||
}
|
||||
|
||||
interface SearchResult {
|
||||
domain: string
|
||||
status: string
|
||||
@ -58,40 +46,6 @@ interface SearchResult {
|
||||
auctionData?: HotAuction
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ANIMATED RADAR COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
function RadarAnimation() {
|
||||
return (
|
||||
<div className="relative w-full h-full flex items-center justify-center">
|
||||
{/* Outer Ring */}
|
||||
<div className="absolute w-full h-full border border-accent/20 rounded-full" />
|
||||
<div className="absolute w-[75%] h-[75%] border border-accent/15 rounded-full" />
|
||||
<div className="absolute w-[50%] h-[50%] border border-accent/10 rounded-full" />
|
||||
<div className="absolute w-[25%] h-[25%] border border-accent/5 rounded-full" />
|
||||
|
||||
{/* Crosshairs */}
|
||||
<div className="absolute w-full h-px bg-accent/10" />
|
||||
<div className="absolute w-px h-full bg-accent/10" />
|
||||
|
||||
{/* Sweeping Line */}
|
||||
<div
|
||||
className="absolute w-1/2 h-px bg-gradient-to-r from-accent to-transparent origin-left animate-[spin_4s_linear_infinite]"
|
||||
style={{ transformOrigin: 'left center' }}
|
||||
/>
|
||||
|
||||
{/* Center Dot */}
|
||||
<div className="w-2 h-2 bg-accent rounded-full shadow-[0_0_20px_rgba(16,185,129,0.8)]" />
|
||||
|
||||
{/* Blips */}
|
||||
<div className="absolute top-[20%] left-[60%] w-1.5 h-1.5 bg-accent rounded-full animate-ping" />
|
||||
<div className="absolute top-[70%] left-[30%] w-1 h-1 bg-accent/50 rounded-full animate-pulse" />
|
||||
<div className="absolute top-[40%] left-[75%] w-1 h-1 bg-white/30 rounded-full" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// LIVE TICKER
|
||||
// ============================================================================
|
||||
@ -99,16 +53,16 @@ function RadarAnimation() {
|
||||
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-24 bg-gradient-to-r from-[#020202] to-transparent z-10" />
|
||||
<div className="absolute right-0 top-0 bottom-0 w-24 bg-gradient-to-l from-[#020202] to-transparent z-10" />
|
||||
<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-3" style={{ width: 'max-content' }}>
|
||||
<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-4 px-8 border-r border-white/[0.08]">
|
||||
<span className="text-[10px] font-mono uppercase tracking-widest text-white/40">{item.label}</span>
|
||||
<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-sm font-mono font-medium",
|
||||
item.highlight ? "text-accent" : "text-white"
|
||||
"text-xs font-mono font-medium",
|
||||
item.highlight ? "text-accent" : "text-white/70"
|
||||
)}>{item.value}</span>
|
||||
</div>
|
||||
))}
|
||||
@ -139,7 +93,7 @@ export default function RadarPage() {
|
||||
const loadDashboardData = useCallback(async () => {
|
||||
try {
|
||||
const summary = await api.getDashboardSummary()
|
||||
setHotAuctions((summary.market.ending_soon_preview || []).slice(0, 6))
|
||||
setHotAuctions((summary.market.ending_soon_preview || []).slice(0, 5))
|
||||
setMarketStats({
|
||||
totalAuctions: summary.market.total_auctions || 0,
|
||||
endingSoon: summary.market.ending_soon || 0,
|
||||
@ -210,102 +164,89 @@ export default function RadarPage() {
|
||||
const totalDomains = domains?.length || 0
|
||||
|
||||
const tickerItems = [
|
||||
{ label: 'System', value: 'ONLINE', highlight: true },
|
||||
{ label: 'Targets', value: totalDomains.toString() },
|
||||
{ label: 'Opportunities', value: availableDomains.length.toString(), highlight: availableDomains.length > 0 },
|
||||
{ label: 'Market', value: `${marketStats.totalAuctions} Active` },
|
||||
{ label: 'Latency', value: '12ms' },
|
||||
{ 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 (
|
||||
<CommandCenterLayout>
|
||||
<CommandCenterLayout minimal>
|
||||
{toast && <Toast message={toast.message} type={toast.type} onClose={hideToast} />}
|
||||
|
||||
<div className="min-h-screen">
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* HERO SECTION - CINEMATIC */}
|
||||
{/* HERO - Compact for Laptops */}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
<section className="relative min-h-[60vh] flex items-center py-20">
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] opacity-20">
|
||||
<RadarAnimation />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 w-full max-w-[1400px] mx-auto px-6">
|
||||
<div className="grid lg:grid-cols-2 gap-16 items-center">
|
||||
<section className="py-12 lg:py-16">
|
||||
<div className="grid lg:grid-cols-2 gap-10 lg:gap-16 items-center">
|
||||
|
||||
{/* Left: Typography */}
|
||||
<div className="space-y-8">
|
||||
<div className="inline-flex items-center gap-4">
|
||||
<div className="w-2 h-2 bg-accent rounded-full animate-pulse shadow-[0_0_15px_rgba(16,185,129,0.8)]" />
|
||||
<span className="text-[10px] font-mono uppercase tracking-[0.3em] text-accent">
|
||||
Intelligence Hub // Active
|
||||
<div className="space-y-6">
|
||||
<div className="inline-flex items-center gap-3">
|
||||
<div className="w-1.5 h-1.5 bg-accent rounded-full animate-pulse shadow-[0_0_10px_rgba(16,185,129,0.8)]" />
|
||||
<span className="text-[10px] font-mono uppercase tracking-[0.2em] text-accent">
|
||||
Intelligence Hub
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 className="font-display text-[4rem] sm:text-[5rem] lg:text-[6rem] leading-[0.9] tracking-[-0.04em]">
|
||||
<span className="block text-white animate-fade-in">Global</span>
|
||||
<span className="block text-white animate-fade-in" style={{ animationDelay: '0.1s' }}>Recon.</span>
|
||||
<span className="block text-white/30 mt-4 animate-fade-in" style={{ animationDelay: '0.2s' }}>
|
||||
Zero Blind Spots.
|
||||
</span>
|
||||
<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>
|
||||
|
||||
<p className="text-lg text-white/50 max-w-md font-light leading-relaxed">
|
||||
Scanning {marketStats.totalAuctions.toLocaleString()}+ auction listings across all major platforms.
|
||||
<span className="text-white font-medium"> Your targets. Your intel.</span>
|
||||
<p className="text-sm lg:text-base text-white/50 max-w-md font-light leading-relaxed">
|
||||
Real-time monitoring across {marketStats.totalAuctions.toLocaleString()}+ auctions.
|
||||
<span className="text-white/70"> Your targets. Your intel.</span>
|
||||
</p>
|
||||
|
||||
{/* Stats Row */}
|
||||
<div className="flex gap-12 pt-8 border-t border-white/[0.08]">
|
||||
<div className="flex gap-8 lg:gap-10 pt-6 border-t border-white/[0.08]">
|
||||
<div>
|
||||
<div className="text-4xl font-display text-white">{totalDomains}</div>
|
||||
<div className="text-[10px] uppercase tracking-widest text-white/40 font-mono mt-1">Tracking</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-4xl font-display text-accent">{availableDomains.length}</div>
|
||||
<div className="text-[10px] uppercase tracking-widest text-white/40 font-mono mt-1">Ready</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-4xl font-display text-white">{marketStats.endingSoon}</div>
|
||||
<div className="text-[10px] uppercase tracking-widest text-white/40 font-mono mt-1">Ending Soon</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>
|
||||
|
||||
{/* Right: Search Terminal */}
|
||||
<div className="relative">
|
||||
<div className="absolute -inset-4 bg-gradient-to-tr from-accent/10 via-transparent to-accent/5 blur-3xl opacity-50" />
|
||||
<div className="absolute -inset-2 bg-gradient-to-tr from-accent/10 via-transparent to-accent/5 blur-2xl opacity-40" />
|
||||
|
||||
<div className="relative bg-[#0A0A0A] border border-white/20 p-2">
|
||||
<div className="relative bg-[#0A0A0A] border border-white/15 p-1.5">
|
||||
{/* Tech Corners */}
|
||||
<div className="absolute -top-px -left-px w-6 h-6 border-t-2 border-l-2 border-accent" />
|
||||
<div className="absolute -top-px -right-px w-6 h-6 border-t-2 border-r-2 border-accent" />
|
||||
<div className="absolute -bottom-px -left-px w-6 h-6 border-b-2 border-l-2 border-accent" />
|
||||
<div className="absolute -bottom-px -right-px w-6 h-6 border-b-2 border-r-2 border-accent" />
|
||||
<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 className="absolute -bottom-px -left-px w-4 h-4 border-b border-l border-accent/60" />
|
||||
<div className="absolute -bottom-px -right-px w-4 h-4 border-b border-r border-accent/60" />
|
||||
|
||||
<div className="bg-[#050505] p-8 relative overflow-hidden">
|
||||
<div className="bg-[#050505] p-6 lg:p-8 relative">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<span className="text-[10px] font-mono uppercase tracking-[0.2em] text-accent flex items-center gap-2">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<span className="text-[9px] font-mono uppercase tracking-[0.15em] text-accent flex items-center gap-2">
|
||||
<Crosshair className="w-3 h-3" />
|
||||
Target Acquisition
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
<div className="w-1.5 h-1.5 bg-accent/50" />
|
||||
<div className="w-1.5 h-1.5 bg-accent/30" />
|
||||
<div className="w-1.5 h-1.5 bg-accent/10" />
|
||||
<div className="w-1 h-1 bg-accent/50" />
|
||||
<div className="w-1 h-1 bg-accent/30" />
|
||||
<div className="w-1 h-1 bg-accent/10" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div className={clsx(
|
||||
"relative border transition-all duration-300",
|
||||
searchFocused ? "border-accent shadow-[0_0_30px_-10px_rgba(16,185,129,0.3)]" : "border-white/10"
|
||||
searchFocused ? "border-accent/50 shadow-[0_0_20px_-5px_rgba(16,185,129,0.2)]" : "border-white/10"
|
||||
)}>
|
||||
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-accent font-mono">{'>'}</div>
|
||||
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-accent font-mono text-sm">{'>'}</div>
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
@ -314,62 +255,62 @@ export default function RadarPage() {
|
||||
onFocus={() => setSearchFocused(true)}
|
||||
onBlur={() => setSearchFocused(false)}
|
||||
placeholder="ENTER_TARGET..."
|
||||
className="w-full bg-black/50 px-10 py-5 text-2xl text-white placeholder:text-white/20 font-mono uppercase tracking-tight outline-none"
|
||||
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 && (
|
||||
<button
|
||||
onClick={() => { setSearchQuery(''); setSearchResult(null) }}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 text-white/30 hover:text-white"
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-white/20 hover:text-white"
|
||||
>
|
||||
<XCircle className="w-5 h-5" />
|
||||
<XCircle className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Results */}
|
||||
{searchResult && (
|
||||
<div className="mt-6 border-t border-white/[0.08] pt-6">
|
||||
<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-4 h-4 animate-spin" />
|
||||
<span className="text-sm font-mono uppercase tracking-widest">Scanning registries...</span>
|
||||
<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-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{searchResult.is_available ? (
|
||||
<div className="w-3 h-3 bg-accent shadow-[0_0_10px_rgba(16,185,129,0.8)]" />
|
||||
<div className="w-2 h-2 bg-accent shadow-[0_0_8px_rgba(16,185,129,0.8)]" />
|
||||
) : (
|
||||
<div className="w-3 h-3 bg-white/20" />
|
||||
<div className="w-2 h-2 bg-white/20" />
|
||||
)}
|
||||
<span className="text-xl font-mono text-white">{searchResult.domain}</span>
|
||||
<span className="text-base font-mono text-white">{searchResult.domain}</span>
|
||||
</div>
|
||||
<span className={clsx(
|
||||
"text-[10px] font-mono uppercase tracking-widest px-3 py-1 border",
|
||||
"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/40 border-white/10"
|
||||
: "text-white/30 border-white/10"
|
||||
)}>
|
||||
{searchResult.is_available ? 'AVAILABLE' : 'REGISTERED'}
|
||||
{searchResult.is_available ? 'AVAILABLE' : 'TAKEN'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{searchResult.is_available && (
|
||||
<div className="flex gap-3 pt-4">
|
||||
<div className="flex gap-2 pt-2">
|
||||
<button
|
||||
onClick={handleAddToWatchlist}
|
||||
disabled={addingToWatchlist}
|
||||
className="flex-1 py-3 border border-white/20 text-white/80 font-mono text-xs uppercase tracking-widest hover:bg-white/5 transition-colors"
|
||||
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-3 bg-accent text-black font-mono text-xs font-bold uppercase tracking-widest hover:bg-white transition-colors flex items-center justify-center gap-2"
|
||||
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"
|
||||
>
|
||||
ACQUIRE <ArrowRight className="w-3 h-3" />
|
||||
GET <ArrowRight className="w-3 h-3" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
@ -379,10 +320,9 @@ export default function RadarPage() {
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-8 pt-6 border-t border-white/[0.05] flex justify-between items-center text-[10px] text-white/20 font-mono">
|
||||
<span>SECURE_CONNECTION</span>
|
||||
<span>V2.1.0</span>
|
||||
</div>
|
||||
<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>
|
||||
@ -394,137 +334,89 @@ export default function RadarPage() {
|
||||
<LiveTicker items={tickerItems} />
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* LIVE FEED SECTION */}
|
||||
{/* CONTENT GRID */}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
<section className="py-24 px-6">
|
||||
<div className="max-w-[1400px] mx-auto">
|
||||
<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]">
|
||||
|
||||
{/* Section Header */}
|
||||
<div className="flex flex-col lg:flex-row justify-between items-end mb-16 border-b border-white/[0.08] pb-8">
|
||||
<div>
|
||||
<span className="text-accent font-mono text-xs uppercase tracking-[0.2em] mb-4 block">Live Feed</span>
|
||||
<h2 className="font-display text-4xl sm:text-5xl text-white leading-none">
|
||||
Market <br />
|
||||
<span className="text-white/30">Operations.</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div className="mt-6 lg:mt-0 flex items-center gap-6">
|
||||
{/* 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">
|
||||
<div className="w-2 h-2 bg-accent rounded-full animate-pulse" />
|
||||
<span className="text-xs font-mono text-white/40 uppercase">Live Updates</span>
|
||||
</div>
|
||||
<Link
|
||||
href="/terminal/market"
|
||||
className="text-xs font-mono uppercase tracking-widest text-white/40 hover:text-white transition-colors flex items-center gap-2"
|
||||
>
|
||||
Full Feed <ArrowRight className="w-3 h-3" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grid */}
|
||||
<div className="grid lg:grid-cols-3 gap-px bg-white/[0.08]">
|
||||
|
||||
{/* Hot Auctions */}
|
||||
<div className="lg:col-span-2 bg-[#020202] p-8">
|
||||
<div className="flex items-center gap-3 mb-8">
|
||||
<div className="w-8 h-8 border border-accent/30 flex items-center justify-center bg-accent/5">
|
||||
<Gavel className="w-4 h-4 text-accent" />
|
||||
<span className="text-xs font-bold text-white uppercase tracking-wider">Live Auctions</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-bold text-white uppercase tracking-wider">Active Auctions</h3>
|
||||
<p className="text-[10px] text-white/40 font-mono">Real-time market data</p>
|
||||
</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-12">
|
||||
<Loader2 className="w-6 h-6 text-accent animate-spin" />
|
||||
<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 bg-white/[0.05]">
|
||||
<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-4 bg-[#020202] hover:bg-white/[0.02] transition-colors group"
|
||||
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-4">
|
||||
<div className="w-8 h-8 border border-white/10 flex items-center justify-center text-[10px] font-mono text-white/30 group-hover:border-accent/30 group-hover:text-accent transition-colors">
|
||||
{auction.platform.substring(0, 2).toUpperCase()}
|
||||
</div>
|
||||
<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-white group-hover:text-accent transition-colors">{auction.domain}</div>
|
||||
<div className="text-[10px] text-white/30 font-mono uppercase">{auction.time_remaining} LEFT</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="text-right">
|
||||
<div className="font-mono text-accent font-bold">${auction.current_bid.toLocaleString()}</div>
|
||||
<div className="text-[10px] text-white/20 uppercase">Current</div>
|
||||
</div>
|
||||
<div className="font-mono text-sm text-accent">${auction.current_bid.toLocaleString()}</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12 text-white/20 font-mono text-xs uppercase">
|
||||
No active auctions
|
||||
</div>
|
||||
<div className="text-center py-8 text-white/15 font-mono text-xs uppercase">No active auctions</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="bg-[#020202] p-8 flex flex-col">
|
||||
<div className="flex items-center gap-3 mb-8">
|
||||
<div className="w-8 h-8 border border-white/20 flex items-center justify-center bg-white/5">
|
||||
<Zap className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-bold text-white uppercase tracking-wider">Quick Access</h3>
|
||||
<p className="text-[10px] text-white/40 font-mono">Navigation</p>
|
||||
</div>
|
||||
{/* Quick Links */}
|
||||
<div className="bg-[#020202] p-6 lg:p-8">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<Zap className="w-4 h-4 text-white/50" />
|
||||
<span className="text-xs font-bold text-white uppercase tracking-wider">Quick Access</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 flex-1">
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{ label: 'Watchlist', desc: 'Your targets', href: '/terminal/watchlist', icon: Eye },
|
||||
{ label: 'Market', desc: 'All auctions', href: '/terminal/market', icon: Gavel },
|
||||
{ label: 'Intel', desc: 'TLD pricing', href: '/terminal/intel', icon: Globe },
|
||||
{ label: 'Watchlist', href: '/terminal/watchlist', icon: Eye },
|
||||
{ label: 'Market', href: '/terminal/market', icon: Gavel },
|
||||
{ label: 'Intel', href: '/terminal/intel', icon: Globe },
|
||||
].map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="flex items-center gap-4 p-4 border border-white/[0.08] hover:border-accent/30 hover:bg-accent/5 transition-all group"
|
||||
className="flex items-center gap-3 p-3 border border-white/[0.05] hover:border-accent/30 hover:bg-accent/5 transition-all group"
|
||||
>
|
||||
<item.icon className="w-5 h-5 text-white/40 group-hover:text-accent transition-colors" />
|
||||
<div className="flex-1">
|
||||
<div className="text-sm text-white font-medium group-hover:text-accent transition-colors">{item.label}</div>
|
||||
<div className="text-[10px] text-white/30 font-mono uppercase">{item.desc}</div>
|
||||
</div>
|
||||
<ArrowRight className="w-4 h-4 text-white/20 group-hover:text-accent group-hover:translate-x-1 transition-all" />
|
||||
<item.icon className="w-4 h-4 text-white/30 group-hover:text-accent transition-colors" />
|
||||
<span className="text-sm text-white/70 group-hover:text-white transition-colors flex-1">{item.label}</span>
|
||||
<ArrowRight className="w-3 h-3 text-white/15 group-hover:text-accent group-hover:translate-x-0.5 transition-all" />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* System Status */}
|
||||
<div className="mt-8 pt-6 border-t border-white/[0.08]">
|
||||
<div className="text-[10px] font-mono uppercase text-white/30 mb-3">System Status</div>
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Status */}
|
||||
<div className="mt-6 pt-4 border-t border-white/[0.05]">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 bg-accent rounded-full animate-pulse" />
|
||||
<span className="text-xs text-white/60 font-mono">All Systems Operational</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-white/20 font-mono">99.9%</span>
|
||||
<div className="w-1 h-1 bg-accent rounded-full animate-pulse" />
|
||||
<span className="text-[9px] font-mono text-white/30 uppercase">System Online</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<style jsx global>{`
|
||||
@keyframes ticker {
|
||||
0% { transform: translateX(0); }
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState, useMemo, useCallback, memo } 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'
|
||||
@ -13,28 +13,24 @@ import {
|
||||
Bell,
|
||||
BellOff,
|
||||
Eye,
|
||||
Sparkles,
|
||||
X,
|
||||
Activity,
|
||||
Shield,
|
||||
AlertTriangle,
|
||||
Clock,
|
||||
ArrowRight,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Zap,
|
||||
Target,
|
||||
Radio,
|
||||
Crosshair,
|
||||
Globe,
|
||||
ExternalLink,
|
||||
Calendar
|
||||
Calendar,
|
||||
Shield,
|
||||
Crosshair
|
||||
} from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
import Link from 'next/link'
|
||||
|
||||
// ============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// HELPERS
|
||||
// ============================================================================
|
||||
|
||||
function getDaysUntilExpiry(expirationDate: string | null): number | null {
|
||||
@ -64,13 +60,12 @@ function getTimeAgo(date: string | null): string {
|
||||
return `${diffDays}d ago`
|
||||
}
|
||||
|
||||
// Health config
|
||||
const healthConfig: Record<HealthStatus, { label: string; color: string; bg: string }> = {
|
||||
healthy: { label: 'ONLINE', color: 'text-accent', bg: 'bg-accent/10 border-accent/20' },
|
||||
weakening: { label: 'WARNING', 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' },
|
||||
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' },
|
||||
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' },
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@ -78,7 +73,7 @@ const healthConfig: Record<HealthStatus, { label: string; color: string; bg: str
|
||||
// ============================================================================
|
||||
|
||||
export default function WatchlistPage() {
|
||||
const { domains, addDomain, deleteDomain, refreshDomain, updateDomain, subscription } = useStore()
|
||||
const { domains, addDomain, deleteDomain, refreshDomain, updateDomain } = useStore()
|
||||
const { toast, showToast, hideToast } = useToast()
|
||||
|
||||
const [newDomain, setNewDomain] = useState('')
|
||||
@ -186,52 +181,42 @@ export default function WatchlistPage() {
|
||||
load()
|
||||
}, [domains])
|
||||
|
||||
// Selected domain for detail view
|
||||
const selectedDomainData = selectedDomain ? domains?.find(d => d.id === selectedDomain) : null
|
||||
const selectedHealth = selectedDomain ? healthReports[selectedDomain] : null
|
||||
|
||||
return (
|
||||
<CommandCenterLayout>
|
||||
<CommandCenterLayout minimal>
|
||||
{toast && <Toast message={toast.message} type={toast.type} onClose={hideToast} />}
|
||||
|
||||
<div className="min-h-screen">
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* HERO */}
|
||||
{/* HEADER - Compact */}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
<section className="relative py-16 border-b border-white/[0.08]">
|
||||
<div className="max-w-[1400px] mx-auto px-6">
|
||||
<div className="grid lg:grid-cols-2 gap-16 items-end">
|
||||
<section className="pt-10 lg:pt-12 pb-8">
|
||||
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-6">
|
||||
|
||||
{/* Left */}
|
||||
<div className="space-y-6">
|
||||
<div className="inline-flex items-center gap-4">
|
||||
<Target className="w-5 h-5 text-accent" />
|
||||
<span className="text-[10px] font-mono uppercase tracking-[0.3em] text-accent">
|
||||
Active Surveillance
|
||||
</span>
|
||||
<div className="space-y-3">
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<Target className="w-4 h-4 text-accent" />
|
||||
<span className="text-[9px] font-mono uppercase tracking-[0.2em] text-accent">Surveillance</span>
|
||||
</div>
|
||||
|
||||
<h1 className="font-display text-[3.5rem] sm:text-[4.5rem] lg:text-[5rem] leading-[0.9] tracking-[-0.04em]">
|
||||
<span className="block text-white">Target</span>
|
||||
<span className="block text-white/30">Acquisition.</span>
|
||||
<h1 className="font-display text-[2.5rem] lg:text-[3rem] leading-[0.95] tracking-[-0.03em]">
|
||||
<span className="text-white">Watchlist</span>
|
||||
<span className="text-white/30 ml-3">{stats.total}</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-lg text-white/50 max-w-lg font-light">
|
||||
Monitor high-value domains. Get instant alerts when they become available.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Right: Big Numbers */}
|
||||
<div className="grid grid-cols-3 gap-px bg-white/[0.08]">
|
||||
<div className="bg-[#020202] p-6 text-center">
|
||||
<div className="text-4xl lg:text-5xl font-display text-white">{stats.total}</div>
|
||||
<div className="text-[10px] uppercase tracking-widest text-white/40 font-mono mt-2">Tracking</div>
|
||||
</div>
|
||||
<div className="bg-[#020202] p-6 text-center">
|
||||
<div className="text-4xl lg:text-5xl font-display text-accent">{stats.available}</div>
|
||||
<div className="text-[10px] uppercase tracking-widest text-white/40 font-mono mt-2">Available</div>
|
||||
</div>
|
||||
<div className="bg-[#020202] p-6 text-center">
|
||||
<div className="text-4xl lg:text-5xl font-display text-amber-400">{stats.expiring}</div>
|
||||
<div className="text-[10px] uppercase tracking-widest text-white/40 font-mono mt-2">Expiring</div>
|
||||
{/* Right: Stats */}
|
||||
<div className="flex gap-6 lg:gap-8">
|
||||
<div className="text-right">
|
||||
<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>
|
||||
@ -240,248 +225,293 @@ export default function WatchlistPage() {
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* ADD DOMAIN */}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
<section className="py-12 border-b border-white/[0.08] bg-[#050505]">
|
||||
<div className="max-w-[1400px] mx-auto px-6">
|
||||
<form onSubmit={handleAdd} className="relative max-w-2xl mx-auto">
|
||||
<div className="absolute -inset-1 bg-gradient-to-r from-accent/20 via-transparent to-accent/20 blur-xl opacity-30" />
|
||||
|
||||
<div className="relative bg-[#0A0A0A] border border-white/20 p-1.5">
|
||||
<div className="flex items-center bg-black">
|
||||
<div className="px-4 text-accent font-mono text-lg">{'>'}</div>
|
||||
<section className="pb-8">
|
||||
<form onSubmit={handleAdd} className="relative max-w-xl">
|
||||
<div className="flex items-center bg-[#050505] border border-white/10 focus-within:border-accent/40 transition-colors">
|
||||
<div className="pl-4 text-accent font-mono">{'>'}</div>
|
||||
<input
|
||||
type="text"
|
||||
value={newDomain}
|
||||
onChange={(e) => setNewDomain(e.target.value)}
|
||||
placeholder="ENTER_TARGET_DOMAIN..."
|
||||
className="flex-1 bg-transparent py-5 text-xl text-white placeholder:text-white/20 font-mono uppercase outline-none"
|
||||
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"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={adding || !newDomain.trim()}
|
||||
className="px-8 py-5 bg-accent text-black font-mono text-sm font-bold uppercase tracking-widest hover:bg-white transition-colors disabled:opacity-50"
|
||||
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"
|
||||
>
|
||||
{adding ? <Loader2 className="w-5 h-5 animate-spin" /> : '+ LOCK'}
|
||||
{adding ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Plus className="w-3.5 h-3.5" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* FILTERS */}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
<section className="py-8 border-b border-white/[0.08]">
|
||||
<div className="max-w-[1400px] mx-auto px-6 flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<section className="pb-6 border-b border-white/[0.08]">
|
||||
<div className="flex items-center gap-1">
|
||||
{[
|
||||
{ key: 'all', label: 'All Targets', count: stats.total },
|
||||
{ key: 'available', label: 'Available', count: stats.available },
|
||||
{ key: 'expiring', label: 'Expiring', count: stats.expiring },
|
||||
].map((f) => (
|
||||
{ value: 'all', label: 'All', count: stats.total },
|
||||
{ value: 'available', label: 'Available', count: stats.available },
|
||||
{ value: 'expiring', label: 'Expiring', count: stats.expiring },
|
||||
].map((item) => (
|
||||
<button
|
||||
key={f.key}
|
||||
onClick={() => setFilter(f.key as any)}
|
||||
key={item.value}
|
||||
onClick={() => setFilter(item.value as typeof filter)}
|
||||
className={clsx(
|
||||
"px-4 py-2 text-xs font-mono uppercase tracking-widest transition-all border",
|
||||
filter === f.key
|
||||
? "bg-white text-black border-white"
|
||||
: "bg-transparent text-white/50 border-white/10 hover:border-white/30 hover:text-white"
|
||||
"px-4 py-2 text-[10px] font-mono uppercase tracking-wider transition-colors",
|
||||
filter === item.value
|
||||
? "bg-white/10 text-white"
|
||||
: "text-white/30 hover:text-white/50"
|
||||
)}
|
||||
>
|
||||
{f.label} <span className="opacity-50">({f.count})</span>
|
||||
{item.label} ({item.count})
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="text-[10px] font-mono text-white/30 uppercase tracking-widest">
|
||||
Auto-refresh: {subscription?.tier === 'tycoon' ? '10min' : 'Hourly'}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* DATA TABLE */}
|
||||
{/* TABLE */}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
<section className="py-12">
|
||||
<div className="max-w-[1400px] mx-auto px-6">
|
||||
|
||||
{filteredDomains.length === 0 ? (
|
||||
<div className="text-center py-24">
|
||||
<div className="inline-flex items-center justify-center w-20 h-20 border border-white/10 mb-6">
|
||||
<Crosshair className="w-8 h-8 text-white/20" />
|
||||
<section className="py-6">
|
||||
{!filteredDomains.length ? (
|
||||
<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">
|
||||
<Crosshair className="w-5 h-5 text-white/20" />
|
||||
</div>
|
||||
<h3 className="text-xl text-white font-display mb-2">No Targets</h3>
|
||||
<p className="text-white/40 text-sm font-mono">Add domains above to start tracking</p>
|
||||
<p className="text-white/30 font-mono text-sm uppercase">No targets</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="border border-white/[0.08]">
|
||||
{/* Header */}
|
||||
<div className="grid grid-cols-12 gap-4 px-6 py-4 bg-white/[0.02] border-b border-white/[0.08] text-[10px] font-mono uppercase tracking-widest text-white/30">
|
||||
<div className="col-span-4">Target</div>
|
||||
<div className="col-span-2 text-center">Status</div>
|
||||
<div className="col-span-2 text-center">Health</div>
|
||||
<div className="col-span-2 text-center">Expires</div>
|
||||
<div className="col-span-2 text-right">Actions</div>
|
||||
<div className="space-y-px">
|
||||
{/* 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>Domain</div>
|
||||
<div>Status</div>
|
||||
<div>Health</div>
|
||||
<div>Expires</div>
|
||||
<div>Alert</div>
|
||||
<div className="text-right">Actions</div>
|
||||
</div>
|
||||
|
||||
{/* Rows */}
|
||||
<div className="divide-y divide-white/[0.05]">
|
||||
{filteredDomains.map((domain) => {
|
||||
const health = healthReports[domain.id]
|
||||
const hConfig = health ? healthConfig[health.status] : healthConfig.unknown
|
||||
const healthStatus = health?.status || 'unknown'
|
||||
const config = healthConfig[healthStatus]
|
||||
const days = getDaysUntilExpiry(domain.expiration_date)
|
||||
const expiringSoon = days !== null && days <= 30 && days > 0
|
||||
|
||||
return (
|
||||
<div
|
||||
key={domain.id}
|
||||
className={clsx(
|
||||
"grid grid-cols-12 gap-4 px-6 py-5 items-center transition-colors group",
|
||||
domain.is_available ? "bg-accent/[0.02] hover:bg-accent/[0.05]" : "bg-transparent hover:bg-white/[0.02]"
|
||||
)}
|
||||
className="group bg-white/[0.01] hover:bg-white/[0.03] border border-white/[0.05] hover:border-white/[0.08] transition-all"
|
||||
>
|
||||
{/* Domain */}
|
||||
<div className="col-span-4 flex items-center gap-4">
|
||||
{/* Mobile */}
|
||||
<div className="lg:hidden p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={clsx(
|
||||
"w-2 h-2 shrink-0",
|
||||
domain.is_available
|
||||
? "bg-accent shadow-[0_0_10px_rgba(16,185,129,0.8)]"
|
||||
: "bg-white/20"
|
||||
"w-2 h-2",
|
||||
domain.is_available ? "bg-accent shadow-[0_0_8px_rgba(16,185,129,0.8)]" : "bg-white/15"
|
||||
)} />
|
||||
<div className="min-w-0">
|
||||
<div className="font-mono text-white text-sm truncate">{domain.name}</div>
|
||||
<div className="text-[10px] text-white/30 font-mono uppercase truncate">
|
||||
{domain.registrar || 'Unknown registrar'}
|
||||
<span className="font-mono text-sm text-white">{domain.name}</span>
|
||||
</div>
|
||||
<span className={clsx(
|
||||
"text-[9px] font-mono uppercase px-2 py-0.5 border",
|
||||
domain.is_available ? "text-accent border-accent/30" : "text-white/30 border-white/10"
|
||||
)}>
|
||||
{domain.is_available ? 'OPEN' : 'TAKEN'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-[10px] font-mono text-white/30">
|
||||
<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>
|
||||
|
||||
{/* 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 className="col-span-2 flex justify-center">
|
||||
<div>
|
||||
<span className={clsx(
|
||||
"text-[10px] font-mono font-bold uppercase tracking-widest px-3 py-1 border",
|
||||
domain.is_available
|
||||
? "text-accent border-accent/30 bg-accent/5"
|
||||
: "text-white/40 border-white/10 bg-white/5"
|
||||
"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 ? 'OPEN' : 'LOCKED'}
|
||||
{domain.is_available ? 'AVAIL' : 'TAKEN'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Health */}
|
||||
<div className="col-span-2 flex justify-center">
|
||||
{domain.is_available ? (
|
||||
<span className="text-white/20">—</span>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => handleHealthCheck(domain.id)}
|
||||
className={clsx(
|
||||
"text-[10px] font-mono font-bold uppercase px-3 py-1 border transition-all",
|
||||
hConfig.bg, hConfig.color
|
||||
)}
|
||||
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" />
|
||||
) : hConfig.label}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Expiry */}
|
||||
<div className="col-span-2 text-center">
|
||||
{domain.is_available ? (
|
||||
<span className="text-white/20">—</span>
|
||||
) : days !== null ? (
|
||||
<div className={clsx(
|
||||
"font-mono text-sm",
|
||||
expiringSoon ? "text-amber-400" : "text-white/50"
|
||||
)}>
|
||||
{days}d
|
||||
<span className="text-[10px] text-white/30 block uppercase">remaining</span>
|
||||
</div>
|
||||
<Loader2 className="w-3 h-3 animate-spin text-white/30" />
|
||||
) : (
|
||||
<span className="text-white/20 text-xs">Hidden</span>
|
||||
<>
|
||||
<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>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="col-span-2 flex justify-end gap-2">
|
||||
{/* Alert */}
|
||||
<button
|
||||
onClick={() => handleToggleNotify(domain.id, domain.notify_on_available)}
|
||||
disabled={togglingNotifyId === domain.id}
|
||||
className={clsx(
|
||||
"w-8 h-8 flex items-center justify-center border transition-all",
|
||||
domain.notify_on_available
|
||||
? "border-accent/30 bg-accent/10 text-accent"
|
||||
: "border-white/10 text-white/30 hover:text-white hover:border-white/30"
|
||||
"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 h-3 animate-spin" />
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
) : domain.notify_on_available ? (
|
||||
<Bell className="w-3.5 h-3.5" />
|
||||
) : (
|
||||
<Bell className="w-3 h-3" />
|
||||
<BellOff className="w-3.5 h-3.5" />
|
||||
)}
|
||||
</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 border border-white/10 text-white/30 hover:text-white hover:border-white/30 transition-all"
|
||||
className="w-7 h-7 flex items-center justify-center text-white/20 hover:text-white hover:bg-white/5 transition-all"
|
||||
>
|
||||
{refreshingId === domain.id ? (
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="w-3 h-3" />
|
||||
)}
|
||||
<RefreshCw className={clsx("w-3.5 h-3.5", 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 border border-white/10 text-white/30 hover:text-rose-400 hover:border-rose-400/30 transition-all"
|
||||
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 h-3 animate-spin" />
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="w-3 h-3" />
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{domain.is_available && (
|
||||
<a
|
||||
href={`https://www.namecheap.com/domains/registration/results/?domain=${domain.name}`}
|
||||
target="_blank"
|
||||
className="px-4 h-8 flex items-center gap-2 bg-accent text-black text-[10px] font-mono font-bold uppercase tracking-widest hover:bg-white transition-colors"
|
||||
>
|
||||
GET <ArrowRight className="w-3 h-3" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* HEALTH MODAL */}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{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 className="w-full max-w-md bg-[#0A0A0A] border border-white/15 p-1.5" onClick={(e) => e.stopPropagation()}>
|
||||
{/* Corner Decorations */}
|
||||
<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 className="absolute -bottom-px -left-px w-4 h-4 border-b border-l border-accent/60" />
|
||||
<div className="absolute -bottom-px -right-px w-4 h-4 border-b border-r border-accent/60" />
|
||||
|
||||
<div className="bg-[#050505] p-6 relative">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Activity className="w-4 h-4 text-accent" />
|
||||
<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>
|
||||
|
||||
{/* Domain */}
|
||||
<div className="mb-6">
|
||||
<h3 className="font-mono text-lg text-white">{selectedDomainData.name}</h3>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<div className={clsx(
|
||||
"text-[9px] font-mono uppercase px-2 py-0.5 border",
|
||||
healthConfig[selectedHealth?.status || 'unknown'].bg,
|
||||
healthConfig[selectedHealth?.status || 'unknown'].color
|
||||
)}>
|
||||
{healthConfig[selectedHealth?.status || 'unknown'].label}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Checks */}
|
||||
{selectedHealth && (
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{ label: 'DNS', value: selectedHealth.dns_resolves },
|
||||
{ label: 'HTTP', value: selectedHealth.http_status === 200 },
|
||||
{ label: 'SSL', value: selectedHealth.ssl_valid },
|
||||
{ label: 'Parked', value: !selectedHealth.is_parked },
|
||||
].map((check) => (
|
||||
<div key={check.label} className="flex items-center justify-between py-2 border-b border-white/[0.05]">
|
||||
<span className="text-xs font-mono text-white/50 uppercase">{check.label}</span>
|
||||
{check.value ? (
|
||||
<CheckCircle2 className="w-4 h-4 text-accent" />
|
||||
) : (
|
||||
<XCircle className="w-4 h-4 text-rose-400" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Refresh */}
|
||||
<button
|
||||
onClick={() => handleHealthCheck(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"
|
||||
>
|
||||
{loadingHealth[selectedDomainData.id] ? (
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw className="w-3.5 h-3.5" />
|
||||
Refresh
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
{/* FOOTER */}
|
||||
{/* ═══════════════════════════════════════════════════════════════════════ */}
|
||||
<section className="py-8 border-t border-white/[0.08]">
|
||||
<div className="max-w-[1400px] mx-auto px-6 flex items-center justify-between text-[10px] font-mono uppercase text-white/20">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1.5 h-1.5 bg-accent rounded-full animate-pulse" />
|
||||
System Active
|
||||
</div>
|
||||
<div>Last sync: {getTimeAgo(new Date().toISOString())}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
)}
|
||||
</CommandCenterLayout>
|
||||
)
|
||||
}
|
||||
@ -14,13 +14,15 @@ interface CommandCenterLayoutProps {
|
||||
title?: string
|
||||
subtitle?: string
|
||||
actions?: React.ReactNode
|
||||
minimal?: boolean // No header, content starts at top
|
||||
}
|
||||
|
||||
export function CommandCenterLayout({
|
||||
children,
|
||||
title,
|
||||
subtitle,
|
||||
actions
|
||||
actions,
|
||||
minimal = false
|
||||
}: CommandCenterLayoutProps) {
|
||||
const router = useRouter()
|
||||
const { isAuthenticated, isLoading, checkAuth, domains } = useStore()
|
||||
@ -101,13 +103,14 @@ export function CommandCenterLayout({
|
||||
className={clsx(
|
||||
"relative min-h-screen transition-all duration-300",
|
||||
// Desktop: adjust for sidebar
|
||||
"lg:ml-[260px]",
|
||||
"lg:ml-[240px]",
|
||||
sidebarCollapsed && "lg:ml-[72px]",
|
||||
// Mobile: no margin, just padding for menu button
|
||||
"ml-0 pt-16 lg:pt-0"
|
||||
)}
|
||||
>
|
||||
{/* Top Bar */}
|
||||
{/* Top Bar - Hidden in minimal mode */}
|
||||
{!minimal && (
|
||||
<header className="sticky top-0 z-30 bg-gradient-to-r from-background/95 via-background/90 to-background/95 backdrop-blur-xl border-b border-border/30">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-5 sm:py-6 flex items-center justify-between">
|
||||
{/* Left: Title */}
|
||||
@ -226,10 +229,14 @@ export function CommandCenterLayout({
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)}
|
||||
|
||||
{/* Page Content */}
|
||||
<main className="relative">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-8">
|
||||
<div className={clsx(
|
||||
"mx-auto",
|
||||
minimal ? "max-w-[1600px] px-6" : "max-w-7xl px-4 sm:px-6 lg:px-8 py-6 sm:py-8"
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user