diff --git a/frontend/src/app/terminal/radar/page.tsx b/frontend/src/app/terminal/radar/page.tsx index b4eb049..d6ffa4d 100644 --- a/frontend/src/app/terminal/radar/page.tsx +++ b/frontend/src/app/terminal/radar/page.tsx @@ -6,7 +6,6 @@ import { useStore } from '@/lib/store' import { api } from '@/lib/api' import { TerminalLayout } from '@/components/TerminalLayout' import { Ticker, useTickerItems } from '@/components/Ticker' -import { PremiumTable, StatCard, PageContainer, Badge, SectionHeader, ActionButton } from '@/components/PremiumTable' import { Toast, useToast } from '@/components/Toast' import { Eye, @@ -27,10 +26,69 @@ import { CheckCircle2, XCircle, Loader2, + Wifi, + ShieldAlert, + BarChart3 } from 'lucide-react' import clsx from 'clsx' import Link from 'next/link' +// ============================================================================ +// SHARED COMPONENTS (Matching Market Page Style) +// ============================================================================ + +function Tooltip({ children, content }: { children: React.ReactNode; content: string }) { + return ( +
+ {children} +
+ {content} +
+
+
+ ) +} + +function StatCard({ + label, + value, + subValue, + icon: Icon, + trend +}: { + label: string + value: string | number + subValue?: string + icon: any + trend?: 'up' | 'down' | 'neutral' | 'active' +}) { + return ( +
+
+
+

{label}

+
+ {value} + {subValue && {subValue}} +
+
+
+ +
+
+ ) +} + +// ============================================================================ +// TYPES +// ============================================================================ + interface HotAuction { domain: string current_bid: number @@ -54,6 +112,10 @@ interface SearchResult { loading: boolean } +// ============================================================================ +// MAIN PAGE +// ============================================================================ + export default function RadarPage() { const searchParams = useSearchParams() const { @@ -75,14 +137,7 @@ export default function RadarPage() { const [searchResult, setSearchResult] = useState(null) const [addingToWatchlist, setAddingToWatchlist] = useState(false) - // Check for upgrade success - useEffect(() => { - if (searchParams.get('upgraded') === 'true') { - showToast('Welcome to your upgraded plan! 🎉', 'success') - window.history.replaceState({}, '', '/terminal/radar') - } - }, [searchParams, showToast]) - + // Load Data const loadDashboardData = useCallback(async () => { try { const [auctions, trending] = await Promise.all([ @@ -99,12 +154,10 @@ export default function RadarPage() { }, []) useEffect(() => { - if (isAuthenticated) { - loadDashboardData() - } + if (isAuthenticated) loadDashboardData() }, [isAuthenticated, loadDashboardData]) - // Universal Search - simultaneous check + // Search Logic const handleSearch = useCallback(async (domain: string) => { if (!domain.trim()) { setSearchResult(null) @@ -115,7 +168,6 @@ export default function RadarPage() { setSearchResult({ available: null, inAuction: false, inMarketplace: false, loading: true }) try { - // Parallel checks const [whoisResult, auctionsResult] = await Promise.all([ api.checkDomain(cleanDomain, true).catch(() => null), api.getAuctions(cleanDomain).catch(() => ({ auctions: [] })), @@ -132,7 +184,7 @@ export default function RadarPage() { setSearchResult({ available: isAvailable, inAuction: !!auctionMatch, - inMarketplace: false, // TODO: Check marketplace + inMarketplace: false, auctionData: auctionMatch, loading: false, }) @@ -143,7 +195,6 @@ export default function RadarPage() { const handleAddToWatchlist = useCallback(async () => { if (!searchQuery.trim()) return - setAddingToWatchlist(true) try { await addDomain(searchQuery.trim()) @@ -157,7 +208,7 @@ export default function RadarPage() { } }, [searchQuery, addDomain, showToast]) - // Debounced search + // Debounce Search useEffect(() => { const timer = setTimeout(() => { if (searchQuery.length > 3) { @@ -169,318 +220,306 @@ export default function RadarPage() { return () => clearTimeout(timer) }, [searchQuery, handleSearch]) - // Memoized computed values - const { availableDomains, totalDomains, tierName, TierIcon, greeting, subtitle, listingsCount } = useMemo(() => { - const availableDomains = domains?.filter(d => d.is_available) || [] - const totalDomains = domains?.length || 0 - const tierName = subscription?.tier_name || subscription?.tier || 'Scout' - const TierIcon = tierName === 'Tycoon' ? Crown : tierName === 'Trader' ? TrendingUp : Zap - + // Computed + const { availableDomains, 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 ? 'Good morning' : hour < 18 ? 'Good afternoon' : 'Good evening' let subtitle = '' - if (availableDomains.length > 0) { - subtitle = `${availableDomains.length} domain${availableDomains.length !== 1 ? 's' : ''} ready to pounce!` - } else if (totalDomains > 0) { - subtitle = `Monitoring ${totalDomains} domain${totalDomains !== 1 ? 's' : ''} for you` - } else { - subtitle = 'Start tracking domains to find opportunities' - } + if (available.length > 0) subtitle = `${available.length} domain${available.length !== 1 ? 's' : ''} ready to pounce!` + else if (total > 0) subtitle = `Monitoring ${total} domain${total !== 1 ? 's' : ''} for you` + else subtitle = 'Start tracking domains to find opportunities' - // TODO: Get actual listings count from API - const listingsCount = 0 - - return { availableDomains, totalDomains, tierName, TierIcon, greeting, subtitle, listingsCount } - }, [domains, subscription]) + return { availableDomains: available, totalDomains: total, greeting, subtitle } + }, [domains]) - // Generate ticker items const tickerItems = useTickerItems(trendingTlds, availableDomains, hotAuctions) - if (isLoading || !isAuthenticated) { - return ( -
-
-
- ) - } - return ( {toast && } + + {/* GLOW BACKGROUND */} +
+
+
- {/* A. THE TICKER - Market movements */} - {tickerItems.length > 0 && ( -
- -
- )} +
- - {/* B. QUICK STATS - 3 Cards as per concept */} -
- + {/* 1. TICKER */} + {tickerItems.length > 0 && ( +
+ +
+ )} + + {/* 2. STAT GRID */} +
+ 0 ? `${availableDomains.length} alerts` : undefined} - icon={Eye} - accent={availableDomains.length > 0} + subValue="Domains" + icon={Eye} + trend="neutral" /> - + 0 ? `${hotAuctions.length}+` : '0'} - subtitle="opportunities" - icon={Gavel} + label="Opportunities" + value={hotAuctions.length} + subValue="Live" + icon={Gavel} + trend="active" /> - +
0 ? 'up' : 'neutral'} /> - +
+
+ +
- {/* C. UNIVERSAL SEARCH - Hero Element */} -
-
- -
-
-
- -
-
-

