diff --git a/frontend/src/app/terminal/market/page.tsx b/frontend/src/app/terminal/market/page.tsx index 9d249d6..49e9938 100644 --- a/frontend/src/app/terminal/market/page.tsx +++ b/frontend/src/app/terminal/market/page.tsx @@ -322,17 +322,12 @@ export default function MarketPage() { {/* Top Row */}
-
- Live Market +
+ Live Market
-
- +
+ {stats.total.toLocaleString()} auctions + {stats.highScore} premium
@@ -350,13 +345,21 @@ export default function MarketPage() {
{stats.auctionCount}
External
-
-
{stats.highScore}
-
Score 80+
+
+
{stats.highScore}
+
Score 80+
+ + {/* Pull to Refresh Indicator */} + {refreshing && ( +
+ + Refreshing... +
+ )} {/* ═══════════════════════════════════════════════════════════════════════ */} {/* MOBILE SEARCH & FILTERS */} @@ -366,13 +369,13 @@ export default function MarketPage() {
setFiltersOpen(!filtersOpen)} className={clsx( "flex items-center justify-between w-full py-2 px-3 border transition-colors", - filtersOpen ? "border-blue-400/30 bg-blue-400/[0.05]" : "border-white/[0.08] bg-white/[0.02]" + filtersOpen ? "border-accent/30 bg-accent/[0.05]" : "border-white/[0.08] bg-white/[0.02]" )} >
Filters {activeFiltersCount > 0 && ( - {activeFiltersCount} + {activeFiltersCount} )}
@@ -452,7 +455,7 @@ export default function MarketPage() { className={clsx( "px-3 py-1.5 text-[10px] font-mono uppercase border transition-colors", tldFilter === tld - ? "border-blue-400 bg-blue-400/10 text-blue-400" + ? "border-accent bg-accent/10 text-accent" : "border-white/[0.08] text-white/40" )} > @@ -579,7 +582,7 @@ export default function MarketPage() { className={clsx( "px-3 py-1.5 text-xs font-mono transition-colors border", tldFilter === tld - ? "bg-blue-400/20 text-blue-400 border-blue-400/30" + ? "bg-accent/20 text-accent border-accent/30" : "text-white/40 border-transparent hover:text-white/60" )} > @@ -951,11 +954,11 @@ export default function MarketPage() { href={item.href} className={clsx( "flex-1 flex flex-col items-center justify-center gap-0.5 relative transition-colors", - item.active ? "text-blue-400" : "text-white/40 active:text-white/80" + item.active ? "text-accent" : "text-white/40 active:text-white/80" )} > {item.active && ( -
+
)} {item.label} diff --git a/frontend/src/app/terminal/watchlist/page.tsx b/frontend/src/app/terminal/watchlist/page.tsx index 214fe18..fc22dcd 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 } from 'react' import { useStore } from '@/lib/store' import { api, DomainHealthReport, HealthStatus } from '@/lib/api' -import { CommandCenterLayout } from '@/components/CommandCenterLayout' +import { Sidebar } from '@/components/Sidebar' import { Toast, useToast } from '@/components/Toast' import { Plus, @@ -16,18 +16,26 @@ import { X, Activity, AlertTriangle, - ArrowRight, CheckCircle2, XCircle, Target, - Globe, ExternalLink, - Calendar, + Gavel, + TrendingUp, + Menu, + Settings, Shield, - Crosshair + LogOut, + Crown, + Sparkles, + Coins, + Tag, + Zap, + Search } from 'lucide-react' import clsx from 'clsx' import Link from 'next/link' +import Image from 'next/image' // ============================================================================ // HELPERS @@ -46,20 +54,6 @@ function formatExpiryDate(expirationDate: string | null): string { return new Date(expirationDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) } -function getTimeAgo(date: string | null): string { - if (!date) return 'Never' - const now = new Date() - const past = new Date(date) - const diffMs = now.getTime() - past.getTime() - 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` - return `${diffDays}d ago` -} - const healthConfig: Record = { 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' }, @@ -73,11 +67,12 @@ const healthConfig: Record(null) const [deletingId, setDeletingId] = useState(null) const [togglingNotifyId, setTogglingNotifyId] = useState(null) @@ -85,6 +80,14 @@ export default function WatchlistPage() { const [loadingHealth, setLoadingHealth] = useState>({}) const [selectedDomain, setSelectedDomain] = useState(null) const [filter, setFilter] = useState<'all' | 'available' | 'expiring'>('all') + + // Mobile Menu + const [menuOpen, setMenuOpen] = useState(false) + + // Check auth on mount + useEffect(() => { + checkAuth() + }, [checkAuth]) // Stats const stats = useMemo(() => { @@ -136,7 +139,6 @@ export default function WatchlistPage() { 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 }) @@ -206,424 +208,689 @@ export default function WatchlistPage() { const selectedDomainData = selectedDomain ? domains?.find(d => d.id === selectedDomain) : null const selectedHealth = selectedDomain ? healthReports[selectedDomain] : null + // Mobile Nav + const mobileNavItems = [ + { href: '/terminal/radar', label: 'Radar', icon: Target, active: false }, + { href: '/terminal/market', label: 'Market', icon: Gavel, active: false }, + { href: '/terminal/watchlist', label: 'Watch', icon: Eye, active: true }, + { href: '/terminal/intel', label: 'Intel', icon: TrendingUp, active: false }, + ] + + const tierName = subscription?.tier_name || subscription?.tier || 'Scout' + const TierIcon = tierName === 'Tycoon' ? Crown : tierName === 'Trader' ? TrendingUp : Zap + + const drawerNavSections = [ + { + title: 'Discover', + items: [ + { href: '/terminal/radar', label: 'Radar', icon: Target }, + { href: '/terminal/market', label: 'Market', icon: Gavel }, + { href: '/terminal/intel', label: 'Intel', icon: TrendingUp }, + ] + }, + { + title: 'Manage', + items: [ + { href: '/terminal/watchlist', label: 'Watchlist', icon: Eye }, + { href: '/terminal/sniper', label: 'Sniper', icon: Target }, + ] + }, + { + title: 'Monetize', + items: [ + { href: '/terminal/yield', label: 'Yield', icon: Coins, isNew: true }, + { href: '/terminal/listing', label: 'For Sale', icon: Tag }, + ] + } + ] + return ( - - {toast && } +
+ {/* Desktop Sidebar */} +
+ +
- {/* ═══════════════════════════════════════════════════════════════════════ */} - {/* HEADER - Compact */} - {/* ═══════════════════════════════════════════════════════════════════════ */} -
-
- - {/* Left */} -
-
- - Domain Surveillance + {/* Main Content */} +
+ + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* MOBILE HEADER */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+
+ {/* Top Row */} +
+
+
+ Watchlist +
+
+ {stats.total} domains + {stats.available} available +
-

- Watchlist - {stats.total} -

-
- - {/* Right: Stats */} -
-
-
{stats.available}
-
Available
-
-
-
{stats.expiring}
-
Expiring
+ {/* Stats Grid */} +
+
+
{stats.total}
+
Tracked
+
+
+
{stats.available}
+
Available
+
+
+
{stats.expiring}
+
Expiring
+
-
-
+ - {/* ═══════════════════════════════════════════════════════════════════════ */} - {/* ADD DOMAIN */} - {/* ═══════════════════════════════════════════════════════════════════════ */} -
-
-
- 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 ? ( -
-
- + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* ADD DOMAIN */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+
+
+ + setNewDomain(e.target.value)} + onFocus={() => setSearchFocused(true)} + onBlur={() => setSearchFocused(false)} + placeholder="Add domain to watch..." + className="flex-1 bg-transparent px-3 py-3 text-sm text-white placeholder:text-white/20 outline-none font-mono" + /> +
-

No domains in your watchlist

-

Add a domain above to start monitoring

+
+
+ + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* 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 Header */} -
-
Domain
-
Status
-
Health
-
Expires
-
Alert
-
Actions
-
- - {/* Rows */} - {filteredDomains.map((domain) => { - const health = healthReports[domain.id] - const healthStatus = health?.status || 'unknown' - const config = healthConfig[healthStatus] - const days = getDaysUntilExpiry(domain.expiration_date) +
+ + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* DESKTOP HEADER */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+
+
+
+
+ Domain Surveillance +
- return ( -
- {/* Mobile */} -
-
-
-
- {domain.name} +

+ Watchlist + {stats.total} +

+
+ +
+
+
{stats.available}
+
Available
+
+
+
{stats.expiring}
+
Expiring
+
+
+
+
+ + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* DOMAIN LIST */} + {/* ═══════════════════════════════════════════════════════════════════════ */} +
+ {!filteredDomains.length ? ( +
+ +

No domains in your watchlist

+

Add a domain above to start monitoring

+
+ ) : ( +
+ {filteredDomains.map((domain) => { + const health = healthReports[domain.id] + const healthStatus = health?.status || 'unknown' + const config = healthConfig[healthStatus] + const days = getDaysUntilExpiry(domain.expiration_date) + + return ( +
+ {/* Mobile Row */} +
+
+
+
+ {domain.is_available ? ( + + ) : ( + + )} +
+
+
{domain.name}
+
+ {domain.registrar || 'Unknown registrar'} +
+
+
+ +
+
+ {domain.is_available ? 'AVAIL' : 'TAKEN'} +
+ +
- - {domain.is_available ? 'Available' : 'Taken'} - -
- -
- {formatExpiryDate(domain.expiration_date)} + + {/* Actions */}
- + + - +
+
+ + {/* Desktop Row */} +
+
+
+ {domain.is_available ? ( + + ) : ( + + )} +
+
+
{domain.name}
+
+ {domain.registrar || 'Unknown'} +
+
+ + + +
+ + {/* Status */} +
+ + {domain.is_available ? 'AVAIL' : 'TAKEN'} + +
+ + {/* Health */} + + + {/* Expires */} +
+ {days !== null && days <= 30 && days > 0 ? ( + {days}d + ) : ( + formatExpiryDate(domain.expiration_date) + )} +
+ + {/* Alert */} + + + {/* Actions */} +
+ +
- - {/* 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 */} -
- - -
+ ) + })} +
+ )} +
+ + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* MOBILE BOTTOM NAV */} + {/* ═══════════════════════════════════════════════════════════════════════ */} + + + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* MOBILE DRAWER */} + {/* ═══════════════════════════════════════════════════════════════════════ */} + {menuOpen && ( +
+
setMenuOpen(false)} + /> + +
+ +
+
+ Pounce +
+

POUNCE

+

Terminal v1.0

- ) - })} + +
+ +
+ {drawerNavSections.map((section) => ( +
+
+
+ {section.title} +
+
+ {section.items.map((item: any) => ( + setMenuOpen(false)} + className="flex items-center gap-3 px-4 py-2.5 text-white/60 active:text-white active:bg-white/[0.03] transition-colors border-l-2 border-transparent active:border-accent" + > + + {item.label} + {item.isNew && ( + NEW + )} + + ))} +
+
+ ))} + +
+ setMenuOpen(false)} + className="flex items-center gap-3 py-2.5 text-white/50 active:text-white transition-colors" + > + + Settings + + + {user?.is_admin && ( + setMenuOpen(false)} + className="flex items-center gap-3 py-2.5 text-amber-500/70 active:text-amber-400 transition-colors" + > + + Admin + + )} +
+
+ +
+
+
+ +
+
+

