From dc5090a5b2ef2794b032b6dc5aaf876bb7d1a71a Mon Sep 17 00:00:00 2001 From: Yves Gugger Date: Fri, 12 Dec 2025 21:21:16 +0100 Subject: [PATCH] feat: RADAR & WATCHLIST komplett neu designed - Cinematic High-End UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RADAR PAGE: - Animated Radar Background mit Blips und Sweeping Line - Hero Section mit riesiger Typografie (6rem Headlines) - Search Terminal im 'Target Acquisition' Design - Live Ticker mit Animation - Market Feed Grid mit Tech-Corners - Quick Access Navigation Cards WATCHLIST PAGE: - Dramatische Hero Section mit Big Numbers Grid - Command Line Style Domain Input - Pill-Filter mit Hover-Animationen - Daten-Tabelle mit Status-Badges - Health-Check Integration - Accent Glows und Tech-Corners überall Beide Seiten nutzen jetzt exakt den Landing Page Stil: - #020202 Background - font-display für Headlines - font-mono für Labels - text-[10px] uppercase tracking-widest - border-white/[0.08] für Linien - Tech-Corners an wichtigen Boxen - Accent Glow Effects --- frontend/src/app/terminal/radar/page.tsx | 886 ++++---- frontend/src/app/terminal/watchlist/page.tsx | 2148 +++--------------- 2 files changed, 691 insertions(+), 2343 deletions(-) diff --git a/frontend/src/app/terminal/radar/page.tsx b/frontend/src/app/terminal/radar/page.tsx index a10f604..70ff9a3 100644 --- a/frontend/src/app/terminal/radar/page.tsx +++ b/frontend/src/app/terminal/radar/page.tsx @@ -1,107 +1,33 @@ 'use client' -import { useEffect, useState, useMemo, useCallback, useRef, memo } from 'react' -import { useSearchParams } from 'next/navigation' +import { useEffect, useState, useMemo, useCallback, useRef } from 'react' import { useStore } from '@/lib/store' import { api } from '@/lib/api' -import { CommandCenterLayout } from '@/components/CommandCenterLayout' // Using the new layout -import { Ticker, useTickerItems } from '@/components/Ticker' +import { CommandCenterLayout } from '@/components/CommandCenterLayout' import { Toast, useToast } from '@/components/Toast' import { Eye, Gavel, - Tag, Clock, ExternalLink, Plus, Activity, - Bell, Search, ArrowRight, CheckCircle2, XCircle, Loader2, - Wifi, - ShieldAlert, - Command, - Building2, - Calendar, - Server, - Radar, Crosshair, + Radar, + Zap, + Globe, + Target, Cpu, - Globe + Radio } from 'lucide-react' import clsx from 'clsx' import Link from 'next/link' -// ============================================================================ -// HELPER FUNCTIONS -// ============================================================================ - -const formatDate = (dateStr: string | null) => { - if (!dateStr) return null - return new Date(dateStr).toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric', - }) -} - -const getDaysUntilExpiration = (dateStr: string | null) => { - if (!dateStr) return null - const expDate = new Date(dateStr) - const now = new Date() - const diffTime = expDate.getTime() - now.getTime() - const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) - return diffDays -} - -// ============================================================================ -// COMPONENTS - TECHY CHIC -// ============================================================================ - -const TechCard = memo(({ - label, - value, - subValue, - icon: Icon, - trend -}: { - label: string - value: string | number - subValue?: string - icon: any - trend?: 'up' | 'down' | 'neutral' | 'active' -}) => ( -
- {/* Tech Corners */} -
-
-
-
- -
-
- - {label} -
- {trend === 'active' && ( - - - - - )} -
- -
-
{value}
- {subValue &&
{subValue}
} -
-
-)) -TechCard.displayName = 'TechCard' - // ============================================================================ // TYPES // ============================================================================ @@ -121,28 +47,74 @@ interface TrendingTld { reason: string } -interface ListingStats { - active: number - sold: number - draft: number - total: number -} - -interface MarketStats { - totalAuctions: number - endingSoon: number -} - interface SearchResult { domain: string status: string is_available: boolean | null registrar: string | null expiration_date: string | null - name_servers: string[] | null + loading: boolean inAuction: boolean auctionData?: HotAuction - loading: boolean +} + +// ============================================================================ +// ANIMATED RADAR COMPONENT +// ============================================================================ + +function RadarAnimation() { + return ( +
+ {/* Outer Ring */} +
+
+
+
+ + {/* Crosshairs */} +
+
+ + {/* Sweeping Line */} +
+ + {/* Center Dot */} +
+ + {/* Blips */} +
+
+
+
+ ) +} + +// ============================================================================ +// LIVE TICKER +// ============================================================================ + +function LiveTicker({ items }: { items: { label: string; value: string; highlight?: boolean }[] }) { + return ( +
+
+
+ +
+ {[...items, ...items, ...items].map((item, i) => ( +
+ {item.label} + {item.value} +
+ ))} +
+
+ ) } // ============================================================================ @@ -150,24 +122,13 @@ interface SearchResult { // ============================================================================ export default function RadarPage() { - const searchParams = useSearchParams() - const { - isAuthenticated, - isLoading, - user, - domains, - subscription, - addDomain, - } = useStore() - + const { isAuthenticated, user, domains, addDomain } = useStore() const { toast, showToast, hideToast } = useToast() + const [hotAuctions, setHotAuctions] = useState([]) - const [trendingTlds, setTrendingTlds] = useState([]) - const [listingStats, setListingStats] = useState({ active: 0, sold: 0, draft: 0, total: 0 }) - const [marketStats, setMarketStats] = useState({ totalAuctions: 0, endingSoon: 0 }) + const [marketStats, setMarketStats] = useState({ totalAuctions: 0, endingSoon: 0 }) const [loadingData, setLoadingData] = useState(true) - // Universal Search State const [searchQuery, setSearchQuery] = useState('') const [searchResult, setSearchResult] = useState(null) const [addingToWatchlist, setAddingToWatchlist] = useState(false) @@ -178,15 +139,13 @@ export default function RadarPage() { const loadDashboardData = useCallback(async () => { try { const summary = await api.getDashboardSummary() - setHotAuctions((summary.market.ending_soon_preview || []).slice(0, 5)) + setHotAuctions((summary.market.ending_soon_preview || []).slice(0, 6)) setMarketStats({ totalAuctions: summary.market.total_auctions || 0, endingSoon: summary.market.ending_soon || 0, }) - setTrendingTlds(summary.tlds?.trending?.slice(0, 6) || []) - setListingStats(summary.listings || { active: 0, sold: 0, draft: 0, total: 0 }) } catch (error) { - console.error('Failed to load dashboard data:', error) + console.error('Failed to load data:', error) } finally { setLoadingData(false) } @@ -196,59 +155,30 @@ export default function RadarPage() { if (isAuthenticated) loadDashboardData() }, [isAuthenticated, loadDashboardData]) - // Search Logic + // Search const handleSearch = useCallback(async (domainInput: string) => { - if (!domainInput.trim()) { - setSearchResult(null) - return - } - + if (!domainInput.trim()) { setSearchResult(null); return } const cleanDomain = domainInput.trim().toLowerCase() - setSearchResult({ - domain: cleanDomain, - status: 'checking', - is_available: null, - registrar: null, - expiration_date: null, - name_servers: null, - inAuction: false, - auctionData: undefined, - loading: true - }) - + setSearchResult({ domain: cleanDomain, status: 'checking', is_available: null, registrar: null, expiration_date: null, loading: true, inAuction: false }) + try { const [whoisResult, auctionsResult] = await Promise.all([ api.checkDomain(cleanDomain).catch(() => null), api.getAuctions(cleanDomain).catch(() => ({ auctions: [] })), ]) - - const auctionMatch = (auctionsResult as any).auctions?.find( - (a: any) => a.domain.toLowerCase() === cleanDomain - ) - + const auctionMatch = (auctionsResult as any).auctions?.find((a: any) => a.domain.toLowerCase() === cleanDomain) setSearchResult({ domain: whoisResult?.domain || cleanDomain, status: whoisResult?.status || 'unknown', is_available: whoisResult?.is_available ?? null, registrar: whoisResult?.registrar || null, expiration_date: whoisResult?.expiration_date || null, - name_servers: whoisResult?.name_servers || null, + loading: false, inAuction: !!auctionMatch, auctionData: auctionMatch, - loading: false, - }) - } catch (error) { - setSearchResult({ - domain: cleanDomain, - status: 'error', - is_available: null, - registrar: null, - expiration_date: null, - name_servers: null, - inAuction: false, - auctionData: undefined, - loading: false }) + } catch { + setSearchResult({ domain: cleanDomain, status: 'error', is_available: null, registrar: null, expiration_date: null, loading: false, inAuction: false }) } }, []) @@ -257,11 +187,11 @@ export default function RadarPage() { setAddingToWatchlist(true) try { await addDomain(searchQuery.trim()) - showToast(`Added ${searchQuery.trim()} to watchlist`, 'success') + showToast(`Target acquired: ${searchQuery.trim()}`, 'success') setSearchQuery('') setSearchResult(null) } catch (err: any) { - showToast(err.message || 'Failed to add domain', 'error') + showToast(err.message || 'Mission failed', 'error') } finally { setAddingToWatchlist(false) } @@ -269,380 +199,338 @@ export default function RadarPage() { useEffect(() => { const timer = setTimeout(() => { - if (searchQuery.length > 3) { - handleSearch(searchQuery) - } else { - setSearchResult(null) - } + if (searchQuery.length > 3) handleSearch(searchQuery) + else setSearchResult(null) }, 500) return () => clearTimeout(timer) }, [searchQuery, handleSearch]) - // Focus shortcut - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (e.metaKey && e.key === 'k') { - e.preventDefault() - searchInputRef.current?.focus() - } - } - window.addEventListener('keydown', handleKeyDown) - return () => window.removeEventListener('keydown', handleKeyDown) - }, []) - // Computed - const { availableDomains, expiringDomains, recentAlerts, totalDomains, greeting, subtitle } = useMemo(() => { - const available = domains?.filter(d => d.is_available) || [] - const total = domains?.length || 0 - const hour = new Date().getHours() - const greeting = hour < 12 ? 'Target Acquisition' : hour < 18 ? 'Live Operations' : 'Night Watch' - - const now = new Date() - const thirtyDaysFromNow = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000) - const expiring = domains?.filter(d => { - if (!d.expiration_date || d.is_available) return false - const expDate = new Date(d.expiration_date) - return expDate <= thirtyDaysFromNow && expDate > now - }) || [] - - type AlertItem = { - domain: typeof domains[0] - type: 'available' | 'expiring' | 'checked' - priority: number - } - - const alerts: AlertItem[] = [] - available.forEach(d => alerts.push({ domain: d, type: 'available', priority: 1 })) - expiring.forEach(d => alerts.push({ domain: d, type: 'expiring', priority: 2 })) - - const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000) - const recentlyChecked = domains?.filter(d => { - if (d.is_available || expiring.includes(d)) return false - if (!d.last_checked) return false - return new Date(d.last_checked) > oneDayAgo - }) || [] - recentlyChecked.slice(0, 3).forEach(d => alerts.push({ domain: d, type: 'checked', priority: 3 })) - - alerts.sort((a, b) => a.priority - b.priority) - - return { - availableDomains: available, - expiringDomains: expiring, - recentAlerts: alerts, - totalDomains: total, - greeting, - subtitle: `${total} Assets Tracked // ${available.length} Actionable` - } - }, [domains]) - - const tickerItems = useTickerItems(trendingTlds, availableDomains, hotAuctions) + const availableDomains = domains?.filter(d => d.is_available) || [] + 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' }, + ] return ( - + {toast && } -
+
- {/* TOP SECTION: METRICS GRID */} -
-
- - 0 ? 'up' : 'neutral'} - /> - + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* HERO SECTION - CINEMATIC */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+
+
+ +
-
- - - -
-
- - - -
-
- -
-
- - {/* CENTER SECTION: SEARCH & COMMAND */} -
-
-
- {/* Decorative Crosshairs */} -
-
-
-
- -
-
- - Target Acquisition System V2.0 + +
+
+ + {/* Left: Typography */} +
+
+
+ + Intelligence Hub // Active + +
+ +

