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
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:
@ -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",
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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",
|
||||
|
||||
Reference in New Issue
Block a user