From 5bab069a756d469cd5814530aee7bf8ed6a1a398 Mon Sep 17 00:00:00 2001 From: Yves Gugger Date: Thu, 11 Dec 2025 06:43:21 +0100 Subject: [PATCH] feat: MARKET - Complete UI Overhaul (Award-Winning Style) Changes: - Redesigned Metric Grid with trend indicators - New separated Control Bar for search & filters - High-end Data Grid with ultra-thin borders and hover effects - Custom SVG 'Score Ring' component for Pounce Score - Modern typography and spacing - Removed 'clutter' badges, replaced with minimal indicators --- frontend/src/app/terminal/market/page.tsx | 758 +++++++++------------- 1 file changed, 312 insertions(+), 446 deletions(-) diff --git a/frontend/src/app/terminal/market/page.tsx b/frontend/src/app/terminal/market/page.tsx index f72f221..da82b4c 100644 --- a/frontend/src/app/terminal/market/page.tsx +++ b/frontend/src/app/terminal/market/page.tsx @@ -21,6 +21,10 @@ import { Activity, Flame, Clock, + Search, + LayoutGrid, + List, + SlidersHorizontal } from 'lucide-react' import clsx from 'clsx' @@ -131,143 +135,123 @@ function parseTimeToSeconds(timeStr?: string): number { // COMPONENTS // ============================================================================ -// Score Badge with color coding -function ScoreBadge({ score }: { score: number }) { - const color = score >= 80 - ? 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30' - : score >= 40 - ? 'bg-amber-500/20 text-amber-400 border-amber-500/30' - : 'bg-red-500/20 text-red-400 border-red-500/30' - +// Stat Card +function StatCard({ + label, + value, + subValue, + icon: Icon, + trend +}: { + label: string + value: string | number + subValue?: string + icon: any + trend?: 'up' | 'down' | 'neutral' +}) { return ( -
- {score} +
+
+

{label}

+
+ {value} + {subValue && {subValue}} +
+
+
+ +
) } -// Source Badge -function SourceBadge({ source, isPounce }: { source: string; isPounce: boolean }) { - if (isPounce) { - return ( -
- - Pounce -
- ) - } - - const colors: Record = { - GoDaddy: 'bg-orange-500/10 border-orange-500/20 text-orange-400', - Sedo: 'bg-blue-500/10 border-blue-500/20 text-blue-400', - NameJet: 'bg-purple-500/10 border-purple-500/20 text-purple-400', - DropCatch: 'bg-cyan-500/10 border-cyan-500/20 text-cyan-400', - } - - return ( - - {source} - - ) -} +// Modern Score Indicator +function ScoreRing({ score }: { score: number }) { + const color = score >= 80 ? 'text-emerald-500' : score >= 50 ? 'text-amber-500' : 'text-zinc-600' + const size = 32 + const strokeWidth = 3 + const radius = (size - strokeWidth) / 2 + const circumference = radius * 2 * Math.PI + const offset = circumference - (score / 100) * circumference -// Status Badge -function StatusBadge({ status, timeLeft }: { status: 'auction' | 'instant'; timeLeft?: string }) { - if (status === 'instant') { - return ( -
- - Instant -
- ) - } - - const isUrgent = timeLeft?.includes('m') && !timeLeft?.includes('d') && !timeLeft?.includes('h') - const isWarning = timeLeft?.includes('h') && parseInt(timeLeft) <= 4 - return ( -
- - - {timeLeft} +
+ {/* Background Ring */} + + + {/* Progress Ring */} + + + = 80 ? 'text-emerald-400' : 'text-zinc-400')}> + {score}
) } -// Toggle Button -function ToggleButton({ +// Refined Source Badge +function SourceBadge({ source, isPounce }: { source: string; isPounce: boolean }) { + if (isPounce) return ( +
+
+ Pounce +
+ ) + + return ( + + {source} + + ) +} + +// Minimal Toggle +function FilterToggle({ active, onClick, - children + label }: { active: boolean onClick: () => void - children: React.ReactNode + label: string }) { return ( ) } -// Dropdown Select -function DropdownSelect({ - value, - onChange, - options, -}: { - value: string - onChange: (v: string) => void - options: { value: string; label: string }[] -}) { - return ( -
- - -
- ) -} - -// Sortable Column Header +// Sort Header function SortableHeader({ label, field, @@ -289,28 +273,23 @@ function SortableHeader({ ) } // ============================================================================ -// MAIN COMPONENT +// MAIN PAGE // ============================================================================ export default function MarketPage() { @@ -324,35 +303,17 @@ export default function MarketPage() { // Filter State const [hideSpam, setHideSpam] = useState(true) const [pounceOnly, setPounceOnly] = useState(false) - const [selectedTld, setSelectedTld] = useState('all') - const [selectedPrice, setSelectedPrice] = useState('all') const [searchQuery, setSearchQuery] = useState('') + const [priceRange, setPriceRange] = useState<'all' | 'low' | 'mid' | 'high'>('all') // Sort State const [sortField, setSortField] = useState('score') const [sortDirection, setSortDirection] = useState('desc') - // Watchlist State + // Watchlist const [trackedDomains, setTrackedDomains] = useState>(new Set()) const [trackingInProgress, setTrackingInProgress] = useState(null) - // Options - const TLD_OPTIONS = [ - { value: 'all', label: 'All TLDs' }, - { value: 'com', label: '.com' }, - { value: 'ai', label: '.ai' }, - { value: 'io', label: '.io' }, - { value: 'ch', label: '.ch' }, - { value: 'net', label: '.net' }, - ] - - const PRICE_OPTIONS = [ - { value: 'all', label: 'Any Price' }, - { value: '100', label: '< $100' }, - { value: '1000', label: '< $1,000' }, - { value: '10000', label: 'High Roller' }, - ] - // Load Data const loadData = useCallback(async () => { setLoading(true) @@ -381,14 +342,12 @@ export default function MarketPage() { setSortDirection(d => d === 'asc' ? 'desc' : 'asc') } else { setSortField(field) - // Default direction based on field type setSortDirection(field === 'score' || field === 'price' ? 'desc' : 'asc') } }, [sortField]) const handleTrack = useCallback(async (domain: string) => { if (trackedDomains.has(domain) || trackingInProgress) return - setTrackingInProgress(domain) try { await api.addDomain(domain) @@ -400,9 +359,8 @@ export default function MarketPage() { } }, [trackedDomains, trackingInProgress]) - // Transform and Filter Data + // Transform Data const marketItems = useMemo(() => { - // Convert auctions to market items const items: MarketItem[] = auctions.map(auction => ({ id: `${auction.domain}-${auction.platform}`, domain: auction.domain, @@ -419,368 +377,276 @@ export default function MarketPage() { numBids: auction.num_bids, })) - // Apply Filters let filtered = items - if (hideSpam) { - filtered = filtered.filter(item => !isSpamDomain(item.domain, item.tld)) - } - - if (pounceOnly) { - filtered = filtered.filter(item => item.isPounce) - } - - if (selectedTld !== 'all') { - filtered = filtered.filter(item => item.tld === selectedTld) - } - - if (selectedPrice !== 'all') { - const maxPrice = parseInt(selectedPrice) - if (selectedPrice === '10000') { - filtered = filtered.filter(item => item.price >= 10000) - } else { - filtered = filtered.filter(item => item.price < maxPrice) - } - } - + // 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)) } - // Apply Sort + // Sort const mult = sortDirection === 'asc' ? 1 : -1 filtered.sort((a, b) => { switch (sortField) { - case 'domain': - return mult * a.domain.localeCompare(b.domain) - case 'score': - return mult * (a.pounceScore - b.pounceScore) - case 'price': - return mult * (a.price - b.price) - case 'time': - return mult * (parseTimeToSeconds(a.timeLeft) - parseTimeToSeconds(b.timeLeft)) - case 'source': - return mult * a.source.localeCompare(b.source) - default: - return 0 + case 'domain': return mult * a.domain.localeCompare(b.domain) + case 'score': return mult * (a.pounceScore - b.pounceScore) + case 'price': return mult * (a.price - b.price) + case 'time': return mult * (parseTimeToSeconds(a.timeLeft) - parseTimeToSeconds(b.timeLeft)) + case 'source': return mult * a.source.localeCompare(b.source) + default: return 0 } }) return filtered - }, [auctions, hideSpam, pounceOnly, selectedTld, selectedPrice, searchQuery, sortField, sortDirection]) + }, [auctions, hideSpam, pounceOnly, priceRange, searchQuery, sortField, sortDirection]) // Stats const stats = useMemo(() => ({ total: marketItems.length, highScore: marketItems.filter(i => i.pounceScore >= 80).length, - endingSoon: marketItems.filter(i => { - const seconds = parseTimeToSeconds(i.timeLeft) - return seconds < 3600 // Less than 1 hour - }).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 }), [marketItems]) - // Format currency const formatPrice = (price: number) => { - if (price >= 1000000) return `$${(price / 1000000).toFixed(1)}M` - if (price >= 1000) return `$${(price / 1000).toFixed(1)}k` - return `$${price.toLocaleString()}` + return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(price) } return ( - -
+ +
- {/* ================================================================ */} - {/* HEADER - Live Feed Style */} - {/* ================================================================ */} -
- {/* Left: Title with Live Indicator */} -
-
-
- - -
-
-