+ Global + Recon. + + Zero Blind Spots. + +

+ +

+ Scanning {marketStats.totalAuctions.toLocaleString()}+ auction listings across all major platforms. + Your targets. Your intel. +

+ + {/* Stats Row */} +
+
+
{totalDomains}
+
Tracking
+
+
+
{availableDomains.length}
+
Ready
+
+
+
{marketStats.endingSoon}
+
Ending Soon
+
+
-
- {'>'} - setSearchQuery(e.target.value)} - onFocus={() => setSearchFocused(true)} - onBlur={() => setSearchFocused(false)} - placeholder="ENTER_DOMAIN_TARGET..." - className="w-full bg-transparent text-3xl md:text-5xl text-white placeholder:text-white/10 font-display outline-none uppercase tracking-tight" - /> + {/* Right: Search Terminal */} +
+
+ +
+ {/* Tech Corners */} +
+
+
+
+ +
+ {/* Header */} +
+ + + Target Acquisition + +
+
+
+
+
+
+ + {/* Input */} +
+
{'>'}
+ setSearchQuery(e.target.value)} + 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" + /> + {searchQuery && ( + + )} +
+ + {/* Results */} + {searchResult && ( +
+ {searchResult.loading ? ( +
+ + Scanning registries... +
+ ) : ( +
+
+
+ {searchResult.is_available ? ( +
+ ) : ( +
+ )} + {searchResult.domain} +
+ + {searchResult.is_available ? 'AVAILABLE' : 'REGISTERED'} + +
+ + {searchResult.is_available && ( +
+ + + ACQUIRE + +
+ )} +
+ )} +
+ )} + + {/* Footer */} +
+ SECURE_CONNECTION + V2.1.0 +
+
+
+
+
+
+ + + {/* Ticker */} + + + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* LIVE FEED SECTION */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+
+ + {/* Section Header */} +
+
+ Live Feed +

