Skip to content

Commit 65f5967

Browse files
committed
page behaviour improvements
1 parent 5fd05b9 commit 65f5967

File tree

3 files changed

+135
-73
lines changed

3 files changed

+135
-73
lines changed

app/[lang]/(resources)/_components/SupportArticleList.tsx

Lines changed: 94 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,21 @@
11
'use client'
2+
/************************************************************************************************
3+
** SupportArticleList Component:
4+
**
5+
** Client component for displaying paginated lists of support articles
6+
** Features interactive pagination and loading states
7+
**
8+
** Features:
9+
** - Pagination with next/previous controls
10+
** - Loading skeleton for better UX
11+
** - Empty state handling
12+
** - Responsive grid layout for different viewports
13+
**
14+
** Usage:
15+
** - Import in support list pages
16+
** - Configure with useFetchSupportArticles hook
17+
** - Add custom empty state message if needed
18+
************************************************************************************************/
219
import {useParams, useRouter, useSearchParams} from 'next/navigation'
320
import {Fragment, useEffect, useMemo, useState} from 'react'
421
import ReactPaginate from 'react-paginate'
@@ -53,8 +70,9 @@ export function SupportArticleList({
5370
// determine the active tag (from URL or prop)
5471
const activeTag = urlTag ?? tag
5572

56-
// server-side filtering: always use the configured pageSize; grouped view will show previews and
57-
// provide "View all" links to the paginated tag view instead of fetching the entire dataset.
73+
// server-side filtering: always fetch the configured pageSize from the server so
74+
// tag discovery and counts are accurate. For grouped preview mode we still limit
75+
// the visual preview to 9 items per tag in the UI (see .slice below).
5876
const fetchPage = activeTag ? page : 1
5977
const fetchPageSize = pageSize
6078

@@ -83,6 +101,46 @@ export function SupportArticleList({
83101
return Array.from(new Set(filteredArticles.flatMap(a => a.tags ?? []))).sort()
84102
}, [filteredArticles])
85103

104+
// Preserve a master list of tags so that when a single tag is active we
105+
// still can display all available tags (like radio buttons) instead of
106+
// hiding the other options. We populate `allTags` from the groupedTags
107+
// when available, and as a fallback we prefetch an unfiltered page to
108+
// discover tags when the page is loaded already filtered by tag.
109+
const [allTags, setAllTags] = useState<string[] | null>(null)
110+
111+
// Prefetch unfiltered articles only when we don't yet have `allTags`.
112+
const {articles: discoveryArticles} = useFetchSupportArticles({
113+
page: 1,
114+
pageSize,
115+
sort,
116+
populateContent: false,
117+
cacheArticles: false,
118+
tag: undefined,
119+
search: undefined,
120+
skip: allTags !== null
121+
})
122+
123+
useEffect(() => {
124+
if (!activeTag && groupedTags.length > 0 && allTags === null) {
125+
setAllTags(groupedTags)
126+
}
127+
}, [activeTag, groupedTags, allTags])
128+
129+
useEffect(() => {
130+
if (discoveryArticles && discoveryArticles.length > 0) {
131+
const discovered = Array.from(new Set(discoveryArticles.flatMap(a => a.tags ?? []))).sort()
132+
// Merge existing allTags (if any), discovered tags, and the activeTag so
133+
// we don't accidentally drop the currently active tag if it's not
134+
// present on the discovery page.
135+
const merged = Array.from(
136+
new Set([...(allTags ?? []), ...(discovered ?? []), ...(activeTag ? [activeTag] : [])])
137+
).sort()
138+
if (allTags === null || merged.join('|') !== (allTags || []).join('|')) {
139+
setAllTags(merged)
140+
}
141+
}
142+
}, [allTags, discoveryArticles, activeTag])
143+
86144
const groupedArticles = useMemo(() => {
87145
const map: Record<string, TSupportArticle[]> = {}
88146
for (const t of groupedTags) {
@@ -117,6 +175,8 @@ export function SupportArticleList({
117175
return <SupportArticleListSkeleton pageSize={pageSize} />
118176
}
119177

178+
const displayTags = allTags ?? groupedTags
179+
120180
return (
121181
<Fragment>
122182
<div className={'container mx-auto'}>
@@ -130,16 +190,7 @@ export function SupportArticleList({
130190
<div className={'flex w-full justify-center'}>
131191
<div className={'w-1/2'}>
132192
<SupportTags
133-
tags={
134-
activeTag
135-
? (() => {
136-
const derived = Array.from(
137-
new Set((articles || []).flatMap(a => a.tags ?? []))
138-
)
139-
return derived.length > 0 ? derived : [activeTag]
140-
})()
141-
: groupedTags
142-
}
193+
tags={displayTags}
143194
active={activeTag}
144195
onClick={(t: string | null) => {
145196
const params = new URLSearchParams(Array.from(searchParams || []))
@@ -168,37 +219,42 @@ export function SupportArticleList({
168219
role={'status'}>
169220
{emptyMessage}
170221
</p>
171-
) : // If a tag is active, show the existing paginated grid. Otherwise show grouped-by-tag sections.
172-
activeTag ? (
173-
<div className={'mb-20'}>
174-
<div className={'mb-4'}>
175-
<h2 className={'text-2xl'}>{activeTag}</h2>
176-
</div>
177-
<div className={cl('grid gap-6 md:grid-cols-2 lg:grid-cols-3', gridClassName)}>
178-
{filteredArticles.map((article: TSupportArticle) => (
179-
<ArticleCard
180-
key={article.slug}
181-
article={article}
182-
tagName={activeTag}
183-
lang={lang}
184-
/>
185-
))}
186-
</div>
187-
</div>
188222
) : (
189223
<div className={'space-y-12 mb-20'}>
190224
{groupedTags.map(tagName => (
191225
<div key={tagName}>
192-
<div className={'mb-4 flex items-center justify-between'}>
193-
<h2 className={'text-2xl'}>{tagName}</h2>
194-
<LocalizedLink
195-
className={'text-sm text-blue-400'}
196-
href={`/${lang}/support?tag=${encodeURIComponent(tagName)}`}>
197-
{'View all'}
198-
</LocalizedLink>
226+
<div className={'mb-4'}>
227+
<div className={'flex items-center justify-between'}>
228+
<div className={'flex items-center gap-4'}>
229+
<h2 className={'text-2xl'}>{tagName}</h2>
230+
{/* Desktop inline View all — hide when a tag/search is active */}
231+
{!activeTag && !searchQuery && (
232+
<LocalizedLink
233+
className={'hidden lg:inline-block text-sm text-gray-400'}
234+
href={`/${lang}/support?tag=${encodeURIComponent(tagName)}`}>
235+
{'View all'}
236+
</LocalizedLink>
237+
)}
238+
</div>
239+
{/* Mobile: small 'View all' under title (visible on sm and down) */}
240+
{!activeTag && !searchQuery && (
241+
<div className={'block lg:hidden'}>
242+
<LocalizedLink
243+
className={'text-sm text-gray-400'}
244+
href={`/${lang}/support?tag=${encodeURIComponent(tagName)}`}>
245+
{'View all'}
246+
</LocalizedLink>
247+
</div>
248+
)}
249+
</div>
199250
</div>
200-
<div className={cl('grid gap-6 md:grid-cols-2 lg:grid-cols-3', gridClassName)}>
201-
{(groupedArticles[tagName] ?? []).slice(0, 3).map((article: TSupportArticle) => (
251+
{/* Render up to 3 rows (9 cards) in preview grouped mode, in case there are many items */}
252+
<div
253+
className={cl(
254+
'grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
255+
gridClassName
256+
)}>
257+
{(groupedArticles[tagName] ?? []).slice(0, 9).map((article: TSupportArticle) => (
202258
<ArticleCard
203259
key={article.slug}
204260
article={article}

app/[lang]/(resources)/support/[slug]/SupportArticleContent.tsx

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable @typescript-eslint/no-explicit-any */
21
import 'highlight.js/styles/github-dark.css'
32
import Image from 'next/image'
43
import ReactMarkdown from 'react-markdown'
@@ -10,82 +9,86 @@ import remarkMath from 'remark-math'
109

1110
import {isHtml} from '@/app/[lang]/_utils/isHtml'
1211

13-
import type {ReactNode} from 'react'
12+
import type {HTMLAttributes, ReactNode} from 'react'
1413
import type {Components} from 'react-markdown'
1514

1615
export function SupportArticleContent({content}: {content: string}): ReactNode {
1716
const components: Partial<Components> = {
1817
// Headers
19-
h1: ({...props}: any) => (
18+
h1: props => (
2019
<h1
2120
className={'mb-4 mt-8 text-4xl font-bold'}
22-
{...props}
21+
{...(props as HTMLAttributes<HTMLHeadingElement>)}
2322
/>
2423
),
25-
h2: ({...props}: any) => (
24+
h2: props => (
2625
<h2
2726
className={'mb-3 mt-6 text-3xl font-bold'}
28-
{...props}
27+
{...(props as HTMLAttributes<HTMLHeadingElement>)}
2928
/>
3029
),
31-
h3: ({...props}: any) => (
30+
h3: props => (
3231
<h3
3332
className={'mb-2 mt-4 text-2xl font-bold'}
34-
{...props}
33+
{...(props as HTMLAttributes<HTMLHeadingElement>)}
3534
/>
3635
),
3736

3837
// Code blocks
39-
code: ({className, children, ...props}: any) => {
38+
code: ({className, children, ...props}) => {
4039
const match = /language-(\w+)/.exec(className || '')
4140
return match ? (
4241
<div className={'relative'}>
4342
<div className={'absolute right-2 top-2 text-xs text-gray-400'}>{match[1]}</div>
4443
<pre className={className}>
4544
<code
4645
className={className}
47-
{...props}>
46+
{...(props as HTMLAttributes<HTMLElement>)}>
4847
{children}
4948
</code>
5049
</pre>
5150
</div>
5251
) : (
5352
<code
5453
className={'rounded bg-gray-800 px-1.5 py-0.5'}
55-
{...props}>
54+
{...(props as HTMLAttributes<HTMLElement>)}>
5655
{children}
5756
</code>
5857
)
5958
},
6059

6160
// Tables
62-
table: ({...props}: any) => (
61+
table: ({...props}) => (
6362
<div className={'my-8 overflow-x-auto'}>
6463
<table
6564
className={'min-w-full'}
66-
{...props}
65+
{...(props as HTMLAttributes<HTMLTableElement>)}
6766
/>
6867
</div>
6968
),
70-
th: ({...props}: any) => (
69+
th: ({...props}) => (
7170
<th
7271
className={'bg-gray-800 px-6 py-3 text-left'}
73-
{...props}
72+
{...(props as HTMLAttributes<HTMLTableCellElement>)}
7473
/>
7574
),
76-
td: ({...props}: any) => (
75+
td: ({...props}) => (
7776
<td
7877
className={'border-t border-gray-700 px-6 py-4'}
79-
{...props}
78+
{...(props as HTMLAttributes<HTMLTableCellElement>)}
8079
/>
8180
),
8281

8382
// Images
84-
img: (props: any) => {
83+
img: props => {
8584
const {src, alt, ...rest} = props || {}
8685
// Coerce src to string safely (react-markdown may pass string or object)
8786
const srcString =
88-
src === null || src === undefined ? '' : typeof src === 'string' ? src : String((src as any).src ?? src)
87+
src === null || src === undefined
88+
? ''
89+
: typeof src === 'string'
90+
? src
91+
: String(((src as Record<string, unknown>).src as string) ?? src)
8992
const altText = alt ?? ''
9093

9194
if (!srcString) {
@@ -103,47 +106,47 @@ export function SupportArticleContent({content}: {content: string}): ReactNode {
103106
style={{objectFit: 'contain'}}
104107
loading={'lazy'}
105108
sizes={'(max-width: 768px) 100vw, 800px'}
106-
{...(rest as any)}
109+
{...(rest as unknown as Record<string, unknown>)}
107110
/>
108111
</div>
109112
)
110113
},
111114

112115
// Blockquotes
113-
blockquote: ({...props}: any) => (
116+
blockquote: ({...props}) => (
114117
<blockquote
115118
className={'border-blue-500 my-6 border-l-4 pl-4 italic text-gray-300'}
116-
{...props}
119+
{...(props as HTMLAttributes<HTMLElement>)}
117120
/>
118121
),
119122

120123
// Lists
121-
ul: ({...props}: any) => (
124+
ul: ({...props}) => (
122125
<ul
123126
className={'my-4 list-inside list-disc'}
124-
{...props}
127+
{...(props as HTMLAttributes<HTMLUListElement>)}
125128
/>
126129
),
127-
ol: ({...props}: any) => (
130+
ol: ({...props}) => (
128131
<ol
129132
className={'my-4 list-inside list-decimal'}
130-
{...props}
133+
{...(props as HTMLAttributes<HTMLOListElement>)}
131134
/>
132135
),
133136

134137
// Links
135-
a: ({...props}: any) => (
138+
a: ({...props}) => (
136139
<a
137140
className={'text-blue underline transition-colors hover:text-blueHover'}
138141
target={'_blank'}
139142
rel={'noopener noreferrer'}
140-
{...props}
143+
{...(props as HTMLAttributes<HTMLAnchorElement>)}
141144
/>
142145
),
143-
p: ({...props}: any) => (
146+
p: ({...props}) => (
144147
<p
145148
className={'mb-4'}
146-
{...props}
149+
{...(props as HTMLAttributes<HTMLParagraphElement>)}
147150
/>
148151
)
149152
}
@@ -157,7 +160,7 @@ export function SupportArticleContent({content}: {content: string}): ReactNode {
157160
<ReactMarkdown
158161
remarkPlugins={[remarkGfm, remarkEmoji, remarkMath]}
159162
rehypePlugins={[rehypeHighlight, rehypeKatex]}
160-
components={components as any}>
163+
components={components as Components}>
161164
{content}
162165
</ReactMarkdown>
163166
)}

app/[lang]/(resources)/support/_components/SupportTags.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ export function SupportTags({
1616
}
1717
return (
1818
<div className={`${className ?? ''} mb-10`}>
19-
<div className={'flex flex-wrap gap-3 justify-center'}>
19+
{/* Horizontal scroll on small screens, normal wrapped layout on sm+ */}
20+
<div className={'flex flex-wrap gap-3 justify-center -mx-4 px-4 py-2'}>
2021
<button
2122
type={'button'}
2223
onClick={() => onClick(null)}
23-
className={`rounded-full border px-4 py-2 text-sm ${
24-
active === null || active === undefined ? 'bg-white/10' : 'bg-transparent'
24+
className={`rounded-full border text-sm whitespace-nowrap px-3 py-1.5 sm:px-4 sm:py-2 ${
25+
active === null || active === undefined ? 'bg-blue' : 'bg-transparent'
2526
}`}>
2627
{'All'}
2728
</button>
@@ -30,7 +31,9 @@ export function SupportTags({
3031
type={'button'}
3132
key={t}
3233
onClick={() => onClick(t)}
33-
className={`rounded-full border px-4 py-2 text-sm ${active === t ? 'bg-white/10' : 'bg-transparent'}`}>
34+
className={`rounded-full border text-sm whitespace-nowrap px-3 py-1.5 sm:px-4 sm:py-2 ${
35+
active === t ? 'bg-blue' : 'bg-transparent'
36+
}`}>
3437
{t}
3538
</button>
3639
))}

0 commit comments

Comments
 (0)