)
}
@@ -192,16 +193,18 @@ function StatusBadge({ status, timeLeft }: { status: 'auction' | 'instant'; time
return (
{timeLeft}
@@ -209,15 +212,70 @@ function StatusBadge({ status, timeLeft }: { status: 'auction' | 'instant'; time
)
}
-// Sortable Column Header
-function SortHeader({
- label,
- field,
- currentSort,
- currentDirection,
- onSort,
- align = 'left'
+// Toggle Button
+function ToggleButton({
+ active,
+ onClick,
+ children
}: {
+ active: boolean
+ onClick: () => void
+ children: React.ReactNode
+}) {
+ return (
+
+ )
+}
+
+// Dropdown Select
+function DropdownSelect({
+ value,
+ onChange,
+ options,
+}: {
+ value: string
+ onChange: (v: string) => void
+ options: { value: string; label: string }[]
+}) {
+ return (
+
+
+
+
+ )
+}
+
+// Sortable Column Header
+function SortableHeader({
+ label,
+ field,
+ currentSort,
+ currentDirection,
+ onSort,
+ align = 'left',
+}: {
label: string
field: SortField
currentSort: SortField
@@ -231,64 +289,26 @@ function SortHeader({
)
}
-// Toggle Button
-function ToggleButton({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode }) {
- return (
-
- )
-}
-
-// Dropdown
-function Dropdown({ value, onChange, options }: { value: string; onChange: (v: string) => void; options: { value: string; label: string }[] }) {
- return (
-
- )
-}
-
// ============================================================================
// MAIN COMPONENT
// ============================================================================
@@ -296,26 +316,27 @@ function Dropdown({ value, onChange, options }: { value: string; onChange: (v: s
export default function MarketPage() {
const { subscription } = useStore()
- // Data
+ // Data State
const [auctions, setAuctions] = useState
([])
const [loading, setLoading] = useState(true)
const [refreshing, setRefreshing] = useState(false)
- // Filters
+ // 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('')
- // Sorting
+ // Sort State
const [sortField, setSortField] = useState('score')
const [sortDirection, setSortDirection] = useState('desc')
- // Watchlist
+ // Watchlist State
const [trackedDomains, setTrackedDomains] = useState>(new Set())
const [trackingInProgress, setTrackingInProgress] = useState(null)
+ // Options
const TLD_OPTIONS = [
{ value: 'all', label: 'All TLDs' },
{ value: 'com', label: '.com' },
@@ -328,7 +349,7 @@ export default function MarketPage() {
const PRICE_OPTIONS = [
{ value: 'all', label: 'Any Price' },
{ value: '100', label: '< $100' },
- { value: '1000', label: '< $1k' },
+ { value: '1000', label: '< $1,000' },
{ value: '10000', label: 'High Roller' },
]
@@ -339,7 +360,7 @@ export default function MarketPage() {
const data = await api.getAuctions()
setAuctions(data.auctions || [])
} catch (error) {
- console.error('Failed to load:', error)
+ console.error('Failed to load market data:', error)
} finally {
setLoading(false)
}
@@ -360,234 +381,339 @@ export default function MarketPage() {
setSortDirection(d => d === 'asc' ? 'desc' : 'asc')
} else {
setSortField(field)
- setSortDirection(field === 'domain' || field === 'source' ? 'asc' : 'desc')
+ // 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)
setTrackedDomains(prev => new Set([...Array.from(prev), domain]))
} catch (error) {
- console.error('Failed:', error)
+ console.error('Failed to track:', error)
} finally {
setTrackingInProgress(null)
}
}, [trackedDomains, trackingInProgress])
- // Process Data
+ // Transform and Filter Data
const marketItems = useMemo(() => {
- let items: MarketItem[] = auctions.map(a => ({
- id: `${a.domain}-${a.platform}`,
- domain: a.domain,
- pounceScore: calculatePounceScore(a.domain, a.tld, a.num_bids, a.age_years ?? undefined),
- price: a.current_bid,
+ // Convert auctions to market items
+ 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,
- timeLeft: a.time_remaining,
- endTime: a.end_time,
- source: a.platform as any,
+ timeLeft: auction.time_remaining,
+ endTime: auction.end_time,
+ source: auction.platform as any,
isPounce: false,
- affiliateUrl: a.affiliate_url,
- tld: a.tld,
- numBids: a.num_bids,
+ affiliateUrl: auction.affiliate_url,
+ tld: auction.tld,
+ numBids: auction.num_bids,
}))
- // Filter
- if (hideSpam) items = items.filter(i => !isSpamDomain(i.domain, i.tld))
- if (pounceOnly) items = items.filter(i => i.isPounce)
- if (selectedTld !== 'all') items = items.filter(i => i.tld === selectedTld)
- if (selectedPrice !== 'all') {
- const max = parseInt(selectedPrice)
- items = selectedPrice === '10000'
- ? items.filter(i => i.price >= 10000)
- : items.filter(i => i.price < max)
- }
- if (searchQuery) {
- const q = searchQuery.toLowerCase()
- items = items.filter(i => i.domain.toLowerCase().includes(q))
+ // Apply Filters
+ let filtered = items
+
+ if (hideSpam) {
+ filtered = filtered.filter(item => !isSpamDomain(item.domain, item.tld))
}
- // Sort
- items.sort((a, b) => {
- const mult = sortDirection === 'asc' ? 1 : -1
+ 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)
+ }
+ }
+
+ if (searchQuery) {
+ const q = searchQuery.toLowerCase()
+ filtered = filtered.filter(item => item.domain.toLowerCase().includes(q))
+ }
+
+ // Apply 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 items
+ return filtered
}, [auctions, hideSpam, pounceOnly, selectedTld, selectedPrice, searchQuery, sortField, sortDirection])
+ // Stats
const stats = useMemo(() => ({
total: marketItems.length,
highScore: marketItems.filter(i => i.pounceScore >= 80).length,
- avgScore: marketItems.length > 0
- ? Math.round(marketItems.reduce((s, i) => s + i.pounceScore, 0) / marketItems.length) : 0,
+ endingSoon: marketItems.filter(i => {
+ const seconds = parseTimeToSeconds(i.timeLeft)
+ return seconds < 3600 // Less than 1 hour
+ }).length,
}), [marketItems])
- const formatPrice = (p: number) => p >= 1000 ? `$${(p / 1000).toFixed(1)}k` : `$${p.toLocaleString()}`
+ // 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 (
-
+
{/* ================================================================ */}
- {/* HEADER - New Professional Style */}
+ {/* HEADER - Live Feed Style */}
{/* ================================================================ */}
-
-
-
-
-
+
+ {/* Left: Title with Live Indicator */}
+
+
+
+
+
Live Market Feed
+
Updated in real-time
-
Market Feed
-
- {loading ? 'Loading...' : (
- <>
- {stats.total} domains
- •
- {stats.highScore} high-score
- •
- Avg score: {stats.avgScore}
- >
+
+ {/* Quick Stats Pills */}
+
+
+ {stats.total}
+ listings
+
+
+
+ {stats.highScore}
+ high score
+
+ {stats.endingSoon > 0 && (
+
+
+ {stats.endingSoon}
+ ending soon
+
)}
-
-
-
-
-
+
+
+ {/* Right: Refresh */}
+
{/* ================================================================ */}
{/* FILTER BAR */}
{/* ================================================================ */}
-
-
-
-
setHideSpam(!hideSpam)}>
-
- Hide Spam
-
-
-
setPounceOnly(!pounceOnly)}>
-
- Pounce Only
-
-
-
-
-
-
-
-
-
-
setSearchQuery(e.target.value)}
- placeholder="Search..."
- className="w-40 px-3 py-1.5 bg-zinc-800/30 border border-zinc-700/50 rounded
- text-xs text-zinc-300 placeholder:text-zinc-600
- focus:outline-none focus:border-emerald-500/50"
- />
+
+
+
+
+ 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"
+ />
+
+
{/* ================================================================ */}
- {/* TABLE */}
+ {/* MARKET TABLE */}
{/* ================================================================ */}
-
+
- {/* Header Row */}
-
+ {/* Table Header - Sortable */}
+
-
+
-
+
-
+
-
+
-
+
- Action
+ Action
- {/* Body */}
+ {/* Table Body */}
{loading ? (
-
-
+
+
) : marketItems.length === 0 ? (
-
-
-
No domains match your filters
+
+
+
No domains match your filters
+
Try adjusting your filter settings
) : (
-
+
{marketItems.map((item) => (
{/* Domain */}
-
- {item.isPounce &&
}
-
{item.domain}
- {item.verified && (
-
✓
+
+ {item.isPounce && (
+
)}
-
- {/* Mobile info */}
-
-
-
+
+
{item.domain}
+ {item.verified && (
+
+ ✓ Verified
+
+ )}
+ {/* Mobile: Show score inline */}
+
+
+
+
+
- {/* Score */}
+ {/* Pounce Score */}
- {/* Price */}
+ {/* Price / Bid */}
-
{formatPrice(item.price)}
- {item.priceType === 'bid' &&
bid}
+
+ {formatPrice(item.price)}
+
+ {item.priceType === 'bid' && (
+
(bid)
+ )}
{item.numBids && item.numBids > 0 && (
-
{item.numBids} bids
+
{item.numBids} bids
)}
- {/* Status */}
+ {/* Status / Time */}
@@ -598,38 +724,42 @@ export default function MarketPage() {
{/* Actions */}
-
+
+ {/* Track Button */}
+
+ {/* Action Button */}
{item.isPounce ? 'Buy' : 'Bid'}
-
+
@@ -638,11 +768,19 @@ export default function MarketPage() {
)}
- {/* Footer */}
-
-
{marketItems.length} of {auctions.length} listings
-
GoDaddy • Sedo • NameJet • DropCatch
+ {/* ================================================================ */}
+ {/* FOOTER INFO */}
+ {/* ================================================================ */}
+
+
+ Showing {marketItems.length} of {auctions.length} total listings
+
+
+
+ Data from GoDaddy, Sedo, NameJet, DropCatch
+
+
)
diff --git a/frontend/src/app/terminal/radar/page.tsx b/frontend/src/app/terminal/radar/page.tsx
index be78530..b4eb049 100644
--- a/frontend/src/app/terminal/radar/page.tsx
+++ b/frontend/src/app/terminal/radar/page.tsx
@@ -225,7 +225,7 @@ export default function RadarPage() {
0 ? `${availableDomains.length} alerts` : undefined}
icon={Eye}
accent={availableDomains.length > 0}