Skip to content

Commit cf3d501

Browse files
committed
improved url param handling
1 parent 1cbacd0 commit cf3d501

File tree

4 files changed

+363
-69
lines changed

4 files changed

+363
-69
lines changed

src/features/feeds/components/FeedList.tsx

Lines changed: 91 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ import { getFeedCategories } from "../../../db/feedCategories.js"
1313
import SectionWrapper from "~/components/SectionWrapper/SectionWrapper.tsx"
1414
import button from "@chainlink/design-system/button.module.css"
1515
import { updateTableOfContents } from "~/components/TableOfContents/tocStore.ts"
16-
import alertIcon from "../../../components/Alert/Assets/alert-icon.svg"
1716
import { ChainSelector } from "~/components/ChainSelector/ChainSelector.tsx"
1817
import { isFeedVisible } from "../utils/feedVisibility.ts"
18+
import { updateUrlClean, clearFilters } from "./urlStateHelpers.ts"
1919

2020
export type DataFeedType =
2121
| "default"
@@ -260,10 +260,16 @@ export const FeedList = ({
260260
}, [])
261261

262262
// Regular query string states
263-
const [searchValue, setSearchValue] = useQueryString("search", "")
264-
const [testnetSearchValue, setTestnetSearchValue] = useQueryString("testnetSearch", "")
265-
const [selectedFeedCategories, setSelectedFeedCategories] = useQueryString("categories", [])
266-
const [currentPage, setCurrentPage] = useQueryString("page", "1")
263+
const [searchValue, setSearchValue] = useQueryString("search")
264+
const [testnetSearchValue, setTestnetSearchValue] = useQueryString("testnetSearch")
265+
const [selectedFeedCategoriesRaw, setSelectedFeedCategories] = useQueryString("categories")
266+
// Ensure categories is always an array
267+
const selectedFeedCategories = Array.isArray(selectedFeedCategoriesRaw)
268+
? selectedFeedCategoriesRaw
269+
: selectedFeedCategoriesRaw
270+
? [selectedFeedCategoriesRaw]
271+
: []
272+
const [currentPage, setCurrentPage] = useQueryString("page")
267273

