|
1 | 1 | import { LayoutGrid } from "lucide-react"; |
2 | 2 | import { useEffect, useMemo, useRef, useState } from "react"; |
3 | 3 | import { useDebounce } from "../hooks/useDebounce"; |
| 4 | +import { useSearchParams } from "react-router-dom"; |
4 | 5 | import { Campaign, CampaignStatus } from "../types/campaign"; |
5 | 6 | import { EmptyState } from "./EmptyState"; |
6 | 7 | import { AssetFilterDropdown } from "./AssetFilterDropdown"; |
@@ -30,6 +31,7 @@ interface CampaignsTableProps { |
30 | 31 | selectedCampaignId: string | null; |
31 | 32 | onSelect: (campaignId: string) => void; |
32 | 33 | onSearchChange?: (query: string) => void; |
| 34 | + onSortChange?: (sort: SortOption, order: 'asc' | 'desc') => void; |
33 | 35 | onLoadMore?: () => void; |
34 | 36 | hasMore?: boolean; |
35 | 37 | isLoadingMore?: boolean; |
@@ -64,19 +66,36 @@ export function CampaignsTable({ |
64 | 66 | selectedCampaignId, |
65 | 67 | onSelect, |
66 | 68 | onSearchChange, |
| 69 | + onSortChange, |
67 | 70 | onLoadMore, |
68 | 71 | hasMore = false, |
69 | 72 | isLoadingMore = false, |
70 | 73 | isLoading = false, |
71 | 74 | invalidUrlCampaignId = null, |
72 | 75 | }: CampaignsTableProps) { |
| 76 | + const [searchParams, setSearchParams] = useSearchParams(); |
| 77 | + const urlSort = (searchParams.get('sort') as SortOption | null) ?? 'newest'; |
| 78 | + const urlOrder = (searchParams.get('order') as 'asc' | 'desc' | null) ?? 'desc'; |
| 79 | + const VALID_SORTS: SortOption[] = ['newest', 'deadline', 'percentFunded', 'totalPledged']; |
| 80 | + const sortBy: SortOption = VALID_SORTS.includes(urlSort) ? urlSort : 'newest'; |
73 | 81 | const [assetCode, setAssetCode] = useState(""); |
74 | 82 | const [statusFilter, setStatusFilter] = useState<StatusFilterValue>(""); |
75 | | - const [sortBy, setSortBy] = useState<SortOption>("newest"); |
76 | 83 | const [searchQuery, setSearchQuery] = useState(""); |
77 | 84 | const loadMoreRef = useRef<HTMLDivElement | null>(null); |
78 | 85 | const debouncedSearchQuery = useDebounce(searchQuery, 300); |
79 | 86 |
|
| 87 | + function handleSortChange(newSort: SortOption) { |
| 88 | + // Toggle order if clicking same field, else default to desc |
| 89 | + const newOrder = newSort === sortBy && urlOrder === 'desc' ? 'asc' : 'desc'; |
| 90 | + setSearchParams((prev) => { |
| 91 | + const next = new URLSearchParams(prev); |
| 92 | + next.set('sort', newSort); |
| 93 | + next.set('order', newOrder); |
| 94 | + return next; |
| 95 | + }, { replace: true }); |
| 96 | + onSortChange?.(newSort, newOrder); |
| 97 | + } |
| 98 | + |
80 | 99 | useEffect(() => { |
81 | 100 | onSearchChange?.(debouncedSearchQuery); |
82 | 101 | }, [debouncedSearchQuery, onSearchChange]); |
@@ -131,6 +150,7 @@ export function CampaignsTable({ |
131 | 150 | statusFilter, |
132 | 151 | "", // server-side search, no client search |
133 | 152 | ); |
| 153 | + // Server already sorted; only apply client-side sort as a tie-break fallback |
134 | 154 | return sortCampaigns(filtered, sortBy); |
135 | 155 | }, [campaigns, assetCode, statusFilter, sortBy]); |
136 | 156 |
|
@@ -229,7 +249,7 @@ export function CampaignsTable({ |
229 | 249 | <span>Sort:</span> |
230 | 250 | <SortDropdown |
231 | 251 | value={sortBy} |
232 | | - onChange={setSortBy} |
| 252 | + onChange={handleSortChange} |
233 | 253 | disabled={isLoading} |
234 | 254 | /> |
235 | 255 | </label> |
|
0 commit comments