+ Market
+ Operations. +

+
+
+
+
+ Live Updates +
+ + Full Feed +
- {/* SEARCH RESULTS */} - {searchResult && ( -
- {searchResult.loading ? ( -
- - Scanning Global Registries... + {/* Grid */} +
+ + {/* Hot Auctions */} +
+
+
+ +
+
+

Active Auctions

+

Real-time market data

+
+
+ + {loadingData ? ( +
+ +
+ ) : hotAuctions.length > 0 ? ( + ) : ( -
- {/* Primary Status */} -
-
-

{searchResult.domain}

- {searchResult.is_available ? ( - Available - ) : ( - Taken - )} -
- - {searchResult.is_available ? ( -
-

Asset identified as available for immediate acquisition.

-
- - - Acquire Now - -
-
- ) : ( -
-
-
-
Registrar
-
{searchResult.registrar || 'Unknown'}
-
-
-
Expires
-
- {formatDate(searchResult.expiration_date) || 'Unknown'} -
-
-
- -
- )} -
- - {/* Secondary Intel */} -
-
Intel Report
- - {searchResult.inAuction && searchResult.auctionData && ( -
-
- - Live Auction Detected -
-
-
-
Current Bid
-
${searchResult.auctionData.current_bid}
-
- - View Auction - -
-
- )} - -
-
- TLD Valuation - Calculating... -
-
- Est. Traffic - Low Volume -
-
- Spam Score - Clean (0/100) -
-
-
+
+ No active auctions
)}
- )} -
-
- - {/* BOTTOM SECTION: INTEL FEED */} -
- - {/* Market Feed */} -
-
-
-
-

Live Operations

-
- - View All {'>'} - -
- -
- {loadingData ? ( -
- INITIALIZING FEED... -
- ) : hotAuctions.length > 0 ? ( - hotAuctions.map((auction, i) => ( - -
- {auction.platform.substring(0, 2).toUpperCase()} -
-
- {auction.domain} -
-
- {auction.time_remaining} REMAINING -
-
-
-
-
${auction.current_bid}
-
-
- )) - ) : ( -
- NO ACTIVE SIGNALS -
- )} -
-
- - {/* Alerts Feed */} -
-
-
-
-

Watchlist Intel

-
- - Manage {'>'} - -
- -
- {recentAlerts.length > 0 ? ( - recentAlerts.slice(0, 5).map((alert, idx) => ( -
-
- {alert.type === 'available' ? ( - - ) : alert.type === 'expiring' ? ( - - ) : ( - - )} -
-
{alert.domain.name}
-
- {alert.type === 'available' && "Status: Available"} - {alert.type === 'expiring' && `Expiring: ${new Date(alert.domain.expiration_date!).toLocaleDateString()}`} - {alert.type === 'checked' && "Regular Scan Completed"} -
-
-
- {alert.type === 'available' && ( - - Action Req - - )} + + {/* Quick Actions */} +
+
+
+ +
+
+

Quick Access

+

Navigation

- )) - ) : ( -
- WATCHLIST EMPTY
- )} + +
+ {[ + { 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 }, + ].map((item) => ( + + +
+
{item.label}
+
{item.desc}
+
+ + + ))} +
+ + {/* System Status */} +
+
System Status
+
+
+
+ All Systems Operational +
+ 99.9% +
+
+
+
+
-
+ + ) } \ No newline at end of file diff --git a/frontend/src/app/terminal/watchlist/page.tsx b/frontend/src/app/terminal/watchlist/page.tsx index e90bf25..f4d3f80 100755 --- a/frontend/src/app/terminal/watchlist/page.tsx +++ b/frontend/src/app/terminal/watchlist/page.tsx @@ -3,7 +3,7 @@ import { useEffect, useState, useMemo, useCallback, memo } from 'react' import { useStore } from '@/lib/store' import { api, DomainHealthReport, HealthStatus } from '@/lib/api' -import { TerminalLayout } from '@/components/TerminalLayout' +import { CommandCenterLayout } from '@/components/CommandCenterLayout' import { Toast, useToast } from '@/components/Toast' import { Plus, @@ -14,159 +14,24 @@ import { BellOff, Eye, Sparkles, - ArrowUpRight, X, Activity, Shield, AlertTriangle, - ShoppingCart, - HelpCircle, - Search, - Globe, Clock, - Calendar, ArrowRight, CheckCircle2, XCircle, - Wifi, - Lock, - TrendingDown, Zap, - Diamond, - Tag, - Crown, - DollarSign, - Copy, - Check + Target, + Radio, + Crosshair, + Globe, + ExternalLink, + Calendar } from 'lucide-react' import clsx from 'clsx' import Link from 'next/link' -import { useRouter } from 'next/navigation' - -// ============================================================================ -// SHARED COMPONENTS (Matched to Market/Intel/Radar) -// ============================================================================ - -const Tooltip = memo(({ children, content }: { children: React.ReactNode; content: string }) => ( -
- {children} -
- {content} -
-
-
-)) -Tooltip.displayName = 'Tooltip' - -const StatCard = memo(({ - label, - value, - subValue, - icon: Icon, - highlight, - trend -}: { - label: string - value: string | number - subValue?: string - icon: any - highlight?: boolean - trend?: 'up' | 'down' | 'neutral' | 'active' -}) => ( -
-
- -
-
-
- - {label} -
-
- {value} - {subValue && {subValue}} -
- {highlight && ( -
- ● LIVE -
- )} -
-
-)) -StatCard.displayName = 'StatCard' - -// Health status badge configuration -const healthStatusConfig: Record = { - healthy: { - label: 'Online', - color: 'text-emerald-400', - bgColor: 'bg-emerald-500/10 border-emerald-500/20', - icon: Activity, - description: 'Domain is active and reachable', - }, - weakening: { - label: 'Issues', - color: 'text-amber-400', - bgColor: 'bg-amber-500/10 border-amber-500/20', - icon: AlertTriangle, - description: 'Warning signs detected', - }, - parked: { - label: 'Parked', - color: 'text-blue-400', - bgColor: 'bg-blue-500/10 border-blue-500/20', - icon: ShoppingCart, - description: 'Domain is parked/for sale', - }, - critical: { - label: 'Critical', - color: 'text-rose-400', - bgColor: 'bg-rose-500/10 border-rose-500/20', - icon: AlertTriangle, - description: 'Domain may be dropping soon', - }, - unknown: { - label: 'Unknown', - color: 'text-zinc-400', - bgColor: 'bg-zinc-800 border-zinc-700', - icon: HelpCircle, - description: 'Health check pending', - }, -} - -type MainTab = 'watching' | 'portfolio' -type FilterTab = 'all' | 'available' | 'expiring' | 'critical' - -// Portfolio Domain type - not used anymore since we use domains from store -// Keeping for potential future use -interface PortfolioDomain { - id: number - domain: string - purchase_date: string | null - purchase_price: number | null - registrar: string | null - renewal_date: string | null - renewal_cost: number | null - status: string - is_verified: boolean - verification_code: string | null - last_checked: string | null - last_change: string | null - notify_on_expiry: boolean - notify_on_change: boolean - notes: string | null - created_at: string -} // ============================================================================ // HELPER FUNCTIONS @@ -182,11 +47,7 @@ function getDaysUntilExpiry(expirationDate: string | null): number | null { function formatExpiryDate(expirationDate: string | null): string { if (!expirationDate) return '—' - return new Date(expirationDate).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric' - }) + return new Date(expirationDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) } function getTimeAgo(date: string | null): string { @@ -197,97 +58,40 @@ function getTimeAgo(date: string | null): string { const diffMins = Math.floor(diffMs / 60000) const diffHours = Math.floor(diffMs / 3600000) const diffDays = Math.floor(diffMs / 86400000) - if (diffMins < 1) return 'Just now' if (diffMins < 60) return `${diffMins}m ago` if (diffHours < 24) return `${diffHours}h ago` - if (diffDays < 7) return `${diffDays}d ago` - return formatExpiryDate(date) + return `${diffDays}d ago` +} + +// Health config +const healthConfig: Record = { + 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' }, + 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' }, } // ============================================================================ // MAIN PAGE // ============================================================================ -// Listing interface for counting active listings -interface Listing { - id: number - domain: string - status: string -} - export default function WatchlistPage() { const { domains, addDomain, deleteDomain, refreshDomain, updateDomain, subscription } = useStore() const { toast, showToast, hideToast } = useToast() - const router = useRouter() - - // Main tab state (Watching vs My Portfolio) - const [mainTab, setMainTab] = useState('watching') const [newDomain, setNewDomain] = useState('') const [adding, setAdding] = useState(false) const [refreshingId, setRefreshingId] = useState(null) const [deletingId, setDeletingId] = useState(null) const [togglingNotifyId, setTogglingNotifyId] = useState(null) - const [filterTab, setFilterTab] = useState('all') - const [searchQuery, setSearchQuery] = useState('') - - // Portfolio state - uses same domains from store but filtered/extended - const [newPortfolioDomain, setNewPortfolioDomain] = useState('') - const [addingPortfolio, setAddingPortfolio] = useState(false) - const [showVerifyModal, setShowVerifyModal] = useState(false) - const [verifyingDomainId, setVerifyingDomainId] = useState(null) - const [verifying, setVerifying] = useState(false) - const [showEditModal, setShowEditModal] = useState(false) - const [editingDomainId, setEditingDomainId] = useState(null) - const [editForm, setEditForm] = useState({ registrar: '', notes: '' }) - const [savingEdit, setSavingEdit] = useState(false) - - // Health check state const [healthReports, setHealthReports] = useState>({}) const [loadingHealth, setLoadingHealth] = useState>({}) - const [selectedHealthDomainId, setSelectedHealthDomainId] = useState(null) - - // Listing state for tier limits - const [listings, setListings] = useState([]) - const [loadingListings, setLoadingListings] = useState(false) - - // Sell Modal state (Wizard) - const [showSellModal, setShowSellModal] = useState(false) - const [sellStep, setSellStep] = useState<1 | 2 | 3>(1) // 1: Details, 2: DNS Verify, 3: Published - const [sellDomainName, setSellDomainName] = useState('') - const [sellForm, setSellForm] = useState({ - price: '', - priceType: 'negotiable', - allowOffers: true, - title: '', - }) - const [sellLoading, setSellLoading] = useState(false) - const [sellListingId, setSellListingId] = useState(null) - const [sellVerificationInfo, setSellVerificationInfo] = useState<{ - verification_code: string - dns_record_name: string - dns_record_value: string - } | null>(null) - const [copiedField, setCopiedField] = useState(null) - - // Tier-based limits (from pounce_pricing.md) - const tier = subscription?.tier || 'scout' - - // Watchlist limits: Scout=5, Trader=50, Tycoon=500 - const watchlistLimits: Record = { scout: 5, trader: 50, tycoon: 500 } - const maxWatchlist = watchlistLimits[tier] || 5 - - // Listing limits: Scout=0, Trader=5, Tycoon=50 - const listingLimits: Record = { scout: 0, trader: 5, tycoon: 50 } - const maxListings = listingLimits[tier] || 0 - const canSell = tier !== 'scout' - const isTycoon = tier === 'tycoon' - const currentListingCount = listings.length - const canCreateListing = canSell && currentListingCount < maxListings - + const [selectedDomain, setSelectedDomain] = useState(null) + const [filter, setFilter] = useState<'all' | 'available' | 'expiring'>('all') - // Memoized stats + // Stats const stats = useMemo(() => { const available = domains?.filter(d => d.is_available) || [] const expiringSoon = domains?.filter(d => { @@ -295,71 +99,37 @@ export default function WatchlistPage() { const days = getDaysUntilExpiry(d.expiration_date) return days !== null && days <= 30 && days > 0 }) || [] - const critical = Object.values(healthReports).filter(h => h.status === 'critical').length - - return { - total: domains?.length || 0, - available: available.length, - expiringSoon: expiringSoon.length, - critical, - limit: maxWatchlist, - } - }, [domains, maxWatchlist, healthReports]) + return { total: domains?.length || 0, available: available.length, expiring: expiringSoon.length } + }, [domains]) - const canAddMore = stats.total < stats.limit - - // Memoized filtered domains + // Filtered const filteredDomains = useMemo(() => { if (!domains) return [] - - return domains.filter(domain => { - // Search filter - if (searchQuery && !domain.name.toLowerCase().includes(searchQuery.toLowerCase())) { - return false + return domains.filter(d => { + if (filter === 'available') return d.is_available + if (filter === 'expiring') { + const days = getDaysUntilExpiry(d.expiration_date) + return days !== null && days <= 30 && days > 0 } - - // Tab filter - switch (filterTab) { - case 'available': - return domain.is_available - case 'expiring': - if (domain.is_available || !domain.expiration_date) return false - const days = getDaysUntilExpiry(domain.expiration_date) - return days !== null && days <= 30 && days > 0 - case 'critical': - const health = healthReports[domain.id] - return health?.status === 'critical' || health?.status === 'weakening' - default: return true - } }).sort((a, b) => { - // Sort available first, then by expiry date if (a.is_available && !b.is_available) return -1 if (!a.is_available && b.is_available) return 1 - - // Then by expiry (soonest first) - const daysA = getDaysUntilExpiry(a.expiration_date) - const daysB = getDaysUntilExpiry(b.expiration_date) - if (daysA !== null && daysB !== null) return daysA - daysB - if (daysA !== null) return -1 - if (daysB !== null) return 1 - return a.name.localeCompare(b.name) }) - }, [domains, searchQuery, filterTab, healthReports]) + }, [domains, filter]) - // Callbacks - const handleAddDomain = useCallback(async (e: React.FormEvent) => { + // Handlers + const handleAdd = useCallback(async (e: React.FormEvent) => { e.preventDefault() if (!newDomain.trim()) return - setAdding(true) try { await addDomain(newDomain.trim()) + showToast(`Target locked: ${newDomain.trim()}`, 'success') setNewDomain('') - showToast(`Added ${newDomain.trim()} to watchlist`, 'success') } catch (err: any) { - showToast(err.message || 'Failed to add domain', 'error') + showToast(err.message || 'Failed', 'error') } finally { setAdding(false) } @@ -369,1659 +139,349 @@ export default function WatchlistPage() { setRefreshingId(id) try { await refreshDomain(id) - showToast('Domain status refreshed', 'success') - } catch (err: any) { - showToast(err.message || 'Failed to refresh', 'error') - } finally { - setRefreshingId(null) - } + showToast('Intel updated', 'success') + } catch { showToast('Update failed', 'error') } + finally { setRefreshingId(null) } }, [refreshDomain, showToast]) const handleDelete = useCallback(async (id: number, name: string) => { - if (!confirm(`Remove ${name} from your watchlist?`)) return - + if (!confirm(`Drop target: ${name}?`)) return setDeletingId(id) try { await deleteDomain(id) - showToast(`Removed ${name} from watchlist`, 'success') - } catch (err: any) { - showToast(err.message || 'Failed to remove', 'error') - } finally { - setDeletingId(null) - } + showToast('Target dropped', 'success') + } catch { showToast('Failed', 'error') } + finally { setDeletingId(null) } }, [deleteDomain, showToast]) - const handleToggleNotify = useCallback(async (id: number, currentState: boolean) => { + const handleToggleNotify = useCallback(async (id: number, current: boolean) => { setTogglingNotifyId(id) try { - await api.updateDomainNotify(id, !currentState) - // Instant optimistic update - updateDomain(id, { notify_on_available: !currentState }) - showToast(!currentState ? 'Notifications enabled' : 'Notifications disabled', 'success') - } catch (err: any) { - showToast(err.message || 'Failed to update', 'error') - } finally { - setTogglingNotifyId(null) - } - }, [showToast, updateDomain]) + await api.updateDomainNotify(id, !current) + updateDomain(id, { notify_on_available: !current }) + showToast(!current ? 'Alerts armed' : 'Alerts disarmed', 'success') + } catch { showToast('Failed', 'error') } + finally { setTogglingNotifyId(null) } + }, [updateDomain, showToast]) - const handleHealthCheck = useCallback(async (domainId: number) => { - if (loadingHealth[domainId]) return - - setLoadingHealth(prev => ({ ...prev, [domainId]: true })) + const handleHealthCheck = useCallback(async (id: number) => { + if (loadingHealth[id]) return + setLoadingHealth(prev => ({ ...prev, [id]: true })) try { - // Force a live refresh when user explicitly triggers a check - const report = await api.getDomainHealth(domainId, { refresh: true }) - setHealthReports(prev => ({ ...prev, [domainId]: report })) - setSelectedHealthDomainId(domainId) - } catch (err: any) { - showToast(err.message || 'Health check failed', 'error') - } finally { - setLoadingHealth(prev => ({ ...prev, [domainId]: false })) - } - }, [loadingHealth, showToast]) + const report = await api.getDomainHealth(id, { refresh: true }) + setHealthReports(prev => ({ ...prev, [id]: report })) + } catch {} + finally { setLoadingHealth(prev => ({ ...prev, [id]: false })) } + }, [loadingHealth]) - - // Load health data for all domains on mount + // Load health useEffect(() => { - const loadHealthData = async () => { - if (!domains || domains.length === 0) return - - // Load cached health for all domains in one request (fast path) + const load = async () => { + if (!domains?.length) return try { const data = await api.getDomainsHealthCache() - if (data?.reports) { - // API returns string keys; JS handles number indexing transparently. - setHealthReports(prev => ({ ...(prev as any), ...(data.reports as any) })) - } - } catch { - // Silently fail - health data is optional - } + if (data?.reports) setHealthReports(prev => ({ ...prev, ...data.reports })) + } catch {} } - - loadHealthData() + load() }, [domains]) - - // Load user's listings to check limits - useEffect(() => { - const loadListings = async () => { - if (!canSell) return // Scout users can't list, no need to load - - setLoadingListings(true) - try { - const data = await api.request('/listings/my') - setListings(data) - } catch (err) { - console.error('Failed to load listings:', err) - } finally { - setLoadingListings(false) - } - } - - loadListings() - }, [canSell]) - - // Handle "Sell" button click - open wizard modal - const handleSellDomain = useCallback((domainName: string) => { - if (!canSell) { - showToast('Upgrade to Trader or Tycoon to sell domains', 'error') - return - } - - if (!canCreateListing) { - showToast(`Listing limit reached (${currentListingCount}/${maxListings}). Upgrade to list more.`, 'error') - return - } - - // Open sell wizard modal - setSellDomainName(domainName) - setSellStep(1) - setSellForm({ price: '', priceType: 'negotiable', allowOffers: true, title: '' }) - setSellListingId(null) - setSellVerificationInfo(null) - setShowSellModal(true) - }, [canSell, canCreateListing, currentListingCount, maxListings, showToast]) - - // Step 1: Create listing - const handleCreateListing = useCallback(async () => { - setSellLoading(true) - try { - const response = await api.request<{ id: number }>('/listings', { - method: 'POST', - body: JSON.stringify({ - domain: sellDomainName, - title: sellForm.title || null, - asking_price: sellForm.price ? parseFloat(sellForm.price) : null, - price_type: sellForm.priceType, - allow_offers: sellForm.allowOffers, - }), - }) - setSellListingId(response.id) - - // Start DNS verification - const verifyResponse = await api.request<{ - verification_code: string - dns_record_name: string - dns_record_value: string - }>(`/listings/${response.id}/verify-dns`, { method: 'POST' }) - - setSellVerificationInfo(verifyResponse) - setSellStep(2) - showToast('Listing created! Now verify ownership.', 'success') - } catch (err: any) { - showToast(err.message || 'Failed to create listing', 'error') - } finally { - setSellLoading(false) - } - }, [sellDomainName, sellForm, showToast]) - - // Step 2: Check DNS verification - const handleCheckVerification = useCallback(async () => { - if (!sellListingId) return - setSellLoading(true) - try { - const result = await api.request<{ verified: boolean; message: string }>( - `/listings/${sellListingId}/verify-dns/check` - ) - - if (result.verified) { - // Publish the listing - await api.request(`/listings/${sellListingId}`, { - method: 'PUT', - body: JSON.stringify({ status: 'active' }), - }) - setSellStep(3) - showToast('Domain verified and published!', 'success') - // Reload listings - const data = await api.request('/listings/my') - setListings(data) - } else { - showToast(result.message || 'Verification pending. Check your DNS settings.', 'error') - } - } catch (err: any) { - showToast(err.message || 'Verification failed', 'error') - } finally { - setSellLoading(false) - } - }, [sellListingId, showToast]) - - // Copy to clipboard helper - const copyToClipboard = useCallback((text: string, field: string) => { - navigator.clipboard.writeText(text) - setCopiedField(field) - setTimeout(() => setCopiedField(null), 2000) - }, []) - - // Portfolio uses the SAME domains as Watchlist - just a different view - // This ensures consistent monitoring behavior - - // Portfolio domains = all non-available domains (domains user "owns") - const portfolioDomains = useMemo(() => { - if (!domains) return [] - // Show all domains that are NOT available (i.e., registered/owned) - return domains.filter(d => !d.is_available).sort((a, b) => { - // Sort by expiry date (soonest first) - const daysA = getDaysUntilExpiry(a.expiration_date) - const daysB = getDaysUntilExpiry(b.expiration_date) - if (daysA !== null && daysB !== null) return daysA - daysB - if (daysA !== null) return -1 - if (daysB !== null) return 1 - return a.name.localeCompare(b.name) - }) - }, [domains]) - - // Add domain to portfolio (uses same addDomain as Watchlist) - const handleAddPortfolioDomain = useCallback(async (e: React.FormEvent) => { - e.preventDefault() - if (!newPortfolioDomain.trim()) return - - setAddingPortfolio(true) - try { - await addDomain(newPortfolioDomain.trim()) - setNewPortfolioDomain('') - showToast(`Added ${newPortfolioDomain.trim()} to your portfolio`, 'success') - } catch (err: any) { - showToast(err.message || 'Failed to add domain', 'error') - } finally { - setAddingPortfolio(false) - } - }, [newPortfolioDomain, addDomain, showToast]) - - // Verify domain ownership via DNS (placeholder - would need backend support) - const handleVerifyDomain = useCallback(async () => { - if (verifyingDomainId === null) return - const domain = domains?.find(d => d.id === verifyingDomainId) - if (!domain) return - - setVerifying(true) - try { - // For now, just show success - real verification would check DNS TXT record - await api.request(`/domains/${verifyingDomainId}/verify`, { method: 'POST' }).catch(() => {}) - showToast(`${domain.name} ownership noted! ✓`, 'success') - setShowVerifyModal(false) - setVerifyingDomainId(null) - } catch (err: any) { - showToast(err.message || 'Verification noted', 'success') - setShowVerifyModal(false) - setVerifyingDomainId(null) - } finally { - setVerifying(false) - } - }, [verifyingDomainId, domains, showToast]) - - // Edit portfolio domain (updates notes via store) - const handleEditPortfolioDomain = useCallback(async (e: React.FormEvent) => { - e.preventDefault() - if (editingDomainId === null) return - - setSavingEdit(true) - try { - // Update via API if available, otherwise just close - await api.request(`/domains/${editingDomainId}`, { - method: 'PATCH', - body: JSON.stringify({ notes: editForm.notes }), - }).catch(() => {}) - showToast('Domain updated', 'success') - setShowEditModal(false) - setEditingDomainId(null) - } catch (err: any) { - showToast('Updated locally', 'success') - setShowEditModal(false) - setEditingDomainId(null) - } finally { - setSavingEdit(false) - } - }, [editingDomainId, editForm, showToast]) - - // Open edit modal for portfolio domain - const openEditModal = useCallback((domain: typeof domains[0]) => { - setEditingDomainId(domain.id) - setEditForm({ - registrar: domain.registrar || '', - notes: '', - }) - setShowEditModal(true) - }, []) return ( - -
- {toast && } + + {toast && } + +
- {/* Ambient Background Glow */} -
-
-
-
- -
- - {/* Header Section */} -
-
-
-
-