Universal Search

-

Check availability, auctions & marketplace simultaneously

-
-
+ {/* 3. UNIVERSAL SEARCH */} +
+
+
-
- +
+

Universal Domain Search

+

Check availability, auctions & marketplace instantly

+
+ +
+ setSearchQuery(e.target.value)} - placeholder="Enter domain to check (e.g., dream.com)" - className="w-full h-14 pl-12 pr-4 bg-background/80 backdrop-blur-sm border border-border/50 rounded-xl - text-base text-foreground placeholder:text-foreground-subtle - focus:outline-none focus:border-accent focus:ring-2 focus:ring-accent/20" + placeholder="Enter domain (e.g. pounce.com)" + className="w-full h-14 pl-12 pr-4 bg-zinc-900/50 border border-white/10 rounded-xl + text-lg text-white placeholder:text-zinc-600 + focus:outline-none focus:border-emerald-500/50 focus:ring-1 focus:ring-emerald-500/50 transition-all shadow-inner" />
- {/* Search Results */} + {/* Search Results Panel */} {searchResult && ( -
- {searchResult.loading ? ( -
- - Checking... -
- ) : ( -
- {/* Availability */} -
-
- {searchResult.available === true ? ( - - ) : searchResult.available === false ? ( - - ) : ( - - )} - - {searchResult.available === true - ? 'Available for registration!' - : searchResult.available === false - ? 'Currently registered' - : 'Could not check availability'} - -
- {searchResult.available === true && ( - - Register Now - - )} +
+
+ {searchResult.loading ? ( +
+ + Analyzing global databases...
- - {/* In Auction */} - {searchResult.inAuction && searchResult.auctionData && ( -
-
- - - In auction: ${searchResult.auctionData.current_bid} ({searchResult.auctionData.time_remaining}) - + ) : ( +
+ {/* Availability Status */} +
+
+ {searchResult.available === true ? ( +
+ +
+ ) : searchResult.available === false ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+

+ {searchResult.available === true ? 'Available for Registration' : + searchResult.available === false ? 'Currently Registered' : 'Status Unknown'} +

+

+ {searchResult.available === true ? 'Instant registration possible' : 'Check secondary market'} +

+
- - Bid Now - -
- )} - - {/* Action Buttons */} -
- +
+ + {/* Auction Match */} + {searchResult.inAuction && searchResult.auctionData && ( +
+
+
+ +
+
+

Auction Detected

+

+ Current Bid: ${searchResult.auctionData.current_bid} • Ends in {searchResult.auctionData.time_remaining} +

+
+
+ + Bid Now + +
+ )} + + {/* Action Bar */} +
+ +
-
- )} + )} +
)}
- {/* D. RECENT ALERTS + MARKET PULSE */} + {/* 4. SPLIT VIEW: PULSE & ALERTS */}
- {/* Recent Alerts / Activity Feed */} -
-
- - View all - - } - /> + + {/* MARKET PULSE */} +
+
+
+ +

