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}
-
- 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 */} -
+