yves.gugger ed250b4e44 refactor: Separate public pages from authenticated Command Center
STRUCTURE:
- Public pages: /auctions, /intelligence (with Header/Footer, no login needed)
- Command Center: /command/* (with Sidebar, login required)
  - /command/dashboard
  - /command/watchlist
  - /command/portfolio
  - /command/auctions
  - /command/intelligence
  - /command/settings

CHANGES:
- Created new /command directory structure
- Public auctions page: shows all auction data, CTA for login
- Public intelligence page: shows TLD data, CTA for login
- Updated Sidebar navigation to point to /command/*
- Updated all internal links (Header, Footer, AdminLayout)
- Updated login redirect to /command/dashboard
- Updated keyboard shortcuts to use /command paths
- Added /command redirect to /command/dashboard
2025-12-10 11:02:27 +01:00

424 lines
17 KiB
TypeScript

'use client'
import { useEffect, useState } from 'react'
import { useStore } from '@/lib/store'
import { api } from '@/lib/api'
import { Header } from '@/components/Header'
import { Footer } from '@/components/Footer'
import { PremiumTable, Badge, PlatformBadge, StatCard } from '@/components/PremiumTable'
import {
Clock,
ExternalLink,
Search,
Flame,
Timer,
Gavel,
DollarSign,
RefreshCw,
Target,
X,
ArrowRight,
Lock,
Sparkles,
} 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
}
type TabType = 'all' | 'ending' | 'hot'
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' },
]
export default function AuctionsPage() {
const { isAuthenticated, checkAuth } = useStore()
const [allAuctions, setAllAuctions] = useState<Auction[]>([])
const [endingSoon, setEndingSoon] = useState<Auction[]>([])
const [hotAuctions, setHotAuctions] = useState<Auction[]>([])
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')
const [searchQuery, setSearchQuery] = useState('')
const [selectedPlatform, setSelectedPlatform] = useState('All')
const [maxBid, setMaxBid] = useState('')
useEffect(() => {
checkAuth()
loadAuctions()
}, [checkAuth])
const loadAuctions = async () => {
setLoading(true)
try {
const [all, ending, hot] = await Promise.all([
api.getAuctions(undefined, undefined, undefined, undefined, undefined, false, 'ending', 100, 0),
api.getEndingSoonAuctions(50),
api.getHotAuctions(50),
])
setAllAuctions(all.auctions || [])
setEndingSoon(ending || [])
setHotAuctions(hot || [])
} catch (error) {
console.error('Failed to load auctions:', error)
} finally {
setLoading(false)
}
}
const handleRefresh = async () => {
setRefreshing(true)
await loadAuctions()
setRefreshing(false)
}
const getCurrentAuctions = (): Auction[] => {
switch (activeTab) {
case 'ending': return endingSoon
case 'hot': return hotAuctions
default: return allAuctions
}
}
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 = [...filteredAuctions].sort((a, b) => {
switch (sortBy) {
case 'bid_asc':
return sortDirection === 'asc' ? a.current_bid - b.current_bid : b.current_bid - a.current_bid
case 'bid_desc':
return sortDirection === 'asc' ? b.current_bid - a.current_bid : a.current_bid - b.current_bid
case 'bids':
return sortDirection === 'asc' ? a.num_bids - b.num_bids : b.num_bids - a.num_bids
default:
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') => {
return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount)
}
const getTimeColor = (timeRemaining: string) => {
if (timeRemaining.includes('m') && !timeRemaining.includes('h')) return 'text-red-400'
if (timeRemaining.includes('h') && parseInt(timeRemaining) < 12) return 'text-amber-400'
return 'text-foreground-muted'
}
const getSubtitle = () => {
if (loading) return 'Loading live auctions...'
const total = allAuctions.length
if (total === 0) return 'No active auctions found'
return `${total.toLocaleString()} live auctions across 4 platforms`
}
return (
<div className="min-h-screen bg-background relative overflow-hidden">
{/* Background Effects */}
<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="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8 animate-fade-in">
<div>
<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">
Domain Auctions
</h1>
<p className="mt-2 text-foreground-muted">{getSubtitle()}</p>
</div>
<button
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>
{/* Stats */}
<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 && (
<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="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-accent/20 rounded-xl flex items-center justify-center">
<Sparkles className="w-6 h-6 text-accent" />
</div>
<div>
<h3 className="text-lg font-semibold text-foreground">Unlock Smart Opportunities</h3>
<p className="text-sm text-foreground-muted">Get AI-powered auction analysis and personalized recommendations</p>
</div>
</div>
<Link
href="/login"
className="flex items-center justify-center gap-2 px-6 py-3 bg-gradient-to-r from-accent to-accent/80
text-background rounded-xl font-medium hover:shadow-[0_0_20px_-5px_rgba(16,185,129,0.4)] transition-all"
>
Sign In <ArrowRight className="w-4 h-4" />
</Link>
</div>
</div>
)}
{/* Tabs */}
<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">
{[
{ 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 },
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={clsx(
"flex items-center gap-2 px-4 py-2.5 text-sm font-medium rounded-xl transition-all",
activeTab === tab.id
? tab.color === 'warning'
? "bg-amber-500 text-background"
: "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" />
<span className="hidden sm:inline">{tab.label}</span>
<span className={clsx(
"text-xs px-1.5 py-0.5 rounded tabular-nums",
activeTab === tab.id ? "bg-background/20" : "bg-foreground/10"
)}>{tab.count}</span>
</button>
))}
</div>
{/* Filters */}
<div className="flex flex-wrap gap-3 mb-6 animate-slide-up">
<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" />
<input
type="text"
placeholder="Search domains..."
value={searchQuery}
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
text-sm text-foreground placeholder:text-foreground-subtle
focus:outline-none focus:border-accent/50 transition-all"
/>
{searchQuery && (
<button onClick={() => setSearchQuery('')} className="absolute right-4 top-1/2 -translate-y-1/2 text-foreground-subtle hover:text-foreground">
<X className="w-4 h-4" />
</button>
)}
</div>
<select
value={selectedPlatform}
onChange={(e) => setSelectedPlatform(e.target.value)}
className="px-4 py-3 bg-background-secondary/50 border border-border/50 rounded-xl
text-sm text-foreground cursor-pointer focus:outline-none focus:border-accent/50"
>
{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-4 h-4 text-foreground-subtle" />
<input
type="number"
placeholder="Max bid"
value={maxBid}
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
text-sm text-foreground placeholder:text-foreground-subtle
focus:outline-none focus:border-accent/50"
/>
</div>
</div>
{/* Auctions Table */}
<div className="animate-slide-up">
<PremiumTable
data={sortedAuctions}
keyExtractor={(a) => `${a.domain}-${a.platform}`}
loading={loading}
sortBy={sortBy}
sortDirection={sortDirection}
onSort={(key) => handleSort(key as SortField)}
emptyIcon={<Gavel className="w-12 h-12 text-foreground-subtle" />}
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) => (
<div>
<a
href={a.affiliate_url}
target="_blank"
rel="noopener noreferrer"
className="font-mono font-medium text-foreground hover:text-accent transition-colors"
>
{a.domain}
</a>
<div className="flex items-center gap-2 mt-1 lg:hidden">
<PlatformBadge platform={a.platform} />
{a.age_years && <span className="text-xs text-foreground-subtle">{a.age_years}y</span>}
</div>
</div>
),
},
{
key: 'platform',
header: 'Platform',
hideOnMobile: true,
render: (a) => (
<div className="space-y-1">
<PlatformBadge platform={a.platform} />
{a.age_years && (
<span className="text-xs text-foreground-subtle flex items-center gap-1">
<Clock className="w-3 h-3" /> {a.age_years}y
</span>
)}
</div>
),
},
{
key: 'bid_asc',
header: 'Bid',
sortable: true,
align: 'right',
render: (a) => (
<div>
<span className="font-medium text-foreground tabular-nums">{formatCurrency(a.current_bid)}</span>
{a.buy_now_price && (
<p className="text-xs text-accent">Buy: {formatCurrency(a.buy_now_price)}</p>
)}
</div>
),
},
{
key: 'bids',
header: 'Bids',
sortable: true,
align: 'right',
hideOnMobile: true,
render: (a) => (
<span className={clsx(
"font-medium flex items-center justify-end gap-1 tabular-nums",
a.num_bids >= 20 ? "text-accent" : a.num_bids >= 10 ? "text-amber-400" : "text-foreground-muted"
)}>
{a.num_bids}
{a.num_bids >= 20 && <Flame className="w-3 h-3" />}
</span>
),
},
{
key: 'ending',
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>
),
},
{
key: 'action',
header: '',
align: 'right',
render: (a) => (
<a
href={a.affiliate_url}
target="_blank"
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
hover:bg-foreground/90 transition-all opacity-70 group-hover:opacity-100"
>
Bid <ExternalLink className="w-3 h-3" />
</a>
),
},
]}
/>
</div>
</div>
</main>
<Footer />
</div>
)
}