diff --git a/frontend/src/app/terminal/radar/page.tsx b/frontend/src/app/terminal/radar/page.tsx index 64f4e8e..431fef1 100644 --- a/frontend/src/app/terminal/radar/page.tsx +++ b/frontend/src/app/terminal/radar/page.tsx @@ -20,23 +20,18 @@ import { Globe, Target, Search, + X, Home, - BarChart3, + TrendingUp, Settings, Bell, ChevronRight, - TrendingUp, - RefreshCw, - Menu, - X, - Tag, - Coins, - Shield, - LogOut, - User + Clock, + Wifi } from 'lucide-react' import clsx from 'clsx' import Link from 'next/link' +import Image from 'next/image' // ============================================================================ // TYPES @@ -62,151 +57,38 @@ interface SearchResult { } // ============================================================================ -// FULLSCREEN NAVIGATION MENU +// MOBILE BOTTOM NAV // ============================================================================ -function FullscreenNav({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) { - const { user, logout, subscription } = useStore() - - const mainLinks = [ - { label: 'Radar', href: '/terminal/radar', icon: Crosshair, desc: 'Domain search & overview' }, - { label: 'Market', href: '/terminal/market', icon: Gavel, desc: 'Live auctions' }, - { label: 'Watchlist', href: '/terminal/watchlist', icon: Eye, desc: 'Track domains' }, - { label: 'Intel', href: '/terminal/intel', icon: BarChart3, desc: 'TLD pricing data' }, +function MobileBottomNav({ currentPath }: { currentPath: string }) { + const navItems = [ + { href: '/terminal/radar', label: 'Radar', icon: Target }, + { href: '/terminal/market', label: 'Market', icon: Gavel }, + { href: '/terminal/watchlist', label: 'Watch', icon: Eye }, + { href: '/terminal/intel', label: 'Intel', icon: TrendingUp }, + { href: '/terminal/settings', label: 'Settings', icon: Settings }, ] - const secondaryLinks = [ - { label: 'Sniper', href: '/terminal/sniper', icon: Target }, - { label: 'Yield', href: '/terminal/yield', icon: Coins }, - { label: 'For Sale', href: '/terminal/listing', icon: Tag }, - { label: 'Settings', href: '/terminal/settings', icon: Settings }, - ] - - if (!isOpen) return null - return ( -
- {/* Header */} -
-
-
- -
- Menu -
- -
- - {/* Main Links */} -
- {mainLinks.map((item) => ( - -
- -
-
-
{item.label}
-
{item.desc}
-
- - - ))} -
- - {/* Secondary Links */} -
-
More
-
- {secondaryLinks.map((item) => ( +
- - {/* User Section */} -
-
-
- -
-
-
{user?.name || user?.email?.split('@')[0]}
-
{subscription?.tier || 'Scout'}
-
- -
-
-
- ) -} - -// ============================================================================ -// MOBILE BOTTOM NAV - With Menu Button -// ============================================================================ - -function MobileBottomNav({ active, onMenuOpen }: { active: string; onMenuOpen: () => void }) { - const navItems = [ - { id: 'radar', label: 'Radar', icon: Crosshair, href: '/terminal/radar' }, - { id: 'market', label: 'Market', icon: Gavel, href: '/terminal/market' }, - { id: 'watchlist', label: 'Watch', icon: Eye, href: '/terminal/watchlist' }, - ] - - return ( - ) @@ -216,45 +98,30 @@ function MobileBottomNav({ active, onMenuOpen }: { active: string; onMenuOpen: ( // MOBILE HEADER // ============================================================================ -function MobileHeader({ - onSearchOpen, - isRefreshing, - onRefresh -}: { - onSearchOpen: () => void - isRefreshing: boolean - onRefresh: () => void -}) { +function MobileHeader({ stats }: { stats: { tracking: number; available: number } }) { return ( -
-
+
+
-
- -
+ Pounce
-

Radar

+

Radar

- Live + Live
-
- - +
+
+
{stats.tracking}
+
tracking
+
+
+
{stats.available}
+
available
+
@@ -262,213 +129,186 @@ function MobileHeader({ } // ============================================================================ -// MOBILE SEARCH MODAL +// MOBILE SEARCH CARD // ============================================================================ -function MobileSearchModal({ - isOpen, - onClose, +function MobileSearchCard({ searchQuery, setSearchQuery, searchResult, + searchFocused, + setSearchFocused, + handleAddToWatchlist, addingToWatchlist, - onAddToWatchlist }: { - isOpen: boolean - onClose: () => void searchQuery: string setSearchQuery: (q: string) => void searchResult: SearchResult | null + searchFocused: boolean + setSearchFocused: (f: boolean) => void + handleAddToWatchlist: () => void addingToWatchlist: boolean - onAddToWatchlist: () => void }) { - const inputRef = useRef(null) - - useEffect(() => { - if (isOpen && inputRef.current) { - setTimeout(() => inputRef.current?.focus(), 100) - } - }, [isOpen]) - - if (!isOpen) return null - return ( -
- {/* Header */} -
- -
- +
+ {/* Search Input - Full width, app-like */} +
+
+ setSearchQuery(e.target.value)} - placeholder="Search domain..." - className="w-full h-11 bg-white/10 border border-white/[0.2] pl-11 pr-4 text-white placeholder:text-white/40 outline-none focus:border-accent/50 rounded-xl text-base" - autoComplete="off" - autoCorrect="off" - autoCapitalize="none" + onFocus={() => setSearchFocused(true)} + onBlur={() => setSearchFocused(false)} + placeholder="Search domains..." + className="flex-1 bg-transparent px-3 py-4 text-base text-white placeholder:text-white/25 outline-none" /> + {searchQuery && ( + + )}
- - {/* Results */} -
- {searchResult?.loading && ( -
- - Checking availability... -
- )} - - {searchResult && !searchResult.loading && ( -
- {/* Status Banner */} + + {/* Results - App-like card */} + {searchResult && ( +
+ {searchResult.loading ? ( +
+ + Checking... +
+ ) : (
- {searchResult.is_available ? ( -
- + {/* Status Banner */} +
+
+ {searchResult.is_available ? ( + + ) : ( + + )} +
+
{searchResult.domain}
+ {!searchResult.is_available && searchResult.registrar && ( +
{searchResult.registrar}
+ )} +
- ) : ( -
- -
- )} -
-
{searchResult.domain}
-
- {searchResult.is_available ? 'Available for registration' : 'Already registered'} -
+ {searchResult.is_available ? 'Available' : 'Taken'} +
-
- - {/* Details */} - {!searchResult.is_available && searchResult.registrar && ( -
-
- Registrar - {searchResult.registrar} -
-
- )} - - {/* Actions */} -
- - {searchResult.is_available && ( - + + + {searchResult.is_available && ( + + Register Now + + + )} +
-
- )} - - {!searchResult && searchQuery.length === 0 && ( -
-
- -
-

Enter a domain to check availability

-
- )} -
+ )} +
+ )}
) } // ============================================================================ -// STAT CARD - Higher Contrast +// MOBILE AUCTION CARD // ============================================================================ -function StatCard({ label, value, highlight, icon: Icon }: { - label: string - value: string | number - highlight?: boolean - icon: any -}) { - return ( -
-
- - {label} -
-
- {value} -
-
- ) -} - -// ============================================================================ -// AUCTION CARD - Higher Contrast -// ============================================================================ - -function AuctionCard({ auction }: { auction: HotAuction }) { +function MobileAuctionCard({ auction }: { auction: HotAuction }) { return ( -
- {auction.platform.substring(0, 2).toUpperCase()} +
+
-
{auction.domain}
-
{auction.time_remaining}
+
{auction.domain}
+
+ {auction.platform} + · + + + {auction.time_remaining} + +
-
${auction.current_bid.toLocaleString()}
-
Current bid
+
${auction.current_bid.toLocaleString()}
+
current bid
- +
) } // ============================================================================ -// DESKTOP LIVE TICKER +// MOBILE QUICK ACTION +// ============================================================================ + +function MobileQuickAction({ href, icon: Icon, label, badge }: { href: string; icon: any; label: string; badge?: number }) { + return ( + + + {label} + {badge !== undefined && badge > 0 && ( + + {badge} + + )} + + ) +} + +// ============================================================================ +// LIVE TICKER (Desktop) // ============================================================================ function LiveTicker({ items }: { items: { label: string; value: string; highlight?: boolean }[] }) { @@ -503,15 +343,11 @@ export default function RadarPage() { const [hotAuctions, setHotAuctions] = useState([]) const [marketStats, setMarketStats] = useState({ totalAuctions: 0, endingSoon: 0 }) const [loadingData, setLoadingData] = useState(true) - const [isRefreshing, setIsRefreshing] = useState(false) const [searchQuery, setSearchQuery] = useState('') const [searchResult, setSearchResult] = useState(null) const [addingToWatchlist, setAddingToWatchlist] = useState(false) const [searchFocused, setSearchFocused] = useState(false) - const [mobileSearchOpen, setMobileSearchOpen] = useState(false) - const [menuOpen, setMenuOpen] = useState(false) - const searchInputRef = useRef(null) // Load Data const loadDashboardData = useCallback(async () => { @@ -526,15 +362,9 @@ export default function RadarPage() { console.error('Failed to load data:', error) } finally { setLoadingData(false) - setIsRefreshing(false) } }, []) - const handleRefresh = useCallback(async () => { - setIsRefreshing(true) - await loadDashboardData() - }, [loadDashboardData]) - useEffect(() => { if (isAuthenticated) loadDashboardData() }, [isAuthenticated, loadDashboardData]) @@ -571,12 +401,11 @@ export default function RadarPage() { setAddingToWatchlist(true) try { await addDomain(searchQuery.trim()) - showToast(`Added: ${searchQuery.trim()}`, 'success') + showToast(`Target acquired: ${searchQuery.trim()}`, 'success') setSearchQuery('') setSearchResult(null) - setMobileSearchOpen(false) } catch (err: any) { - showToast(err.message || 'Failed', 'error') + showToast(err.message || 'Mission failed', 'error') } finally { setAddingToWatchlist(false) } @@ -601,399 +430,366 @@ export default function RadarPage() { { label: 'Auctions', value: marketStats.totalAuctions.toString() }, ] - return ( - <> - {/* Fullscreen Navigation Menu */} - setMenuOpen(false)} /> - + // ============================================================================ + // MOBILE VIEW + // ============================================================================ + + const MobileView = () => ( +
{/* Mobile Header */} - setMobileSearchOpen(true)} - isRefreshing={isRefreshing} - onRefresh={handleRefresh} - /> - - {/* Mobile Search Modal */} - setMobileSearchOpen(false)} + + + {/* Search Section */} + - - {/* Mobile Content */} -
- {toast && } + + {/* Quick Actions Grid */} +
+
+ + + +
+
+ + {/* Live Status Bar */} +
+
+ + Live monitoring active +
+
+ {marketStats.totalAuctions} auctions + {marketStats.endingSoon} ending soon +
+
+ + {/* Hot Auctions Section */} +
+
+

