WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit f3bdf82

Browse files
authored
reuse multiQueryParams pattern to persist landing filter, dropdown, and page # (#58558)
1 parent 03945b8 commit f3bdf82

File tree

5 files changed

+235
-72
lines changed

5 files changed

+235
-72
lines changed

src/landings/components/bespoke/BespokeLanding.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { LandingHero } from '@/landings/components/shared/LandingHero'
44
import { ArticleGrid } from '@/landings/components/shared/LandingArticleGridWithFilter'
55
import { UtmPreserver } from '@/frame/components/UtmPreserver'
66
import { LandingCarousel } from '@/landings/components/shared/LandingCarousel'
7+
import { useMultiQueryParams } from '@/search/components/hooks/useMultiQueryParams'
78

89
export const BespokeLanding = () => {
910
const {
@@ -16,6 +17,10 @@ export const BespokeLanding = () => {
1617
includedCategories,
1718
landingType,
1819
} = useLandingContext()
20+
const { params, updateParams } = useMultiQueryParams({
21+
useHistory: true,
22+
excludeFromHistory: ['articles-filter'],
23+
})
1924

2025
return (
2126
<DefaultLayout>
@@ -29,6 +34,8 @@ export const BespokeLanding = () => {
2934
tocItems={tocItems}
3035
includedCategories={includedCategories}
3136
landingType={landingType}
37+
params={params}
38+
updateParams={updateParams}
3239
/>
3340
</div>
3441
</div>

src/landings/components/discovery/DiscoveryLanding.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { LandingHero } from '@/landings/components/shared/LandingHero'
44
import { ArticleGrid } from '@/landings/components/shared/LandingArticleGridWithFilter'
55
import { LandingCarousel } from '@/landings/components/shared/LandingCarousel'
66
import { UtmPreserver } from '@/frame/components/UtmPreserver'
7+
import { useMultiQueryParams } from '@/search/components/hooks/useMultiQueryParams'
78

89
export const DiscoveryLanding = () => {
910
const {
@@ -16,6 +17,10 @@ export const DiscoveryLanding = () => {
1617
includedCategories,
1718
landingType,
1819
} = useLandingContext()
20+
const { params, updateParams } = useMultiQueryParams({
21+
useHistory: true,
22+
excludeFromHistory: ['articles-filter'],
23+
})
1924

2025
return (
2126
<DefaultLayout>
@@ -28,6 +33,8 @@ export const DiscoveryLanding = () => {
2833
tocItems={tocItems}
2934
includedCategories={includedCategories}
3035
landingType={landingType}
36+
params={params}
37+
updateParams={updateParams}
3138
/>
3239
</div>
3340
</div>

src/landings/components/shared/LandingArticleGridWithFilter.tsx

Lines changed: 121 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@ import { Link } from '@/frame/components/Link'
77
import { useTranslation } from '@/languages/components/useTranslation'
88
import { ArticleCardItems, ChildTocItem, TocItem } from '@/landings/types'
99
import { LandingType } from '@/landings/context/LandingContext'
10+
import type { QueryParams } from '@/search/components/hooks/useMultiQueryParams'
1011

1112
import styles from './LandingArticleGridWithFilter.module.scss'
1213

1314
type ArticleGridProps = {
1415
tocItems: TocItem[]
1516
includedCategories?: string[]
1617
landingType: LandingType
18+
params: QueryParams
19+
updateParams: (updates: Partial<QueryParams>, shouldPushHistory?: boolean) => void
1720
}
1821

1922
const ALL_CATEGORIES = 'all_categories'
@@ -69,17 +72,24 @@ const useResponsiveArticlesPerPage = () => {
6972
return articlesPerPage
7073
}
7174

72-
export const ArticleGrid = ({ tocItems, includedCategories, landingType }: ArticleGridProps) => {
75+
export const ArticleGrid = ({
76+
tocItems,
77+
includedCategories,
78+
landingType,
79+
params,
80+
updateParams,
81+
}: ArticleGridProps) => {
7382
const { t } = useTranslation('product_landing')
74-
const [searchQuery, setSearchQuery] = useState('')
75-
const [selectedCategory, setSelectedCategory] = useState(ALL_CATEGORIES)
76-
const [selectedCategoryIndex, setSelectedCategoryIndex] = useState(0)
77-
const [currentPage, setCurrentPage] = useState(1)
7883
const articlesPerPage = useResponsiveArticlesPerPage()
7984

8085
const inputRef = useRef<HTMLInputElement>(null)
8186
const headingRef = useRef<HTMLHeadingElement>(null)
8287

88+
// Read filter state directly from query params
89+
const searchQuery = params['articles-filter'] || ''
90+
const selectedCategory = params['articles-category'] || ALL_CATEGORIES
91+
const currentPage = parseInt(params['articles-page'] || '1', 10)
92+
8393
// Recursively flatten all articles from tocItems, including both direct children and nested articles
8494
const allArticles = useMemo(() => flattenArticles(tocItems), [tocItems])
8595

@@ -99,25 +109,43 @@ export const ArticleGrid = ({ tocItems, includedCategories, landingType }: Artic
99109
return allArticles
100110
}, [allArticles, includedCategories, landingType])
101111

102-
// Reset to first page when articlesPerPage changes (screen size changes)
112+
// Extract unique categories for dropdown from filtered articles (so all dropdown options have matching articles)
113+
const categories: string[] = useMemo(
114+
() => [
115+
ALL_CATEGORIES,
116+
...Array.from(
117+
new Set(filteredArticlesByLandingType.flatMap((item) => (item.category || []) as string[])),
118+
)
119+
.filter((category: string) => {
120+
if (!includedCategories || includedCategories.length === 0) return true
121+
// Case-insensitive comparison for dropdown filtering
122+
const lowerCategory = category.toLowerCase()
123+
return includedCategories.some((included) => included.toLowerCase() === lowerCategory)
124+
})
125+
.sort((a, b) => a.localeCompare(b)),
126+
],
127+
[filteredArticlesByLandingType, includedCategories],
128+
)
129+
130+
// Calculate the selected category index based on the current query param
131+
const selectedCategoryIndex = useMemo(() => {
132+
const index = categories.indexOf(selectedCategory)
133+
return index !== -1 ? index : 0
134+
}, [categories, selectedCategory])
135+
136+
// Clear invalid category from query params if it doesn't exist in available categories
103137
useEffect(() => {
104-
setCurrentPage(1)
105-
}, [articlesPerPage])
138+
if (selectedCategory !== ALL_CATEGORIES && selectedCategoryIndex === 0) {
139+
updateParams({ 'articles-category': '' })
140+
}
141+
}, [selectedCategory, selectedCategoryIndex, updateParams])
106142

107-
// Extract unique categories for dropdown from filtered articles (so all dropdown options have matching articles)
108-
const categories: string[] = [
109-
ALL_CATEGORIES,
110-
...Array.from(
111-
new Set(filteredArticlesByLandingType.flatMap((item) => (item.category || []) as string[])),
112-
)
113-
.filter((category: string) => {
114-
if (!includedCategories || includedCategories.length === 0) return true
115-
// Case-insensitive comparison for dropdown filtering
116-
const lowerCategory = category.toLowerCase()
117-
return includedCategories.some((included) => included.toLowerCase() === lowerCategory)
118-
})
119-
.sort((a, b) => a.localeCompare(b)),
120-
]
143+
// Sync the input field value with query params
144+
useEffect(() => {
145+
if (inputRef.current) {
146+
inputRef.current.value = searchQuery
147+
}
148+
}, [searchQuery])
121149

122150
const applyFilters = () => {
123151
let results = filteredArticlesByLandingType
@@ -154,31 +182,86 @@ export const ArticleGrid = ({ tocItems, includedCategories, landingType }: Artic
154182
const paginatedResults = filteredResults.slice(startIndex, startIndex + articlesPerPage)
155183

156184
const handleSearch = (query: string) => {
157-
setSearchQuery(query)
158-
setCurrentPage(1) // Reset to first page when searching
185+
// Update query params, clear if empty, and reset to first page
186+
// Don't add to history for search filtering
187+
updateParams({ 'articles-filter': query || '', 'articles-page': '' }, false)
159188
}
160189

161-
const handleFilter = (option: string, index: number) => {
162-
setSelectedCategory(option)
163-
setSelectedCategoryIndex(index)
164-
setCurrentPage(1) // Reset to first page when filtering
190+
const handleFilter = (option: string) => {
191+
// Update query params, clear if "all categories", and reset to first page
192+
updateParams(
193+
{
194+
'articles-category': option === ALL_CATEGORIES ? '' : option,
195+
'articles-page': '',
196+
},
197+
true,
198+
)
165199
}
166200

201+
// Track previous page to determine if we should scroll
202+
const prevPageRef = useRef(currentPage)
203+
const hasMountedRef = useRef(false)
204+
167205
const handlePageChange = (e: React.MouseEvent, pageNumber: number) => {
168206
e.preventDefault()
169207
if (pageNumber >= 1 && pageNumber <= totalPages) {
170-
setCurrentPage(pageNumber)
171-
if (headingRef.current) {
172-
const elementPosition = headingRef.current.getBoundingClientRect().top + window.scrollY
173-
const offsetPosition = elementPosition - 140 // 140px offset from top
174-
window.scrollTo({
175-
top: offsetPosition,
176-
behavior: 'smooth',
177-
})
178-
}
208+
// Update page in query params, clear if page 1
209+
updateParams({ 'articles-page': pageNumber === 1 ? '' : String(pageNumber) }, true)
179210
}
180211
}
181212

213+
// Scroll to heading on initial mount if query params are present
214+
useEffect(() => {
215+
if (!hasMountedRef.current) {
216+
hasMountedRef.current = true
217+
218+
// Check if any VALID article grid query params are present on initial load
219+
// Don't scroll if category is invalid (selectedCategoryIndex === 0 means invalid or "all")
220+
const hasValidCategory = selectedCategory !== ALL_CATEGORIES && selectedCategoryIndex !== 0
221+
const hasQueryParams = searchQuery || hasValidCategory || currentPage > 1
222+
223+
if (hasQueryParams && headingRef.current) {
224+
// Use setTimeout to ensure the component is fully rendered
225+
setTimeout(() => {
226+
if (headingRef.current) {
227+
const elementPosition = headingRef.current.getBoundingClientRect().top + window.scrollY
228+
const offsetPosition = elementPosition - 140 // 140px offset from top
229+
window.scrollTo({
230+
top: offsetPosition,
231+
behavior: 'smooth',
232+
})
233+
}
234+
}, 100)
235+
}
236+
}
237+
}, []) // Only run on mount
238+
239+
// Scroll to heading when page changes via pagination
240+
useEffect(() => {
241+
const pageChanged = currentPage !== prevPageRef.current
242+
const isPaginationClick = pageChanged && prevPageRef.current !== 1
243+
244+
// Scroll if page changed via pagination (not from filter/category reset to page 1)
245+
// This includes: going to page 2+, or going back to page 1 from a higher page
246+
const shouldScroll = pageChanged && (currentPage > 1 || isPaginationClick)
247+
248+
if (shouldScroll && headingRef.current) {
249+
// Delay scroll slightly to let router finish and restore scroll position first
250+
setTimeout(() => {
251+
if (headingRef.current) {
252+
const elementPosition = headingRef.current.getBoundingClientRect().top + window.scrollY
253+
const offsetPosition = elementPosition - 140 // 140px offset from top
254+
window.scrollTo({
255+
top: offsetPosition,
256+
behavior: 'smooth',
257+
})
258+
}
259+
}, 150) // Slightly longer than router debounce (100ms) + execution time
260+
}
261+
262+
prevPageRef.current = currentPage
263+
}, [currentPage])
264+
182265
return (
183266
<div data-testid="article-grid-container">
184267
{/* Filter and Search Controls */}
@@ -204,7 +287,7 @@ export const ArticleGrid = ({ tocItems, includedCategories, landingType }: Artic
204287
<ActionList.Item
205288
key={index}
206289
selected={index === selectedCategoryIndex}
207-
onSelect={() => handleFilter(category, index)}
290+
onSelect={() => handleFilter(category)}
208291
>
209292
{category === ALL_CATEGORIES ? t('article_grid.all_categories') : category}
210293
</ActionList.Item>

0 commit comments

Comments
 (0)