All tables: Unified sortable headers for Radar, Market, Watchlist, Intel
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled

This commit is contained in:
2025-12-13 14:40:21 +01:00
parent 09fb4e2931
commit d56081aca0
3 changed files with 202 additions and 32 deletions

View File

@ -12,6 +12,8 @@ import {
Zap,
ChevronLeft,
ChevronRight,
ChevronUp,
ChevronDown,
TrendingUp,
RefreshCw,
Clock,
@ -139,6 +141,10 @@ export default function MarketPage() {
const [menuOpen, setMenuOpen] = useState(false)
const [filtersOpen, setFiltersOpen] = useState(false)
// Sorting
const [sortField, setSortField] = useState<'domain' | 'score' | 'price' | 'time'>('time')
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc')
// Check auth on mount
useEffect(() => {
checkAuth()
@ -254,8 +260,32 @@ export default function MarketPage() {
filtered = filtered.filter(item => !isSpam(item.domain))
}
// 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.pounce_score || 0) - (b.pounce_score || 0))
case 'price': return mult * (a.price - b.price)
case 'time':
const aTime = a.end_time ? new Date(a.end_time).getTime() : Infinity
const bTime = b.end_time ? new Date(b.end_time).getTime() : Infinity
return mult * (aTime - bTime)
default: return 0
}
})
return filtered
}, [items, searchQuery, loading, hideSpam])
}, [items, searchQuery, loading, hideSpam, sortField, sortDirection])
const handleSort = useCallback((field: typeof sortField) => {
if (sortField === field) {
setSortDirection(d => d === 'asc' ? 'desc' : 'asc')
} else {
setSortField(field)
setSortDirection(field === 'time' || field === 'price' ? 'asc' : 'desc')
}
}, [sortField])
// Active filters count
const activeFiltersCount = [
@ -663,6 +693,27 @@ export default function MarketPage() {
<>
{/* Results List */}
<div className="space-y-px bg-white/[0.04] border border-white/[0.08]">
{/* Desktop Table Header */}
<div className="hidden lg:grid grid-cols-[1fr_80px_100px_80px_120px] gap-4 px-3 py-2 text-[10px] font-mono text-white/40 uppercase tracking-wider border-b border-white/[0.08]">
<button onClick={() => handleSort('domain')} className="flex items-center gap-1 hover:text-white/60 text-left">
Domain
{sortField === 'domain' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<button onClick={() => handleSort('score')} className="flex items-center gap-1 justify-center hover:text-white/60">
Score
{sortField === 'score' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<button onClick={() => handleSort('price')} className="flex items-center gap-1 justify-end hover:text-white/60">
Price
{sortField === 'price' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<button onClick={() => handleSort('time')} className="flex items-center gap-1 justify-center hover:text-white/60">
Time
{sortField === 'time' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<div className="text-right">Actions</div>
</div>
{filteredItems.map((item) => {
const timeLeftSec = getSecondsUntilEnd(item.end_time)
const isUrgent = timeLeftSec > 0 && timeLeftSec < 3600
@ -763,7 +814,7 @@ export default function MarketPage() {
</div>
{/* Desktop Row */}
<div className="hidden lg:flex items-center justify-between p-3 gap-4 group">
<div className="hidden lg:grid grid-cols-[1fr_80px_100px_80px_120px] gap-4 items-center p-3 group">
<div className="flex items-center gap-3 min-w-0 flex-1">
<div className={clsx(
"w-8 h-8 flex items-center justify-center border shrink-0",

View File

@ -1,6 +1,6 @@
'use client'
import { useEffect, useState, useCallback, useRef } from 'react'
import { useEffect, useState, useCallback, useRef, useMemo } from 'react'
import { useStore } from '@/lib/store'
import { api } from '@/lib/api'
import { Sidebar } from '@/components/Sidebar'
@ -22,6 +22,8 @@ import {
Settings,
Clock,
ChevronRight,
ChevronUp,
ChevronDown,
Sparkles,
Radio,
Activity,
@ -81,6 +83,10 @@ export default function RadarPage() {
// Mobile Menu State
const [menuOpen, setMenuOpen] = useState(false)
// Sorting for Auctions
const [auctionSort, setAuctionSort] = useState<'domain' | 'time' | 'bid'>('time')
const [auctionSortDir, setAuctionSortDir] = useState<'asc' | 'desc'>('asc')
// Check auth on mount
useEffect(() => {
checkAuth()
@ -112,6 +118,36 @@ export default function RadarPage() {
}
}, [authLoading, isAuthenticated, loadDashboardData])
// Sorted auctions
const sortedAuctions = useMemo(() => {
const mult = auctionSortDir === 'asc' ? 1 : -1
return [...hotAuctions].sort((a, b) => {
switch (auctionSort) {
case 'domain': return mult * a.domain.localeCompare(b.domain)
case 'bid': return mult * (a.current_bid - b.current_bid)
case 'time':
// Parse time_remaining like "2h 30m" or "5d 12h"
const parseTime = (t: string) => {
const d = t.match(/(\d+)d/)?.[1] || 0
const h = t.match(/(\d+)h/)?.[1] || 0
const m = t.match(/(\d+)m/)?.[1] || 0
return Number(d) * 86400 + Number(h) * 3600 + Number(m) * 60
}
return mult * (parseTime(a.time_remaining) - parseTime(b.time_remaining))
default: return 0
}
})
}, [hotAuctions, auctionSort, auctionSortDir])
const handleAuctionSort = useCallback((field: typeof auctionSort) => {
if (auctionSort === field) {
setAuctionSortDir(d => d === 'asc' ? 'desc' : 'asc')
} else {
setAuctionSort(field)
setAuctionSortDir(field === 'bid' ? 'desc' : 'asc')
}
}, [auctionSort])
// Search
const handleSearch = useCallback(async (domainInput: string) => {
if (!domainInput.trim()) { setSearchResult(null); return }
@ -498,7 +534,7 @@ export default function RadarPage() {
<div className="flex items-center gap-2">
<Activity className="w-4 h-4 text-accent" />
<span className="text-xs font-bold text-white uppercase tracking-wider">Live Auctions</span>
<span className="text-[10px] font-mono text-white/30">({hotAuctions.length})</span>
<span className="text-[10px] font-mono text-white/30">({sortedAuctions.length})</span>
</div>
<Link href="/terminal/market" className="text-[10px] font-mono text-accent hover:text-white transition-colors flex items-center gap-1">
View all
@ -510,41 +546,68 @@ export default function RadarPage() {
<div className="flex items-center justify-center py-12">
<Loader2 className="w-6 h-6 text-accent animate-spin" />
</div>
) : hotAuctions.length > 0 ? (
) : sortedAuctions.length > 0 ? (
<div className="space-y-px bg-white/[0.04] border border-white/[0.08]">
{hotAuctions.map((auction, i) => (
{/* Desktop Table Header */}
<div className="hidden lg:grid grid-cols-[1fr_100px_80px_100px_40px] gap-4 px-3 py-2 text-[10px] font-mono text-white/40 uppercase tracking-wider border-b border-white/[0.08]">
<button onClick={() => handleAuctionSort('domain')} className="flex items-center gap-1 hover:text-white/60 text-left">
Domain
{auctionSort === 'domain' && (auctionSortDir === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<div className="text-center">Platform</div>
<button onClick={() => handleAuctionSort('time')} className="flex items-center gap-1 justify-center hover:text-white/60">
Time
{auctionSort === 'time' && (auctionSortDir === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<button onClick={() => handleAuctionSort('bid')} className="flex items-center gap-1 justify-end hover:text-white/60">
Bid
{auctionSort === 'bid' && (auctionSortDir === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<div></div>
</div>
{sortedAuctions.map((auction, i) => (
<a
key={i}
href={auction.affiliate_url || '#'}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-between p-3 bg-[#020202] hover:bg-white/[0.02] active:bg-white/[0.03] transition-all group"
className="grid grid-cols-[1fr_100px_80px_100px_40px] gap-4 items-center p-3 bg-[#020202] hover:bg-white/[0.02] active:bg-white/[0.03] transition-all group"
>
<div className="flex items-center gap-3 min-w-0 flex-1">
{/* Domain */}
<div className="flex items-center gap-3 min-w-0">
<div className="w-8 h-8 bg-white/[0.02] border border-white/[0.06] flex items-center justify-center shrink-0">
<Gavel className="w-4 h-4 text-white/40 group-hover:text-accent transition-colors" />
</div>
<div className="min-w-0 flex-1">
<div className="text-sm font-bold text-white font-mono truncate group-hover:text-accent transition-colors">
{auction.domain}
</div>
<div className="flex items-center gap-2 text-[10px] font-mono text-white/30">
<span className="uppercase">{auction.platform}</span>
<span className="text-white/10">|</span>
<span className="flex items-center gap-1">
</div>
{/* Platform */}
<div className="text-center">
<span className="text-[10px] font-mono text-white/40 uppercase">{auction.platform}</span>
</div>
{/* Time */}
<div className="text-center">
<span className="text-xs font-mono text-white/50 flex items-center justify-center gap-1">
<Clock className="w-3 h-3" />
{auction.time_remaining}
</span>
</div>
</div>
</div>
<div className="text-right shrink-0 ml-3">
<div className="text-base font-bold text-accent font-mono">
{/* Bid */}
<div className="text-right">
<div className="text-sm font-bold text-accent font-mono">
${auction.current_bid.toLocaleString()}
</div>
<div className="text-[9px] font-mono text-white/20 uppercase">Current Bid</div>
</div>
<ExternalLink className="w-4 h-4 text-white/10 group-hover:text-accent ml-3 shrink-0" />
{/* Link */}
<div className="flex justify-end">
<ExternalLink className="w-4 h-4 text-white/20 group-hover:text-accent transition-colors" />
</div>
</a>
))}
</div>

View File

@ -31,7 +31,9 @@ import {
Coins,
Tag,
Zap,
Search
Search,
ChevronUp,
ChevronDown
} from 'lucide-react'
import clsx from 'clsx'
import Link from 'next/link'
@ -81,6 +83,10 @@ export default function WatchlistPage() {
const [selectedDomain, setSelectedDomain] = useState<number | null>(null)
const [filter, setFilter] = useState<'all' | 'available' | 'expiring'>('all')
// Sorting
const [sortField, setSortField] = useState<'domain' | 'status' | 'health' | 'expiry'>('domain')
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc')
// Mobile Menu
const [menuOpen, setMenuOpen] = useState(false)
@ -103,19 +109,47 @@ export default function WatchlistPage() {
// Filtered
const filteredDomains = useMemo(() => {
if (!domains) return []
return domains.filter(d => {
let filtered = domains.filter(d => {
if (filter === 'available') return d.is_available
if (filter === 'expiring') {
const days = getDaysUntilExpiry(d.expiration_date)
return days !== null && days <= 30 && days > 0
}
return true
}).sort((a, b) => {
if (a.is_available && !b.is_available) return -1
if (!a.is_available && b.is_available) return 1
return a.name.localeCompare(b.name)
})
}, [domains, filter])
// Sort
const mult = sortDirection === 'asc' ? 1 : -1
filtered.sort((a, b) => {
switch (sortField) {
case 'domain': return mult * a.name.localeCompare(b.name)
case 'status':
if (a.is_available && !b.is_available) return mult * -1
if (!a.is_available && b.is_available) return mult * 1
return 0
case 'health':
const aHealth = healthReports[a.id]?.score || 0
const bHealth = healthReports[b.id]?.score || 0
return mult * (aHealth - bHealth)
case 'expiry':
const aExp = a.expiration_date ? new Date(a.expiration_date).getTime() : Infinity
const bExp = b.expiration_date ? new Date(b.expiration_date).getTime() : Infinity
return mult * (aExp - bExp)
default: return 0
}
})
return filtered
}, [domains, filter, sortField, sortDirection, healthReports])
const handleSortWatch = useCallback((field: typeof sortField) => {
if (sortField === field) {
setSortDirection(d => d === 'asc' ? 'desc' : 'asc')
} else {
setSortField(field)
setSortDirection('asc')
}
}, [sortField])
// Handlers
const handleAdd = useCallback(async (e: React.FormEvent) => {
@ -395,6 +429,28 @@ export default function WatchlistPage() {
</div>
) : (
<div className="space-y-px bg-white/[0.04] border border-white/[0.08]">
{/* Desktop Table Header */}
<div className="hidden lg:grid grid-cols-[1fr_80px_90px_90px_60px_100px] gap-4 px-3 py-2 text-[10px] font-mono text-white/40 uppercase tracking-wider border-b border-white/[0.08]">
<button onClick={() => handleSortWatch('domain')} className="flex items-center gap-1 hover:text-white/60 text-left">
Domain
{sortField === 'domain' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<button onClick={() => handleSortWatch('status')} className="flex items-center gap-1 justify-center hover:text-white/60">
Status
{sortField === 'status' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<button onClick={() => handleSortWatch('health')} className="flex items-center gap-1 justify-center hover:text-white/60">
Health
{sortField === 'health' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<button onClick={() => handleSortWatch('expiry')} className="flex items-center gap-1 justify-center hover:text-white/60">
Expiry
{sortField === 'expiry' && (sortDirection === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />)}
</button>
<div className="text-center">Alert</div>
<div className="text-right">Actions</div>
</div>
{filteredDomains.map((domain) => {
const health = healthReports[domain.id]
const healthStatus = health?.status || 'unknown'
@ -510,7 +566,7 @@ export default function WatchlistPage() {
</div>
{/* Desktop Row */}
<div className="hidden lg:flex items-center justify-between p-3 gap-4 group">
<div className="hidden lg:grid grid-cols-[1fr_80px_90px_90px_60px_100px] gap-4 items-center p-3 group">
<div className="flex items-center gap-3 min-w-0 flex-1">
<div className={clsx(
"w-8 h-8 flex items-center justify-center border shrink-0",