'use client' import { useEffect, useState, useMemo, useCallback, memo } from 'react' import { useStore } from '@/lib/store' import { api } from '@/lib/api' import { CommandCenterLayout } from '@/components/CommandCenterLayout' import { ExternalLink, Loader2, Diamond, Zap, ChevronDown, ChevronLeft, ChevronRight, Plus, Check, TrendingUp, RefreshCw, Clock, Search, Eye, EyeOff, ShieldCheck, Store, Gavel, Ban, Activity, Target, ArrowRight } from 'lucide-react' import clsx from 'clsx' import Link from 'next/link' // ============================================================================ // 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 SortField = 'domain' | 'score' | 'price' | 'time' | 'source' type SortDirection = 'asc' | 'desc' 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 } = useStore() 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 [sortField, setSortField] = useState('score') const [sortDirection, setSortDirection] = useState('desc') 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 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: sortField === 'score' ? 'score' : sortField === 'price' ? (sortDirection === 'asc' ? 'price_asc' : 'price_desc') : sortField === 'time' ? 'time' : '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, sortField, sortDirection, 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]) const handleSort = useCallback((field: SortField) => { if (sortField === field) { setSortDirection(d => d === 'asc' ? 'desc' : 'asc') } else { setSortField(field) setSortDirection(field === 'score' || field === 'price' ? 'desc' : 'asc') } }, [sortField]) // Load user's tracked domains on mount useEffect(() => { const loadTrackedDomains = async () => { try { const result = await api.getDomains(1, 100) const domainSet = new Set(result.items.map(d => d.domain)) 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)) { // Find and delete the domain const result = await api.getDomains(1, 100) const domainToDelete = result.items.find(d => d.domain === domain) if (domainToDelete) { await api.deleteDomain(domainToDelete.id) setTrackedDomains(prev => { const next = new Set(Array.from(prev)) next.delete(domain) return next }) } } else { // Add the domain await api.addDomain(domain) setTrackedDomains(prev => new Set([...Array.from(prev), domain])) } } catch (error) { console.error(error) } finally { setTrackingInProgress(null) } }, [trackedDomains, trackingInProgress]) 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)) } const mult = sortDirection === 'asc' ? 1 : -1 filtered = [...filtered].sort((a, b) => { if (sortField === 'score' && a.is_pounce !== b.is_pounce) { return a.is_pounce ? -1 : 1 } switch (sortField) { case 'domain': return mult * a.domain.localeCompare(b.domain) case 'score': return mult * (a.pounce_score - b.pounce_score) case 'price': return mult * (a.price - b.price) case 'time': return mult * (getSecondsUntilEnd(a.end_time) - getSecondsUntilEnd(b.end_time)) case 'source': return mult * a.source.localeCompare(b.source) default: return 0 } }) return filtered }, [items, searchQuery, sortField, sortDirection, loading, hideSpam]) return ( {/* ═══════════════════════════════════════════════════════════════════════ */} {/* HEADER */} {/* ═══════════════════════════════════════════════════════════════════════ */}
Live Auctions

Market {stats.total}

{stats.pounceCount}
Pounce Direct
{stats.auctionCount}
External
{stats.highScore}
Score 80+
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* FILTERS */} {/* ═══════════════════════════════════════════════════════════════════════ */}
{/* Source Filters */}
{/* TLD Filter */}
{/* Price Filters */} {[ { value: 'low', label: '< $100' }, { value: 'mid', label: '< $1k' }, { value: 'high', label: '$1k+' }, ].map((item) => ( ))}
{/* Hide Spam */} {/* Search - Right aligned */}
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" />
{/* Refresh */}
{/* ═══════════════════════════════════════════════════════════════════════ */} {/* TABLE */} {/* ═══════════════════════════════════════════════════════════════════════ */}
{loading ? (
) : filteredItems.length === 0 ? (

No domains found

Try adjusting your filters

) : (
{/* Table Header */}
Domain
Score
Price
Time
Actions
{/* Rows */} {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 (
{/* Mobile */}
{isPounce ? ( ) : ( {item.source.substring(0, 3)} )} {item.domain}
{formatPrice(item.price)}
Score: {item.pounce_score} {displayTime || 'Instant'}
{/* Mobile Actions */}
{isPounce ? 'Buy' : 'Bid'} {!isPounce && }
{/* Desktop */}
{/* Domain */}
{isPounce ? (
) : (
{item.source.substring(0, 2).toUpperCase()}
)}
{item.domain}
{item.source} {isPounce && item.verified && ( <> · Verified )} {item.num_bids ? <>·{item.num_bids} bids : null}
{/* Score */}
= 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}
{/* Price */}
{formatPrice(item.price)}
{item.price_type === 'bid' ? 'Bid' : 'Buy Now'}
{/* Time */}
{isPounce ? ( Instant ) : ( {displayTime || 'N/A'} )}
{/* Actions */}
{isPounce ? 'Buy' : 'Bid'} {!isPounce && }
) })}
)} {/* Pagination */} {!loading && totalPages > 1 && (
Page {page} of {totalPages} · {stats.total} domains
{/* Page Numbers */}
{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 ( ) })}
)}
) }