reuse multiQueryParams pattern to persist landing filter, dropdown, and page # (#58558)
This commit is contained in:
@@ -4,6 +4,7 @@ import { LandingHero } from '@/landings/components/shared/LandingHero'
|
|||||||
import { ArticleGrid } from '@/landings/components/shared/LandingArticleGridWithFilter'
|
import { ArticleGrid } from '@/landings/components/shared/LandingArticleGridWithFilter'
|
||||||
import { UtmPreserver } from '@/frame/components/UtmPreserver'
|
import { UtmPreserver } from '@/frame/components/UtmPreserver'
|
||||||
import { LandingCarousel } from '@/landings/components/shared/LandingCarousel'
|
import { LandingCarousel } from '@/landings/components/shared/LandingCarousel'
|
||||||
|
import { useMultiQueryParams } from '@/search/components/hooks/useMultiQueryParams'
|
||||||
|
|
||||||
export const BespokeLanding = () => {
|
export const BespokeLanding = () => {
|
||||||
const {
|
const {
|
||||||
@@ -16,6 +17,10 @@ export const BespokeLanding = () => {
|
|||||||
includedCategories,
|
includedCategories,
|
||||||
landingType,
|
landingType,
|
||||||
} = useLandingContext()
|
} = useLandingContext()
|
||||||
|
const { params, updateParams } = useMultiQueryParams({
|
||||||
|
useHistory: true,
|
||||||
|
excludeFromHistory: ['articles-filter'],
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DefaultLayout>
|
<DefaultLayout>
|
||||||
@@ -29,6 +34,8 @@ export const BespokeLanding = () => {
|
|||||||
tocItems={tocItems}
|
tocItems={tocItems}
|
||||||
includedCategories={includedCategories}
|
includedCategories={includedCategories}
|
||||||
landingType={landingType}
|
landingType={landingType}
|
||||||
|
params={params}
|
||||||
|
updateParams={updateParams}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { LandingHero } from '@/landings/components/shared/LandingHero'
|
|||||||
import { ArticleGrid } from '@/landings/components/shared/LandingArticleGridWithFilter'
|
import { ArticleGrid } from '@/landings/components/shared/LandingArticleGridWithFilter'
|
||||||
import { LandingCarousel } from '@/landings/components/shared/LandingCarousel'
|
import { LandingCarousel } from '@/landings/components/shared/LandingCarousel'
|
||||||
import { UtmPreserver } from '@/frame/components/UtmPreserver'
|
import { UtmPreserver } from '@/frame/components/UtmPreserver'
|
||||||
|
import { useMultiQueryParams } from '@/search/components/hooks/useMultiQueryParams'
|
||||||
|
|
||||||
export const DiscoveryLanding = () => {
|
export const DiscoveryLanding = () => {
|
||||||
const {
|
const {
|
||||||
@@ -16,6 +17,10 @@ export const DiscoveryLanding = () => {
|
|||||||
includedCategories,
|
includedCategories,
|
||||||
landingType,
|
landingType,
|
||||||
} = useLandingContext()
|
} = useLandingContext()
|
||||||
|
const { params, updateParams } = useMultiQueryParams({
|
||||||
|
useHistory: true,
|
||||||
|
excludeFromHistory: ['articles-filter'],
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DefaultLayout>
|
<DefaultLayout>
|
||||||
@@ -28,6 +33,8 @@ export const DiscoveryLanding = () => {
|
|||||||
tocItems={tocItems}
|
tocItems={tocItems}
|
||||||
includedCategories={includedCategories}
|
includedCategories={includedCategories}
|
||||||
landingType={landingType}
|
landingType={landingType}
|
||||||
|
params={params}
|
||||||
|
updateParams={updateParams}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Link } from '@/frame/components/Link'
|
|||||||
import { useTranslation } from '@/languages/components/useTranslation'
|
import { useTranslation } from '@/languages/components/useTranslation'
|
||||||
import { ArticleCardItems, ChildTocItem, TocItem } from '@/landings/types'
|
import { ArticleCardItems, ChildTocItem, TocItem } from '@/landings/types'
|
||||||
import { LandingType } from '@/landings/context/LandingContext'
|
import { LandingType } from '@/landings/context/LandingContext'
|
||||||
|
import type { QueryParams } from '@/search/components/hooks/useMultiQueryParams'
|
||||||
|
|
||||||
import styles from './LandingArticleGridWithFilter.module.scss'
|
import styles from './LandingArticleGridWithFilter.module.scss'
|
||||||
|
|
||||||
@@ -14,6 +15,8 @@ type ArticleGridProps = {
|
|||||||
tocItems: TocItem[]
|
tocItems: TocItem[]
|
||||||
includedCategories?: string[]
|
includedCategories?: string[]
|
||||||
landingType: LandingType
|
landingType: LandingType
|
||||||
|
params: QueryParams
|
||||||
|
updateParams: (updates: Partial<QueryParams>, shouldPushHistory?: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ALL_CATEGORIES = 'all_categories'
|
const ALL_CATEGORIES = 'all_categories'
|
||||||
@@ -69,17 +72,24 @@ const useResponsiveArticlesPerPage = () => {
|
|||||||
return articlesPerPage
|
return articlesPerPage
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ArticleGrid = ({ tocItems, includedCategories, landingType }: ArticleGridProps) => {
|
export const ArticleGrid = ({
|
||||||
|
tocItems,
|
||||||
|
includedCategories,
|
||||||
|
landingType,
|
||||||
|
params,
|
||||||
|
updateParams,
|
||||||
|
}: ArticleGridProps) => {
|
||||||
const { t } = useTranslation('product_landing')
|
const { t } = useTranslation('product_landing')
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
|
||||||
const [selectedCategory, setSelectedCategory] = useState(ALL_CATEGORIES)
|
|
||||||
const [selectedCategoryIndex, setSelectedCategoryIndex] = useState(0)
|
|
||||||
const [currentPage, setCurrentPage] = useState(1)
|
|
||||||
const articlesPerPage = useResponsiveArticlesPerPage()
|
const articlesPerPage = useResponsiveArticlesPerPage()
|
||||||
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
const headingRef = useRef<HTMLHeadingElement>(null)
|
const headingRef = useRef<HTMLHeadingElement>(null)
|
||||||
|
|
||||||
|
// Read filter state directly from query params
|
||||||
|
const searchQuery = params['articles-filter'] || ''
|
||||||
|
const selectedCategory = params['articles-category'] || ALL_CATEGORIES
|
||||||
|
const currentPage = parseInt(params['articles-page'] || '1', 10)
|
||||||
|
|
||||||
// Recursively flatten all articles from tocItems, including both direct children and nested articles
|
// Recursively flatten all articles from tocItems, including both direct children and nested articles
|
||||||
const allArticles = useMemo(() => flattenArticles(tocItems), [tocItems])
|
const allArticles = useMemo(() => flattenArticles(tocItems), [tocItems])
|
||||||
|
|
||||||
@@ -99,25 +109,43 @@ export const ArticleGrid = ({ tocItems, includedCategories, landingType }: Artic
|
|||||||
return allArticles
|
return allArticles
|
||||||
}, [allArticles, includedCategories, landingType])
|
}, [allArticles, includedCategories, landingType])
|
||||||
|
|
||||||
// Reset to first page when articlesPerPage changes (screen size changes)
|
|
||||||
useEffect(() => {
|
|
||||||
setCurrentPage(1)
|
|
||||||
}, [articlesPerPage])
|
|
||||||
|
|
||||||
// Extract unique categories for dropdown from filtered articles (so all dropdown options have matching articles)
|
// Extract unique categories for dropdown from filtered articles (so all dropdown options have matching articles)
|
||||||
const categories: string[] = [
|
const categories: string[] = useMemo(
|
||||||
ALL_CATEGORIES,
|
() => [
|
||||||
...Array.from(
|
ALL_CATEGORIES,
|
||||||
new Set(filteredArticlesByLandingType.flatMap((item) => (item.category || []) as string[])),
|
...Array.from(
|
||||||
)
|
new Set(filteredArticlesByLandingType.flatMap((item) => (item.category || []) as string[])),
|
||||||
.filter((category: string) => {
|
)
|
||||||
if (!includedCategories || includedCategories.length === 0) return true
|
.filter((category: string) => {
|
||||||
// Case-insensitive comparison for dropdown filtering
|
if (!includedCategories || includedCategories.length === 0) return true
|
||||||
const lowerCategory = category.toLowerCase()
|
// Case-insensitive comparison for dropdown filtering
|
||||||
return includedCategories.some((included) => included.toLowerCase() === lowerCategory)
|
const lowerCategory = category.toLowerCase()
|
||||||
})
|
return includedCategories.some((included) => included.toLowerCase() === lowerCategory)
|
||||||
.sort((a, b) => a.localeCompare(b)),
|
})
|
||||||
]
|
.sort((a, b) => a.localeCompare(b)),
|
||||||
|
],
|
||||||
|
[filteredArticlesByLandingType, includedCategories],
|
||||||
|
)
|
||||||
|
|
||||||
|
// Calculate the selected category index based on the current query param
|
||||||
|
const selectedCategoryIndex = useMemo(() => {
|
||||||
|
const index = categories.indexOf(selectedCategory)
|
||||||
|
return index !== -1 ? index : 0
|
||||||
|
}, [categories, selectedCategory])
|
||||||
|
|
||||||
|
// Clear invalid category from query params if it doesn't exist in available categories
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedCategory !== ALL_CATEGORIES && selectedCategoryIndex === 0) {
|
||||||
|
updateParams({ 'articles-category': '' })
|
||||||
|
}
|
||||||
|
}, [selectedCategory, selectedCategoryIndex, updateParams])
|
||||||
|
|
||||||
|
// Sync the input field value with query params
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.value = searchQuery
|
||||||
|
}
|
||||||
|
}, [searchQuery])
|
||||||
|
|
||||||
const applyFilters = () => {
|
const applyFilters = () => {
|
||||||
let results = filteredArticlesByLandingType
|
let results = filteredArticlesByLandingType
|
||||||
@@ -154,31 +182,86 @@ export const ArticleGrid = ({ tocItems, includedCategories, landingType }: Artic
|
|||||||
const paginatedResults = filteredResults.slice(startIndex, startIndex + articlesPerPage)
|
const paginatedResults = filteredResults.slice(startIndex, startIndex + articlesPerPage)
|
||||||
|
|
||||||
const handleSearch = (query: string) => {
|
const handleSearch = (query: string) => {
|
||||||
setSearchQuery(query)
|
// Update query params, clear if empty, and reset to first page
|
||||||
setCurrentPage(1) // Reset to first page when searching
|
// Don't add to history for search filtering
|
||||||
|
updateParams({ 'articles-filter': query || '', 'articles-page': '' }, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFilter = (option: string, index: number) => {
|
const handleFilter = (option: string) => {
|
||||||
setSelectedCategory(option)
|
// Update query params, clear if "all categories", and reset to first page
|
||||||
setSelectedCategoryIndex(index)
|
updateParams(
|
||||||
setCurrentPage(1) // Reset to first page when filtering
|
{
|
||||||
|
'articles-category': option === ALL_CATEGORIES ? '' : option,
|
||||||
|
'articles-page': '',
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track previous page to determine if we should scroll
|
||||||
|
const prevPageRef = useRef(currentPage)
|
||||||
|
const hasMountedRef = useRef(false)
|
||||||
|
|
||||||
const handlePageChange = (e: React.MouseEvent, pageNumber: number) => {
|
const handlePageChange = (e: React.MouseEvent, pageNumber: number) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (pageNumber >= 1 && pageNumber <= totalPages) {
|
if (pageNumber >= 1 && pageNumber <= totalPages) {
|
||||||
setCurrentPage(pageNumber)
|
// Update page in query params, clear if page 1
|
||||||
if (headingRef.current) {
|
updateParams({ 'articles-page': pageNumber === 1 ? '' : String(pageNumber) }, true)
|
||||||
const elementPosition = headingRef.current.getBoundingClientRect().top + window.scrollY
|
|
||||||
const offsetPosition = elementPosition - 140 // 140px offset from top
|
|
||||||
window.scrollTo({
|
|
||||||
top: offsetPosition,
|
|
||||||
behavior: 'smooth',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scroll to heading on initial mount if query params are present
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasMountedRef.current) {
|
||||||
|
hasMountedRef.current = true
|
||||||
|
|
||||||
|
// Check if any VALID article grid query params are present on initial load
|
||||||
|
// Don't scroll if category is invalid (selectedCategoryIndex === 0 means invalid or "all")
|
||||||
|
const hasValidCategory = selectedCategory !== ALL_CATEGORIES && selectedCategoryIndex !== 0
|
||||||
|
const hasQueryParams = searchQuery || hasValidCategory || currentPage > 1
|
||||||
|
|
||||||
|
if (hasQueryParams && headingRef.current) {
|
||||||
|
// Use setTimeout to ensure the component is fully rendered
|
||||||
|
setTimeout(() => {
|
||||||
|
if (headingRef.current) {
|
||||||
|
const elementPosition = headingRef.current.getBoundingClientRect().top + window.scrollY
|
||||||
|
const offsetPosition = elementPosition - 140 // 140px offset from top
|
||||||
|
window.scrollTo({
|
||||||
|
top: offsetPosition,
|
||||||
|
behavior: 'smooth',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []) // Only run on mount
|
||||||
|
|
||||||
|
// Scroll to heading when page changes via pagination
|
||||||
|
useEffect(() => {
|
||||||
|
const pageChanged = currentPage !== prevPageRef.current
|
||||||
|
const isPaginationClick = pageChanged && prevPageRef.current !== 1
|
||||||
|
|
||||||
|
// Scroll if page changed via pagination (not from filter/category reset to page 1)
|
||||||
|
// This includes: going to page 2+, or going back to page 1 from a higher page
|
||||||
|
const shouldScroll = pageChanged && (currentPage > 1 || isPaginationClick)
|
||||||
|
|
||||||
|
if (shouldScroll && headingRef.current) {
|
||||||
|
// Delay scroll slightly to let router finish and restore scroll position first
|
||||||
|
setTimeout(() => {
|
||||||
|
if (headingRef.current) {
|
||||||
|
const elementPosition = headingRef.current.getBoundingClientRect().top + window.scrollY
|
||||||
|
const offsetPosition = elementPosition - 140 // 140px offset from top
|
||||||
|
window.scrollTo({
|
||||||
|
top: offsetPosition,
|
||||||
|
behavior: 'smooth',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, 150) // Slightly longer than router debounce (100ms) + execution time
|
||||||
|
}
|
||||||
|
|
||||||
|
prevPageRef.current = currentPage
|
||||||
|
}, [currentPage])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-testid="article-grid-container">
|
<div data-testid="article-grid-container">
|
||||||
{/* Filter and Search Controls */}
|
{/* Filter and Search Controls */}
|
||||||
@@ -204,7 +287,7 @@ export const ArticleGrid = ({ tocItems, includedCategories, landingType }: Artic
|
|||||||
<ActionList.Item
|
<ActionList.Item
|
||||||
key={index}
|
key={index}
|
||||||
selected={index === selectedCategoryIndex}
|
selected={index === selectedCategoryIndex}
|
||||||
onSelect={() => handleFilter(category, index)}
|
onSelect={() => handleFilter(category)}
|
||||||
>
|
>
|
||||||
{category === ALL_CATEGORIES ? t('article_grid.all_categories') : category}
|
{category === ALL_CATEGORIES ? t('article_grid.all_categories') : category}
|
||||||
</ActionList.Item>
|
</ActionList.Item>
|
||||||
|
|||||||
@@ -1,22 +1,37 @@
|
|||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useState, useEffect, useRef } from 'react'
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||||
|
|
||||||
export type QueryParams = {
|
export type QueryParams = {
|
||||||
'search-overlay-input': string
|
'search-overlay-input': string
|
||||||
'search-overlay-ask-ai': string // "true" or ""
|
'search-overlay-ask-ai': string // "true" or ""
|
||||||
debug: string
|
debug: string
|
||||||
|
'articles-category': string
|
||||||
|
'articles-filter': string
|
||||||
|
'articles-page': string
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialKeys: (keyof QueryParams)[] = [
|
const initialKeys: (keyof QueryParams)[] = [
|
||||||
|
// Used to persist search state
|
||||||
'search-overlay-input',
|
'search-overlay-input',
|
||||||
'search-overlay-ask-ai',
|
'search-overlay-ask-ai',
|
||||||
|
// Used to debug search result
|
||||||
'debug',
|
'debug',
|
||||||
|
// Used to filter category and search results of Articles on landing pages
|
||||||
|
'articles-category',
|
||||||
|
'articles-filter',
|
||||||
|
'articles-page',
|
||||||
]
|
]
|
||||||
|
|
||||||
// When we need to update 2 query params simultaneously, we can use this hook to prevent race conditions
|
// When we need to update 2 query params simultaneously, we can use this hook to prevent race conditions
|
||||||
export function useMultiQueryParams() {
|
export function useMultiQueryParams(options?: {
|
||||||
|
useHistory?: boolean
|
||||||
|
excludeFromHistory?: (keyof QueryParams)[]
|
||||||
|
}) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const pushTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const pushTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
const useHistory = options?.useHistory ?? false
|
||||||
|
// When using browser history, exclude these params from being updated on back/forward navigation (like search input which causes race conditions)
|
||||||
|
const excludeFromHistory = options?.excludeFromHistory ?? []
|
||||||
|
|
||||||
const getInitialParams = (): QueryParams => {
|
const getInitialParams = (): QueryParams => {
|
||||||
const searchParams =
|
const searchParams =
|
||||||
@@ -27,6 +42,9 @@ export function useMultiQueryParams() {
|
|||||||
'search-overlay-input': searchParams.get('search-overlay-input') || '',
|
'search-overlay-input': searchParams.get('search-overlay-input') || '',
|
||||||
'search-overlay-ask-ai': searchParams.get('search-overlay-ask-ai') || '',
|
'search-overlay-ask-ai': searchParams.get('search-overlay-ask-ai') || '',
|
||||||
debug: searchParams.get('debug') || '',
|
debug: searchParams.get('debug') || '',
|
||||||
|
'articles-category': searchParams.get('articles-category') || '',
|
||||||
|
'articles-filter': searchParams.get('articles-filter') || '',
|
||||||
|
'articles-page': searchParams.get('articles-page') || '',
|
||||||
}
|
}
|
||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
@@ -38,43 +56,87 @@ export function useMultiQueryParams() {
|
|||||||
setParams(getInitialParams())
|
setParams(getInitialParams())
|
||||||
}, [router.pathname])
|
}, [router.pathname])
|
||||||
|
|
||||||
const updateParams = (updates: Partial<QueryParams>) => {
|
// Listen to browser back/forward button navigation (only if history is being used)
|
||||||
const newParams = { ...params, ...updates }
|
useEffect(() => {
|
||||||
const [asPathWithoutHash] = router.asPath.split('#')
|
if (!useHistory) return
|
||||||
const [asPathRoot, asPathQuery = ''] = asPathWithoutHash.split('?')
|
|
||||||
const searchParams = new URLSearchParams(asPathQuery)
|
const handleRouteChange = () => {
|
||||||
for (const key of initialKeys) {
|
// When the route changes (e.g., back button), update state from URL
|
||||||
if (key === 'search-overlay-ask-ai') {
|
// But preserve excluded params from current state to avoid race conditions
|
||||||
if (newParams[key] === 'true') {
|
setParams((currentParams) => {
|
||||||
searchParams.set(key, 'true')
|
const newParams = getInitialParams()
|
||||||
} else {
|
// Keep excluded params from current state instead of reading from URL
|
||||||
searchParams.delete(key)
|
for (const key of excludeFromHistory) {
|
||||||
|
newParams[key] = currentParams[key]
|
||||||
}
|
}
|
||||||
} else {
|
return newParams
|
||||||
if (newParams[key]) {
|
})
|
||||||
searchParams.set(key, newParams[key])
|
|
||||||
} else {
|
|
||||||
searchParams.delete(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const paramsString = searchParams.toString() ? `?${searchParams.toString()}` : ''
|
|
||||||
let newUrl = `${asPathRoot}${paramsString}`
|
|
||||||
if (asPathRoot !== '/' && router.locale) {
|
|
||||||
newUrl = `${router.locale}${asPathRoot}${paramsString}`
|
|
||||||
}
|
|
||||||
if (!newUrl.startsWith('/')) {
|
|
||||||
newUrl = `/${newUrl}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debounce the router push so we don't push a new URL for every keystroke
|
router.events.on('routeChangeComplete', handleRouteChange)
|
||||||
if (pushTimeoutRef.current) clearTimeout(pushTimeoutRef.current)
|
return () => {
|
||||||
pushTimeoutRef.current = setTimeout(() => {
|
router.events.off('routeChangeComplete', handleRouteChange)
|
||||||
router.replace(newUrl, undefined, { shallow: true, locale: router.locale, scroll: false })
|
}
|
||||||
}, 100)
|
}, [router.events, useHistory, excludeFromHistory])
|
||||||
|
|
||||||
setParams(newParams)
|
const updateParams = useCallback(
|
||||||
}
|
(updates: Partial<QueryParams>, shouldPushHistory = false) => {
|
||||||
|
// Use functional state update to avoid depending on params in the closure
|
||||||
|
setParams((currentParams) => {
|
||||||
|
const newParams = { ...currentParams, ...updates }
|
||||||
|
const [asPathWithoutHash] = router.asPath.split('#')
|
||||||
|
const [asPathRoot, asPathQuery = ''] = asPathWithoutHash.split('?')
|
||||||
|
const searchParams = new URLSearchParams(asPathQuery)
|
||||||
|
for (const key of initialKeys) {
|
||||||
|
if (key === 'search-overlay-ask-ai') {
|
||||||
|
if (newParams[key] === 'true') {
|
||||||
|
searchParams.set(key, 'true')
|
||||||
|
} else {
|
||||||
|
searchParams.delete(key)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (newParams[key]) {
|
||||||
|
searchParams.set(key, newParams[key])
|
||||||
|
} else {
|
||||||
|
searchParams.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const paramsString = searchParams.toString() ? `?${searchParams.toString()}` : ''
|
||||||
|
let newUrl = `${asPathRoot}${paramsString}`
|
||||||
|
if (asPathRoot !== '/' && router.locale) {
|
||||||
|
newUrl = `${router.locale}${asPathRoot}${paramsString}`
|
||||||
|
}
|
||||||
|
if (!newUrl.startsWith('/')) {
|
||||||
|
newUrl = `/${newUrl}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce the router push so we don't push a new URL for every keystroke
|
||||||
|
if (pushTimeoutRef.current) clearTimeout(pushTimeoutRef.current)
|
||||||
|
pushTimeoutRef.current = setTimeout(async () => {
|
||||||
|
// Always preserve scroll position during router update to prevent jumps
|
||||||
|
// Component-level scroll logic (like pagination scroll) will handle intentional scrolling
|
||||||
|
const scrollY = window.scrollY
|
||||||
|
const scrollX = window.scrollX
|
||||||
|
|
||||||
|
// Use router.push for history entries (category/page changes), router.replace for others (search)
|
||||||
|
const routerMethod = shouldPushHistory ? router.push : router.replace
|
||||||
|
await routerMethod(newUrl, undefined, {
|
||||||
|
shallow: true,
|
||||||
|
locale: router.locale,
|
||||||
|
scroll: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Restore scroll position after router update
|
||||||
|
// This prevents unintended scrolling; intentional scrolling is handled by components
|
||||||
|
window.scrollTo(scrollX, scrollY)
|
||||||
|
}, 100)
|
||||||
|
|
||||||
|
return newParams
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[router],
|
||||||
|
)
|
||||||
|
|
||||||
return { params, updateParams }
|
return { params, updateParams }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ const RECOGNIZED_KEYS_BY_ANY = new Set([
|
|||||||
'search-overlay-ask-ai',
|
'search-overlay-ask-ai',
|
||||||
// The drop-downs on "Webhook events and payloads"
|
// The drop-downs on "Webhook events and payloads"
|
||||||
'actionType',
|
'actionType',
|
||||||
|
// Landing page article grid filters
|
||||||
|
'articles-category',
|
||||||
|
'articles-filter',
|
||||||
|
'articles-page',
|
||||||
// Legacy domain tracking parameter (no longer processed but still recognized)
|
// Legacy domain tracking parameter (no longer processed but still recognized)
|
||||||
'ghdomain',
|
'ghdomain',
|
||||||
// UTM campaign tracking
|
// UTM campaign tracking
|
||||||
|
|||||||
Reference in New Issue
Block a user