Live Market Feed

-

Updated in real-time

-
-
- - {/* Quick Stats Pills */} -
-
- {stats.total} - listings -
-
- - {stats.highScore} - high score -
- {stats.endingSoon > 0 && ( -
- - {stats.endingSoon} - ending soon -
- )} + {/* ============================================================================ */} + {/* METRICS GRID */} + {/* ============================================================================ */} +
+ + + 5 ? 'down' : 'neutral'} + /> + +
+ + {/* ============================================================================ */} + {/* CONTROL BAR */} + {/* ============================================================================ */} +
+ + {/* Search */} +
+
+
+ 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" + />
- {/* Right: Refresh */} +
+ + {/* Filters */} +
+ setHideSpam(!hideSpam)} label="Hide Spam" /> + setPounceOnly(!pounceOnly)} label="Pounce Exclusive" /> +
+ setPriceRange(p => p === 'low' ? 'all' : 'low')} label="< $100" /> + setPriceRange(p => p === 'high' ? 'all' : 'high')} label="$1k+" /> +
+ +
+ + {/* Refresh */}
- {/* ================================================================ */} - {/* FILTER BAR */} - {/* ================================================================ */} -
-
-
- - Filters -
- - setHideSpam(!hideSpam)}> - Hide Spam - - - setPounceOnly(!pounceOnly)}> - - Pounce Only - - -
- - - - - -
- setSearchQuery(e.target.value)} - placeholder="Search domains..." - className="w-full px-4 py-2 bg-zinc-800/50 border border-zinc-700/50 rounded-lg - text-sm text-zinc-300 placeholder:text-zinc-600 - focus:outline-none focus:border-emerald-500/50 transition-all" - /> -
-
-
- - {/* ================================================================ */} - {/* MARKET TABLE */} - {/* ================================================================ */} -
+ {/* ============================================================================ */} + {/* DATA GRID */} + {/* ============================================================================ */} +
- {/* Table Header - Sortable */} -
-
- + {/* Header */} +
+
+
-
- +
+
-
- +
+
-
- +
+
-
- +
+
-
- Action +
+ Action
- {/* Table Body */} + {/* Body */} {loading ? ( -
- +
+ +

Scanning global markets...

) : marketItems.length === 0 ? ( -
- -

No domains match your filters

-

Try adjusting your filter settings

+
+
+ +
+

No assets found

+

Adjust filters to see more results

) : ( -
- {marketItems.map((item) => ( -
- {/* Domain */} -
-
- {item.isPounce && ( - - )} -
- {item.domain} - {item.verified && ( - - ✓ Verified - - )} - {/* Mobile: Show score inline */} -
- - +
+ {marketItems.map((item) => { + const timeLeftSec = parseTimeToSeconds(item.timeLeft) + const isUrgent = timeLeftSec < 3600 + + return ( +
+ {/* Domain */} +
+
+
+
+
+ {item.domain.split('.')[0]} + .{item.tld} +
+
+ = 80 ? "text-emerald-400" : "text-zinc-500")}> + Score {item.pounceScore} + + + + {item.timeLeft} + +
-
- {/* Pounce Score */} -
- -
+ {/* Score */} +
+ +
- {/* Price / Bid */} -
- - {formatPrice(item.price)} - - {item.priceType === 'bid' && ( - (bid) - )} - {item.numBids && item.numBids > 0 && ( -

{item.numBids} bids

- )} -
- - {/* Status / Time */} -
- -
- - {/* Source */} -
- -
- - {/* Actions */} -
- {/* Track Button */} - +
- {/* Action Button */} - - {item.isPounce ? 'Buy' : 'Bid'} - - + {/* Time */} +
+
+ + {item.timeLeft} +
+
+ + {/* Source */} +
+ +
+ + {/* Action */} +
+ + + + {item.isPounce ? 'Buy' : 'Bid'} + + +
-
- ))} + ) + })}
)}
- - {/* ================================================================ */} - {/* FOOTER INFO */} - {/* ================================================================ */} -
- - Showing {marketItems.length} of {auctions.length} total listings - - - - Data from GoDaddy, Sedo, NameJet, DropCatch - -
-
)