diff --git a/frontend/src/app/terminal/market/page.tsx b/frontend/src/app/terminal/market/page.tsx
index 49e9938..f28fa7a 100644
--- a/frontend/src/app/terminal/market/page.tsx
+++ b/frontend/src/app/terminal/market/page.tsx
@@ -12,6 +12,8 @@ import {
Zap,
ChevronLeft,
ChevronRight,
+ ChevronUp,
+ ChevronDown,
TrendingUp,
RefreshCw,
Clock,
@@ -138,6 +140,10 @@ export default function MarketPage() {
// Mobile Menu & Filters
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(() => {
@@ -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 */}
+ {/* Desktop Table Header */}
+
+
+
+
+
+
Actions
+
+
{filteredItems.map((item) => {
const timeLeftSec = getSecondsUntilEnd(item.end_time)
const isUrgent = timeLeftSec > 0 && timeLeftSec < 3600
@@ -763,7 +814,7 @@ export default function MarketPage() {
{/* Desktop Row */}
-
+
('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() {
Live Auctions
-
({hotAuctions.length})
+
({sortedAuctions.length})
View all
@@ -510,41 +546,68 @@ export default function RadarPage() {
- ) : hotAuctions.length > 0 ? (
+ ) : sortedAuctions.length > 0 ? (
- {hotAuctions.map((auction, i) => (
+ {/* Desktop Table Header */}
+
+
+
Platform
+
+
+
+
+
+ {sortedAuctions.map((auction, i) => (
-
+ {/* Domain */}
+
-
-
- {auction.domain}
-
-
- {auction.platform}
- |
-
-
- {auction.time_remaining}
-
-
+
+ {auction.domain}
-
-
+
+ {/* Platform */}
+
+ {auction.platform}
+
+
+ {/* Time */}
+
+
+
+ {auction.time_remaining}
+
+
+
+ {/* Bid */}
+
+
${auction.current_bid.toLocaleString()}
-
Current Bid
-
+
+ {/* Link */}
+
+
+
))}
diff --git a/frontend/src/app/terminal/watchlist/page.tsx b/frontend/src/app/terminal/watchlist/page.tsx
index 1ef96ad..94fbe53 100755
--- a/frontend/src/app/terminal/watchlist/page.tsx
+++ b/frontend/src/app/terminal/watchlist/page.tsx
@@ -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
(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() {
) : (
+ {/* Desktop Table Header */}
+
+
+
+
+
+
Alert
+
Actions
+
+
{filteredDomains.map((domain) => {
const health = healthReports[domain.id]
const healthStatus = health?.status || 'unknown'
@@ -510,7 +566,7 @@ export default function WatchlistPage() {
{/* Desktop Row */}
-