Watchlist

+ {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* HERO */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+
+
+ + {/* Left */} +
+
+ + + Active Surveillance + +
+ +

+ Target + Acquisition. +

+ +

+ Monitor high-value domains. Get instant alerts when they become available. +

- {/* Main Tab Switcher (Watching | My Portfolio) */} -
- - -
-
- - {/* Quick Stats Pills */} -
- {mainTab === 'watching' && stats.available > 0 && ( -
- - {stats.available} Available! + {/* Right: Big Numbers */} +
+
+
{stats.total}
+
Tracking
+
+
+
{stats.available}
+
Available
+
+
+
{stats.expiring}
+
Expiring
- )} -
- - {mainTab === 'watching' ? 'Auto-Monitoring' : 'Verified Domains'}
+
- {/* ══════════════════════════════════════════════════════════════════ */} - {/* WATCHING TAB CONTENT */} - {/* ══════════════════════════════════════════════════════════════════ */} - {mainTab === 'watching' && ( - <> - {/* Metric Grid */} -
- 0} - trend={stats.available > 0 ? 'up' : 'active'} - /> - 0 ? 'up' : 'neutral'} - /> - 0 ? 'down' : 'neutral'} - /> - 0 ? 'down' : 'neutral'} - /> + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* ADD DOMAIN */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+
+
+
+ +
+
+
{'>'}
+ 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" + /> + +
+ +
+
- {/* Control Bar */} -
- {/* Filter Tabs */} -
- {([ - { key: 'all', label: 'All', count: stats.total }, + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* FILTERS */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+
+
+ {[ + { key: 'all', label: 'All Targets', count: stats.total }, { key: 'available', label: 'Available', count: stats.available }, - { key: 'expiring', label: 'Expiring', count: stats.expiringSoon }, - { key: 'critical', label: 'Issues', count: stats.critical }, - ] as const).map((tab) => ( + { key: 'expiring', label: 'Expiring', count: stats.expiring }, + ].map((f) => ( ))}
- - {/* Add Domain Input */} -
- setNewDomain(e.target.value)} - placeholder="Add domain (e.g. apple.com)..." - className="w-full bg-black/50 border border-white/10 rounded-lg pl-10 pr-12 py-2 text-sm text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 focus:ring-1 focus:ring-emerald-500/50 transition-all" - /> - - - - - {/* Search Filter */} -
- - setSearchQuery(e.target.value)} - placeholder="Filter domains..." - className="w-full bg-black/50 border border-white/10 rounded-lg pl-9 pr-4 py-2 text-sm text-white placeholder:text-zinc-600 focus:outline-none focus:border-white/20 transition-all" - /> + +
+ Auto-refresh: {subscription?.tier === 'tycoon' ? '10min' : 'Hourly'}
+
- {/* Limit Warning */} - {!canAddMore && ( -
-
- - Limit reached ({stats.total}/{stats.limit}). Upgrade to track more. + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* DATA TABLE */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+
+ + {filteredDomains.length === 0 ? ( +
+
+ +
+

No Targets

+

Add domains above to start tracking

- - Upgrade - -
- )} - - {/* Data Grid */} -
- {/* Table Header */} -
-
-
-
Domain
+ ) : ( +
+ {/* Header */} +
+
Target
Status
Health
Expires
-
Alerts
Actions
-
- - {filteredDomains.length === 0 ? ( -
-
-
-

- {searchQuery ? "No matches found" : filterTab !== 'all' ? `No ${filterTab} domains` : "Watchlist is empty"} -

-

- {searchQuery ? "Try adjusting your filter." : "Start by adding domains you want to track."} -

- {!searchQuery && filterTab === 'all' && ( - - )} -
- ) : ( -
- {filteredDomains.map((domain) => { - const health = healthReports[domain.id] - const healthConfig = health ? healthStatusConfig[health.status] : healthStatusConfig.unknown - const daysUntilExpiry = getDaysUntilExpiry(domain.expiration_date) - const isExpiringSoon = daysUntilExpiry !== null && daysUntilExpiry <= 30 && daysUntilExpiry > 0 - const isExpired = daysUntilExpiry !== null && daysUntilExpiry <= 0 - - return ( -
- - {/* Domain */} -
-
-
-
- {domain.is_available && ( -
- )} + + {/* Rows */} +
+ {filteredDomains.map((domain) => { + const health = healthReports[domain.id] + const hConfig = health ? healthConfig[health.status] : healthConfig.unknown + const days = getDaysUntilExpiry(domain.expiration_date) + const expiringSoon = days !== null && days <= 30 && days > 0 + + return ( +
+ {/* Domain */} +
+
+
+
{domain.name}
+
+ {domain.registrar || 'Unknown registrar'} +
-
- {domain.name} - {domain.registrar && ( -

- {domain.registrar} -

- )} -
-
- - {/* Status */} -
- {domain.is_available ? ( - - - Available + + {/* Status */} +
+ + {domain.is_available ? 'OPEN' : 'LOCKED'} - ) : ( - - - Registered - - )} -
- - {/* Health */} -
- {domain.is_available ? ( - - ) : health ? ( - - - - ) : ( - - - - )} -
- - {/* Expiry */} -
- {domain.is_available ? ( - - ) : domain.expiration_date ? ( -
-

- {formatExpiryDate(domain.expiration_date)} -

- {daysUntilExpiry !== null && ( -

- {isExpired ? 'EXPIRED' : `${daysUntilExpiry}d left`} -

- )} -
- ) : ( - - - - Not public - - - )} -
- - {/* Alerts */} -
- - - -
- - {/* Actions */} -
- -
+ + {/* Expiry */} +
+ {domain.is_available ? ( + + ) : days !== null ? ( +
+ {days}d + remaining +
+ ) : ( + Hidden + )} +
+ + {/* Actions */} +
+ - - - + + + - - - {domain.is_available && ( + + {domain.is_available && ( - Buy + GET - )} + )} +
-
- ) - })} + ) + })} +
- )} -
-
-
- - {/* Subtle Footer Info */} -
- - - Checks: {subscription?.tier === 'tycoon' ? '10min' : subscription?.tier === 'trader' ? 'hourly' : 'daily'} - - {subscription?.tier !== 'tycoon' && ( - - Upgrade - )} +
- - )} +
- {/* ══════════════════════════════════════════════════════════════════ */} - {/* MY PORTFOLIO TAB CONTENT */} - {/* ══════════════════════════════════════════════════════════════════ */} - {mainTab === 'portfolio' && ( - <> - {/* Portfolio Metric Grid - uses same data as Watching */} -
- - h.status === 'healthy').length} - subValue="Healthy" - icon={CheckCircle2} - trend="up" - /> - h.status === 'critical' || h.status === 'weakening').length} - subValue="Need attention" - icon={AlertTriangle} - trend={Object.values(healthReports).filter(h => h.status === 'critical' || h.status === 'weakening').length > 0 ? 'down' : 'neutral'} - /> - { - const days = getDaysUntilExpiry(d.expiration_date) - return days !== null && days <= 30 && days > 0 - }).length} - subValue="< 30 days" - icon={Calendar} - trend={portfolioDomains.filter(d => { - const days = getDaysUntilExpiry(d.expiration_date) - return days !== null && days <= 30 && days > 0 - }).length > 0 ? 'down' : 'neutral'} - /> -
- - {/* Control Bar - Add Domain */} -
-
- - Add domains you own to track expiry & health -
- - {/* Add Domain Input */} -
- setNewPortfolioDomain(e.target.value)} - placeholder="Add your domain (e.g. mydomain.com)..." - className="w-full bg-black/50 border border-white/10 rounded-lg pl-10 pr-12 py-2 text-sm text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 focus:ring-1 focus:ring-emerald-500/50 transition-all" - /> - - - -
- - {/* Portfolio Data Grid - uses same domains/health as Watching */} -
- {portfolioDomains.length === 0 ? ( -
-
- -
-

No domains in portfolio

-

- Add domains you own to track renewals, health status, and changes. -

- -
- ) : ( - <> - {/* Desktop Table - IDENTICAL to Watching tab */} -
-
- {/* Table Header */} -
-
Domain
-
Health
-
Expiry
-
Last Update
-
Alerts
-
Actions
-
- - {/* Table Rows - uses same data/logic as Watching */} -
- {portfolioDomains.map((domain) => { - const expiryDays = getDaysUntilExpiry(domain.expiration_date) - const isExpiringSoon = expiryDays !== null && expiryDays <= 30 && expiryDays > 0 - const health = healthReports[domain.id] - const healthConfig = health ? healthStatusConfig[health.status] : null - - return ( -
- {/* Domain */} -
-
-
- -
-
-
{domain.name}
-
- {domain.registrar || 'Unknown registrar'} • Added {getTimeAgo(domain.created_at)} -
-
-
-
- - {/* Health - uses SAME healthReports as Watching */} -
- - - -
- - {/* Expiry - uses expiration_date like Watching */} -
- {domain.expiration_date ? ( - -
- - {expiryDays !== null ? `${expiryDays}d` : formatExpiryDate(domain.expiration_date)} -
-
- ) : ( - Unknown - )} -
- - {/* Last Update */} -
- - {domain.last_checked ? getTimeAgo(domain.last_checked) : 'Never'} - -
- - {/* Alerts - uses SAME handleToggleNotify as Watching */} -
- - - -
- - {/* Actions - uses SAME handleRefresh/handleDelete as Watching */} -
- - - - - - - - {/* Sell Button - Tier-based */} - {canSell ? ( - - - - ) : ( - - - - Upgrade - - - )} -
-
- ) - })} -
-
-
- - {/* Mobile Cards - IDENTICAL logic to Watching */} -
- {portfolioDomains.map((domain) => { - const expiryDays = getDaysUntilExpiry(domain.expiration_date) - const isExpiringSoon = expiryDays !== null && expiryDays <= 30 && expiryDays > 0 - const health = healthReports[domain.id] - const healthConfig = health ? healthStatusConfig[health.status] : null - - return ( -
- {/* Header */} -
-
-
- -
-
-
{domain.name}
-
{domain.registrar || 'Unknown registrar'}
-
-
-
- - {/* Info Grid - uses same data as Watching */} -
-
-
Health
- -
-
-
Expiry
-
- {expiryDays !== null ? `${expiryDays}d` : '—'} -
-
-
-
Checked
-
- {domain.last_checked ? getTimeAgo(domain.last_checked) : '—'} -
-
-
- - {/* Actions - uses SAME functions as Watching */} -
-
- - - -
- {/* Sell Button - Tier-based (Mobile) */} - {canSell ? ( - - ) : ( - - - Upgrade - - )} -
-
- ) - })} -
- - )} -
- - {/* Portfolio Footer with Listing Info */} -
- - - Same monitoring as Watching tab - - - - Get alerts for changes - - {canSell && ( - - - {currentListingCount}/{maxListings} listings - {isTycoon && } - - )} - {!canSell && ( - - - Upgrade to sell - - )} -
- - )} -
- - {/* Health Report Modal */} - {selectedHealthDomainId && healthReports[selectedHealthDomainId] && ( - setSelectedHealthDomainId(null)} - /> - )} - - {/* Sell Wizard Modal */} - {showSellModal && ( -
setShowSellModal(false)} - > -
e.stopPropagation()} - > - {/* Header with Steps */} -
-
-
-
- -
-
-

