'use client' import { useEffect, useState, useMemo, useCallback } from 'react' import { useStore } from '@/lib/store' import { api } from '@/lib/api' import { Sidebar } from '@/components/Sidebar' import { Toast, useToast } from '@/components/Toast' import { ExternalLink, Loader2, Diamond, Zap, ChevronLeft, ChevronRight, TrendingUp, RefreshCw, Clock, Search, Eye, EyeOff, ShieldCheck, Gavel, Ban, Target, X, Menu, Settings, Shield, LogOut, Crown, Sparkles, Coins, Tag } from 'lucide-react' import clsx from 'clsx' import Link from 'next/link' import Image from 'next/image' // ============================================================================ // TYPES // ============================================================================ interface MarketItem { id: string domain: string tld: string price: number currency: string price_type: 'bid' | 'fixed' | 'negotiable' status: 'auction' | 'instant' source: string is_pounce: boolean verified: boolean time_remaining?: string end_time?: string num_bids?: number slug?: string seller_verified: boolean url: string is_external: boolean pounce_score: number } type SourceFilter = 'all' | 'pounce' | 'external' type PriceRange = 'all' | 'low' | 'mid' | 'high' // ============================================================================ // HELPERS // ============================================================================ function calcTimeRemaining(endTimeIso?: string): string { if (!endTimeIso) return 'N/A' const end = new Date(endTimeIso).getTime() const now = Date.now() const diff = end - now if (diff <= 0) return 'Ended' const seconds = Math.floor(diff / 1000) const days = Math.floor(seconds / 86400) const hours = Math.floor((seconds % 86400) / 3600) const mins = Math.floor((seconds % 3600) / 60) if (days > 0) return `${days}d ${hours}h` if (hours > 0) return `${hours}h ${mins}m` if (mins > 0) return `${mins}m` return '< 1m' } function getSecondsUntilEnd(endTimeIso?: string): number { if (!endTimeIso) return Infinity const diff = new Date(endTimeIso).getTime() - Date.now() return diff > 0 ? diff / 1000 : -1 } function formatPrice(price: number, currency = 'USD'): string { return new Intl.NumberFormat('en-US', { style: 'currency', currency, maximumFractionDigits: 0 }).format(price) } function isSpam(domain: string): boolean { const name = domain.split('.')[0] return /[-\d]/.test(name) } // ============================================================================ // MAIN PAGE // ============================================================================ export default function MarketPage() { const { subscription, user, logout } = useStore() const { toast, showToast, hideToast } = useToast() const [items, setItems] = useState([]) const [loading, setLoading] = useState(true) const [refreshing, setRefreshing] = useState(false) const [stats, setStats] = useState({ total: 0, pounceCount: 0, auctionCount: 0, highScore: 0 }) const [sourceFilter, setSourceFilter] = useState('all') const [searchQuery, setSearchQuery] = useState('') const [priceRange, setPriceRange] = useState('all') const [hideSpam, setHideSpam] = useState(true) const [tldFilter, setTldFilter] = useState('all') const [searchFocused, setSearchFocused] = useState(false) const [trackedDomains, setTrackedDomains] = useState>(new Set()) const [trackingInProgress, setTrackingInProgress] = useState(null) // Pagination const [page, setPage] = useState(1) const [totalPages, setTotalPages] = useState(1) const ITEMS_PER_PAGE = 50 // Mobile Menu const [menuOpen, setMenuOpen] = useState(false) const loadData = useCallback(async (currentPage = 1) => { setLoading(true) try { const result = await api.getMarketFeed({ source: sourceFilter, keyword: searchQuery || undefined, tld: tldFilter === 'all' ? undefined : tldFilter, minPrice: priceRange === 'low' ? undefined : priceRange === 'mid' ? 100 : priceRange === 'high' ? 1000 : undefined, maxPrice: priceRange === 'low' ? 100 : priceRange === 'mid' ? 1000 : undefined, sortBy: 'newest', limit: ITEMS_PER_PAGE, offset: (currentPage - 1) * ITEMS_PER_PAGE }) setItems(result.items || []) setStats({ total: result.total, pounceCount: result.pounce_direct_count, auctionCount: result.auction_count, highScore: (result.items || []).filter(i => i.pounce_score >= 80).length }) setTotalPages(Math.ceil((result.total || 0) / ITEMS_PER_PAGE)) } catch (error) { console.error('Failed to load market data:', error) setItems([]) } finally { setLoading(false) } }, [sourceFilter, searchQuery, priceRange, tldFilter]) useEffect(() => { setPage(1) loadData(1) }, [loadData]) const handlePageChange = useCallback((newPage: number) => { setPage(newPage) loadData(newPage) }, [loadData]) const handleRefresh = useCallback(async () => { setRefreshing(true) await loadData() setRefreshing(false) }, [loadData]) // Load user's tracked domains on mount useEffect(() => { const loadTrackedDomains = async () => { try { const result = await api.getDomains(1, 100) const domainSet = new Set(result.domains.map(d => d.name)) setTrackedDomains(domainSet) } catch (error) { console.error('Failed to load tracked domains:', error) } } loadTrackedDomains() }, []) const handleTrack = useCallback(async (domain: string) => { if (trackingInProgress) return setTrackingInProgress(domain) try { if (trackedDomains.has(domain)) { const result = await api.getDomains(1, 100) const domainToDelete = result.domains.find(d => d.name === domain) if (domainToDelete) { await api.deleteDomain(domainToDelete.id) setTrackedDomains(prev => { const next = new Set(Array.from(prev)) next.delete(domain) return next }) showToast(`Removed: ${domain}`, 'success') } } else { await api.addDomain(domain) setTrackedDomains(prev => new Set([...Array.from(prev), domain])) showToast(`Tracking: ${domain}`, 'success') } } catch (error: any) { showToast(error.message || 'Failed', 'error') } finally { setTrackingInProgress(null) } }, [trackedDomains, trackingInProgress, showToast]) const filteredItems = useMemo(() => { let filtered = items const nowMs = Date.now() filtered = filtered.filter(item => { if (item.status !== 'auction') return true if (!item.end_time) return true const t = Date.parse(item.end_time) if (Number.isNaN(t)) return true return t > (nowMs - 2000) }) if (searchQuery && !loading) { const query = searchQuery.toLowerCase() filtered = filtered.filter(item => item.domain.toLowerCase().includes(query)) } if (hideSpam) { filtered = filtered.filter(item => !isSpam(item.domain)) } return filtered }, [items, searchQuery, loading, hideSpam]) // Mobile Nav const mobileNavItems = [ { href: '/terminal/radar', label: 'Radar', icon: Target, active: false }, { href: '/terminal/market', label: 'Market', icon: Gavel, active: true }, { href: '/terminal/watchlist', label: 'Watch', icon: Eye, active: false }, { 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 (
{/* Desktop Sidebar */}
{/* Main Content */}
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* MOBILE HEADER */} {/* ═══════════════════════════════════════════════════════════════════════ */}