+ + Hot Auctions +

+ + View all + +
- {/* Stats Grid */} -
- - 0} icon={CheckCircle2} /> - - -
- - {/* Available Alert */} - {availableDomains.length > 0 && ( -
-
-
- -
-
-
{availableDomains.length} Domain{availableDomains.length > 1 ? 's' : ''} Available!
-
Grab them now
-
- - - -
+ {loadingData ? ( +
+
- )} - - {/* Section: Live Auctions */} -
-
-
- - Live Auctions -
- - See all - -
- - {loadingData ? ( -
- -
- ) : hotAuctions.length > 0 ? ( -
- {hotAuctions.map((auction, i) => ( - - ))} -
- ) : ( -
- -

No active auctions

-
- )} -
- - {/* Section: Quick Links */} -
-
- - Quick Actions -
- -
- {[ - { label: 'TLD Intel', desc: 'Price trends', href: '/terminal/intel', icon: TrendingUp }, - { label: 'Sniper', desc: 'Set alerts', href: '/terminal/sniper', icon: Target }, - { label: 'Yield', desc: 'Monetize', href: '/terminal/yield', icon: Coins }, - { label: 'For Sale', desc: 'List domains', href: '/terminal/listing', icon: Tag }, - ].map((item) => ( - - -
{item.label}
-
{item.desc}
- + ) : hotAuctions.length > 0 ? ( +
+ {hotAuctions.map((auction, i) => ( + ))}
-
+ ) : ( +
+ No active auctions at the moment +
+ )}
+ + {/* Bottom Navigation */} + + + {toast && } +
+ ) - {/* Mobile Bottom Nav with Menu Button */} - setMenuOpen(true)} /> - - {/* ═══════════════════════════════════════════════════════════════════════ */} - {/* DESKTOP LAYOUT */} - {/* ═══════════════════════════════════════════════════════════════════════ */} -
- - {toast && } + // ============================================================================ + // DESKTOP VIEW (Original with minor tweaks) + // ============================================================================ + + const DesktopView = () => ( + + {toast && } + + {/* HERO */} +
+
- {/* HERO */} -
-
- - {/* Left: Typography */} -
-
-
- - Intelligence Hub - -
- -

- Domain Radar - Find your next acquisition. -

- -

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

- - {/* Stats Row */} -
-
-
{totalDomains}
-
Tracking
-
-
-
{availableDomains.length}
-
Available
-
-
-
{marketStats.endingSoon}
-
Ending Soon
-
+ {/* Left: Typography */} +
+
+
+ Intelligence Hub +
+ +

+ Domain Radar + Find your next acquisition. +

+ +

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

+ + {/* Stats Row */} +
+
+
{totalDomains}
+
Tracking
+
+
+
{availableDomains.length}
+
Available
+
+
+
{marketStats.endingSoon}
+
Ending Soon
+
+
+
+ + {/* Right: Search Terminal */} +
+
+ +
+ {/* Header Bar */} +
+ + + Domain Search + +
+
+
+
- {/* Right: Search Terminal */} -
-
+
+ {/* Input */} +
+ setSearchQuery(e.target.value)} + onFocus={() => setSearchFocused(true)} + onBlur={() => setSearchFocused(false)} + placeholder="example.com" + className="w-full bg-transparent px-4 py-4 text-lg text-white placeholder:text-white/20 outline-none" + /> + {searchQuery && ( + + )} +
-
- {/* Header Bar */} -
- - - Domain Search - -
-
-
-
-
-
- -
- {/* Input */} -
- setSearchQuery(e.target.value)} - onFocus={() => setSearchFocused(true)} - onBlur={() => setSearchFocused(false)} - placeholder="example.com" - className="w-full bg-transparent px-4 py-4 text-lg text-white placeholder:text-white/20 outline-none" - /> - {searchQuery && ( - - )} -
- - {/* Results */} - {searchResult && ( -
- {searchResult.loading ? ( -
- - Checking availability... -
- ) : ( -
- {/* Status Header */} -
-
- {searchResult.is_available ? ( - - ) : ( - - )} - {searchResult.domain} -
- - {searchResult.is_available ? 'Available' : 'Taken'} - -
- - {/* Registrar Info for taken domains */} - {!searchResult.is_available && searchResult.registrar && ( -

- Registered with {searchResult.registrar} -

+ {/* Results */} + {searchResult && ( +
+ {searchResult.loading ? ( +
+ + Checking availability... +
+ ) : ( +
+
+
+ {searchResult.is_available ? ( + + ) : ( + )} - - {/* Actions */} -
- - {searchResult.is_available && ( - - Register Now - - )} -
+ {searchResult.domain}
+ + {searchResult.is_available ? 'Available' : 'Taken'} + +
+ + {!searchResult.is_available && searchResult.registrar && ( +

Registered with {searchResult.registrar}

)} + +
+ + {searchResult.is_available && ( + + Register Now + + )} +
)} - - {/* Hint */} - {!searchResult && ( -

- Enter a domain name to check availability -

- )}
-
-
-
-
- - {/* Ticker */} - - - {/* CONTENT GRID */} -
-
- - {/* Hot Auctions - 2 cols */} -
-
-
- - Live Auctions -
- - View all → - -
+ )} - {loadingData ? ( -
- -
- ) : hotAuctions.length > 0 ? ( - - ) : ( -
No active auctions
+ {!searchResult && ( +

Enter a domain name to check availability

)}
- - {/* Quick Links */} -
-
- - Quick Access -
- -
- {[ - { label: 'Watchlist', href: '/terminal/watchlist', icon: Eye }, - { label: 'Market', href: '/terminal/market', icon: Gavel }, - { label: 'Intel', href: '/terminal/intel', icon: Globe }, - ].map((item) => ( - - - {item.label} - - - ))} -
- - {/* Status */} -
-
-
- System online -
-
-
-
-
- -
+
+
+ + + {/* Ticker */} + + + {/* CONTENT GRID */} +
+
+ + {/* Hot Auctions - 2 cols */} +
+
+
+ + Live Auctions +
+ + View all → + +
+ + {loadingData ? ( +
+ +
+ ) : hotAuctions.length > 0 ? ( + + ) : ( +
No active auctions
+ )} +
+ + {/* Quick Links */} +
+
+ + Quick Access +
+ +
+ {[ + { label: 'Watchlist', href: '/terminal/watchlist', icon: Eye }, + { label: 'Market', href: '/terminal/market', icon: Gavel }, + { label: 'Intel', href: '/terminal/intel', icon: Globe }, + ].map((item) => ( + + + {item.label} + + + ))} +
+ +
+
+
+ System online +
+
+
+ +
+
+ + ) + + // ============================================================================ + // RENDER + // ============================================================================ + + return ( + <> + {/* Mobile View */} +
+ +
+ + {/* Desktop View */} +
+ +
+ + {/* Global Styles for Safe Areas */} + diff --git a/frontend/src/app/terminal/watchlist/page.tsx b/frontend/src/app/terminal/watchlist/page.tsx index bdf6f91..214fe18 100755 --- a/frontend/src/app/terminal/watchlist/page.tsx +++ b/frontend/src/app/terminal/watchlist/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect, useState, useMemo, useCallback, useRef } 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' @@ -24,21 +24,7 @@ import { ExternalLink, Calendar, Shield, - Crosshair, - Menu, - Search, - Gavel, - BarChart3, - Settings, - Tag, - Coins, - LogOut, - User, - ChevronRight, - Clock, - Wifi, - WifiOff, - Lock + Crosshair } from 'lucide-react' import clsx from 'clsx' import Link from 'next/link' @@ -57,7 +43,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' }) + return new Date(expirationDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) } function getTimeAgo(date: string | null): string { @@ -75,462 +61,11 @@ function getTimeAgo(date: string | null): string { } const healthConfig: Record = { - healthy: { label: 'Healthy', color: 'text-accent', bg: 'bg-accent/10' }, - weakening: { label: 'Weak', color: 'text-amber-400', bg: 'bg-amber-500/10' }, - parked: { label: 'Parked', color: 'text-blue-400', bg: 'bg-blue-500/10' }, - critical: { label: 'Critical', color: 'text-rose-400', bg: 'bg-rose-500/10' }, - unknown: { label: 'Unknown', color: 'text-white/40', bg: 'bg-white/5' }, -} - -// ============================================================================ -// FULLSCREEN NAVIGATION (reuse) -// ============================================================================ - -function FullscreenNav({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) { - const { user, logout, subscription } = useStore() - - const mainLinks = [ - { label: 'Radar', href: '/terminal/radar', icon: Crosshair, desc: 'Search & overview' }, - { label: 'Market', href: '/terminal/market', icon: Gavel, desc: 'Live auctions' }, - { label: 'Watchlist', href: '/terminal/watchlist', icon: Eye, desc: 'Track domains' }, - { label: 'Intel', href: '/terminal/intel', icon: BarChart3, desc: 'TLD pricing' }, - ] - - const secondaryLinks = [ - { label: 'Sniper', href: '/terminal/sniper', icon: Target }, - { label: 'Yield', href: '/terminal/yield', icon: Coins }, - { label: 'For Sale', href: '/terminal/listing', icon: Tag }, - { label: 'Settings', href: '/terminal/settings', icon: Settings }, - ] - - if (!isOpen) return null - - return ( -
-
-
-
- -
- Menu -
- -
- -
- {mainLinks.map((item) => ( - -
- -
-
-
{item.label}
-
{item.desc}
-
- - - ))} -
- -
-
More
-
- {secondaryLinks.map((item) => ( - - - {item.label} - - ))} -
-
- -
-
-
- -
-
-
{user?.name || user?.email?.split('@')[0]}
-
{subscription?.tier || 'Scout'}
-
- -
-
-
- ) -} - -// ============================================================================ -// MOBILE BOTTOM NAV -// ============================================================================ - -function MobileBottomNav({ active, onMenuOpen }: { active: string; onMenuOpen: () => void }) { - const navItems = [ - { id: 'radar', label: 'Radar', icon: Crosshair, href: '/terminal/radar' }, - { id: 'market', label: 'Market', icon: Gavel, href: '/terminal/market' }, - { id: 'watchlist', label: 'Watch', icon: Eye, href: '/terminal/watchlist' }, - ] - - return ( - - ) -} - -// ============================================================================ -// MOBILE HEADER -// ============================================================================ - -function MobileHeader({ - onAddOpen, - onRefreshAll, - isRefreshing, - availableCount -}: { - onAddOpen: () => void - onRefreshAll: () => void - isRefreshing: boolean - availableCount: number -}) { - return ( -
-
-
-
- - {availableCount > 0 && ( -
- {availableCount} -
- )} -
-
-

Watchlist

- Tracking domains -
-
- -
- - -
-
-
- ) -} - -// ============================================================================ -// MOBILE ADD MODAL -// ============================================================================ - -function MobileAddModal({ isOpen, onClose, onAdd, adding }: { - isOpen: boolean - onClose: () => void - onAdd: (domain: string) => Promise - adding: boolean -}) { - const [value, setValue] = useState('') - const inputRef = useRef(null) - - useEffect(() => { - if (isOpen) setTimeout(() => inputRef.current?.focus(), 100) - }, [isOpen]) - - const handleSubmit = async () => { - if (!value.trim()) return - await onAdd(value.trim()) - setValue('') - onClose() - } - - if (!isOpen) return null - - return ( -
-
- - Add Domain - -
- -
-
- - setValue(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleSubmit()} - placeholder="example.com" - className="w-full h-14 bg-white/10 border border-white/[0.2] pl-12 pr-4 text-white text-lg placeholder:text-white/40 outline-none focus:border-accent/50 rounded-xl" - autoComplete="off" - autoCorrect="off" - autoCapitalize="none" - /> -
- -