List for Sale

-

{sellDomainName}

-
-
- -
- - {/* Step Indicator */} -
- {[1, 2, 3].map((step) => ( -
-
= step - ? "bg-emerald-500 text-black border-emerald-500" - : "bg-zinc-800 text-zinc-500 border-zinc-700" - )}> - {sellStep > step ? : step} -
- - {step < 3 &&
step ? "bg-emerald-500" : "bg-zinc-800")} />} -
- ))} -
-
- - {/* Step 1: Listing Details */} - {sellStep === 1 && ( -
-
- - setSellForm({ ...sellForm, title: e.target.value })} - placeholder="e.g. Perfect for AI Startups" - className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all" - /> -
- -
-
- -
- - setSellForm({ ...sellForm, price: e.target.value })} - placeholder="Make Offer" - className="w-full pl-9 pr-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder:text-zinc-600 focus:outline-none focus:border-emerald-500/50 transition-all font-mono" - /> -
-
- -
- - -
-
- - - - {/* Limits Info */} -
- Your listing slots: - {currentListingCount}/{maxListings} -
- -
- - -
-
- )} - - {/* Step 2: DNS Verification */} - {sellStep === 2 && sellVerificationInfo && ( -
-
-
- -
-

Verify Ownership

-

- Add this TXT record to your domain's DNS to prove ownership. -

-
-
-
- -
-
-
Type
-
- TXT -
-
- -
-
Name / Host
-
copyToClipboard(sellVerificationInfo.dns_record_name, 'name')} - > - {sellVerificationInfo.dns_record_name} - {copiedField === 'name' - ? - : - } -
-
- -
-
Value
-
copyToClipboard(sellVerificationInfo.dns_record_value, 'value')} - > - {sellVerificationInfo.dns_record_value} - {copiedField === 'value' - ? - : - } -
-
-
- -
-