Market

Live Auctions

{stats.total}
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* SEARCH - Mobile */} {/* ═══════════════════════════════════════════════════════════════════════ */}
setSearchQuery(e.target.value)} onFocus={() => setSearchFocused(true)} onBlur={() => setSearchFocused(false)} placeholder="Search auctions..." className="flex-1 bg-transparent px-3 py-3.5 text-base text-white placeholder:text-white/20 outline-none font-medium" /> {searchQuery && ( )}
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* FILTERS - Mobile Horizontal Scroll */} {/* ═══════════════════════════════════════════════════════════════════════ */}
{['com', 'ai', 'io', 'net'].map((tld) => ( ))} {[ { value: 'low', label: '< $100' }, { value: 'mid', label: '< $1k' }, { value: 'high', label: '$1k+' }, ].map((item) => ( ))}
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* DESKTOP HEADER */} {/* ═══════════════════════════════════════════════════════════════════════ */}
Live Auctions

Market {stats.total}

{stats.pounceCount}
Pounce Direct
{stats.auctionCount}
External
{stats.highScore}
Score 80+
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* DESKTOP FILTERS */} {/* ═══════════════════════════════════════════════════════════════════════ */}
{[ { value: 'low', label: '< $100' }, { value: 'mid', label: '< $1k' }, { value: 'high', label: '$1k+' }, ].map((item) => ( ))}
setSearchQuery(e.target.value)} placeholder="Search domains..." className="bg-[#050505] border border-white/10 pl-9 pr-4 py-2 text-sm text-white placeholder:text-white/25 outline-none focus:border-accent/40 w-48 lg:w-64" />
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* CONTENT */} {/* ═══════════════════════════════════════════════════════════════════════ */}
{loading ? (
) : filteredItems.length === 0 ? (

No domains found

Try adjusting your filters

) : ( <> {/* Mobile Cards */}
{filteredItems.map((item) => { const timeLeftSec = getSecondsUntilEnd(item.end_time) const isUrgent = timeLeftSec > 0 && timeLeftSec < 3600 const isPounce = item.is_pounce const displayTime = item.status === 'auction' ? calcTimeRemaining(item.end_time) : null const isTracked = trackedDomains.has(item.domain) const isTracking = trackingInProgress === item.domain return (
{isPounce ? (
) : (
{item.source.substring(0, 2).toUpperCase()}
)}
{item.domain}
{item.source}
{formatPrice(item.price)}
{isPounce ? ( Instant ) : ( {displayTime || 'N/A'} )}
{/* Score & Actions */}
= 80 ? "text-accent bg-accent/10" : item.pounce_score >= 50 ? "text-amber-400 bg-amber-400/10" : "text-white/40 bg-white/5" )}> Score {item.pounce_score}
{isPounce ? 'Buy' : 'Bid'} {!isPounce && }
) })}
{/* Desktop Table */}
Domain
Score
Price
Time
Actions
{filteredItems.map((item) => { const timeLeftSec = getSecondsUntilEnd(item.end_time) const isUrgent = timeLeftSec > 0 && timeLeftSec < 3600 const isPounce = item.is_pounce const displayTime = item.status === 'auction' ? calcTimeRemaining(item.end_time) : null const isTracked = trackedDomains.has(item.domain) const isTracking = trackingInProgress === item.domain return (
{isPounce ? (
) : (
{item.source.substring(0, 2).toUpperCase()}
)}
{item.domain}
{item.source} {isPounce && item.verified && ( <> · Verified )} {item.num_bids ? <>·{item.num_bids} bids : null}
= 80 ? "text-accent bg-accent/10" : item.pounce_score >= 50 ? "text-amber-400 bg-amber-400/10" : "text-white/40 bg-white/5" )}> {item.pounce_score}
{formatPrice(item.price)}
{item.price_type === 'bid' ? 'Bid' : 'Buy Now'}
{isPounce ? ( Instant ) : ( {displayTime || 'N/A'} )}
{isPounce ? 'Buy' : 'Bid'} {!isPounce && }
) })}
{/* Pagination */} {totalPages > 1 && (
Page {page} of {totalPages}
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => { let pageNum: number if (totalPages <= 5) { pageNum = i + 1 } else if (page <= 3) { pageNum = i + 1 } else if (page >= totalPages - 2) { pageNum = totalPages - 4 + i } else { pageNum = page - 2 + i } return ( ) })}
{page} / {totalPages}
)} )}
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* MOBILE BOTTOM NAV */} {/* ═══════════════════════════════════════════════════════════════════════ */} {/* ═══════════════════════════════════════════════════════════════════════ */} {/* MOBILE DRAWER */} {/* ═══════════════════════════════════════════════════════════════════════ */} {menuOpen && (
setMenuOpen(false)} />
Pounce

POUNCE

Terminal

{drawerNavSections.map((section) => (
{section.title}
{section.items.map((item: any) => ( setMenuOpen(false)} className="flex items-center gap-4 px-6 py-3.5 text-white/70 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-3 text-white/60 active:text-white transition-colors" > Settings {user?.is_admin && ( setMenuOpen(false)} className="flex items-center gap-3 py-3 text-amber-500/80 active:text-amber-400 transition-colors" > Admin Panel )}

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

{tierName} Plan

{tierName === 'Scout' && ( setMenuOpen(false)} className="flex items-center justify-center gap-2 w-full py-3 bg-white text-black text-sm font-bold rounded-lg active:scale-[0.98] transition-all mb-3 shadow-lg" > Upgrade Now )}
)}
{toast && }
) }