feat: MARKET - Final Polish & Mobile Optimization

Changes:
- Implemented responsive layout: Desktop Table vs Mobile Cards
- Desktop: Glassmorphism header, refined spacing, hover effects
- Mobile: Elegant 'Trading Card' layout with optimized touch targets
- Visual: New 'ScoreDisplay' component (Ring for Desktop, Badge for Mobile)
- UX: Sticky search bar on mobile, better empty states
- Polish: Improved typography, consistent borders, micro-interactions
This commit is contained in:
2025-12-11 06:49:15 +01:00
parent 2dbf4e2eb9
commit e85a5a65a4

View File

@ -24,7 +24,8 @@ import {
Search,
LayoutGrid,
List,
SlidersHorizontal
SlidersHorizontal,
MoreHorizontal
} from 'lucide-react'
import clsx from 'clsx'
@ -77,7 +78,7 @@ function calculatePounceScore(domain: string, tld: string, numBids?: number, age
let score = 50
const name = domain.split('.')[0]
// Length bonus (shorter = better)
// Length bonus
if (name.length <= 3) score += 30
else if (name.length === 4) score += 25
else if (name.length === 5) score += 20
@ -95,12 +96,12 @@ function calculatePounceScore(domain: string, tld: string, numBids?: number, age
else if (ageYears && ageYears > 10) score += 7
else if (ageYears && ageYears > 5) score += 3
// Activity bonus (more bids = more valuable)
// Activity bonus
if (numBids && numBids >= 20) score += 8
else if (numBids && numBids >= 10) score += 5
else if (numBids && numBids >= 5) score += 2
// SPAM PENALTIES
// Penalties
if (name.includes('-')) score -= 25
if (/\d/.test(name) && name.length > 3) score -= 20
if (name.length > 15) score -= 15
@ -118,7 +119,7 @@ function isSpamDomain(domain: string, tld: string): boolean {
return false
}
// Parse time remaining to seconds for sorting
// Parse time remaining to seconds
function parseTimeToSeconds(timeStr?: string): number {
if (!timeStr) return Infinity
let seconds = 0
@ -150,52 +151,59 @@ function StatCard({
trend?: 'up' | 'down' | 'neutral'
}) {
return (
<div className="bg-zinc-900/40 border border-white/5 rounded-xl p-4 flex items-start justify-between group hover:border-white/10 transition-all">
<div>
<p className="text-xs font-medium text-zinc-500 uppercase tracking-wider mb-1">{label}</p>
<div className="bg-zinc-900/40 border border-white/5 rounded-xl p-4 flex items-start justify-between hover:bg-white/[0.02] transition-colors relative overflow-hidden group">
<div className="absolute inset-0 bg-gradient-to-br from-white/[0.03] to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="relative z-10">
<p className="text-[11px] font-semibold text-zinc-500 uppercase tracking-wider mb-1">{label}</p>
<div className="flex items-baseline gap-2">
<span className="text-2xl font-bold text-white tracking-tight">{value}</span>
{subValue && <span className="text-xs text-zinc-500 font-medium">{subValue}</span>}
</div>
</div>
<div className={clsx(
"p-2 rounded-lg bg-zinc-800/50 text-zinc-400 group-hover:text-white transition-colors",
"relative z-10 p-2 rounded-lg bg-zinc-800/50 transition-colors",
trend === 'up' && "text-emerald-400 bg-emerald-500/10",
trend === 'down' && "text-red-400 bg-red-500/10"
trend === 'down' && "text-red-400 bg-red-500/10",
trend === 'neutral' && "text-zinc-400"
)}>
<Icon className="w-5 h-5" />
<Icon className="w-4 h-4" />
</div>
</div>
)
}
// Modern Score Indicator
function ScoreRing({ score }: { score: number }) {
// Score Ring (Desktop) / Badge (Mobile)
function ScoreDisplay({ score, mobile = false }: { score: number; mobile?: boolean }) {
const color = score >= 80 ? 'text-emerald-500' : score >= 50 ? 'text-amber-500' : 'text-zinc-600'
const size = 32
if (mobile) {
return (
<div className={clsx(
"px-2 py-0.5 rounded text-[10px] font-bold font-mono border",
score >= 80 ? "bg-emerald-500/10 text-emerald-400 border-emerald-500/20" :
score >= 50 ? "bg-amber-500/10 text-amber-400 border-amber-500/20" :
"bg-zinc-800 text-zinc-400 border-zinc-700"
)}>
{score}
</div>
)
}
const size = 36
const strokeWidth = 3
const radius = (size - strokeWidth) / 2
const circumference = radius * 2 * Math.PI
const offset = circumference - (score / 100) * circumference
return (
<div className="relative flex items-center justify-center" style={{ width: size, height: size }}>
{/* Background Ring */}
<div className="relative flex items-center justify-center group" style={{ width: size, height: size }}>
<svg className="absolute w-full h-full -rotate-90">
<circle cx={size/2} cy={size/2} r={radius} className="stroke-zinc-800" strokeWidth={strokeWidth} fill="none" />
<circle
cx={size / 2}
cy={size / 2}
cx={size/2}
cy={size/2}
r={radius}
className="stroke-zinc-800"
strokeWidth={strokeWidth}
fill="none"
/>
{/* Progress Ring */}
<circle
cx={size / 2}
cy={size / 2}
r={radius}
className={clsx("transition-all duration-500 ease-out", color)}
className={clsx("transition-all duration-700 ease-out", color)}
strokeWidth={strokeWidth}
strokeDasharray={circumference}
strokeDashoffset={offset}
@ -203,46 +211,26 @@ function ScoreRing({ score }: { score: number }) {
fill="none"
/>
</svg>
<span className={clsx("text-[10px] font-bold font-mono", score >= 80 ? 'text-emerald-400' : 'text-zinc-400')}>
<span className={clsx("text-[11px] font-bold font-mono", score >= 80 ? 'text-emerald-400' : 'text-zinc-400')}>
{score}
</span>
{/* Tooltip */}
<div className="absolute top-full mt-2 left-1/2 -translate-x-1/2 bg-zinc-900 border border-zinc-800 px-2 py-1 rounded text-[10px] text-zinc-400 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap z-20 pointer-events-none">
Pounce Score
</div>
</div>
)
}
// Refined Source Badge
function SourceBadge({ source, isPounce }: { source: string; isPounce: boolean }) {
if (isPounce) return (
<div className="flex items-center gap-1.5">
<div className="w-1.5 h-1.5 rounded-full bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]" />
<span className="text-xs font-semibold text-white tracking-tight">Pounce</span>
</div>
)
return (
<span className="text-xs font-medium text-zinc-500 hover:text-zinc-300 transition-colors">
{source}
</span>
)
}
// Minimal Toggle
function FilterToggle({
active,
onClick,
label
}: {
active: boolean
onClick: () => void
label: string
}) {
function FilterToggle({ active, onClick, label }: { active: boolean; onClick: () => void; label: string }) {
return (
<button
onClick={onClick}
className={clsx(
"px-3 py-1.5 rounded-full text-xs font-medium transition-all border",
"px-4 py-1.5 rounded-full text-xs font-medium transition-all border whitespace-nowrap",
active
? "bg-white text-zinc-950 border-white"
? "bg-white text-black border-white shadow-[0_0_10px_rgba(255,255,255,0.1)]"
: "bg-transparent text-zinc-400 border-zinc-800 hover:border-zinc-700 hover:text-zinc-300"
)}
>
@ -253,34 +241,23 @@ function FilterToggle({
// Sort Header
function SortableHeader({
label,
field,
currentSort,
currentDirection,
onSort,
align = 'left',
label, field, currentSort, currentDirection, onSort, align = 'left'
}: {
label: string
field: SortField
currentSort: SortField
currentDirection: SortDirection
onSort: (field: SortField) => void
align?: 'left' | 'center' | 'right'
label: string; field: SortField; currentSort: SortField; currentDirection: SortDirection; onSort: (field: SortField) => void; align?: 'left'|'center'|'right'
}) {
const isActive = currentSort === field
return (
<button
onClick={() => onSort(field)}
className={clsx(
"flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest transition-all group select-none",
"flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest transition-all group select-none py-2",
align === 'right' && "justify-end ml-auto",
align === 'center' && "justify-center mx-auto",
isActive ? "text-white" : "text-zinc-500 hover:text-zinc-300"
)}
>
{label}
<div className={clsx("flex flex-col -space-y-1", isActive ? "opacity-100" : "opacity-0 group-hover:opacity-50")}>
<div className={clsx("flex flex-col -space-y-1 transition-opacity", isActive ? "opacity-100" : "opacity-0 group-hover:opacity-30")}>
<ChevronUp className={clsx("w-2 h-2", isActive && currentDirection === 'asc' ? "text-white" : "text-zinc-600")} />
<ChevronDown className={clsx("w-2 h-2", isActive && currentDirection === 'desc' ? "text-white" : "text-zinc-600")} />
</div>
@ -295,18 +272,18 @@ function SortableHeader({
export default function MarketPage() {
const { subscription } = useStore()
// Data State
// Data
const [auctions, setAuctions] = useState<Auction[]>([])
const [loading, setLoading] = useState(true)
const [refreshing, setRefreshing] = useState(false)
// Filter State
// Filters
const [hideSpam, setHideSpam] = useState(true)
const [pounceOnly, setPounceOnly] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [priceRange, setPriceRange] = useState<'all' | 'low' | 'mid' | 'high'>('all')
// Sort State
// Sort
const [sortField, setSortField] = useState<SortField>('score')
const [sortDirection, setSortDirection] = useState<SortDirection>('desc')
@ -314,7 +291,7 @@ export default function MarketPage() {
const [trackedDomains, setTrackedDomains] = useState<Set<string>>(new Set())
const [trackingInProgress, setTrackingInProgress] = useState<string | null>(null)
// Load Data
// Load
const loadData = useCallback(async () => {
setLoading(true)
try {
@ -327,9 +304,7 @@ export default function MarketPage() {
}
}, [])
useEffect(() => {
loadData()
}, [loadData])
useEffect(() => { loadData() }, [loadData])
const handleRefresh = useCallback(async () => {
setRefreshing(true)
@ -338,9 +313,8 @@ export default function MarketPage() {
}, [loadData])
const handleSort = useCallback((field: SortField) => {
if (sortField === field) {
setSortDirection(d => d === 'asc' ? 'desc' : 'asc')
} else {
if (sortField === field) setSortDirection(d => d === 'asc' ? 'desc' : 'asc')
else {
setSortField(field)
setSortDirection(field === 'score' || field === 'price' ? 'desc' : 'asc')
}
@ -352,22 +326,18 @@ export default function MarketPage() {
try {
await api.addDomain(domain)
setTrackedDomains(prev => new Set([...Array.from(prev), domain]))
} catch (error) {
console.error('Failed to track:', error)
} finally {
setTrackingInProgress(null)
}
} catch (error) { console.error(error) } finally { setTrackingInProgress(null) }
}, [trackedDomains, trackingInProgress])
// Transform Data
// Transform & Filter
const marketItems = useMemo(() => {
const items: MarketItem[] = auctions.map(auction => ({
id: `${auction.domain}-${auction.platform}`,
domain: auction.domain,
pounceScore: calculatePounceScore(auction.domain, auction.tld, auction.num_bids, auction.age_years ?? undefined),
price: auction.current_bid,
priceType: 'bid' as const,
status: 'auction' as const,
priceType: 'bid',
status: 'auction',
timeLeft: auction.time_remaining,
endTime: auction.end_time,
source: auction.platform as any,
@ -378,20 +348,13 @@ export default function MarketPage() {
}))
let filtered = items
// Filters
if (hideSpam) filtered = filtered.filter(item => !isSpamDomain(item.domain, item.tld))
if (pounceOnly) filtered = filtered.filter(item => item.isPounce)
if (priceRange === 'low') filtered = filtered.filter(item => item.price < 100)
if (priceRange === 'mid') filtered = filtered.filter(item => item.price >= 100 && item.price < 1000)
if (priceRange === 'high') filtered = filtered.filter(item => item.price >= 1000)
if (searchQuery) {
const q = searchQuery.toLowerCase()
filtered = filtered.filter(item => item.domain.toLowerCase().includes(q))
}
if (searchQuery) filtered = filtered.filter(item => item.domain.toLowerCase().includes(searchQuery.toLowerCase()))
// Sort
const mult = sortDirection === 'asc' ? 1 : -1
filtered.sort((a, b) => {
switch (sortField) {
@ -403,7 +366,6 @@ export default function MarketPage() {
default: return 0
}
})
return filtered
}, [auctions, hideSpam, pounceOnly, priceRange, searchQuery, sortField, sortDirection])
@ -412,239 +374,191 @@ export default function MarketPage() {
total: marketItems.length,
highScore: marketItems.filter(i => i.pounceScore >= 80).length,
endingSoon: marketItems.filter(i => parseTimeToSeconds(i.timeLeft) < 3600).length,
avgPrice: marketItems.length > 0
? Math.round(marketItems.reduce((acc, i) => acc + i.price, 0) / marketItems.length)
: 0
avgPrice: marketItems.length > 0 ? Math.round(marketItems.reduce((acc, i) => acc + i.price, 0) / marketItems.length) : 0
}), [marketItems])
const formatPrice = (price: number) => {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(price)
}
const formatPrice = (price: number) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(price)
return (
<TerminalLayout title="Market" subtitle="Real-time Domain Auctions">
<div className="space-y-8">
<TerminalLayout title="Market" subtitle="Global Domain Opportunities">
<div className="space-y-6 pb-20 md:pb-0">
{/* ============================================================================ */}
{/* METRICS GRID */}
{/* ============================================================================ */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard
label="Active Listings"
value={stats.total}
icon={Activity}
trend="neutral"
/>
<StatCard
label="High Potential"
value={stats.highScore}
subValue="Score 80+"
icon={TrendingUp}
trend="up"
/>
<StatCard
label="Ending Soon"
value={stats.endingSoon}
subValue="< 1h"
icon={Flame}
trend={stats.endingSoon > 5 ? 'down' : 'neutral'}
/>
<StatCard
label="Avg. Price"
value={formatPrice(stats.avgPrice)}
icon={Zap}
trend="neutral"
/>
{/* METRICS */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
<StatCard label="Active" value={stats.total} icon={Activity} trend="neutral" />
<StatCard label="Top Tier" value={stats.highScore} subValue="80+ Score" icon={TrendingUp} trend="up" />
<StatCard label="Ending Soon" value={stats.endingSoon} subValue="< 1h" icon={Flame} trend={stats.endingSoon > 5 ? 'down' : 'neutral'} />
<StatCard label="Avg Price" value={formatPrice(stats.avgPrice)} icon={Zap} trend="neutral" />
</div>
{/* ============================================================================ */}
{/* CONTROL BAR */}
{/* ============================================================================ */}
<div className="flex flex-col md:flex-row items-center gap-4 p-1">
{/* Search */}
<div className="relative w-full md:w-96 group">
<div className="absolute inset-y-0 left-3 flex items-center pointer-events-none">
<Search className="w-4 h-4 text-zinc-500 group-focus-within:text-white transition-colors" />
{/* CONTROLS */}
<div className="sticky top-0 z-30 bg-zinc-950/80 backdrop-blur-md py-4 border-b border-white/5 -mx-4 px-4 md:mx-0 md:px-0 md:border-none md:bg-transparent md:static">
<div className="flex flex-col md:flex-row gap-4">
{/* Search */}
<div className="relative w-full md:w-80 group">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-500 group-focus-within:text-white transition-colors" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search domains..."
className="w-full pl-10 pr-4 py-2.5 bg-zinc-900 border border-white/10 rounded-xl
text-sm text-white placeholder:text-zinc-600
focus:outline-none focus:border-white/20 focus:ring-1 focus:ring-white/20 transition-all"
/>
</div>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search domains..."
className="w-full pl-10 pr-4 py-2.5 bg-zinc-900/50 border border-white/5 rounded-lg
text-sm text-white placeholder:text-zinc-600
focus:outline-none focus:border-white/20 focus:bg-zinc-900 transition-all"
/>
{/* Filters */}
<div className="flex items-center gap-2 overflow-x-auto w-full pb-1 md:pb-0 scrollbar-hide mask-fade-right">
<FilterToggle active={hideSpam} onClick={() => setHideSpam(!hideSpam)} label="No Spam" />
<FilterToggle active={pounceOnly} onClick={() => setPounceOnly(!pounceOnly)} label="Pounce Exclusive" />
<div className="w-px h-5 bg-white/10 mx-2 flex-shrink-0" />
<FilterToggle active={priceRange === 'low'} onClick={() => setPriceRange(p => p === 'low' ? 'all' : 'low')} label="< $100" />
<FilterToggle active={priceRange === 'high'} onClick={() => setPriceRange(p => p === 'high' ? 'all' : 'high')} label="$1k+" />
</div>
<div className="hidden md:block flex-1" />
<button onClick={handleRefresh} className="hidden md:flex items-center gap-2 text-xs font-medium text-zinc-500 hover:text-white transition-colors">
<RefreshCw className={clsx("w-3.5 h-3.5", refreshing && "animate-spin")} />
Refresh
</button>
</div>
<div className="w-px h-6 bg-white/10 hidden md:block" />
{/* Filters */}
<div className="flex items-center gap-2 overflow-x-auto w-full md:w-auto pb-2 md:pb-0 scrollbar-hide">
<FilterToggle active={hideSpam} onClick={() => setHideSpam(!hideSpam)} label="Hide Spam" />
<FilterToggle active={pounceOnly} onClick={() => setPounceOnly(!pounceOnly)} label="Pounce Exclusive" />
<div className="w-px h-4 bg-white/10 mx-1" />
<FilterToggle active={priceRange === 'low'} onClick={() => setPriceRange(p => p === 'low' ? 'all' : 'low')} label="< $100" />
<FilterToggle active={priceRange === 'high'} onClick={() => setPriceRange(p => p === 'high' ? 'all' : 'high')} label="$1k+" />
</div>
<div className="flex-1" />
{/* Refresh */}
<button
onClick={handleRefresh}
className={clsx(
"flex items-center gap-2 text-xs font-medium text-zinc-500 hover:text-white transition-colors",
refreshing && "opacity-50 cursor-not-allowed"
)}
>
<RefreshCw className={clsx("w-3.5 h-3.5", refreshing && "animate-spin")} />
<span className="hidden sm:inline">Refresh Data</span>
</button>
</div>
{/* ============================================================================ */}
{/* DATA GRID */}
{/* ============================================================================ */}
<div className="border border-white/5 rounded-xl overflow-hidden bg-zinc-900/20 backdrop-blur-sm">
{/* Header */}
<div className="grid grid-cols-12 gap-4 px-6 py-3 border-b border-white/5 bg-zinc-900/50">
<div className="col-span-5 md:col-span-4">
<SortableHeader label="Domain Asset" field="domain" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} />
</div>
<div className="col-span-2 hidden md:block text-center">
<SortableHeader label="Pounce Score" field="score" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} align="center" />
</div>
<div className="col-span-3 md:col-span-2 text-right">
<SortableHeader label="Bid Price" field="price" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} align="right" />
</div>
<div className="col-span-2 hidden lg:block text-center">
<SortableHeader label="Time Left" field="time" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} align="center" />
</div>
<div className="col-span-2 hidden xl:block text-center">
<SortableHeader label="Source" field="source" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} align="center" />
</div>
<div className="col-span-4 md:col-span-2 xl:col-span-2 text-right">
<span className="text-[10px] font-bold uppercase tracking-widest text-zinc-600">Action</span>
</div>
</div>
{/* Body */}
<div className="min-h-[400px]">
{loading ? (
<div className="flex flex-col items-center justify-center py-32 space-y-4">
<Loader2 className="w-8 h-8 text-emerald-500 animate-spin" />
<p className="text-zinc-500 text-sm animate-pulse">Scanning global markets...</p>
<p className="text-zinc-500 text-sm animate-pulse">Scanning markets...</p>
</div>
) : marketItems.length === 0 ? (
<div className="flex flex-col items-center justify-center py-32 text-center">
<div className="w-16 h-16 bg-zinc-900 rounded-full flex items-center justify-center mb-4 border border-zinc-800">
<Search className="w-6 h-6 text-zinc-600" />
</div>
<h3 className="text-white font-medium mb-1">No assets found</h3>
<p className="text-zinc-500 text-sm">Adjust filters to see more results</p>
<h3 className="text-white font-medium mb-1">No matches found</h3>
<p className="text-zinc-500 text-sm">Try adjusting your filters</p>
</div>
) : (
<div className="divide-y divide-white/5">
{marketItems.map((item) => {
const timeLeftSec = parseTimeToSeconds(item.timeLeft)
const isUrgent = timeLeftSec < 3600
return (
<div
key={item.id}
className="grid grid-cols-12 gap-4 px-6 py-3.5 items-center hover:bg-white/[0.02] transition-colors group"
>
{/* Domain */}
<div className="col-span-5 md:col-span-4 min-w-0">
<div className="flex items-center gap-3">
<div className={clsx(
"w-1.5 h-1.5 rounded-full flex-shrink-0",
item.isPounce ? "bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.5)]" : "bg-zinc-700"
)} />
<div className="truncate">
<div className="flex items-baseline gap-0.5 truncate">
<span className="font-medium text-white text-[15px] tracking-tight">{item.domain.split('.')[0]}</span>
<span className="text-zinc-500 text-sm">.{item.tld}</span>
</div>
<div className="flex items-center gap-2 mt-0.5 lg:hidden">
<span className={clsx("text-xs", item.pounceScore >= 80 ? "text-emerald-400" : "text-zinc-500")}>
Score {item.pounceScore}
</span>
<span className="text-zinc-700 text-[10px]"></span>
<span className={clsx("text-xs", isUrgent ? "text-red-400" : "text-zinc-500")}>
{item.timeLeft}
</span>
<>
{/* DESKTOP TABLE */}
<div className="hidden md:block border border-white/5 rounded-xl overflow-hidden bg-zinc-900/40 backdrop-blur-sm shadow-xl">
<div className="grid grid-cols-12 gap-4 px-6 py-3 border-b border-white/5 bg-white/[0.02]">
<div className="col-span-4"><SortableHeader label="Domain Asset" field="domain" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} /></div>
<div className="col-span-2 text-center"><SortableHeader label="Score" field="score" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} align="center" /></div>
<div className="col-span-2 text-right"><SortableHeader label="Price" field="price" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} align="right" /></div>
<div className="col-span-2 text-center"><SortableHeader label="Time" field="time" currentSort={sortField} currentDirection={sortDirection} onSort={handleSort} align="center" /></div>
<div className="col-span-2 text-right"><span className="text-[10px] font-bold uppercase tracking-widest text-zinc-600 py-2 block">Action</span></div>
</div>
<div className="divide-y divide-white/5">
{marketItems.map((item) => {
const timeLeftSec = parseTimeToSeconds(item.timeLeft)
const isUrgent = timeLeftSec < 3600
return (
<div key={item.id} className="grid grid-cols-12 gap-4 px-6 py-4 items-center hover:bg-white/[0.04] transition-all group relative">
{/* Domain */}
<div className="col-span-4">
<div className="flex items-center gap-3">
{item.isPounce && <Diamond className="w-4 h-4 text-emerald-400 fill-emerald-400/20" />}
<div>
<div className="font-medium text-white text-[15px] tracking-tight">{item.domain}</div>
<div className="text-[11px] text-zinc-500 mt-0.5">{item.source}</div>
</div>
</div>
</div>
</div>
</div>
{/* Score */}
<div className="col-span-2 hidden md:flex justify-center">
<ScoreRing score={item.pounceScore} />
</div>
{/* Price */}
<div className="col-span-3 md:col-span-2 text-right">
<div className="font-mono text-white font-medium tracking-tight">
{formatPrice(item.price)}
</div>
{item.numBids !== undefined && item.numBids > 0 && (
<div className="text-[11px] text-zinc-500 mt-0.5">
{item.numBids} bids
{/* Score */}
<div className="col-span-2 flex justify-center"><ScoreDisplay score={item.pounceScore} /></div>
{/* Price */}
<div className="col-span-2 text-right">
<div className="font-mono text-white font-medium">{formatPrice(item.price)}</div>
{item.numBids !== undefined && item.numBids > 0 && <div className="text-[10px] text-zinc-500 mt-0.5">{item.numBids} bids</div>}
</div>
{/* Time */}
<div className="col-span-2 flex justify-center">
<div className={clsx("flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium", isUrgent ? "text-red-400 bg-red-500/10" : "text-zinc-400 bg-zinc-800/50")}>
<Clock className="w-3 h-3" />
{item.timeLeft}
</div>
</div>
{/* Actions */}
<div className="col-span-2 flex items-center justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
<button onClick={() => handleTrack(item.domain)} disabled={trackedDomains.has(item.domain)} className={clsx("w-8 h-8 flex items-center justify-center rounded-lg border transition-colors", trackedDomains.has(item.domain) ? "bg-emerald-500 text-white border-emerald-500" : "border-zinc-700 text-zinc-400 hover:text-white hover:border-zinc-500 hover:bg-zinc-800")}>
{trackedDomains.has(item.domain) ? <Check className="w-4 h-4" /> : <Plus className="w-4 h-4" />}
</button>
<a href={item.affiliateUrl || '#'} target="_blank" rel="noopener noreferrer" className="h-8 px-3 flex items-center gap-2 bg-white text-zinc-950 rounded-lg text-xs font-semibold hover:bg-zinc-200 transition-colors">
{item.isPounce ? 'Buy' : 'Bid'}
<ExternalLink className="w-3 h-3" />
</a>
</div>
)}
</div>
{/* Time */}
<div className="col-span-2 hidden lg:flex justify-center">
<div className={clsx(
"flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium border",
isUrgent
? "bg-red-500/10 text-red-400 border-red-500/20"
: "bg-zinc-900 border-zinc-800 text-zinc-400"
)}>
<Clock className="w-3 h-3" />
{item.timeLeft}
</div>
</div>
)
})}
</div>
</div>
{/* Source */}
<div className="col-span-2 hidden xl:flex justify-center">
<SourceBadge source={item.source} isPounce={item.isPounce} />
</div>
{/* Action */}
<div className="col-span-4 md:col-span-2 xl:col-span-2 flex items-center justify-end gap-2 opacity-100 lg:opacity-60 lg:group-hover:opacity-100 transition-opacity">
<button
onClick={() => handleTrack(item.domain)}
disabled={trackedDomains.has(item.domain)}
className={clsx(
"w-8 h-8 flex items-center justify-center rounded-lg border transition-all",
trackedDomains.has(item.domain)
? "bg-emerald-500 text-white border-emerald-500"
: "border-zinc-700 text-zinc-400 hover:text-white hover:border-zinc-500 hover:bg-zinc-800"
)}
>
{trackedDomains.has(item.domain) ? <Check className="w-4 h-4" /> : <Plus className="w-4 h-4" />}
</button>
{/* MOBILE CARDS */}
<div className="md:hidden space-y-3">
{marketItems.map((item) => {
const timeLeftSec = parseTimeToSeconds(item.timeLeft)
const isUrgent = timeLeftSec < 3600
return (
<div key={item.id} className="bg-zinc-900/40 border border-white/5 rounded-xl p-4 active:bg-zinc-900/60 transition-colors">
<div className="flex justify-between items-start mb-3">
<div className="flex items-center gap-2">
{item.isPounce && <Diamond className="w-3.5 h-3.5 text-emerald-400 fill-emerald-400/20" />}
<span className="font-medium text-white text-base">{item.domain}</span>
</div>
<ScoreDisplay score={item.pounceScore} mobile />
</div>
<a
href={item.affiliateUrl || '#'}
target="_blank"
rel="noopener noreferrer"
className="h-8 px-3 flex items-center gap-2 bg-white text-zinc-950 rounded-lg text-xs font-semibold hover:bg-zinc-200 transition-colors"
>
{item.isPounce ? 'Buy' : 'Bid'}
<ExternalLink className="w-3 h-3" />
</a>
<div className="flex items-center justify-between mb-4">
<div>
<div className="text-[10px] text-zinc-500 uppercase tracking-wider mb-0.5">Current Bid</div>
<div className="font-mono text-lg font-medium text-white">{formatPrice(item.price)}</div>
</div>
<div className="text-right">
<div className="text-[10px] text-zinc-500 uppercase tracking-wider mb-0.5">Ends In</div>
<div className={clsx("flex items-center gap-1.5 justify-end font-medium", isUrgent ? "text-red-400" : "text-zinc-400")}>
<Clock className="w-3 h-3" />
{item.timeLeft}
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<button
onClick={() => handleTrack(item.domain)}
disabled={trackedDomains.has(item.domain)}
className={clsx(
"flex items-center justify-center gap-2 py-2.5 rounded-lg text-sm font-medium border transition-all",
trackedDomains.has(item.domain)
? "bg-emerald-500/10 text-emerald-400 border-emerald-500/20"
: "bg-zinc-800/50 text-zinc-400 border-zinc-700/50"
)}
>
{trackedDomains.has(item.domain) ? (
<><Check className="w-4 h-4" /> Tracked</>
) : (
<><Plus className="w-4 h-4" /> Watch</>
)}
</button>
<a
href={item.affiliateUrl || '#'}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-2 py-2.5 rounded-lg text-sm font-semibold bg-white text-black hover:bg-zinc-200 transition-colors"
>
{item.isPounce ? 'Buy Now' : 'Place Bid'}
<ExternalLink className="w-3 h-3 opacity-50" />
</a>
</div>
</div>
</div>
)
})}
</div>
)
})}
</div>
</>
)}
</div>
</div>