- Enter a domain name to start tracking -

-
-
- ) -} - -// ============================================================================ -// MOBILE DOMAIN CARD -// ============================================================================ - -function MobileDomainCard({ - domain, - health, - loadingHealth, - onRefresh, - onDelete, - onToggleNotify, - onHealthClick, - refreshing, - deleting, - togglingNotify -}: { - domain: any - health?: DomainHealthReport - loadingHealth: boolean - onRefresh: () => void - onDelete: () => void - onToggleNotify: () => void - onHealthClick: () => void - refreshing: boolean - deleting: boolean - togglingNotify: boolean -}) { - const config = healthConfig[health?.status || 'unknown'] - const days = getDaysUntilExpiry(domain.expiration_date) - const isExpiringSoon = days !== null && days <= 30 && days > 0 - - return ( -
- {/* Main Content */} -
-
-
-
- {domain.name} -
- - {domain.is_available ? 'Available!' : 'Taken'} - -
- - {/* Info Row */} -
- {/* Health */} - - - - - {/* Expiry */} -
- - - {formatExpiryDate(domain.expiration_date)} - -
-
-
- - {/* Actions */} -
- - -
- - - -
- - -
-
- ) -} - -// ============================================================================ -// MOBILE HEALTH MODAL -// ============================================================================ - -function MobileHealthModal({ - isOpen, - onClose, - domain, - health, - onRefresh, - loading -}: { - isOpen: boolean - onClose: () => void - domain: any - health?: DomainHealthReport - onRefresh: () => void - loading: boolean -}) { - if (!isOpen || !domain) return null - - const checks = [ - { - label: 'DNS Resolution', - ok: health?.dns?.has_a || health?.dns?.has_ns, - icon: Globe, - detail: health?.dns?.error || (health?.dns?.has_a ? 'A record found' : 'No A record') - }, - { - label: 'HTTP Reachable', - ok: health?.http?.is_reachable, - icon: Wifi, - detail: health?.http?.error || (health?.http?.status_code ? `Status ${health.http.status_code}` : 'Not checked') - }, - { - label: 'SSL Certificate', - ok: health?.ssl?.has_certificate, - icon: Lock, - detail: health?.ssl?.error || (health?.ssl?.has_certificate ? 'Valid SSL' : 'No SSL') - }, - { - label: 'Not Parked', - ok: !(health?.dns?.is_parked || health?.http?.is_parked), - icon: Shield, - detail: (health?.dns?.is_parked || health?.http?.is_parked) ? 'Domain is parked' : 'Active site' - }, - ] - - const score = health?.score ?? 0 - - return ( -
-
- - Health Report - -
- -
- {/* Domain Header */} -
-
{domain.name}
-
= 75 ? "bg-accent/15 text-accent" : - score >= 50 ? "bg-amber-500/15 text-amber-400" : - "bg-rose-500/15 text-rose-400" - )}> - - Score: {score}/100 -
-
- - {/* Checks */} -
- {checks.map((check, i) => ( -
-
- -
-
-
{check.label}
-
{check.detail}
-
- {check.ok ? ( - - ) : ( - - )} -
- ))} -
- - {/* Warning */} - {health?.http?.error === 'timeout' && ( -
-
- -
-
Network Issue
-
- Could not reach domain from our servers. This may be a temporary issue or firewall restriction. -
-
-
-
- )} -
-
- ) + healthy: { label: 'Healthy', color: 'text-accent', bg: 'bg-accent/10 border-accent/20' }, + weakening: { label: 'Weak', color: 'text-amber-400', bg: 'bg-amber-500/10 border-amber-500/20' }, + parked: { label: 'Parked', color: 'text-blue-400', bg: 'bg-blue-500/10 border-blue-500/20' }, + critical: { label: 'Critical', color: 'text-rose-400', bg: 'bg-rose-500/10 border-rose-500/20' }, + unknown: { label: 'Unknown', color: 'text-white/40', bg: 'bg-white/5 border-white/10' }, } // ============================================================================ @@ -550,10 +85,6 @@ export default function WatchlistPage() { const [loadingHealth, setLoadingHealth] = useState>({}) const [selectedDomain, setSelectedDomain] = useState(null) const [filter, setFilter] = useState<'all' | 'available' | 'expiring'>('all') - const [menuOpen, setMenuOpen] = useState(false) - const [addModalOpen, setAddModalOpen] = useState(false) - const [healthModalOpen, setHealthModalOpen] = useState(false) - const [isRefreshingAll, setIsRefreshingAll] = useState(false) // Stats const stats = useMemo(() => { @@ -584,46 +115,57 @@ export default function WatchlistPage() { }, [domains, filter]) // Handlers - const handleAdd = useCallback(async (domainName: string) => { + const handleAdd = useCallback(async (e: React.FormEvent) => { + e.preventDefault() + if (!newDomain.trim()) return + const domainName = newDomain.trim().toLowerCase() setAdding(true) try { - await addDomain(domainName.toLowerCase()) + await addDomain(domainName) showToast(`Added: ${domainName}`, 'success') + setNewDomain('') } catch (err: any) { showToast(err.message || 'Failed', 'error') } finally { setAdding(false) } - }, [addDomain, showToast]) + }, [newDomain, addDomain, showToast]) + + // Auto-trigger health check for newly added domains + useEffect(() => { + if (!domains?.length) return + + domains.forEach(domain => { + // If no health report exists and not currently loading, trigger check + if (!healthReports[domain.id] && !loadingHealth[domain.id]) { + setLoadingHealth(prev => ({ ...prev, [domain.id]: true })) + api.getDomainHealth(domain.id, { refresh: false }) + .then(report => { + setHealthReports(prev => ({ ...prev, [domain.id]: report })) + }) + .catch(() => {}) + .finally(() => { + setLoadingHealth(prev => ({ ...prev, [domain.id]: false })) + }) + } + }) + }, [domains]) const handleRefresh = useCallback(async (id: number) => { setRefreshingId(id) try { await refreshDomain(id) - // Also refresh health - const report = await api.getDomainHealth(id, { refresh: true }) - setHealthReports(prev => ({ ...prev, [id]: report })) - showToast('Updated', 'success') - } catch { showToast('Failed', 'error') } + showToast('Intel updated', 'success') + } catch { showToast('Update failed', 'error') } finally { setRefreshingId(null) } }, [refreshDomain, showToast]) - const handleRefreshAll = useCallback(async () => { - setIsRefreshingAll(true) - try { - for (const domain of (domains || [])) { - await refreshDomain(domain.id) - } - showToast('All refreshed', 'success') - } catch {} - finally { setIsRefreshingAll(false) } - }, [domains, refreshDomain, showToast]) - const handleDelete = useCallback(async (id: number, name: string) => { + if (!confirm(`Drop target: ${name}?`)) return setDeletingId(id) try { await deleteDomain(id) - showToast('Removed', 'success') + showToast('Target dropped', 'success') } catch { showToast('Failed', 'error') } finally { setDeletingId(null) } }, [deleteDomain, showToast]) @@ -633,7 +175,7 @@ export default function WatchlistPage() { try { await api.updateDomainNotify(id, !current) updateDomain(id, { notify_on_available: !current }) - showToast(!current ? 'Alert on' : 'Alert off', 'success') + showToast(!current ? 'Alerts armed' : 'Alerts disarmed', 'success') } catch { showToast('Failed', 'error') } finally { setTogglingNotifyId(null) } }, [updateDomain, showToast]) @@ -660,298 +202,428 @@ export default function WatchlistPage() { load() }, [domains]) - // Auto-trigger health for new domains - useEffect(() => { - if (!domains?.length) return - domains.forEach(domain => { - if (!healthReports[domain.id] && !loadingHealth[domain.id]) { - setLoadingHealth(prev => ({ ...prev, [domain.id]: true })) - api.getDomainHealth(domain.id, { refresh: false }) - .then(report => setHealthReports(prev => ({ ...prev, [domain.id]: report }))) - .catch(() => {}) - .finally(() => setLoadingHealth(prev => ({ ...prev, [domain.id]: false }))) - } - }) - }, [domains]) - + // Selected domain for detail view const selectedDomainData = selectedDomain ? domains?.find(d => d.id === selectedDomain) : null const selectedHealth = selectedDomain ? healthReports[selectedDomain] : null return ( - <> - {/* Fullscreen Navigation */} - setMenuOpen(false)} /> - - {/* Mobile Header */} - setAddModalOpen(true)} - onRefreshAll={handleRefreshAll} - isRefreshing={isRefreshingAll} - availableCount={stats.available} - /> - - {/* Mobile Add Modal */} - setAddModalOpen(false)} - onAdd={handleAdd} - adding={adding} - /> - - {/* Mobile Health Modal */} - setHealthModalOpen(false)} - domain={selectedDomainData} - health={selectedHealth ?? undefined} - onRefresh={() => selectedDomain && handleHealthCheck(selectedDomain)} - loading={selectedDomain ? loadingHealth[selectedDomain] || false : false} - /> + + {toast && } + + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* HEADER - Compact */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+
+ + {/* Left */} +
+
+ + Domain Surveillance +
+ +

