- Landing: 'TLD price explorer' → 'Market overview' - Auctions: Title to 'Curated Opportunities' (no small numbers) - TLD Pricing: First row (.com) visible without blur for preview - Footer: Updated branding, simplified, added tagline - All Sign In links redirect back to original page
692 lines
30 KiB
TypeScript
692 lines
30 KiB
TypeScript
'use client'
|
||
|
||
import { useEffect, useState } from 'react'
|
||
import { Header } from '@/components/Header'
|
||
import { Footer } from '@/components/Footer'
|
||
import { useStore } from '@/lib/store'
|
||
import { api } from '@/lib/api'
|
||
import {
|
||
Clock,
|
||
TrendingUp,
|
||
ExternalLink,
|
||
Search,
|
||
Flame,
|
||
Timer,
|
||
Users,
|
||
ArrowUpRight,
|
||
Lock,
|
||
Gavel,
|
||
ChevronUp,
|
||
ChevronDown,
|
||
ChevronsUpDown,
|
||
DollarSign,
|
||
RefreshCw,
|
||
Target,
|
||
Info,
|
||
X,
|
||
} 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
|
||
// Legacy fields
|
||
estimated_value?: number
|
||
current_bid?: number
|
||
value_ratio?: number
|
||
potential_profit?: number
|
||
}
|
||
}
|
||
|
||
type TabType = 'all' | 'ending' | 'hot' | 'opportunities'
|
||
type SortField = 'ending' | 'bid_asc' | 'bid_desc' | 'bids'
|
||
|
||
const PLATFORMS = [
|
||
{ id: 'All', name: 'All Sources' },
|
||
{ id: 'GoDaddy', name: 'GoDaddy' },
|
||
{ id: 'Sedo', name: 'Sedo' },
|
||
{ id: 'NameJet', name: 'NameJet' },
|
||
{ id: 'DropCatch', name: 'DropCatch' },
|
||
{ id: 'ExpiredDomains', name: 'Expired Domains' },
|
||
]
|
||
|
||
const TAB_DESCRIPTIONS: Record<TabType, { title: string; description: string }> = {
|
||
all: {
|
||
title: 'All Auctions',
|
||
description: 'All active auctions from all platforms, sorted by ending time by default.',
|
||
},
|
||
ending: {
|
||
title: 'Ending Soon',
|
||
description: 'Auctions ending within the next 24 hours. Best for last-minute sniping opportunities.',
|
||
},
|
||
hot: {
|
||
title: 'Hot Auctions',
|
||
description: 'Auctions with the most bidding activity (20+ bids). High competition but proven demand.',
|
||
},
|
||
opportunities: {
|
||
title: 'Smart Opportunities',
|
||
description: 'Our algorithm scores auctions based on: Time urgency (ending soon = higher score), Competition (fewer bids = higher score), and Price point (lower entry = higher score). Only auctions with a combined score ≥ 3 are shown.',
|
||
},
|
||
}
|
||
|
||
function SortIcon({ field, currentField, direction }: { field: SortField, currentField: SortField, direction: 'asc' | 'desc' }) {
|
||
if (field !== currentField) {
|
||
return <ChevronsUpDown className="w-4 h-4 text-foreground-subtle" />
|
||
}
|
||
return direction === 'asc'
|
||
? <ChevronUp className="w-4 h-4 text-accent" />
|
||
: <ChevronDown className="w-4 h-4 text-accent" />
|
||
}
|
||
|
||
function getPlatformBadgeClass(platform: string) {
|
||
switch (platform) {
|
||
case 'GoDaddy': return 'text-blue-400 bg-blue-400/10'
|
||
case 'Sedo': return 'text-orange-400 bg-orange-400/10'
|
||
case 'NameJet': return 'text-purple-400 bg-purple-400/10'
|
||
case 'DropCatch': return 'text-teal-400 bg-teal-400/10'
|
||
default: return 'text-foreground-muted bg-foreground/5'
|
||
}
|
||
}
|
||
|
||
export default function AuctionsPage() {
|
||
const { isAuthenticated, checkAuth, isLoading: authLoading } = useStore()
|
||
|
||
const [allAuctions, setAllAuctions] = useState<Auction[]>([])
|
||
const [endingSoon, setEndingSoon] = useState<Auction[]>([])
|
||
const [hotAuctions, setHotAuctions] = useState<Auction[]>([])
|
||
const [opportunities, setOpportunities] = useState<Opportunity[]>([])
|
||
const [loading, setLoading] = useState(true)
|
||
const [refreshing, setRefreshing] = useState(false)
|
||
const [activeTab, setActiveTab] = useState<TabType>('all')
|
||
const [sortBy, setSortBy] = useState<SortField>('ending')
|
||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc')
|
||
|
||
// Filters
|
||
const [searchQuery, setSearchQuery] = useState('')
|
||
const [selectedPlatform, setSelectedPlatform] = useState('All')
|
||
const [maxBid, setMaxBid] = useState<string>('')
|
||
|
||
useEffect(() => {
|
||
checkAuth()
|
||
loadData()
|
||
}, [checkAuth])
|
||
|
||
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
|
||
}
|
||
|
||
const filteredAuctions = getCurrentAuctions().filter(auction => {
|
||
if (searchQuery && !auction.domain.toLowerCase().includes(searchQuery.toLowerCase())) {
|
||
return false
|
||
}
|
||
if (selectedPlatform !== 'All' && auction.platform !== selectedPlatform) {
|
||
return false
|
||
}
|
||
if (maxBid && auction.current_bid > parseFloat(maxBid)) {
|
||
return false
|
||
}
|
||
return true
|
||
})
|
||
|
||
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)
|
||
default:
|
||
return 0
|
||
}
|
||
})
|
||
|
||
const getTimeColor = (timeRemaining: string) => {
|
||
if (timeRemaining.includes('m') && !timeRemaining.includes('h') && !timeRemaining.includes('d')) {
|
||
return 'text-danger'
|
||
}
|
||
if (timeRemaining.includes('h') && parseInt(timeRemaining) < 2) {
|
||
return 'text-warning'
|
||
}
|
||
return 'text-foreground-muted'
|
||
}
|
||
|
||
const handleSort = (field: SortField) => {
|
||
if (sortBy === field) {
|
||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc')
|
||
} else {
|
||
setSortBy(field)
|
||
setSortDirection('asc')
|
||
}
|
||
}
|
||
|
||
if (authLoading) {
|
||
return (
|
||
<div className="min-h-screen flex items-center justify-center bg-background">
|
||
<div className="w-5 h-5 border-2 border-accent border-t-transparent rounded-full animate-spin" />
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-background relative overflow-hidden">
|
||
{/* Background Effects - matching landing page */}
|
||
<div className="fixed inset-0 pointer-events-none">
|
||
<div className="absolute top-[-20%] left-1/2 -translate-x-1/2 w-[1200px] h-[800px] bg-accent/[0.03] rounded-full blur-[120px]" />
|
||
<div className="absolute bottom-[-10%] right-[-10%] w-[600px] h-[600px] bg-accent/[0.02] rounded-full blur-[100px]" />
|
||
<div
|
||
className="absolute inset-0 opacity-[0.015]"
|
||
style={{
|
||
backgroundImage: `linear-gradient(rgba(255,255,255,.1) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.1) 1px, transparent 1px)`,
|
||
backgroundSize: '64px 64px',
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
<Header />
|
||
|
||
<main className="relative pt-32 sm:pt-40 pb-20 sm:pb-28 px-4 sm:px-6 flex-1">
|
||
<div className="max-w-7xl mx-auto">
|
||
{/* Header */}
|
||
<div className="text-center mb-16 sm:mb-20 animate-fade-in">
|
||
<span className="text-sm font-semibold text-accent uppercase tracking-wider">Auction Aggregator</span>
|
||
<h1 className="mt-4 font-display text-[2.5rem] sm:text-[3.5rem] md:text-[4.5rem] lg:text-[5rem] leading-[0.95] tracking-[-0.03em] text-foreground">
|
||
Curated Opportunities
|
||
</h1>
|
||
<p className="mt-5 text-lg sm:text-xl text-foreground-muted max-w-2xl mx-auto">
|
||
Real-time from GoDaddy, Sedo, NameJet & DropCatch. Find opportunities.
|
||
</p>
|
||
</div>
|
||
|
||
{/* Login Banner for non-authenticated users */}
|
||
{!isAuthenticated && (
|
||
<div className="mb-8 p-5 bg-accent-muted border border-accent/20 rounded-xl flex flex-col sm:flex-row items-center justify-between gap-4 animate-fade-in">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-10 h-10 bg-accent/20 rounded-xl flex items-center justify-center">
|
||
<Target className="w-5 h-5 text-accent" />
|
||
</div>
|
||
<div>
|
||
<p className="text-body-sm font-medium text-foreground">Unlock Smart Opportunities</p>
|
||
<p className="text-ui-sm text-foreground-muted">
|
||
Sign in for algorithmic deal-finding and alerts.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<Link
|
||
href="/register"
|
||
className="shrink-0 px-5 py-2.5 bg-accent text-background text-ui font-medium rounded-lg
|
||
hover:bg-accent-hover transition-all duration-300"
|
||
>
|
||
Hunt Free
|
||
</Link>
|
||
</div>
|
||
)}
|
||
|
||
{/* Tabs - flex-wrap to avoid horizontal scroll */}
|
||
<div className="mb-6 animate-slide-up">
|
||
<div className="flex flex-wrap items-center gap-2 mb-6">
|
||
<button
|
||
onClick={() => setActiveTab('all')}
|
||
title="View all active auctions from all platforms"
|
||
className={clsx(
|
||
"flex items-center gap-2 px-4 py-2.5 text-ui-sm font-medium rounded-lg transition-all",
|
||
activeTab === 'all'
|
||
? "bg-foreground text-background"
|
||
: "bg-background-secondary/50 text-foreground-muted hover:text-foreground hover:bg-background-secondary"
|
||
)}
|
||
>
|
||
<Gavel className="w-4 h-4" />
|
||
All
|
||
<span className={clsx(
|
||
"text-ui-xs px-1.5 py-0.5 rounded",
|
||
activeTab === 'all' ? "bg-background/20" : "bg-foreground/10"
|
||
)}>{allAuctions.length}</span>
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab('ending')}
|
||
title="Auctions ending in the next 24 hours - best for sniping"
|
||
className={clsx(
|
||
"flex items-center gap-2 px-4 py-2.5 text-ui-sm font-medium rounded-lg transition-all",
|
||
activeTab === 'ending'
|
||
? "bg-warning text-background"
|
||
: "bg-background-secondary/50 text-foreground-muted hover:text-foreground hover:bg-background-secondary"
|
||
)}
|
||
>
|
||
<Timer className="w-4 h-4" />
|
||
Ending Soon
|
||
<span className={clsx(
|
||
"text-ui-xs px-1.5 py-0.5 rounded",
|
||
activeTab === 'ending' ? "bg-background/20" : "bg-foreground/10"
|
||
)}>{endingSoon.length}</span>
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab('hot')}
|
||
title="Auctions with 20+ bids - high demand, proven interest"
|
||
className={clsx(
|
||
"flex items-center gap-2 px-4 py-2.5 text-ui-sm font-medium rounded-lg transition-all",
|
||
activeTab === 'hot'
|
||
? "bg-accent text-background"
|
||
: "bg-background-secondary/50 text-foreground-muted hover:text-foreground hover:bg-background-secondary"
|
||
)}
|
||
>
|
||
<Flame className="w-4 h-4" />
|
||
Hot
|
||
<span className={clsx(
|
||
"text-ui-xs px-1.5 py-0.5 rounded",
|
||
activeTab === 'hot' ? "bg-background/20" : "bg-foreground/10"
|
||
)}>{hotAuctions.length}</span>
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab('opportunities')}
|
||
title="Smart algorithm: Time urgency × Competition × Price = Score"
|
||
className={clsx(
|
||
"flex items-center gap-2 px-4 py-2.5 text-ui-sm font-medium rounded-lg transition-all",
|
||
activeTab === 'opportunities'
|
||
? "bg-accent text-background"
|
||
: "bg-background-secondary/50 text-foreground-muted hover:text-foreground hover:bg-background-secondary"
|
||
)}
|
||
>
|
||
<Target className="w-4 h-4" />
|
||
Opportunities
|
||
{!isAuthenticated && <Lock className="w-3 h-3 ml-1" />}
|
||
{isAuthenticated && (
|
||
<span className={clsx(
|
||
"text-ui-xs px-1.5 py-0.5 rounded",
|
||
activeTab === 'opportunities' ? "bg-background/20" : "bg-foreground/10"
|
||
)}>{opportunities.length}</span>
|
||
)}
|
||
</button>
|
||
|
||
<button
|
||
onClick={handleRefresh}
|
||
disabled={refreshing}
|
||
title="Refresh auction data from all platforms"
|
||
className="ml-auto flex items-center gap-2 px-4 py-2.5 text-ui-sm text-foreground-muted hover:text-foreground hover:bg-background-secondary/50 rounded-lg transition-all disabled:opacity-50"
|
||
>
|
||
<RefreshCw className={clsx("w-4 h-4", refreshing && "animate-spin")} />
|
||
Refresh
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Search & Filters */}
|
||
<div className="mb-6 animate-slide-up">
|
||
<div className="flex flex-wrap gap-3">
|
||
<div className="relative flex-1 min-w-[200px] max-w-md">
|
||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-foreground-subtle" />
|
||
<input
|
||
type="text"
|
||
placeholder="Search domains..."
|
||
value={searchQuery}
|
||
onChange={(e) => setSearchQuery(e.target.value)}
|
||
className="w-full pl-12 pr-10 py-3 bg-background-secondary/50 border border-border rounded-xl
|
||
text-body text-foreground placeholder:text-foreground-subtle
|
||
focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent
|
||
transition-all duration-300"
|
||
/>
|
||
{searchQuery && (
|
||
<button
|
||
onClick={() => setSearchQuery('')}
|
||
className="absolute right-4 top-1/2 -translate-y-1/2 p-1 text-foreground-subtle hover:text-foreground transition-colors"
|
||
>
|
||
<X className="w-4 h-4" />
|
||
</button>
|
||
)}
|
||
</div>
|
||
<select
|
||
value={selectedPlatform}
|
||
onChange={(e) => setSelectedPlatform(e.target.value)}
|
||
title="Filter by auction platform"
|
||
className="px-4 py-3 bg-background-secondary/50 border border-border rounded-xl
|
||
text-body text-foreground focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent cursor-pointer transition-all"
|
||
>
|
||
{PLATFORMS.map(p => (
|
||
<option key={p.id} value={p.id}>{p.name}</option>
|
||
))}
|
||
</select>
|
||
<div className="relative">
|
||
<DollarSign className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-foreground-subtle" />
|
||
<input
|
||
type="number"
|
||
placeholder="Max bid"
|
||
title="Filter auctions under this bid amount"
|
||
value={maxBid}
|
||
onChange={(e) => setMaxBid(e.target.value)}
|
||
className="w-32 pl-11 pr-4 py-3 bg-background-secondary/50 border border-border rounded-xl
|
||
text-body text-foreground placeholder:text-foreground-subtle
|
||
focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent transition-all"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Content */}
|
||
{loading ? (
|
||
<div className="flex items-center justify-center py-20">
|
||
<div className="w-6 h-6 border-2 border-accent border-t-transparent rounded-full animate-spin" />
|
||
</div>
|
||
) : activeTab === 'opportunities' && !isAuthenticated ? (
|
||
<div className="text-center py-20 border border-dashed border-border rounded-2xl bg-background-secondary/20">
|
||
<div className="w-14 h-14 bg-accent/10 rounded-2xl flex items-center justify-center mx-auto mb-5">
|
||
<Target className="w-7 h-7 text-accent" />
|
||
</div>
|
||
<h3 className="text-body-lg font-medium text-foreground mb-2">Unlock Smart Opportunities</h3>
|
||
<p className="text-body-sm text-foreground-muted max-w-md mx-auto mb-6">
|
||
Our algorithm analyzes ending times, bid activity, and price points to find the best opportunities.
|
||
</p>
|
||
<Link
|
||
href="/register"
|
||
className="inline-flex items-center gap-2 px-6 py-3 bg-accent text-background text-ui font-medium rounded-xl
|
||
hover:bg-accent-hover transition-all shadow-lg shadow-accent/20"
|
||
>
|
||
Get Started Free
|
||
<ArrowUpRight className="w-4 h-4" />
|
||
</Link>
|
||
</div>
|
||
) : (
|
||
/* Table - using proper <table> like TLD Prices */
|
||
<div className="bg-background-secondary/30 border border-border rounded-xl overflow-hidden animate-slide-up">
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full">
|
||
<thead>
|
||
<tr className="bg-background-secondary border-b border-border">
|
||
<th className="text-left px-4 sm:px-6 py-4">
|
||
<button
|
||
onClick={() => handleSort('ending')}
|
||
title="Sort by ending time"
|
||
className="flex items-center gap-2 text-ui-sm text-foreground-subtle font-medium hover:text-foreground transition-colors"
|
||
>
|
||
Domain
|
||
<SortIcon field="ending" currentField={sortBy} direction={sortDirection} />
|
||
</button>
|
||
</th>
|
||
<th className="text-left px-4 sm:px-6 py-4 hidden lg:table-cell">
|
||
<span className="text-ui-sm text-foreground-subtle font-medium">Platform</span>
|
||
</th>
|
||
<th className="text-right px-4 sm:px-6 py-4">
|
||
<button
|
||
onClick={() => handleSort('bid_asc')}
|
||
title="Current highest bid in USD"
|
||
className="flex items-center gap-2 ml-auto text-ui-sm text-foreground-subtle font-medium hover:text-foreground transition-colors"
|
||
>
|
||
Bid
|
||
<SortIcon field="bid_asc" currentField={sortBy} direction={sortDirection} />
|
||
</button>
|
||
</th>
|
||
<th className="text-right px-4 sm:px-6 py-4 hidden sm:table-cell">
|
||
<button
|
||
onClick={() => handleSort('bids')}
|
||
title="Number of bids placed"
|
||
className="flex items-center gap-2 ml-auto text-ui-sm text-foreground-subtle font-medium hover:text-foreground transition-colors"
|
||
>
|
||
Bids
|
||
<SortIcon field="bids" currentField={sortBy} direction={sortDirection} />
|
||
</button>
|
||
</th>
|
||
<th className="text-right px-4 sm:px-6 py-4 hidden md:table-cell">
|
||
<span className="text-ui-sm text-foreground-subtle font-medium" title="Time remaining">Time Left</span>
|
||
</th>
|
||
{activeTab === 'opportunities' && (
|
||
<th className="text-center px-4 sm:px-6 py-4">
|
||
<span className="text-ui-sm text-foreground-subtle font-medium" title="Opportunity score">Score</span>
|
||
</th>
|
||
)}
|
||
<th className="px-4 sm:px-6 py-4"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-border">
|
||
{sortedAuctions.length === 0 ? (
|
||
<tr>
|
||
<td colSpan={activeTab === 'opportunities' ? 7 : 6} className="px-6 py-12 text-center text-foreground-muted">
|
||
{activeTab === 'opportunities'
|
||
? 'No opportunities right now — check back later!'
|
||
: searchQuery
|
||
? `No auctions found matching "${searchQuery}"`
|
||
: 'No auctions found'}
|
||
</td>
|
||
</tr>
|
||
) : (
|
||
sortedAuctions.map((auction, idx) => {
|
||
const oppData = getOpportunityData(auction.domain)
|
||
return (
|
||
<tr
|
||
key={`${auction.domain}-${idx}`}
|
||
className="hover:bg-background-secondary/50 transition-colors group"
|
||
>
|
||
{/* Domain */}
|
||
<td className="px-4 sm:px-6 py-4">
|
||
<div className="flex flex-col gap-1">
|
||
<a
|
||
href={auction.affiliate_url}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
title={`Go to ${auction.platform} to bid on ${auction.domain}`}
|
||
className="font-mono text-body-sm sm:text-body font-medium text-foreground hover:text-accent transition-colors"
|
||
>
|
||
{auction.domain}
|
||
</a>
|
||
<div className="flex items-center gap-2 text-body-xs text-foreground-subtle lg:hidden">
|
||
<span className={clsx("text-ui-xs px-1.5 py-0.5 rounded", getPlatformBadgeClass(auction.platform))}>
|
||
{auction.platform}
|
||
</span>
|
||
{auction.age_years && (
|
||
<span title={`Domain age: ${auction.age_years} years`}>{auction.age_years}y</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</td>
|
||
|
||
{/* Platform */}
|
||
<td className="px-4 sm:px-6 py-4 hidden lg:table-cell">
|
||
<div className="flex flex-col gap-1">
|
||
<span
|
||
className={clsx("text-ui-sm px-2 py-0.5 rounded-full w-fit", getPlatformBadgeClass(auction.platform))}
|
||
title={`${auction.platform} - Click Bid to go to auction`}
|
||
>
|
||
{auction.platform}
|
||
</span>
|
||
{auction.age_years && (
|
||
<span className="text-body-xs text-foreground-subtle" title={`Domain age: ${auction.age_years} years`}>
|
||
<Clock className="w-3 h-3 inline mr-1" />
|
||
{auction.age_years}y
|
||
</span>
|
||
)}
|
||
</div>
|
||
</td>
|
||
|
||
{/* Current Bid */}
|
||
<td className="px-4 sm:px-6 py-4 text-right">
|
||
<span
|
||
className="text-body-sm font-medium text-foreground"
|
||
title={`Current highest bid: ${formatCurrency(auction.current_bid)}`}
|
||
>
|
||
{formatCurrency(auction.current_bid)}
|
||
</span>
|
||
{auction.buy_now_price && (
|
||
<p className="text-ui-xs text-accent" title={`Buy Now for ${formatCurrency(auction.buy_now_price)}`}>
|
||
Buy: {formatCurrency(auction.buy_now_price)}
|
||
</p>
|
||
)}
|
||
</td>
|
||
|
||
{/* Bids */}
|
||
<td className="px-4 sm:px-6 py-4 text-right hidden sm:table-cell">
|
||
<span
|
||
className={clsx(
|
||
"text-body-sm font-medium inline-flex items-center gap-1",
|
||
auction.num_bids >= 20 ? "text-accent" :
|
||
auction.num_bids >= 10 ? "text-warning" :
|
||
"text-foreground-muted"
|
||
)}
|
||
title={`${auction.num_bids} bids - ${auction.num_bids >= 20 ? 'High competition!' : auction.num_bids >= 10 ? 'Moderate interest' : 'Low competition'}`}
|
||
>
|
||
{auction.num_bids}
|
||
{auction.num_bids >= 20 && <Flame className="w-3 h-3" />}
|
||
</span>
|
||
</td>
|
||
|
||
{/* Time Left */}
|
||
<td className="px-4 sm:px-6 py-4 text-right hidden md:table-cell">
|
||
<span
|
||
className={clsx("text-body-sm font-medium", getTimeColor(auction.time_remaining))}
|
||
title={`Auction ends: ${new Date(auction.end_time).toLocaleString()}`}
|
||
>
|
||
{auction.time_remaining}
|
||
</span>
|
||
</td>
|
||
|
||
{/* Score (opportunities only) */}
|
||
{activeTab === 'opportunities' && oppData && (
|
||
<td className="px-4 sm:px-6 py-4 text-center">
|
||
<span
|
||
className="inline-flex items-center justify-center w-9 h-9 bg-accent/10 text-accent font-bold rounded-lg text-body-sm"
|
||
title={`Score: ${oppData.opportunity_score}${oppData.reasoning ? ' - ' + oppData.reasoning : ''}`}
|
||
>
|
||
{oppData.opportunity_score}
|
||
</span>
|
||
</td>
|
||
)}
|
||
|
||
{/* Action */}
|
||
<td className="px-4 sm:px-6 py-4 text-right">
|
||
<a
|
||
href={auction.affiliate_url}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
title={`Open ${auction.platform} to place your bid`}
|
||
className="inline-flex items-center gap-1.5 px-4 py-2 bg-foreground text-background text-ui-sm font-medium rounded-lg
|
||
hover:bg-foreground/90 transition-all opacity-70 group-hover:opacity-100"
|
||
>
|
||
Bid
|
||
<ExternalLink className="w-3.5 h-3.5" />
|
||
</a>
|
||
</td>
|
||
</tr>
|
||
)
|
||
})
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Info Footer */}
|
||
<div className="mt-10 p-5 bg-background-secondary/30 border border-border rounded-xl animate-slide-up">
|
||
<div className="flex items-start gap-4">
|
||
<div className="w-10 h-10 bg-foreground/5 rounded-xl flex items-center justify-center shrink-0">
|
||
<Info className="w-5 h-5 text-foreground-muted" />
|
||
</div>
|
||
<div>
|
||
<h4 className="text-body font-medium text-foreground mb-1.5">
|
||
{TAB_DESCRIPTIONS[activeTab].title}
|
||
</h4>
|
||
<p className="text-body-sm text-foreground-subtle leading-relaxed mb-3">
|
||
{TAB_DESCRIPTIONS[activeTab].description}
|
||
</p>
|
||
<p className="text-body-sm text-foreground-subtle leading-relaxed">
|
||
<span className="text-foreground-muted font-medium">Sources:</span> GoDaddy, Sedo, NameJet, DropCatch, ExpiredDomains.
|
||
Click "Bid" to go to the auction — we don't handle transactions.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<Footer />
|
||
</div>
|
||
)
|
||
}
|