'use client' import { useEffect, useState, useMemo } from 'react' import { useStore } from '@/lib/store' import { api } from '@/lib/api' import { CommandCenterLayout } from '@/components/CommandCenterLayout' import { PremiumTable, Badge, PlatformBadge, StatCard, PageContainer } from '@/components/PremiumTable' import { Clock, ExternalLink, Search, Flame, Timer, Gavel, ChevronUp, ChevronDown, ChevronsUpDown, DollarSign, RefreshCw, Target, X, TrendingUp, Loader2, Sparkles, Eye, Filter, Zap, Crown, Plus, Check, } from 'lucide-react' import Link from 'next/link' import clsx from 'clsx' interface Auction { domain: string platform: string platform_url: string current_bid: number currency: string num_bids: number end_time: string time_remaining: string buy_now_price: number | null reserve_met: boolean | null traffic: number | null age_years: number | null tld: string affiliate_url: string } interface Opportunity { auction: Auction analysis: { opportunity_score: number urgency?: string competition?: string price_range?: string recommendation: string reasoning?: string } } type TabType = 'all' | 'ending' | 'hot' | 'opportunities' type SortField = 'ending' | 'bid_asc' | 'bid_desc' | 'bids' | 'score' type FilterPreset = 'all' | 'no-trash' | 'short' | 'high-value' | 'low-competition' const PLATFORMS = [ { id: 'All', name: 'All Sources' }, { id: 'GoDaddy', name: 'GoDaddy' }, { id: 'Sedo', name: 'Sedo' }, { id: 'NameJet', name: 'NameJet' }, { id: 'DropCatch', name: 'DropCatch' }, ] // Smart Filter Presets (from GAP_ANALYSIS.md) const FILTER_PRESETS: { id: FilterPreset, label: string, icon: typeof Filter, description: string, proOnly?: boolean }[] = [ { id: 'all', label: 'All', icon: Gavel, description: 'Show all auctions' }, { id: 'no-trash', label: 'No Trash', icon: Sparkles, description: 'Clean domains only (no spam)', proOnly: true }, { id: 'short', label: 'Short', icon: Zap, description: '4-letter domains or less' }, { id: 'high-value', label: 'High Value', icon: Crown, description: 'Premium TLDs with high activity', proOnly: true }, { id: 'low-competition', label: 'Low Competition', icon: Target, description: 'Under 5 bids' }, ] // Premium TLDs for filtering const PREMIUM_TLDS = ['com', 'io', 'ai', 'co', 'net', 'org', 'app', 'dev'] // Vanity/Clean domain check (no trash) function isCleanDomain(auction: Auction): boolean { const name = auction.domain.split('.')[0] // No hyphens if (name.includes('-')) return false // No numbers (unless short) if (name.length > 4 && /\d/.test(name)) return false // Max 12 chars if (name.length > 12) return false // Premium TLD only if (!PREMIUM_TLDS.includes(auction.tld)) return false return true } // Calculate Deal Score for an auction function calculateDealScore(auction: Auction): number { let score = 50 // Short domains are more valuable const name = auction.domain.split('.')[0] if (name.length <= 4) score += 25 else if (name.length <= 6) score += 15 else if (name.length <= 8) score += 5 // Premium TLDs if (['com', 'io', 'ai'].includes(auction.tld)) score += 15 else if (['co', 'net', 'org'].includes(auction.tld)) score += 5 // Age bonus if (auction.age_years && auction.age_years > 10) score += 15 else if (auction.age_years && auction.age_years > 5) score += 10 // High competition = good domain if (auction.num_bids >= 20) score += 10 else if (auction.num_bids >= 10) score += 5 // Clean domain bonus if (isCleanDomain(auction)) score += 10 return Math.min(score, 100) } export default function AuctionsPage() { const { isAuthenticated, subscription } = useStore() const [allAuctions, setAllAuctions] = useState([]) const [endingSoon, setEndingSoon] = useState([]) const [hotAuctions, setHotAuctions] = useState([]) const [opportunities, setOpportunities] = useState([]) const [loading, setLoading] = useState(true) const [refreshing, setRefreshing] = useState(false) const [activeTab, setActiveTab] = useState('all') const [sortBy, setSortBy] = useState('ending') const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc') // Filters const [searchQuery, setSearchQuery] = useState('') const [selectedPlatform, setSelectedPlatform] = useState('All') const [maxBid, setMaxBid] = useState('') const [filterPreset, setFilterPreset] = useState('all') const [trackedDomains, setTrackedDomains] = useState>(new Set()) const [trackingInProgress, setTrackingInProgress] = useState(null) // Check if user is on a paid tier (Trader or Tycoon) const isPaidUser = subscription?.tier === 'trader' || subscription?.tier === 'tycoon' useEffect(() => { loadData() }, []) useEffect(() => { if (isAuthenticated && opportunities.length === 0) { loadOpportunities() } }, [isAuthenticated]) const loadOpportunities = async () => { try { const oppData = await api.getAuctionOpportunities() setOpportunities(oppData.opportunities || []) } catch (e) { console.error('Failed to load opportunities:', e) } } const loadData = async () => { setLoading(true) try { const [auctionsData, hotData, endingData] = await Promise.all([ api.getAuctions(), api.getHotAuctions(50), api.getEndingSoonAuctions(24, 50), ]) setAllAuctions(auctionsData.auctions || []) setHotAuctions(hotData || []) setEndingSoon(endingData || []) if (isAuthenticated) { await loadOpportunities() } } catch (error) { console.error('Failed to load auction data:', error) } finally { setLoading(false) } } const handleRefresh = async () => { setRefreshing(true) await loadData() setRefreshing(false) } const formatCurrency = (value: number) => { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 0, maximumFractionDigits: 0, }).format(value) } const getCurrentAuctions = (): Auction[] => { switch (activeTab) { case 'ending': return endingSoon case 'hot': return hotAuctions case 'opportunities': return opportunities.map(o => o.auction) default: return allAuctions } } const getOpportunityData = (domain: string) => { if (activeTab !== 'opportunities') return null return opportunities.find(o => o.auction.domain === domain)?.analysis } // Track domain to watchlist const handleTrackDomain = async (domain: string) => { if (trackedDomains.has(domain)) return setTrackingInProgress(domain) try { await api.addDomainToWatchlist({ domain }) setTrackedDomains(prev => new Set([...prev, domain])) } catch (error) { console.error('Failed to track domain:', error) } finally { setTrackingInProgress(null) } } // Apply filter presets const applyPresetFilter = (auctions: Auction[]): Auction[] => { // Scout users (free tier) see raw feed, Trader+ see filtered feed by default const baseFilter = filterPreset === 'all' && isPaidUser ? 'no-trash' : filterPreset switch (baseFilter) { case 'no-trash': return auctions.filter(isCleanDomain) case 'short': return auctions.filter(a => a.domain.split('.')[0].length <= 4) case 'high-value': return auctions.filter(a => PREMIUM_TLDS.slice(0, 3).includes(a.tld) && // com, io, ai a.num_bids >= 5 && calculateDealScore(a) >= 70 ) case 'low-competition': return auctions.filter(a => a.num_bids < 5) default: return auctions } } const filteredAuctions = useMemo(() => { let auctions = getCurrentAuctions() // Apply preset filter auctions = applyPresetFilter(auctions) // Apply search query if (searchQuery) { auctions = auctions.filter(a => a.domain.toLowerCase().includes(searchQuery.toLowerCase()) ) } // Apply platform filter if (selectedPlatform !== 'All') { auctions = auctions.filter(a => a.platform === selectedPlatform) } // Apply max bid filter if (maxBid) { auctions = auctions.filter(a => a.current_bid <= parseFloat(maxBid)) } return auctions }, [activeTab, allAuctions, endingSoon, hotAuctions, opportunities, filterPreset, searchQuery, selectedPlatform, maxBid, isPaidUser]) const sortedAuctions = activeTab === 'opportunities' ? filteredAuctions : [...filteredAuctions].sort((a, b) => { const mult = sortDirection === 'asc' ? 1 : -1 switch (sortBy) { case 'ending': return mult * (new Date(a.end_time).getTime() - new Date(b.end_time).getTime()) case 'bid_asc': case 'bid_desc': return mult * (a.current_bid - b.current_bid) case 'bids': return mult * (b.num_bids - a.num_bids) case 'score': return mult * (calculateDealScore(b) - calculateDealScore(a)) default: return 0 } }) const getTimeColor = (timeRemaining: string) => { if (timeRemaining.includes('m') && !timeRemaining.includes('h') && !timeRemaining.includes('d')) return 'text-red-400' if (timeRemaining.includes('h') && parseInt(timeRemaining) < 2) return 'text-amber-400' return 'text-foreground-muted' } const handleSort = (field: SortField) => { if (sortBy === field) { setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc') } else { setSortBy(field) setSortDirection('asc') } } // Dynamic subtitle const getSubtitle = () => { if (loading) return 'Loading live auctions...' const total = allAuctions.length if (total === 0) return 'No active auctions found' const filtered = filteredAuctions.length const filterName = FILTER_PRESETS.find(p => p.id === filterPreset)?.label || 'All' if (filtered < total && filterPreset !== 'all') { return `${filtered.toLocaleString()} ${filterName} auctions (${total.toLocaleString()} total)` } return `${total.toLocaleString()} live auctions across 4 platforms` } return ( Refresh } > {/* Stats */}
{/* Tabs */}
{[ { id: 'all' as const, label: 'All', icon: Gavel, count: allAuctions.length }, { id: 'ending' as const, label: 'Ending Soon', icon: Timer, count: endingSoon.length, color: 'warning' }, { id: 'hot' as const, label: 'Hot', icon: Flame, count: hotAuctions.length }, { id: 'opportunities' as const, label: 'Opportunities', icon: Target, count: opportunities.length }, ].map((tab) => ( ))}
{/* Smart Filter Presets (from GAP_ANALYSIS.md) */}
{FILTER_PRESETS.map((preset) => { const isDisabled = preset.proOnly && !isPaidUser const isActive = filterPreset === preset.id return ( ) })}
{/* Tier notification for Scout users */} {!isPaidUser && (

You're seeing the raw auction feed

Upgrade to Trader for spam-free listings, Deal Scores, and Smart Filters.

Upgrade
)} {/* Filters */}
setSearchQuery(e.target.value)} className="w-full pl-11 pr-10 py-3 bg-background-secondary/50 border border-border/50 rounded-xl text-sm text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent/50 transition-all" /> {searchQuery && ( )}
setMaxBid(e.target.value)} className="w-32 pl-10 pr-4 py-3 bg-background-secondary/50 border border-border/50 rounded-xl text-sm text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-accent/50" />
{/* Table */} `${a.domain}-${a.platform}`} loading={loading} sortBy={sortBy} sortDirection={sortDirection} onSort={(key) => handleSort(key as SortField)} emptyIcon={} emptyTitle={searchQuery ? `No auctions matching "${searchQuery}"` : "No auctions found"} emptyDescription="Try adjusting your filters or check back later" columns={[ { key: 'domain', header: 'Domain', sortable: true, render: (a) => (
{a.domain}
{a.age_years && {a.age_years}y}
), }, { key: 'platform', header: 'Platform', hideOnMobile: true, render: (a) => (
{a.age_years && ( {a.age_years}y )}
), }, { key: 'bid_asc', header: 'Bid', sortable: true, align: 'right', render: (a) => (
{formatCurrency(a.current_bid)} {a.buy_now_price && (

Buy: {formatCurrency(a.buy_now_price)}

)}
), }, // Deal Score column - visible for Trader+ users { key: 'score', header: 'Deal Score', sortable: true, align: 'center', hideOnMobile: true, render: (a) => { // For opportunities tab, show opportunity score if (activeTab === 'opportunities') { const oppData = getOpportunityData(a.domain) if (oppData) { return ( {oppData.opportunity_score} ) } } // For other tabs, show calculated deal score (Trader+ only) if (!isPaidUser) { return ( ) } const score = calculateDealScore(a) return (
= 75 ? "bg-accent/20 text-accent" : score >= 50 ? "bg-amber-500/20 text-amber-400" : "bg-foreground/10 text-foreground-muted" )}> {score} {score >= 75 && ( Undervalued )}
) }, }, { key: 'bids', header: 'Bids', sortable: true, align: 'right', hideOnMobile: true, render: (a) => ( = 20 ? "text-accent" : a.num_bids >= 10 ? "text-amber-400" : "text-foreground-muted" )}> {a.num_bids} {a.num_bids >= 20 && } ), }, { key: 'ending', header: 'Time Left', sortable: true, align: 'right', hideOnMobile: true, render: (a) => ( {a.time_remaining} ), }, { key: 'actions', header: '', align: 'right', render: (a) => (
{/* Track Button */} {/* Bid Button */} Bid
), }, ]} />
) }