+ Watchlist + {stats.total} +

+
+ + {/* Right: Stats */} +
+
+
{stats.available}
+
Available
+
+
+
{stats.expiring}
+
Expiring
+
+
+
+
{/* ═══════════════════════════════════════════════════════════════════════ */} - {/* MOBILE CONTENT */} + {/* ADD DOMAIN */} {/* ═══════════════════════════════════════════════════════════════════════ */} -
- {toast && } - - {/* Stats */} -
+
+
+
+ setNewDomain(e.target.value)} + placeholder="Add domain to watch..." + className="flex-1 bg-transparent px-4 py-3 text-sm text-white placeholder:text-white/25 outline-none" + /> + +
+
+
+ + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* FILTERS */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+
{[ - { label: 'Total', value: stats.total, active: filter === 'all' }, - { label: 'Available', value: stats.available, active: filter === 'available', highlight: stats.available > 0 }, - { label: 'Expiring', value: stats.expiring, active: filter === 'expiring', warning: stats.expiring > 0 }, + { value: 'all', label: 'All', count: stats.total }, + { value: 'available', label: 'Available', count: stats.available }, + { value: 'expiring', label: 'Expiring', count: stats.expiring }, ].map((item) => ( ))}
+
- {/* Available Banner */} - {stats.available > 0 && ( -
-
-
- -
-
-
{stats.available} Domain{stats.available > 1 ? 's' : ''} Available!
-
Grab them before someone else does
-
+ {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* TABLE */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+ {!filteredDomains.length ? ( +
+
+
+

No domains in your watchlist

+

Add a domain above to start monitoring

- )} - - {/* Domains List */} -
- {filteredDomains.length === 0 ? ( -
-
- -
-

No domains yet

- + ) : ( +
+ {/* Table Header */} +
+
Domain
+
Status
+
Health
+
Expires
+
Alert
+
Actions
- ) : ( - filteredDomains.map((domain) => ( - handleRefresh(domain.id)} - onDelete={() => handleDelete(domain.id, domain.name)} - onToggleNotify={() => handleToggleNotify(domain.id, domain.notify_on_available)} - onHealthClick={() => { setSelectedDomain(domain.id); setHealthModalOpen(true) }} - refreshing={refreshingId === domain.id} - deleting={deletingId === domain.id} - togglingNotify={togglingNotifyId === domain.id} - /> - )) - )} -
-
- - {/* Mobile Bottom Nav */} - setMenuOpen(true)} /> - - {/* ═══════════════════════════════════════════════════════════════════════ */} - {/* DESKTOP LAYOUT */} - {/* ═══════════════════════════════════════════════════════════════════════ */} -
- - {toast && } - - {/* HEADER */} -
-
-
-
- - Domain Surveillance -
-

- Watchlist - {stats.total} -

-
-
-
-
{stats.available}
-
Available
-
-
-
{stats.expiring}
-
Expiring
-
-
-
-
- - {/* ADD DOMAIN */} -
-
{ e.preventDefault(); if (newDomain.trim()) handleAdd(newDomain.trim()).then(() => setNewDomain('')) }} className="relative max-w-xl"> -
- setNewDomain(e.target.value)} - placeholder="Add domain to watch..." - className="flex-1 bg-transparent px-4 py-3 text-sm text-white placeholder:text-white/25 outline-none" - /> - -
-
-
- - {/* FILTERS */} -
-
- {[ - { value: 'all', label: 'All', count: stats.total }, - { value: 'available', label: 'Available', count: stats.available }, - { value: 'expiring', label: 'Expiring', count: stats.expiring }, - ].map((item) => ( - - ))} -
-
- - {/* TABLE */} -
- {!filteredDomains.length ? ( -
-
- -
-

No domains in your watchlist

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

{selectedDomainData.name}

+
+ + {healthConfig[selectedHealth?.status || 'unknown'].label} + + {selectedHealth?.score !== undefined && ( + Score: {selectedHealth.score}/100 + )} +
+
+ + {/* Checks */} + {selectedHealth ? ( +
+ {/* DNS */} +
+ DNS Resolution + {selectedHealth.dns?.has_a || selectedHealth.dns?.has_ns ? ( +
+ + OK +
+ ) : selectedHealth.dns?.error ? ( +
+ + Error +
+ ) : ( +
+ + Failed +
+ )} +
+ + {/* HTTP */} +
+
+ HTTP Reachable + {selectedHealth.http?.error && ( +

{selectedHealth.http.error}

+ )} +
+ {selectedHealth.http?.is_reachable ? ( +
+ + OK +
+ ) : selectedHealth.http?.error === 'timeout' ? ( +
+ + Timeout +
+ ) : ( +
+ + Failed +
+ )} +
+ + {/* SSL */} +
+
+ SSL Certificate + {selectedHealth.ssl?.error && ( +

{selectedHealth.ssl.error}

+ )} +
+ {selectedHealth.ssl?.has_certificate ? ( +
+ + Valid +
+ ) : selectedHealth.ssl?.error ? ( +
+ + Error +
+ ) : ( +
+ + Missing +
+ )} +
+ + {/* Parked */} +
+ Not Parked + {!selectedHealth.dns?.is_parked && !selectedHealth.http?.is_parked ? ( +
+ + OK +
+ ) : ( +
+ + Parked +
+ )} +
+ + {/* Network warning if HTTP/SSL failed with timeout */} + {(selectedHealth.http?.error === 'timeout' || selectedHealth.ssl?.error?.includes('timeout')) && ( +
+

+ ⚠️ Network issue: Server could not reach this domain. This may be a firewall or routing problem. +

+
+ )} +
+ ) : ( +
+ Click the button below to run a health check +
+ )} + + {/* Refresh Button */} + +
+
+
+ )} +
) -} +} \ No newline at end of file diff --git a/start.sh b/start.sh index 8974c15..699ca04 100755 --- a/start.sh +++ b/start.sh @@ -20,7 +20,7 @@ NC='\033[0m' # No Color # Funktion zum Beenden von Prozessen stop_services() { -echo "" + echo "" echo -e "${YELLOW}🛑 Beende laufende Prozesse...${NC}" # Backend (uvicorn) - mehrere Versuche @@ -50,9 +50,9 @@ echo "" if lsof -i:3000 >/dev/null 2>&1; then echo -e "${RED}✗ Port 3000 ist noch belegt!${NC}" lsof -i:3000 - exit 1 -fi - + exit 1 + fi + echo -e "${GREEN}✓ Alle Prozesse beendet, Ports frei${NC}" } @@ -69,14 +69,14 @@ start_backend() { exit 1 fi -source venv/bin/activate + source venv/bin/activate # Lösche altes Log > backend.log # Starte uvicorn im Hintergrund nohup uvicorn app.main:app --host 0.0.0.0 --port 8000 > backend.log 2>&1 & -BACKEND_PID=$! + BACKEND_PID=$! echo "Backend PID: $BACKEND_PID" @@ -107,15 +107,15 @@ start_frontend() { # Prüfe ob .next existiert if [ ! -d ".next" ]; then echo -e "${RED}✗ .next nicht gefunden! Bitte erst 'npm run build' ausführen.${NC}" - exit 1 -fi - + exit 1 + fi + # Lösche altes Log > frontend.log # Starte Frontend im Hintergrund PORT=3000 nohup npm start > frontend.log 2>&1 & -FRONTEND_PID=$! + FRONTEND_PID=$! echo "Frontend PID: $FRONTEND_PID" @@ -139,8 +139,8 @@ FRONTEND_PID=$! fi echo -n "." done - -echo "" + + echo "" echo -e "${RED}✗ Frontend konnte nicht gestartet werden${NC}" echo "Letzte 30 Zeilen vom Log:" tail -30 frontend.log @@ -165,15 +165,15 @@ show_status() { echo "" echo "Laufende Prozesse:" ps aux | grep -E "(uvicorn|next start)" | grep -v grep | awk '{print " PID " $2 ": " $11 " " $12 " " $13}' -echo "" + echo "" echo "Ports:" lsof -i:8000 -i:3000 2>/dev/null | grep LISTEN || echo " Keine Port-Info verfügbar" -echo "" + echo "" } # Funktion zum Testen der Services test_services() { -echo "" + echo "" echo -e "${YELLOW}🧪 Teste Services...${NC}" # Test Backend Health