- 💡 DNS changes can take up to 24 hours to propagate, but usually work within minutes. -

-
- -
- - -
-
- )} - - {/* Step 3: Success */} - {sellStep === 3 && ( -
-
- -
- -
-

Domain Listed!

-

- {sellDomainName} is now live on the Pounce Marketplace. -

-
- - {isTycoon && ( -
- - Featured Listing Active -
- )} - -
- - - View Listings - -
-
- )} + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* FOOTER */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+
+
+
+ System Active
+
Last sync: {getTimeAgo(new Date().toISOString())}
- )} +
+
- + ) -} - -// Health Report Modal Component -const HealthReportModal = memo(function HealthReportModal({ - report, - onClose -}: { - report: DomainHealthReport - onClose: () => void -}) { - const config = healthStatusConfig[report.status] - const Icon = config.icon - - // Safely access nested properties - const dns = report.dns || {} - const http = report.http || {} - const ssl = report.ssl || {} - - return ( -
-
e.stopPropagation()} - > - {/* Header */} -
-
-
- -
-
-

{report.domain}

-

{config.description}

-
-
- -
- - {/* Score */} -
-
- Health Score - = 70 ? "text-emerald-400" : - report.score >= 40 ? "text-amber-400" : "text-rose-400" - )}> - {report.score}/100 - -
-
-
= 70 ? "bg-emerald-500 shadow-[0_0_10px_rgba(16,185,129,0.5)]" : - report.score >= 40 ? "bg-amber-500 shadow-[0_0_10px_rgba(245,158,11,0.5)]" : "bg-rose-500 shadow-[0_0_10px_rgba(244,63,94,0.5)]" - )} - style={{ width: `${report.score}%` }} - /> -
-
- - {/* Check Results */} -
- - {/* Infrastructure */} -
-

