style: Align public auctions page with TLD Intel styling
- Centered hero header matching TLD pricing page - Large display title with auction count - Consistent login banner design - Hot auctions preview section (like Trending TLDs) - Native table with sortable columns (no PremiumTable) - Consistent filters and tab buttons styling - Loading skeleton and empty states
This commit is contained in:
@ -5,7 +5,7 @@ import { useStore } from '@/lib/store'
|
|||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
import { Header } from '@/components/Header'
|
import { Header } from '@/components/Header'
|
||||||
import { Footer } from '@/components/Footer'
|
import { Footer } from '@/components/Footer'
|
||||||
import { PremiumTable, Badge, PlatformBadge, StatCard } from '@/components/PremiumTable'
|
import { PremiumTable, PlatformBadge } from '@/components/PremiumTable'
|
||||||
import {
|
import {
|
||||||
Clock,
|
Clock,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
@ -14,12 +14,12 @@ import {
|
|||||||
Timer,
|
Timer,
|
||||||
Gavel,
|
Gavel,
|
||||||
DollarSign,
|
DollarSign,
|
||||||
RefreshCw,
|
|
||||||
Target,
|
|
||||||
X,
|
X,
|
||||||
ArrowRight,
|
|
||||||
Lock,
|
Lock,
|
||||||
Sparkles,
|
TrendingUp,
|
||||||
|
ChevronUp,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronsUpDown,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
@ -42,7 +42,8 @@ interface Auction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TabType = 'all' | 'ending' | 'hot'
|
type TabType = 'all' | 'ending' | 'hot'
|
||||||
type SortField = 'ending' | 'bid_asc' | 'bid_desc' | 'bids'
|
type SortField = 'domain' | 'ending' | 'bid' | 'bids'
|
||||||
|
type SortDirection = 'asc' | 'desc'
|
||||||
|
|
||||||
const PLATFORMS = [
|
const PLATFORMS = [
|
||||||
{ id: 'All', name: 'All Sources' },
|
{ id: 'All', name: 'All Sources' },
|
||||||
@ -52,17 +53,25 @@ const PLATFORMS = [
|
|||||||
{ id: 'DropCatch', name: 'DropCatch' },
|
{ id: 'DropCatch', name: 'DropCatch' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
function SortIcon({ field, currentField, direction }: { field: SortField, currentField: SortField, direction: SortDirection }) {
|
||||||
|
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" />
|
||||||
|
}
|
||||||
|
|
||||||
export default function AuctionsPage() {
|
export default function AuctionsPage() {
|
||||||
const { isAuthenticated, checkAuth } = useStore()
|
const { isAuthenticated, checkAuth, isLoading: authLoading } = useStore()
|
||||||
|
|
||||||
const [allAuctions, setAllAuctions] = useState<Auction[]>([])
|
const [allAuctions, setAllAuctions] = useState<Auction[]>([])
|
||||||
const [endingSoon, setEndingSoon] = useState<Auction[]>([])
|
const [endingSoon, setEndingSoon] = useState<Auction[]>([])
|
||||||
const [hotAuctions, setHotAuctions] = useState<Auction[]>([])
|
const [hotAuctions, setHotAuctions] = useState<Auction[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
|
||||||
const [activeTab, setActiveTab] = useState<TabType>('all')
|
const [activeTab, setActiveTab] = useState<TabType>('all')
|
||||||
const [sortBy, setSortBy] = useState<SortField>('ending')
|
const [sortField, setSortField] = useState<SortField>('ending')
|
||||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc')
|
const [sortDirection, setSortDirection] = useState<SortDirection>('asc')
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
const [selectedPlatform, setSelectedPlatform] = useState('All')
|
const [selectedPlatform, setSelectedPlatform] = useState('All')
|
||||||
@ -91,12 +100,6 @@ export default function AuctionsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
|
||||||
setRefreshing(true)
|
|
||||||
await loadAuctions()
|
|
||||||
setRefreshing(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getCurrentAuctions = (): Auction[] => {
|
const getCurrentAuctions = (): Auction[] => {
|
||||||
switch (activeTab) {
|
switch (activeTab) {
|
||||||
case 'ending': return endingSoon
|
case 'ending': return endingSoon
|
||||||
@ -118,28 +121,29 @@ export default function AuctionsPage() {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleSort = (field: SortField) => {
|
||||||
|
if (sortField === field) {
|
||||||
|
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc')
|
||||||
|
} else {
|
||||||
|
setSortField(field)
|
||||||
|
setSortDirection('asc')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sortedAuctions = [...filteredAuctions].sort((a, b) => {
|
const sortedAuctions = [...filteredAuctions].sort((a, b) => {
|
||||||
switch (sortBy) {
|
const modifier = sortDirection === 'asc' ? 1 : -1
|
||||||
case 'bid_asc':
|
switch (sortField) {
|
||||||
return sortDirection === 'asc' ? a.current_bid - b.current_bid : b.current_bid - a.current_bid
|
case 'domain':
|
||||||
case 'bid_desc':
|
return a.domain.localeCompare(b.domain) * modifier
|
||||||
return sortDirection === 'asc' ? b.current_bid - a.current_bid : a.current_bid - b.current_bid
|
case 'bid':
|
||||||
|
return (a.current_bid - b.current_bid) * modifier
|
||||||
case 'bids':
|
case 'bids':
|
||||||
return sortDirection === 'asc' ? a.num_bids - b.num_bids : b.num_bids - a.num_bids
|
return (a.num_bids - b.num_bids) * modifier
|
||||||
default:
|
default:
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleSort = (field: SortField) => {
|
|
||||||
if (sortBy === field) {
|
|
||||||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc')
|
|
||||||
} else {
|
|
||||||
setSortBy(field)
|
|
||||||
setSortDirection('asc')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatCurrency = (amount: number, currency = 'USD') => {
|
const formatCurrency = (amount: number, currency = 'USD') => {
|
||||||
return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount)
|
return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount)
|
||||||
}
|
}
|
||||||
@ -150,16 +154,20 @@ export default function AuctionsPage() {
|
|||||||
return 'text-foreground-muted'
|
return 'text-foreground-muted'
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSubtitle = () => {
|
// Hot auctions preview for the hero section
|
||||||
if (loading) return 'Loading live auctions...'
|
const hotPreview = hotAuctions.slice(0, 4)
|
||||||
const total = allAuctions.length
|
|
||||||
if (total === 0) return 'No active auctions found'
|
if (authLoading) {
|
||||||
return `${total.toLocaleString()} live auctions across 4 platforms`
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
|
<div className="w-5 h-5 border-2 border-accent border-t-transparent rounded-full animate-spin" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background relative overflow-hidden">
|
<div className="min-h-screen bg-background relative overflow-hidden">
|
||||||
{/* Background Effects */}
|
{/* Background Effects - matching landing page */}
|
||||||
<div className="fixed inset-0 pointer-events-none">
|
<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 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 bottom-[-10%] right-[-10%] w-[600px] h-[600px] bg-accent/[0.02] rounded-full blur-[100px]" />
|
||||||
@ -176,108 +184,103 @@ export default function AuctionsPage() {
|
|||||||
|
|
||||||
<main className="relative pt-32 sm:pt-40 pb-20 sm:pb-28 px-4 sm:px-6 flex-1">
|
<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">
|
<div className="max-w-7xl mx-auto">
|
||||||
{/* Header */}
|
{/* Hero Header - centered like TLD pricing */}
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8 animate-fade-in">
|
<div className="text-center mb-16 sm:mb-20 animate-fade-in">
|
||||||
<div>
|
|
||||||
<span className="text-sm font-semibold text-accent uppercase tracking-wider">Live Market</span>
|
<span className="text-sm font-semibold text-accent uppercase tracking-wider">Live Market</span>
|
||||||
<h1 className="mt-2 font-display text-3xl sm:text-4xl md:text-5xl tracking-tight text-foreground">
|
<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">
|
||||||
Domain Auctions
|
{allAuctions.length}+ Auctions. Real-Time.
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-2 text-foreground-muted">{getSubtitle()}</p>
|
<p className="mt-5 text-lg sm:text-xl text-foreground-muted max-w-2xl mx-auto">
|
||||||
</div>
|
Track domain auctions across GoDaddy, Sedo, NameJet & DropCatch.
|
||||||
<button
|
</p>
|
||||||
onClick={handleRefresh}
|
|
||||||
disabled={refreshing}
|
|
||||||
className="flex items-center gap-2 px-4 py-2.5 text-sm text-foreground-muted hover:text-foreground
|
|
||||||
bg-foreground/5 hover:bg-foreground/10 border border-border/50 rounded-xl
|
|
||||||
transition-all disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<RefreshCw className={clsx("w-4 h-4", refreshing && "animate-spin")} />
|
|
||||||
Refresh
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats */}
|
{/* Login Banner for non-authenticated users */}
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8 animate-slide-up">
|
|
||||||
<StatCard title="All Auctions" value={allAuctions.length} icon={Gavel} />
|
|
||||||
<StatCard title="Ending Soon" value={endingSoon.length} icon={Timer} accent />
|
|
||||||
<StatCard title="Hot Auctions" value={hotAuctions.length} subtitle="20+ bids" icon={Flame} />
|
|
||||||
<StatCard
|
|
||||||
title="Opportunities"
|
|
||||||
value={isAuthenticated ? '—' : '—'}
|
|
||||||
subtitle="Login to unlock"
|
|
||||||
icon={Target}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* CTA Banner for non-authenticated users */}
|
|
||||||
{!isAuthenticated && (
|
{!isAuthenticated && (
|
||||||
<div className="mb-8 p-5 bg-gradient-to-r from-accent/10 via-accent/5 to-transparent border border-accent/20 rounded-2xl animate-fade-in">
|
<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 flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-4">
|
<div className="w-10 h-10 bg-accent/20 rounded-xl flex items-center justify-center">
|
||||||
<div className="w-12 h-12 bg-accent/20 rounded-xl flex items-center justify-center">
|
<Lock className="w-5 h-5 text-accent" />
|
||||||
<Sparkles className="w-6 h-6 text-accent" />
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold text-foreground">Unlock Smart Opportunities</h3>
|
<p className="text-body-sm font-medium text-foreground">Unlock Smart Opportunities</p>
|
||||||
<p className="text-sm text-foreground-muted">Get AI-powered auction analysis and personalized recommendations</p>
|
<p className="text-ui-sm text-foreground-muted">
|
||||||
|
Sign in for AI-powered analysis and personalized recommendations.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href="/login"
|
href="/register"
|
||||||
className="flex items-center justify-center gap-2 px-6 py-3 bg-gradient-to-r from-accent to-accent/80
|
className="shrink-0 px-5 py-2.5 bg-accent text-background text-ui font-medium rounded-lg
|
||||||
text-background rounded-xl font-medium hover:shadow-[0_0_20px_-5px_rgba(16,185,129,0.4)] transition-all"
|
hover:bg-accent-hover transition-all duration-300"
|
||||||
>
|
>
|
||||||
Sign In <ArrowRight className="w-4 h-4" />
|
Hunt Free
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Hot Auctions Preview */}
|
||||||
<div className="flex flex-wrap items-center gap-2 p-1.5 bg-background-secondary/30 border border-border/30 rounded-2xl w-fit mb-6 animate-slide-up">
|
{hotPreview.length > 0 && (
|
||||||
{[
|
<div className="mb-12 sm:mb-16 animate-slide-up">
|
||||||
{ id: 'all' as const, label: 'All', icon: Gavel, count: allAuctions.length },
|
<h2 className="text-body-lg sm:text-heading-sm font-medium text-foreground mb-4 sm:mb-6 flex items-center gap-2">
|
||||||
{ id: 'ending' as const, label: 'Ending Soon', icon: Timer, count: endingSoon.length, color: 'warning' },
|
<Flame className="w-5 h-5 text-accent" />
|
||||||
{ id: 'hot' as const, label: 'Hot', icon: Flame, count: hotAuctions.length },
|
Hot Right Now
|
||||||
].map((tab) => (
|
</h2>
|
||||||
<button
|
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
|
||||||
key={tab.id}
|
{hotPreview.map((auction) => (
|
||||||
onClick={() => setActiveTab(tab.id)}
|
<a
|
||||||
className={clsx(
|
key={`${auction.domain}-${auction.platform}`}
|
||||||
"flex items-center gap-2 px-4 py-2.5 text-sm font-medium rounded-xl transition-all",
|
href={auction.affiliate_url}
|
||||||
activeTab === tab.id
|
target="_blank"
|
||||||
? tab.color === 'warning'
|
rel="noopener noreferrer"
|
||||||
? "bg-amber-500 text-background"
|
className="p-4 sm:p-5 bg-background-secondary/50 border border-border rounded-xl hover:border-border-hover hover:bg-background-secondary transition-all duration-300 text-left group"
|
||||||
: "bg-gradient-to-r from-accent to-accent/80 text-background shadow-lg shadow-accent/20"
|
|
||||||
: "text-foreground-muted hover:text-foreground hover:bg-foreground/5"
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<tab.icon className="w-4 h-4" />
|
<div className="flex items-center justify-between mb-3">
|
||||||
<span className="hidden sm:inline">{tab.label}</span>
|
<span className="font-mono text-body-lg sm:text-heading-sm text-foreground group-hover:text-accent transition-colors">
|
||||||
<span className={clsx(
|
{auction.domain}
|
||||||
"text-xs px-1.5 py-0.5 rounded tabular-nums",
|
</span>
|
||||||
activeTab === tab.id ? "bg-background/20" : "bg-foreground/10"
|
<span className="text-ui-sm font-medium px-2 py-0.5 rounded-full text-accent bg-accent-muted flex items-center gap-1">
|
||||||
)}>{tab.count}</span>
|
<Flame className="w-3 h-3" />
|
||||||
</button>
|
{auction.num_bids}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-body-sm text-foreground-muted">
|
||||||
|
{formatCurrency(auction.current_bid)}
|
||||||
|
</span>
|
||||||
|
<span className={clsx("text-body-sm", getTimeColor(auction.time_remaining))}>
|
||||||
|
{auction.time_remaining}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2">
|
||||||
|
<PlatformBadge platform={auction.platform} />
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Search & Filters */}
|
||||||
<div className="flex flex-wrap gap-3 mb-6 animate-slide-up">
|
<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">
|
<div className="relative flex-1 min-w-[200px] max-w-md">
|
||||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-foreground-subtle" />
|
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-foreground-subtle" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search domains..."
|
placeholder="Search domains..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="w-full pl-11 pr-10 py-3 bg-background-secondary/50 border border-border/50 rounded-xl
|
className="w-full pl-12 pr-10 py-3 bg-background-secondary/50 border border-border rounded-xl
|
||||||
text-sm text-foreground placeholder:text-foreground-subtle
|
text-body text-foreground placeholder:text-foreground-subtle
|
||||||
focus:outline-none focus:border-accent/50 transition-all"
|
focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent
|
||||||
|
transition-all duration-300"
|
||||||
/>
|
/>
|
||||||
{searchQuery && (
|
{searchQuery && (
|
||||||
<button onClick={() => setSearchQuery('')} className="absolute right-4 top-1/2 -translate-y-1/2 text-foreground-subtle hover:text-foreground">
|
<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" />
|
<X className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@ -285,8 +288,8 @@ export default function AuctionsPage() {
|
|||||||
<select
|
<select
|
||||||
value={selectedPlatform}
|
value={selectedPlatform}
|
||||||
onChange={(e) => setSelectedPlatform(e.target.value)}
|
onChange={(e) => setSelectedPlatform(e.target.value)}
|
||||||
className="px-4 py-3 bg-background-secondary/50 border border-border/50 rounded-xl
|
className="px-4 py-3 bg-background-secondary/50 border border-border rounded-xl
|
||||||
text-sm text-foreground cursor-pointer focus:outline-none focus:border-accent/50"
|
text-body text-foreground cursor-pointer focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent"
|
||||||
>
|
>
|
||||||
{PLATFORMS.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
|
{PLATFORMS.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
|
||||||
</select>
|
</select>
|
||||||
@ -297,124 +300,194 @@ export default function AuctionsPage() {
|
|||||||
placeholder="Max bid"
|
placeholder="Max bid"
|
||||||
value={maxBid}
|
value={maxBid}
|
||||||
onChange={(e) => setMaxBid(e.target.value)}
|
onChange={(e) => setMaxBid(e.target.value)}
|
||||||
className="w-32 pl-10 pr-4 py-3 bg-background-secondary/50 border border-border/50 rounded-xl
|
className="w-32 pl-10 pr-4 py-3 bg-background-secondary/50 border border-border rounded-xl
|
||||||
text-sm text-foreground placeholder:text-foreground-subtle
|
text-body text-foreground placeholder:text-foreground-subtle
|
||||||
focus:outline-none focus:border-accent/50"
|
focus:outline-none focus:ring-2 focus:ring-accent/30 focus:border-accent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="flex flex-wrap gap-2 mb-6 animate-slide-up">
|
||||||
|
{[
|
||||||
|
{ id: 'all' as const, label: 'All Auctions', icon: Gavel, count: allAuctions.length },
|
||||||
|
{ id: 'ending' as const, label: 'Ending Soon', icon: Timer, count: endingSoon.length },
|
||||||
|
{ id: 'hot' as const, label: 'Hot', icon: Flame, count: hotAuctions.length },
|
||||||
|
].map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
className={clsx(
|
||||||
|
"flex items-center gap-2 px-4 py-2.5 text-ui-sm font-medium rounded-xl transition-all",
|
||||||
|
activeTab === tab.id
|
||||||
|
? "bg-accent text-background"
|
||||||
|
: "bg-background-secondary/50 text-foreground-muted hover:text-foreground hover:bg-background-secondary border border-border"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<tab.icon className="w-4 h-4" />
|
||||||
|
{tab.label}
|
||||||
|
<span className={clsx(
|
||||||
|
"text-xs px-1.5 py-0.5 rounded",
|
||||||
|
activeTab === tab.id ? "bg-background/20" : "bg-foreground/10"
|
||||||
|
)}>{tab.count}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Auctions Table */}
|
{/* Auctions Table */}
|
||||||
<div className="animate-slide-up">
|
<div className="bg-background-secondary/30 border border-border rounded-xl overflow-hidden animate-slide-up">
|
||||||
<PremiumTable
|
<div className="overflow-x-auto">
|
||||||
data={sortedAuctions}
|
<table className="w-full">
|
||||||
keyExtractor={(a) => `${a.domain}-${a.platform}`}
|
<thead>
|
||||||
loading={loading}
|
<tr className="bg-background-secondary border-b border-border">
|
||||||
sortBy={sortBy}
|
<th className="text-left px-4 sm:px-6 py-4">
|
||||||
sortDirection={sortDirection}
|
<button
|
||||||
onSort={(key) => handleSort(key as SortField)}
|
onClick={() => handleSort('domain')}
|
||||||
emptyIcon={<Gavel className="w-12 h-12 text-foreground-subtle" />}
|
className="flex items-center gap-2 text-ui-sm text-foreground-subtle font-medium hover:text-foreground transition-colors"
|
||||||
emptyTitle={searchQuery ? `No auctions matching "${searchQuery}"` : "No auctions found"}
|
>
|
||||||
emptyDescription="Try adjusting your filters or check back later"
|
Domain
|
||||||
columns={[
|
<SortIcon field="domain" currentField={sortField} direction={sortDirection} />
|
||||||
{
|
</button>
|
||||||
key: 'domain',
|
</th>
|
||||||
header: 'Domain',
|
<th className="text-left px-4 sm:px-6 py-4 hidden lg:table-cell">
|
||||||
sortable: true,
|
<span className="text-ui-sm text-foreground-subtle font-medium">Platform</span>
|
||||||
render: (a) => (
|
</th>
|
||||||
|
<th className="text-right px-4 sm:px-6 py-4">
|
||||||
|
<button
|
||||||
|
onClick={() => handleSort('bid')}
|
||||||
|
className="flex items-center gap-2 ml-auto text-ui-sm text-foreground-subtle font-medium hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
Current Bid
|
||||||
|
<SortIcon field="bid" currentField={sortField} direction={sortDirection} />
|
||||||
|
</button>
|
||||||
|
</th>
|
||||||
|
<th className="text-right px-4 sm:px-6 py-4 hidden sm:table-cell">
|
||||||
|
<button
|
||||||
|
onClick={() => handleSort('bids')}
|
||||||
|
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={sortField} direction={sortDirection} />
|
||||||
|
</button>
|
||||||
|
</th>
|
||||||
|
<th className="text-right px-4 sm:px-6 py-4 hidden md:table-cell">
|
||||||
|
<button
|
||||||
|
onClick={() => handleSort('ending')}
|
||||||
|
className="flex items-center gap-2 ml-auto text-ui-sm text-foreground-subtle font-medium hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
Time Left
|
||||||
|
<SortIcon field="ending" currentField={sortField} direction={sortDirection} />
|
||||||
|
</button>
|
||||||
|
</th>
|
||||||
|
<th className="px-4 sm:px-6 py-4"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-border">
|
||||||
|
{loading ? (
|
||||||
|
// Loading skeleton
|
||||||
|
Array.from({ length: 10 }).map((_, idx) => (
|
||||||
|
<tr key={idx} className="animate-pulse">
|
||||||
|
<td className="px-4 sm:px-6 py-4"><div className="h-4 w-32 bg-background-tertiary rounded" /></td>
|
||||||
|
<td className="px-4 sm:px-6 py-4 hidden lg:table-cell"><div className="h-4 w-20 bg-background-tertiary rounded" /></td>
|
||||||
|
<td className="px-4 sm:px-6 py-4"><div className="h-4 w-16 bg-background-tertiary rounded ml-auto" /></td>
|
||||||
|
<td className="px-4 sm:px-6 py-4 hidden sm:table-cell"><div className="h-4 w-12 bg-background-tertiary rounded ml-auto" /></td>
|
||||||
|
<td className="px-4 sm:px-6 py-4 hidden md:table-cell"><div className="h-4 w-16 bg-background-tertiary rounded ml-auto" /></td>
|
||||||
|
<td className="px-4 sm:px-6 py-4"><div className="h-8 w-16 bg-background-tertiary rounded ml-auto" /></td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
) : sortedAuctions.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={6} className="px-6 py-12 text-center text-foreground-muted">
|
||||||
|
{searchQuery ? `No auctions found matching "${searchQuery}"` : 'No auctions found'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
sortedAuctions.map((auction) => (
|
||||||
|
<tr
|
||||||
|
key={`${auction.domain}-${auction.platform}`}
|
||||||
|
className="hover:bg-background-secondary/50 transition-colors group"
|
||||||
|
>
|
||||||
|
<td className="px-4 sm:px-6 py-4">
|
||||||
<div>
|
<div>
|
||||||
<a
|
<a
|
||||||
href={a.affiliate_url}
|
href={auction.affiliate_url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="font-mono font-medium text-foreground hover:text-accent transition-colors"
|
className="font-mono text-body-sm sm:text-body font-medium text-foreground hover:text-accent transition-colors"
|
||||||
>
|
>
|
||||||
{a.domain}
|
{auction.domain}
|
||||||
</a>
|
</a>
|
||||||
<div className="flex items-center gap-2 mt-1 lg:hidden">
|
<div className="flex items-center gap-2 mt-1 lg:hidden">
|
||||||
<PlatformBadge platform={a.platform} />
|
<PlatformBadge platform={auction.platform} />
|
||||||
{a.age_years && <span className="text-xs text-foreground-subtle">{a.age_years}y</span>}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
</td>
|
||||||
},
|
<td className="px-4 sm:px-6 py-4 hidden lg:table-cell">
|
||||||
{
|
|
||||||
key: 'platform',
|
|
||||||
header: 'Platform',
|
|
||||||
hideOnMobile: true,
|
|
||||||
render: (a) => (
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<PlatformBadge platform={a.platform} />
|
<PlatformBadge platform={auction.platform} />
|
||||||
{a.age_years && (
|
{auction.age_years && (
|
||||||
<span className="text-xs text-foreground-subtle flex items-center gap-1">
|
<span className="text-ui-sm text-foreground-subtle flex items-center gap-1">
|
||||||
<Clock className="w-3 h-3" /> {a.age_years}y
|
<Clock className="w-3 h-3" /> {auction.age_years}y
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
),
|
</td>
|
||||||
},
|
<td className="px-4 sm:px-6 py-4 text-right">
|
||||||
{
|
|
||||||
key: 'bid_asc',
|
|
||||||
header: 'Bid',
|
|
||||||
sortable: true,
|
|
||||||
align: 'right',
|
|
||||||
render: (a) => (
|
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium text-foreground tabular-nums">{formatCurrency(a.current_bid)}</span>
|
<span className="text-body-sm font-medium text-foreground">
|
||||||
{a.buy_now_price && (
|
{formatCurrency(auction.current_bid)}
|
||||||
<p className="text-xs text-accent">Buy: {formatCurrency(a.buy_now_price)}</p>
|
</span>
|
||||||
|
{auction.buy_now_price && (
|
||||||
|
<p className="text-ui-sm text-accent">Buy: {formatCurrency(auction.buy_now_price)}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
),
|
</td>
|
||||||
},
|
<td className="px-4 sm:px-6 py-4 text-right hidden sm:table-cell">
|
||||||
{
|
|
||||||
key: 'bids',
|
|
||||||
header: 'Bids',
|
|
||||||
sortable: true,
|
|
||||||
align: 'right',
|
|
||||||
hideOnMobile: true,
|
|
||||||
render: (a) => (
|
|
||||||
<span className={clsx(
|
<span className={clsx(
|
||||||
"font-medium flex items-center justify-end gap-1 tabular-nums",
|
"font-medium flex items-center justify-end gap-1",
|
||||||
a.num_bids >= 20 ? "text-accent" : a.num_bids >= 10 ? "text-amber-400" : "text-foreground-muted"
|
auction.num_bids >= 20 ? "text-accent" : auction.num_bids >= 10 ? "text-amber-400" : "text-foreground-muted"
|
||||||
)}>
|
)}>
|
||||||
{a.num_bids}
|
{auction.num_bids}
|
||||||
{a.num_bids >= 20 && <Flame className="w-3 h-3" />}
|
{auction.num_bids >= 20 && <Flame className="w-3 h-3" />}
|
||||||
</span>
|
</span>
|
||||||
),
|
</td>
|
||||||
},
|
<td className="px-4 sm:px-6 py-4 text-right hidden md:table-cell">
|
||||||
{
|
<span className={clsx("font-medium", getTimeColor(auction.time_remaining))}>
|
||||||
key: 'ending',
|
{auction.time_remaining}
|
||||||
header: 'Time Left',
|
|
||||||
sortable: true,
|
|
||||||
align: 'right',
|
|
||||||
hideOnMobile: true,
|
|
||||||
render: (a) => (
|
|
||||||
<span className={clsx("font-medium tabular-nums", getTimeColor(a.time_remaining))}>
|
|
||||||
{a.time_remaining}
|
|
||||||
</span>
|
</span>
|
||||||
),
|
</td>
|
||||||
},
|
<td className="px-4 sm:px-6 py-4">
|
||||||
{
|
|
||||||
key: 'action',
|
|
||||||
header: '',
|
|
||||||
align: 'right',
|
|
||||||
render: (a) => (
|
|
||||||
<a
|
<a
|
||||||
href={a.affiliate_url}
|
href={auction.affiliate_url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline-flex items-center gap-1.5 px-4 py-2 bg-foreground text-background text-xs font-medium rounded-lg
|
className="inline-flex items-center gap-1 text-ui-sm text-accent hover:text-accent-hover transition-colors opacity-0 group-hover:opacity-100"
|
||||||
hover:bg-foreground/90 transition-all opacity-70 group-hover:opacity-100"
|
|
||||||
>
|
>
|
||||||
Bid <ExternalLink className="w-3 h-3" />
|
Bid
|
||||||
|
<ExternalLink className="w-3 h-3" />
|
||||||
</a>
|
</a>
|
||||||
),
|
</td>
|
||||||
},
|
</tr>
|
||||||
]}
|
))
|
||||||
/>
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
{!loading && (
|
||||||
|
<div className="mt-6 flex justify-center">
|
||||||
|
<p className="text-ui-sm text-foreground-subtle">
|
||||||
|
{searchQuery
|
||||||
|
? `Found ${sortedAuctions.length} auctions matching "${searchQuery}"`
|
||||||
|
: `${allAuctions.length} auctions available across ${PLATFORMS.length - 1} platforms`
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
Reference in New Issue
Block a user