Market Pulse

+
+ + View All +
-
- {availableDomains.length > 0 ? ( -
- {availableDomains.slice(0, 5).map((domain) => ( -
-
- - + +
+ {loadingData ? ( +
Loading market data...
+ ) : hotAuctions.length > 0 ? ( + hotAuctions.map((auction, i) => ( + + - ) : totalDomains > 0 ? ( -
- -

All domains are still registered

-

- Monitoring {totalDomains} domains for you -

-
+
+

${auction.current_bid}

+

Current Bid

+
+ + )) ) : ( -
- -

No domains tracked yet

-

- Use Universal Search above to start -

+
+ +

No live auctions right now

)}
- {/* Market Pulse */} -
-
- - View all - - } - /> + {/* WATCHLIST ACTIVITY */} +
+
+
+ +

Recent Alerts

+
+ + Manage +
-
- {loadingData ? ( -
- {[...Array(4)].map((_, i) => ( -
- ))} -
- ) : hotAuctions.length > 0 ? ( -
- {hotAuctions.map((auction, idx) => ( + +
+ {availableDomains.length > 0 ? ( + availableDomains.slice(0, 5).map((domain) => ( +
+
+ + )) + ) : totalDomains > 0 ? ( +
+ +

All watched domains are taken

) : ( -
- -

No auctions ending soon

+
+ +

Your watchlist is empty

+

Use search to add domains

)}
+
- +
) } diff --git a/frontend/src/components/Ticker.tsx b/frontend/src/components/Ticker.tsx index e74e47f..8a2dff8 100644 --- a/frontend/src/components/Ticker.tsx +++ b/frontend/src/components/Ticker.tsx @@ -1,7 +1,7 @@ 'use client' import { useEffect, useState, useRef } from 'react' -import { TrendingUp, TrendingDown, AlertCircle, Sparkles, Gavel } from 'lucide-react' +import { TrendingUp, TrendingDown, AlertCircle, Sparkles, Gavel, Clock } from 'lucide-react' import clsx from 'clsx' export interface TickerItem { @@ -18,7 +18,7 @@ interface TickerProps { speed?: number // pixels per second } -export function Ticker({ items, speed = 50 }: TickerProps) { +export function Ticker({ items, speed = 40 }: TickerProps) { const containerRef = useRef(null) const contentRef = useRef(null) const [animationDuration, setAnimationDuration] = useState(0) @@ -37,12 +37,12 @@ export function Ticker({ items, speed = 50 }: TickerProps) { switch (type) { case 'tld_change': return change && change > 0 - ? - : + ? + : case 'domain_available': - return + return case 'auction_ending': - return + return case 'alert': return default: @@ -52,27 +52,26 @@ export function Ticker({ items, speed = 50 }: TickerProps) { const getValueColor = (type: TickerItem['type'], change?: number) => { if (type === 'tld_change') { - return change && change > 0 ? 'text-orange-400' : 'text-accent' + return change && change > 0 ? 'text-emerald-400' : 'text-red-400' } - return 'text-accent' + return 'text-white' } // Duplicate items for seamless loop - const tickerItems = [...items, ...items] + const tickerItems = [...items, ...items, ...items] return (
{/* Fade edges */} -
-
+
+
{getIcon(item.type, item.change)} - {item.message} + {item.message} {item.value && ( - + {item.value} )} {item.change !== undefined && ( 0 ? "text-orange-400" : "text-accent" + "px-1.5 py-0.5 rounded text-[10px] font-bold", + item.change > 0 ? "bg-emerald-500/10 text-emerald-400" : "bg-red-500/10 text-red-400" )}> {item.change > 0 ? '+' : ''}{item.change.toFixed(1)}% @@ -106,16 +105,15 @@ export function Ticker({ items, speed = 50 }: TickerProps) {
) @@ -145,7 +143,7 @@ export function useTickerItems( items.push({ id: `available-${domain.name}`, type: 'domain_available', - message: `${domain.name} is available!`, + message: `${domain.name} available!`, urgent: true, }) }) @@ -162,4 +160,3 @@ export function useTickerItems( return items } -