- Infrastructure -

-
- {/* DNS */} -
-
DNS Status
-
- {dns.has_ns ? '● Active' : '○ No Records'} -
- {dns.nameservers && dns.nameservers.length > 0 && ( -

- {dns.nameservers[0]} -

- )} -
- - {/* Web Server */} -
-
Web Server
-
- {http.is_reachable - ? `● HTTP ${http.status_code || 200}` - : http.error - ? `○ ${http.error}` - : '○ Unreachable' - } -
- {http.content_length !== undefined && http.content_length > 0 && ( -

- {(http.content_length / 1024).toFixed(1)} KB -

- )} -
- - {/* A Record */} -
-
A Record
-
- {dns.has_a ? '● Configured' : '○ Not set'} -
-
- - {/* MX Record */} -
-
Mail (MX)
-
- {dns.has_mx ? '● Configured' : '○ Not set'} -
-
-
-
- - {/* Security */} -
-

- Security -

-
-
- SSL Certificate - - {ssl.has_certificate && ssl.is_valid ? 'Secure' : - ssl.has_certificate ? 'Invalid' : 'None'} - -
- - {ssl.issuer && ( -
- Issuer - {ssl.issuer} -
- )} - - {ssl.days_until_expiry !== undefined && ssl.days_until_expiry !== null && ( -
- Expires in - - {ssl.days_until_expiry} days - -
- )} - - {ssl.error && ( -

- {ssl.error} -

- )} -
-
- - {/* Parking Detection */} - {(dns.is_parked || http.is_parked) && ( -
-

- Parking Detected -

-
-

- This domain appears to be parked or for sale. - {dns.parking_provider && ( - Provider: {dns.parking_provider} - )} -

- {http.parking_keywords && http.parking_keywords.length > 0 && ( -

- Keywords: {http.parking_keywords.slice(0, 3).join(', ')} -

- )} -
-
- )} - - {/* Signals */} - {report.signals && report.signals.length > 0 && ( -
-

- Signals -

-
    - {report.signals.map((signal, i) => ( -
  • - {signal} -
  • - ))} -
-
- )} - - {/* Recommendations */} - {report.recommendations && report.recommendations.length > 0 && ( -
-

- Recommendations -

-
    - {report.recommendations.map((rec, i) => ( -
  • - {rec} -
  • - ))} -
-
- )} -
- - {/* Footer */} -
-

- LAST CHECK: {new Date(report.checked_at).toLocaleString().toUpperCase()} -

-
-
-
- ) -}) +} \ No newline at end of file