+ {user?.name || user?.email?.split('@')[0] || 'User'} +

+

{tierName}

+
+
+ + {tierName === 'Scout' && ( + setMenuOpen(false)} + className="flex items-center justify-center gap-2 w-full py-2.5 bg-accent text-black text-xs font-bold uppercase tracking-wider active:scale-[0.98] transition-all mb-2" + > + + Upgrade + + )} + + +
+
)} -
- {/* ═══════════════════════════════════════════════════════════════════════ */} - {/* HEALTH MODAL */} - {/* ═══════════════════════════════════════════════════════════════════════ */} - {selectedDomainData && ( -
setSelectedDomain(null)} - > + {/* ═══════════════════════════════════════════════════════════════════════ */} + {/* HEALTH MODAL */} + {/* ═══════════════════════════════════════════════════════════════════════ */} + {selectedDomainData && (
e.stopPropagation()} + className="fixed inset-0 z-[110] flex items-center justify-center p-4 bg-black/80" + onClick={() => setSelectedDomain(null)} > - {/* Header */} -
-
- - Health Report -
- -
- - {/* Content */} -
- {/* Domain */} -
-

{selectedDomainData.name}

-
- - {healthConfig[selectedHealth?.status || 'unknown'].label} - - {selectedHealth?.score !== undefined && ( - Score: {selectedHealth.score}/100 - )} +
e.stopPropagation()} + > + {/* Header */} +
+
+ + Health Report
+
- {/* Checks */} - {selectedHealth ? ( -
- {/* DNS */} -
- DNS Resolution - {selectedHealth.dns?.has_a || selectedHealth.dns?.has_ns ? ( -
- - OK -
- ) : selectedHealth.dns?.error ? ( -
- - Error -
- ) : ( -
- - Failed -
+ {/* Content */} +
+ {/* Domain */} +
+

{selectedDomainData.name}

+
+ + {healthConfig[selectedHealth?.status || 'unknown'].label} + + {selectedHealth?.score !== undefined && ( + Score: {selectedHealth.score}/100 )}
- - {/* HTTP */} -
-
- HTTP Reachable - {selectedHealth.http?.error && ( -

{selectedHealth.http.error}

+
+ + {/* Checks */} + {selectedHealth ? ( +
+ {/* DNS */} +
+ DNS + {selectedHealth.dns?.has_a || selectedHealth.dns?.has_ns ? ( +
+ + OK +
+ ) : ( +
+ + FAIL +
)}
- {selectedHealth.http?.is_reachable ? ( -
- - OK -
- ) : selectedHealth.http?.error === 'timeout' ? ( -
- - Timeout -
- ) : ( -
- - Failed -
- )} -
- - {/* SSL */} -
-
- SSL Certificate - {selectedHealth.ssl?.error && ( -

{selectedHealth.ssl.error}

+ + {/* HTTP */} +
+ HTTP + {selectedHealth.http?.is_reachable ? ( +
+ + OK +
+ ) : ( +
+ + {selectedHealth.http?.error || 'FAIL'} +
)}
- {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. -

+ + {/* SSL */} +
+ SSL + {selectedHealth.ssl?.has_certificate ? ( +
+ + VALID +
+ ) : ( +
+ + MISSING +
+ )}
- )} -
- ) : ( -
- Click the button below to run a health check -
- )} - - {/* Refresh Button */} -
) : ( - <> - - Run Health Check - +
+ Run health check to see results +
)} - + + {/* Refresh Button */} + +
-
- )} - + )} + + + {toast && } +
) -} \ No newline at end of file +}