268274
// Initialize all other states
269275
const [showCategoriesDropdown, setShowCategoriesDropdown] = useState<boolean>(false)
@@ -283,10 +289,26 @@ export const FeedList = ({
283289
const setTestnetStreamCategoryFilter = (next: StreamsRwaFeedTypeValue) => {
284290
setTestnetStreamCategoryFilterParam(next === "all" ? [] : next)
285291
}
286-
const [showExtraDetails, setShowExtraDetails] = useState(false)
292+
293+
// Checkbox states backed by URL params
294+
const [showDetailsParam, setShowDetailsParam] = useQueryString("showDetails")
295+
const showExtraDetails = showDetailsParam === "true"
296+
const setShowExtraDetails = (value: boolean) => {
297+
setShowDetailsParam(value ? "true" : "")
298+
updateUrlClean({ showDetails: value || undefined })
299+
}
300+
301+
const [showSvrParam, setShowSvrParam] = useQueryString("showSvr")
302+
const showOnlySVR = showSvrParam === "true"
303+
const setShowOnlySVR = (value: boolean) => {
304+
setShowSvrParam(value ? "true" : "")
305+
updateUrlClean({ showSvr: value || undefined })
306+
if (value) paginate(1)
307+
}
308+
309+
// MVR and DEX filters are not in URL (too specialized)
287310
const [showOnlyMVRFeeds, setShowOnlyMVRFeeds] = useState(false)
288311
const [showOnlyMVRFeedsTestnet, setShowOnlyMVRFeedsTestnet] = useState(false)
289-
const [showOnlySVR, setShowOnlySVR] = useState(false)
290312
const [showOnlyDEXFeeds, setShowOnlyDEXFeeds] = useState(false)
291313
const [showOnlyDEXFeedsTestnet, setShowOnlyDEXFeedsTestnet] = useState(false)
292314
const [rwaSchemaFilterParam, setRwaSchemaFilterParam] = useQueryString("schema")
@@ -313,16 +335,26 @@ export const FeedList = ({
313335
})
314336
}
315337
const closeAllDropdowns = () => setOpenDropdownId(null)
316-
const paginate = (pageNumber) => setCurrentPage(String(pageNumber))
338+
const paginate = (pageNumber) => {
339+
const pageStr = String(pageNumber)
340+
setCurrentPage(pageStr)
341+
updateUrlClean({ page: pageNumber === 1 ? undefined : pageNumber })
342+
}
317343
const addrPerPage = ecosystem === "deprecating" && isStreams ? 10 : ecosystem === "deprecating" ? 10000 : 8
318-
const lastAddr = Number(currentPage) * addrPerPage
344+
const currentPageNum = Number(currentPage) || 1
345+
const lastAddr = currentPageNum * addrPerPage
319346
const firstAddr = lastAddr - addrPerPage
320347

321348
// Pagination for testnet table
322-
const [testnetCurrentPage, setTestnetCurrentPage] = useQueryString("testnetPage", "1")
323-
const testnetPaginate = (pageNumber) => setTestnetCurrentPage(String(pageNumber))
349+
const [testnetCurrentPage, setTestnetCurrentPage] = useQueryString("testnetPage")
350+
const testnetPaginate = (pageNumber) => {
351+
const pageStr = String(pageNumber)
352+
setTestnetCurrentPage(pageStr)
353+
updateUrlClean({ testnetPage: pageNumber === 1 ? undefined : pageNumber })
354+
}
324355
const testnetAddrPerPage = ecosystem === "deprecating" && isStreams ? 10 : ecosystem === "deprecating" ? 10000 : 8
325-
const testnetLastAddr = Number(testnetCurrentPage) * testnetAddrPerPage
356+
const testnetPageNum = Number(testnetCurrentPage) || 1
357+
const testnetLastAddr = testnetPageNum * testnetAddrPerPage
326358
const testnetFirstAddr = testnetLastAddr - testnetAddrPerPage
327359

328360
// Dynamic feed categories loaded from Supabase
@@ -500,56 +532,54 @@ export const FeedList = ({
500532
function handleNetworkSelect(chain: Chain) {
501533
closeAllDropdowns()
502534
if (!isStreams) {
503-
const params = new URLSearchParams(window.location.search)
504-
params.set("network", chain.page)
505-
// Clear hash when changing networks
506-
const newUrl = window.location.pathname + "?" + params.toString()
507-
window.history.replaceState({ path: newUrl }, "", newUrl)
508535
setCurrentNetwork(chain.page)
536+
// Clear all filters and pagination when switching networks
537+
setSearchValue("")
538+
setTestnetSearchValue("")
539+
setSelectedFeedCategories([])
540+
setCurrentPage("")
541+
setTestnetCurrentPage("")
542+
setShowOnlyMVRFeeds(false)
543+
setShowOnlyMVRFeedsTestnet(false)
544+
// Update URL with just the network (and networkType if not mainnet)
545+
const params = new URLSearchParams(window.location.search)
546+
const networkType = params.get("networkType")
547+
updateUrlClean({
548+
network: chain.page,
549+
networkType: networkType === "testnet" ? "testnet" : undefined,
550+
search: undefined,
551+
testnetSearch: undefined,
552+
page: undefined,
553+
testnetPage: undefined,
554+
})
509555
}
510-
setSearchValue("")
511-
setSelectedFeedCategories([])
512-
setCurrentPage("1")
513-
setShowOnlyMVRFeeds(false)
514-
setShowOnlyMVRFeedsTestnet(false)
515556
}
516557

517558
// Network type change handler for testnet/mainnet switching
518559
function handleNetworkTypeChange(networkType: "mainnet" | "testnet") {
519560
closeAllDropdowns()
520-
// Update the selected network type
521561
setSelectedNetworkType(networkType)
522562

523-
// Update URL parameters to reflect network type state
524-
if (typeof window !== "undefined") {
525-
const params = new URLSearchParams(window.location.search)
526-
527-
if (networkType === "testnet") {
528-
// Set networkType parameter to testnet
529-
params.set("networkType", "testnet")
530-
// Ensure testnetPage is set (default to 1 if not present)
531-
if (!params.get("testnetPage")) {
532-
params.set("testnetPage", "1")
533-
}
534-
} else {
535-
// Remove testnet-specific parameters when switching to mainnet
536-
params.delete("networkType")
537-
params.delete("testnetSearch")
538-
// Keep testnetPage for potential future navigation
539-
}
540-
541-
const newUrl = window.location.pathname + "?" + params.toString()
542-
window.history.replaceState({ path: newUrl }, "", newUrl)
543-
}
544-
545563
// Reset filters and pagination when switching network types
546564
setSearchValue("")
547565
setTestnetSearchValue("")
548566
setSelectedFeedCategories([])
549-
setCurrentPage("1")
550-
setTestnetCurrentPage("1")
567+
setCurrentPage("")
568+
setTestnetCurrentPage("")
551569
setShowOnlyMVRFeeds(false)
552570
setShowOnlyMVRFeedsTestnet(false)
571+
572+
// Update URL with clean params
573+
const params = new URLSearchParams(window.location.search)
574+
const network = params.get("network")
575+
updateUrlClean({
576+
network: network || undefined,
577+
networkType: networkType === "testnet" ? "testnet" : undefined,
578+
search: undefined,
579+
testnetSearch: undefined,
580+
page: undefined,
581+
testnetPage: undefined,
582+
})
553583
}
554584

555585
const handleCategorySelection = (category) => {
@@ -567,18 +597,14 @@ export const FeedList = ({
567597
}
568598

569599
useEffect(() => {
600+
// Clean up empty search params
570601
if (searchValue === "") {
571-
const searchParams = new URLSearchParams(window.location.search)
572-
searchParams.delete("search")
573-
const hashFragment = window.location.hash
574-
const newUrl = window.location.pathname + "?" + searchParams.toString() + hashFragment
575-
window.history.replaceState({ path: newUrl }, "", newUrl)
576-
const inputElement = document.getElementById("search") as HTMLInputElement
577-
if (inputElement) {
578-
inputElement.placeholder = "Search"
579-
}
602+
updateUrlClean({ search: undefined })
580603
}
581-
}, [chainMetadata.processedData, searchValue])
604+
if (testnetSearchValue === "") {
605+
updateUrlClean({ testnetSearch: undefined })
606+
}
607+
}, [searchValue, testnetSearchValue])
582608

583609
const useOutsideAlerter = (ref: RefObject<HTMLDivElement>) => {
584610
useEffect(() => {
@@ -1082,7 +1108,7 @@ export const FeedList = ({
10821108
lastAddr={lastAddr}
10831109
firstAddr={firstAddr}
10841110
addrPerPage={addrPerPage}
1085-
currentPage={Number(currentPage)}
1111+
currentPage={currentPageNum}
10861112
paginate={paginate}
10871113
searchValue={typeof searchValue === "string" ? searchValue : ""}
10881114
/>
@@ -1201,7 +1227,7 @@ export const FeedList = ({
12011227
firstAddr={testnetFirstAddr}
12021228
lastAddr={testnetLastAddr}
12031229
addrPerPage={testnetAddrPerPage}
1204-
currentPage={Number(testnetCurrentPage)}
1230+
currentPage={testnetPageNum}
12051231
paginate={testnetPaginate}
12061232
searchValue={typeof testnetSearchValue === "string" ? testnetSearchValue : ""}
12071233
/>
@@ -1294,7 +1320,7 @@ export const FeedList = ({
12941320
lastAddr={lastAddr}
12951321
firstAddr={firstAddr}
12961322
addrPerPage={addrPerPage}
1297-
currentPage={Number(currentPage)}
1323+
currentPage={currentPageNum}
12981324
paginate={paginate}
12991325
searchValue={typeof searchValue === "string" ? searchValue : ""}
13001326
/>
@@ -1485,7 +1511,7 @@ export const FeedList = ({
14851511
type="checkbox"
14861512
style="width:15px;height:15px;display:inline;margin-right:8px;"
14871513
checked={showExtraDetails}
1488-
onChange={() => setShowExtraDetails((old) => !old)}
1514+
onChange={() => setShowExtraDetails(!showExtraDetails)}
14891515
/>
14901516
Show more details
14911517
</label>
@@ -1512,10 +1538,7 @@ export const FeedList = ({
15121538
type="checkbox"
15131539
style="width:15px;height:15px;display:inline;margin-right:8px;"
15141540
checked={showOnlySVR}
1515-
onChange={() => {
1516-
setShowOnlySVR((old) => !old)
1517-
setCurrentPage("1")
1518-
}}
1541+
onChange={() => setShowOnlySVR(!showOnlySVR)}
15191542
/>
15201543
Show Smart Value Recapture (SVR) feeds
15211544
</label>
@@ -1540,7 +1563,7 @@ export const FeedList = ({
15401563
lastAddr={lastAddr}
15411564
firstAddr={firstAddr}
15421565
addrPerPage={addrPerPage}
1543-
currentPage={Number(currentPage)}
1566+
currentPage={currentPageNum}
15441567
paginate={paginate}
15451568
searchValue={typeof searchValue === "string" ? searchValue : ""}
15461569
/>
@@ -1647,7 +1670,7 @@ export const FeedList = ({
16471670
type="checkbox"
16481671
style="width:15px;height:15px;display:inline;margin-right:8px;"
16491672
checked={showExtraDetails}
1650-
onChange={() => setShowExtraDetails((old) => !old)}
1673+
onChange={() => setShowExtraDetails(!showExtraDetails)}
16511674
/>
16521675
Show more details
16531676
</label>
@@ -1717,7 +1740,7 @@ export const FeedList = ({
17171740
firstAddr={testnetFirstAddr}
17181741
lastAddr={testnetLastAddr}
17191742
addrPerPage={testnetAddrPerPage}
1720-
currentPage={Number(testnetCurrentPage)}
1743+
currentPage={testnetPageNum}
17211744
paginate={testnetPaginate}
17221745
searchValue={typeof testnetSearchValue === "string" ? testnetSearchValue : ""}
17231746
/>
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* URL state management helpers for feed tables
3+
* Clean up empty and default values from URLs
4+
*/
5+
6+
/**
7+
* Update URL with only non-empty, non-default values
8+
* Removes clutter like search=&page=1
9+
*/
10+
export function updateUrlClean(updates: Record<string, string | number | boolean | undefined>) {
11+
if (typeof window === "undefined") return
12+
13+
const params = new URLSearchParams(window.location.search)
14+
15+
// Apply updates
16+
Object.entries(updates).forEach(([key, value]) => {
17+
// Remove param if undefined, empty string, or default value
18+
if (
19+
value === undefined ||
20+
value === "" ||
21+
(key === "page" && value === 1) ||
22+
(key === "page" && value === "1") ||
23+
(key === "testnetPage" && value === 1) ||
24+
(key === "testnetPage" && value === "1") ||
25+
(key.includes("Schema") && value === "all") ||
26+
(key.includes("FeedType") && value === "all") ||
27+
value === false
28+
) {
29+
params.delete(key)
30+
} else {
31+
params.set(key, String(value))
32+
}
33+
})
34+
35+
// Build URL with params in logical order: network → networkType → filters → pagination
36+
const orderedParams = new URLSearchParams()
37+
const order = [
38+
"network",
39+
"networkType",
40+
"search",
41+
"testnetSearch",
42+
"categories",
43+
"showSvr",
44+
"showDetails",
45+
"feedType",
46+
"testnetFeedType",
47+
"schema",
48+
"testnetSchema",
49+
"page",
50+
"testnetPage",
51+
]
52+
53+
order.forEach((key) => {
54+
if (params.has(key)) {
55+
const values = params.getAll(key)
56+
values.forEach((v) => orderedParams.append(key, v))
57+
}
58+
})
59+
60+
const queryString = orderedParams.toString()
61+
const newUrl = window.location.pathname + (queryString ? "?" + queryString : "") + window.location.hash
62+
window.history.replaceState({ path: newUrl }, "", newUrl)
63+
}
64+
65+
/**
66+
* Clear all filter-related params, optionally keeping network selection
67+
*/
68+
export function clearFilters(keepNetwork = true) {
69+
if (typeof window === "undefined") return
70+
71+
const params = new URLSearchParams(window.location.search)
72+
const network = params.get("network")
73+
const networkType = params.get("networkType")
74+
75+
// Start fresh
76+
const newParams = new URLSearchParams()
77+
78+
if (keepNetwork) {
79+
if (network) newParams.set("network", network)
80+
if (networkType && networkType !== "mainnet") newParams.set("networkType", networkType)
81+
}
82+
83+
const queryString = newParams.toString()
84+
const newUrl = window.location.pathname + (queryString ? "?" + queryString : "") + window.location.hash
85+
window.history.replaceState({ path: newUrl }, "", newUrl)
86+
}

0 commit comments

Comments
 (0)