@fecommunity/reactpress-theme-starter 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/.env.example +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +286 -0
  4. package/app/api/[[...path]]/route.ts +69 -0
  5. package/app/archives/page.tsx +28 -0
  6. package/app/article/[id]/page.tsx +92 -0
  7. package/app/category/[category]/page.tsx +60 -0
  8. package/app/globals.css +4969 -0
  9. package/app/knowledge/[pId]/[id]/page.tsx +54 -0
  10. package/app/knowledge/[pId]/page.tsx +47 -0
  11. package/app/knowledge/page.tsx +29 -0
  12. package/app/layout.tsx +76 -0
  13. package/app/llms.txt/route.ts +26 -0
  14. package/app/login/page.tsx +17 -0
  15. package/app/nav/[id]/page.tsx +44 -0
  16. package/app/nav/page.tsx +5 -0
  17. package/app/not-found.tsx +21 -0
  18. package/app/page/[id]/page.tsx +57 -0
  19. package/app/page.tsx +61 -0
  20. package/app/register/page.tsx +17 -0
  21. package/app/robots.ts +14 -0
  22. package/app/rss/route.ts +82 -0
  23. package/app/search/page.tsx +46 -0
  24. package/app/sitemap.ts +38 -0
  25. package/app/suggestions/page.tsx +47 -0
  26. package/app/tag/[tag]/page.tsx +58 -0
  27. package/app/tags/page.tsx +18 -0
  28. package/app/template.tsx +6 -0
  29. package/components/article/ArchiveBanner.tsx +40 -0
  30. package/components/article/ArticleActionsBar.tsx +53 -0
  31. package/components/article/ArticleCarousel.tsx +237 -0
  32. package/components/article/ArticleCopyrightBar.tsx +128 -0
  33. package/components/article/ArticleEndBar.tsx +19 -0
  34. package/components/article/ArticleFeedSection.tsx +59 -0
  35. package/components/article/ArticleList.tsx +170 -0
  36. package/components/article/ArticleReadingChrome.tsx +104 -0
  37. package/components/article/ArticleReadingProgress.tsx +75 -0
  38. package/components/article/ArticleRecommend.tsx +362 -0
  39. package/components/article/ArticleTocPanel.tsx +19 -0
  40. package/components/article/ArticleViewClient.tsx +221 -0
  41. package/components/article/CategoryMenu.tsx +67 -0
  42. package/components/article/CmsPageClient.tsx +92 -0
  43. package/components/article/LoadMore.tsx +59 -0
  44. package/components/article/ReadingContent.tsx +51 -0
  45. package/components/auth/AuthPageClient.tsx +314 -0
  46. package/components/comment/CommentEditor.tsx +290 -0
  47. package/components/comment/CommentEmoji.tsx +110 -0
  48. package/components/comment/CommentIconButton.tsx +37 -0
  49. package/components/comment/CommentItem.tsx +135 -0
  50. package/components/comment/CommentSection.tsx +123 -0
  51. package/components/comment/emojis.ts +152 -0
  52. package/components/knowledge/KnowledgeList.tsx +70 -0
  53. package/components/layout/BackToTop.tsx +27 -0
  54. package/components/layout/ConditionalSiteFooter.tsx +29 -0
  55. package/components/layout/ContentStylesLoader.tsx +9 -0
  56. package/components/layout/DoubleColumnLayout.tsx +171 -0
  57. package/components/layout/HeaderSearch.tsx +23 -0
  58. package/components/layout/LocaleSwitcher.tsx +24 -0
  59. package/components/layout/PageContainer.tsx +11 -0
  60. package/components/layout/PageLoadProgress.tsx +169 -0
  61. package/components/layout/SiteFooter.tsx +75 -0
  62. package/components/layout/SiteHeader.tsx +312 -0
  63. package/components/layout/SystemNotification.tsx +176 -0
  64. package/components/layout/ThemeSwitch.tsx +19 -0
  65. package/components/layout/UserAuth.tsx +130 -0
  66. package/components/search/SearchHero.tsx +323 -0
  67. package/components/search/SearchQuickLinks.tsx +285 -0
  68. package/components/shared/DevChunkRecovery.tsx +8 -0
  69. package/components/shared/Link.tsx +30 -0
  70. package/components/shared/SectionHeading.tsx +38 -0
  71. package/components/shared/ThemeImage.tsx +13 -0
  72. package/components/shared/logo.svg +7 -0
  73. package/components/views/ArchivesClient.tsx +137 -0
  74. package/components/views/CategoryClient.tsx +87 -0
  75. package/components/views/HomeClient.tsx +79 -0
  76. package/components/views/KnowledgeBookClient.tsx +127 -0
  77. package/components/views/KnowledgeChapterClient.tsx +175 -0
  78. package/components/views/KnowledgeClient.tsx +69 -0
  79. package/components/views/LoginClient.tsx +80 -0
  80. package/components/views/NavDetailClient.tsx +177 -0
  81. package/components/views/SearchClient.tsx +100 -0
  82. package/components/views/TagArchiveClient.tsx +105 -0
  83. package/components/views/TagsClient.tsx +46 -0
  84. package/components/widgets/AboutUs.tsx +180 -0
  85. package/components/widgets/ArticleActionButton.tsx +95 -0
  86. package/components/widgets/BookmarkWidget.tsx +61 -0
  87. package/components/widgets/CategoriesWidget.tsx +47 -0
  88. package/components/widgets/HomeSidebar.tsx +40 -0
  89. package/components/widgets/LikesWidget.tsx +77 -0
  90. package/components/widgets/ShareWidget.tsx +120 -0
  91. package/components/widgets/TagCloud.tsx +94 -0
  92. package/components/widgets/TagsWidget.tsx +160 -0
  93. package/eslint.config.mjs +80 -0
  94. package/lib/auth/githubOAuth.ts +26 -0
  95. package/lib/auth/sessionUser.ts +41 -0
  96. package/lib/mock-api/auth.ts +129 -0
  97. package/lib/mock-api/config.ts +11 -0
  98. package/lib/mock-api/cors.ts +53 -0
  99. package/lib/mock-api/data.ts +998 -0
  100. package/lib/mock-api/match-route.ts +202 -0
  101. package/lib/mock-api/proxy.ts +28 -0
  102. package/lib/providers/client.ts +58 -0
  103. package/lib/providers/materializeProvider.ts +9 -0
  104. package/lib/providers/server.ts +8 -0
  105. package/lib/reactpress/appearance.ts +69 -0
  106. package/lib/reactpress/bootstrap.ts +48 -0
  107. package/lib/reactpress/coercePublishedPage.ts +12 -0
  108. package/lib/reactpress/contentSeo.ts +123 -0
  109. package/lib/reactpress/env.ts +31 -0
  110. package/lib/reactpress/feedFooterPlacement.ts +16 -0
  111. package/lib/reactpress/fetchSetting.ts +19 -0
  112. package/lib/reactpress/homeFooter.tsx +27 -0
  113. package/lib/reactpress/layout-shell.tsx +19 -0
  114. package/lib/reactpress/materializeBootstrapCatalog.ts +31 -0
  115. package/lib/reactpress/normalizeApiResponse.ts +25 -0
  116. package/lib/reactpress/providers.tsx +199 -0
  117. package/lib/reactpress/resolveSiteNav.ts +56 -0
  118. package/lib/reactpress/seoMetadata.ts +82 -0
  119. package/lib/reactpress/serverLocale.ts +43 -0
  120. package/lib/reactpress/siteMetadata.ts +151 -0
  121. package/lib/reactpress/staticParams.ts +46 -0
  122. package/lib/reactpress/suggestionsContent.ts +48 -0
  123. package/lib/reactpress/useFeedFooterPlacement.ts +22 -0
  124. package/lib/reactpress/useSiteNavConfig.ts +39 -0
  125. package/lib/utils/archiveBanner.ts +8 -0
  126. package/lib/utils/colors.ts +119 -0
  127. package/lib/utils/comment.ts +46 -0
  128. package/lib/utils/headerPosition.ts +55 -0
  129. package/lib/utils/icons.tsx +238 -0
  130. package/lib/utils/reading.ts +16 -0
  131. package/lib/utils/scroll.ts +8 -0
  132. package/lib/utils/tagCloud.ts +222 -0
  133. package/next.config.js +103 -0
  134. package/package.json +98 -0
  135. package/postcss.config.js +5 -0
  136. package/prettier.config.js +10 -0
  137. package/public/apple-touch-icon.png +0 -0
  138. package/public/cli.png +0 -0
  139. package/public/favicon-32.png +0 -0
  140. package/public/favicon.ico +0 -0
  141. package/public/home-dark.png +0 -0
  142. package/public/home-light.png +0 -0
  143. package/public/icon-192.png +0 -0
  144. package/public/icon-512.png +0 -0
  145. package/public/logo-400.png +0 -0
  146. package/public/logo.png +0 -0
  147. package/public/mobile.png +0 -0
  148. package/public/pad.png +0 -0
  149. package/public/site.webmanifest +19 -0
  150. package/scripts/build-mock.mjs +139 -0
  151. package/scripts/dev.mjs +221 -0
  152. package/scripts/ensure-env.mjs +100 -0
  153. package/scripts/remote-dev.mjs +143 -0
  154. package/scripts/resolve-api-env.mjs +114 -0
  155. package/scripts/set-publish-version.mjs +31 -0
  156. package/scripts/smoke-pages.mjs +113 -0
  157. package/scripts/start-mock.mjs +79 -0
  158. package/scripts/verify-toolkit.mjs +37 -0
  159. package/svg.d.ts +6 -0
  160. package/theme.json +123 -0
  161. package/theme.manifest.schema.json +265 -0
  162. package/tsconfig.json +41 -0
  163. package/vercel.json +12 -0
@@ -0,0 +1,130 @@
1
+ 'use client'
2
+
3
+ import Link from '@/components/shared/Link'
4
+ import ThemeImage from '@/components/shared/ThemeImage'
5
+ import { getAdminBaseUrl } from '@/lib/reactpress/env'
6
+ import { useLocale } from '@fecommunity/reactpress-toolkit/ui'
7
+ import { resolveImageUrl, useSiteUser } from '@fecommunity/reactpress-toolkit/theme'
8
+ import { usePathname } from 'next/navigation'
9
+ import { useMemo, useState } from 'react'
10
+
11
+ function resolveAdminHref(): string {
12
+ const configured = process.env.NEXT_PUBLIC_REACTPRESS_ADMIN_URL?.trim()
13
+ if (configured) return configured.endsWith('/') ? configured : `${configured}/`
14
+ if (typeof window !== 'undefined') return `${window.location.origin}/admin/`
15
+ return `${getAdminBaseUrl()}/`
16
+ }
17
+
18
+ function resolveThemeAuthHref(path: 'login' | 'register', from: string): string {
19
+ const safeFrom = from.startsWith('/') ? from : '/'
20
+ return `/${path}?from=${encodeURIComponent(safeFrom)}`
21
+ }
22
+
23
+ type UserAuthLayout = 'header' | 'drawer'
24
+
25
+ export default function UserAuth({ layout = 'header' }: { layout?: UserAuthLayout }) {
26
+ const { t } = useLocale()
27
+ const pathname = usePathname() ?? '/'
28
+ const { user, removeUser } = useSiteUser()
29
+ const [menuOpen, setMenuOpen] = useState(false)
30
+ const authFrom = useMemo(() => pathname, [pathname])
31
+
32
+ if (user?.name) {
33
+ const initial = user.name.trim().charAt(0).toUpperCase()
34
+ return (
35
+ <div className="relative">
36
+ <button
37
+ type="button"
38
+ aria-label={user.name}
39
+ aria-expanded={menuOpen}
40
+ onClick={() => setMenuOpen((open) => !open)}
41
+ className="inline-flex h-10 w-10 items-center justify-center rounded-full border-0 bg-transparent p-0"
42
+ >
43
+ {user.avatar ? (
44
+ <ThemeImage
45
+ src={resolveImageUrl(user.avatar, 'avatar')}
46
+ alt={user.name}
47
+ width={28}
48
+ height={28}
49
+ className="h-7 w-7 rounded-full object-cover"
50
+ />
51
+ ) : (
52
+ <span className="inline-flex h-7 w-7 items-center justify-center rounded-full bg-[#fde3cf] text-xs leading-none font-medium text-[#f56a00]">
53
+ {initial}
54
+ </span>
55
+ )}
56
+ </button>
57
+ {menuOpen ? (
58
+ <>
59
+ <button
60
+ type="button"
61
+ aria-label="Close menu"
62
+ className="fixed inset-0 z-40 cursor-default border-0 bg-transparent p-0"
63
+ onClick={() => setMenuOpen(false)}
64
+ />
65
+ <div className="absolute right-0 z-50 mt-2 min-w-[120px] rounded-lg border border-[var(--border-color)] bg-[var(--bg-box)] py-1 shadow-[var(--box-shadow)]">
66
+ <a
67
+ href={resolveAdminHref()}
68
+ target="_blank"
69
+ rel="noreferrer"
70
+ className="block px-4 py-2 text-sm text-[var(--main-text-color)] no-underline hover:bg-[var(--bg-second)]"
71
+ onClick={() => setMenuOpen(false)}
72
+ >
73
+ {user.name}
74
+ </a>
75
+ <button
76
+ type="button"
77
+ onClick={() => {
78
+ removeUser?.()
79
+ setMenuOpen(false)
80
+ }}
81
+ className="block w-full px-4 py-2 text-left text-sm text-[var(--main-text-color)] hover:bg-[var(--bg-second)]"
82
+ >
83
+ {t('commentNamespace.logout')}
84
+ </button>
85
+ </div>
86
+ </>
87
+ ) : null}
88
+ </div>
89
+ )
90
+ }
91
+
92
+ const loginLabel = t('commentNamespace.userInfoConfirm')
93
+ const registerLabel = t('commentNamespace.register')
94
+
95
+ if (layout === 'drawer') {
96
+ return (
97
+ <div className="flex w-full flex-col gap-2">
98
+ <Link
99
+ href={resolveThemeAuthHref('login', authFrom)}
100
+ className="inline-flex h-11 w-full items-center justify-center rounded-lg border border-[var(--border-color)] bg-[var(--bg-box)] text-sm font-medium text-[var(--main-text-color)] no-underline transition-colors hover:border-[var(--primary-color)] hover:text-[var(--primary-color)]"
101
+ >
102
+ {loginLabel}
103
+ </Link>
104
+ <Link
105
+ href={resolveThemeAuthHref('register', authFrom)}
106
+ className="rp-primary-button inline-flex h-11 w-full items-center justify-center rounded-lg text-sm font-medium no-underline transition-opacity hover:opacity-90"
107
+ >
108
+ {registerLabel}
109
+ </Link>
110
+ </div>
111
+ )
112
+ }
113
+
114
+ return (
115
+ <div className="flex shrink-0 items-center gap-2">
116
+ <Link
117
+ href={resolveThemeAuthHref('login', authFrom)}
118
+ className="inline-flex h-8 items-center justify-center rounded-lg border border-[var(--border-color)] bg-transparent px-3 text-sm text-[var(--main-text-color)] no-underline transition-colors hover:border-[var(--primary-color)] hover:text-[var(--primary-color)]"
119
+ >
120
+ {loginLabel}
121
+ </Link>
122
+ <Link
123
+ href={resolveThemeAuthHref('register', authFrom)}
124
+ className="rp-primary-button inline-flex h-8 items-center justify-center rounded-lg px-3 text-sm font-medium no-underline transition-opacity hover:opacity-90"
125
+ >
126
+ {registerLabel}
127
+ </Link>
128
+ </div>
129
+ )
130
+ }
@@ -0,0 +1,323 @@
1
+ 'use client'
2
+
3
+ import Link from '@/components/shared/Link'
4
+ import { SearchIcon } from '@/lib/utils/icons'
5
+ import { ArticleProvider, SearchProvider } from '@/lib/providers/client'
6
+ import { normalizeList } from '@/lib/reactpress/normalizeApiResponse'
7
+ import { useLocale } from '@fecommunity/reactpress-toolkit/ui'
8
+ import type { IArticle } from '@fecommunity/reactpress-toolkit/types'
9
+ import { getSiteTitle, useSiteSetting } from '@fecommunity/reactpress-toolkit/theme'
10
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
11
+
12
+ interface SearchCategory {
13
+ key: string
14
+ label: string
15
+ }
16
+
17
+ interface SearchSubCategory {
18
+ key: string
19
+ label: string
20
+ url?: string
21
+ }
22
+
23
+ interface SearchHeroProps {
24
+ searchCategories?: {
25
+ categories?: SearchCategory[]
26
+ subCategories?: Record<string, SearchSubCategory[]>
27
+ }
28
+ initialKeyword?: string
29
+ onLocalSearch?: (keyword: string) => void
30
+ compact?: boolean
31
+ }
32
+
33
+ interface Suggestion {
34
+ label: string
35
+ link: string
36
+ description?: string
37
+ external?: boolean
38
+ }
39
+
40
+ export default function SearchHero({
41
+ searchCategories,
42
+ initialKeyword = '',
43
+ onLocalSearch,
44
+ compact = false,
45
+ }: SearchHeroProps) {
46
+ const { t } = useLocale()
47
+ const setting = useSiteSetting()
48
+ const siteTitle = getSiteTitle(setting)
49
+ const categories = useMemo(
50
+ () => searchCategories?.categories ?? [],
51
+ [searchCategories?.categories]
52
+ )
53
+ const subCategories = useMemo(
54
+ () => searchCategories?.subCategories ?? {},
55
+ [searchCategories?.subCategories]
56
+ )
57
+
58
+ const [category, setCategory] = useState(categories[0]?.key || 'local')
59
+ const [subCategory, setSubCategory] = useState(
60
+ subCategories[categories[0]?.key || 'local']?.[0]?.key
61
+ )
62
+ const [keyword, setKeyword] = useState(initialKeyword)
63
+ const [suggestions, setSuggestions] = useState<Suggestion[]>([])
64
+ const [open, setOpen] = useState(false)
65
+ const [loading, setLoading] = useState(false)
66
+ const wrapRef = useRef<HTMLDivElement>(null)
67
+ const inputRef = useRef<HTMLInputElement>(null)
68
+
69
+ const subList = subCategories[category] || []
70
+
71
+ useEffect(() => {
72
+ setSubCategory(subCategories[category]?.[0]?.key)
73
+ }, [category, subCategories])
74
+
75
+ useEffect(() => {
76
+ setKeyword(initialKeyword)
77
+ }, [initialKeyword])
78
+
79
+ useEffect(() => {
80
+ if (!compact) inputRef.current?.focus()
81
+ }, [compact])
82
+
83
+ useEffect(() => {
84
+ const onDocClick = (e: MouseEvent) => {
85
+ if (!wrapRef.current?.contains(e.target as Node)) {
86
+ setOpen(false)
87
+ }
88
+ }
89
+ document.addEventListener('click', onDocClick)
90
+ return () => document.removeEventListener('click', onDocClick)
91
+ }, [])
92
+
93
+ const fetchSuggestions = useCallback(
94
+ (value: string) => {
95
+ setLoading(true)
96
+ if (category === 'local') {
97
+ const request = value.trim()
98
+ ? SearchProvider.searchArticles(value)
99
+ : ArticleProvider.getRecommend()
100
+ return request
101
+ .then((res) => {
102
+ const items = normalizeList<IArticle>(res)
103
+ setSuggestions(
104
+ items
105
+ .filter((item) => !item.status || item.status === 'publish')
106
+ .map((item) => ({
107
+ label: item.title,
108
+ link: `/article/${item.id}`,
109
+ description: item.summary,
110
+ }))
111
+ )
112
+ })
113
+ .catch(() => setSuggestions([]))
114
+ .finally(() => setLoading(false))
115
+ }
116
+
117
+ const sub =
118
+ subCategories[category]?.find((item) => item.key === subCategory) ||
119
+ subCategories[category]?.[0]
120
+ const fallback = value.trim() || 'ReactPress'
121
+ const query = encodeURIComponent(fallback)
122
+
123
+ setSuggestions([
124
+ {
125
+ label: fallback,
126
+ link: sub?.url ? `${sub.url}${query}` : '#',
127
+ external: Boolean(sub?.url),
128
+ },
129
+ ])
130
+ setLoading(false)
131
+ return Promise.resolve()
132
+ },
133
+ [category, subCategory, subCategories]
134
+ )
135
+
136
+ useEffect(() => {
137
+ fetchSuggestions(keyword)
138
+ }, [category, subCategory, fetchSuggestions, keyword])
139
+
140
+ const handleSearch = () => {
141
+ if (category === 'local') {
142
+ const trimmed = keyword.trim()
143
+ if (!trimmed) return
144
+ if (onLocalSearch) {
145
+ onLocalSearch(trimmed)
146
+ } else {
147
+ window.location.href = `/search?keyword=${encodeURIComponent(trimmed)}`
148
+ }
149
+ setOpen(false)
150
+ return
151
+ }
152
+ const sub =
153
+ subCategories[category]?.find((item) => item.key === subCategory) ||
154
+ subCategories[category]?.[0]
155
+ if (sub?.url) {
156
+ const query = encodeURIComponent(keyword.trim() || 'ReactPress')
157
+ window.open(`${sub.url}${query}`, '_blank')
158
+ }
159
+ }
160
+
161
+ const clearKeyword = () => {
162
+ setKeyword('')
163
+ setOpen(false)
164
+ onLocalSearch?.('')
165
+ }
166
+
167
+ return (
168
+ <section
169
+ className={`rp-search-hero ${compact ? 'rp-search-hero--compact' : ''} ${open ? 'is-dropdown-open' : ''}`}
170
+ aria-label={t('searchArticle')}
171
+ >
172
+ <div className="rp-search-hero__backdrop" aria-hidden />
173
+ <div className="rp-search-hero__content">
174
+ {!compact ? (
175
+ <header className="rp-search-hero__header">
176
+ <h1 className="rp-search-hero__title">{t('searchArticle')}</h1>
177
+ {siteTitle ? <p className="rp-search-hero__subtitle">{siteTitle}</p> : null}
178
+ </header>
179
+ ) : null}
180
+
181
+ {categories.length ? (
182
+ <div
183
+ className="rp-search-hero__categories"
184
+ role="tablist"
185
+ aria-label={t('searchArticle')}
186
+ >
187
+ {categories.map((item) => (
188
+ <button
189
+ key={item.key}
190
+ type="button"
191
+ role="tab"
192
+ aria-selected={category === item.key}
193
+ onClick={() => {
194
+ setCategory(item.key)
195
+ setOpen(true)
196
+ }}
197
+ className={`rp-search-hero__category ${category === item.key ? 'is-active' : ''}`}
198
+ >
199
+ {item.label}
200
+ </button>
201
+ ))}
202
+ </div>
203
+ ) : null}
204
+
205
+ <div ref={wrapRef} className="rp-search-hero__field-wrap">
206
+ <div className="rp-search-hero__field">
207
+ <SearchIcon size={compact ? 20 : 22} className="rp-search-hero__field-icon shrink-0" />
208
+ <input
209
+ ref={inputRef}
210
+ value={keyword}
211
+ onChange={(e) => {
212
+ setKeyword(e.target.value)
213
+ fetchSuggestions(e.target.value)
214
+ setOpen(true)
215
+ }}
216
+ onFocus={() => {
217
+ fetchSuggestions(keyword)
218
+ setOpen(true)
219
+ }}
220
+ onKeyDown={(e) => {
221
+ if (e.key === 'Enter') handleSearch()
222
+ if (e.key === 'Escape') setOpen(false)
223
+ }}
224
+ placeholder={t('searchArticlePlaceholder')}
225
+ className="rp-theme-input rp-theme-input-plain min-w-0 flex-1 border-0 bg-transparent text-[15px] text-[var(--main-text-color)] md:text-base"
226
+ />
227
+ {keyword ? (
228
+ <button
229
+ type="button"
230
+ onClick={clearKeyword}
231
+ className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-[var(--second-text-color)] transition-colors hover:bg-[var(--bg-second)] hover:text-[var(--main-text-color)]"
232
+ >
233
+ ×
234
+ </button>
235
+ ) : null}
236
+ <button
237
+ type="button"
238
+ onClick={handleSearch}
239
+ className="rp-search-hero__submit rp-primary-button shrink-0 rounded-full font-medium"
240
+ >
241
+ {t('search')}
242
+ </button>
243
+ </div>
244
+
245
+ {open ? (
246
+ <ul className="rp-search-hero__dropdown m-0 max-h-[min(50vh,320px)] list-none overflow-auto p-0">
247
+ {loading ? (
248
+ <li className="px-4 py-3 text-center text-sm text-[var(--second-text-color)]">
249
+ {t('gettingArticle')}
250
+ </li>
251
+ ) : suggestions.length ? (
252
+ suggestions.slice(0, 10).map((item) => (
253
+ <li
254
+ key={`${item.link}-${item.label}`}
255
+ className="border-b border-[var(--border-color)] last:border-b-0"
256
+ >
257
+ {item.external ? (
258
+ <a
259
+ href={item.link}
260
+ target="_blank"
261
+ rel="noopener noreferrer"
262
+ className="block px-4 py-3 no-underline hover:bg-[var(--bg-second)]"
263
+ onClick={() => setOpen(false)}
264
+ >
265
+ <div className="text-sm font-medium text-[var(--main-text-color)] hover:text-[var(--primary-color)]">
266
+ {item.label}
267
+ </div>
268
+ </a>
269
+ ) : (
270
+ <Link
271
+ href={item.link}
272
+ className="block px-4 py-3 no-underline hover:bg-[var(--bg-second)]"
273
+ onClick={() => setOpen(false)}
274
+ >
275
+ <div className="text-sm font-medium text-[var(--main-text-color)] hover:text-[var(--primary-color)]">
276
+ {item.label}
277
+ </div>
278
+ {item.description ? (
279
+ <p
280
+ className="rp-rich-text m-0 mt-1 line-clamp-1 text-xs text-[var(--second-text-color)]"
281
+ dangerouslySetInnerHTML={{ __html: item.description }}
282
+ />
283
+ ) : null}
284
+ </Link>
285
+ )}
286
+ </li>
287
+ ))
288
+ ) : (
289
+ <li className="px-4 py-3 text-center text-sm text-[var(--second-text-color)]">
290
+ {t('empty')}
291
+ </li>
292
+ )}
293
+ </ul>
294
+ ) : null}
295
+ </div>
296
+
297
+ {subList.length > 0 ? (
298
+ <div
299
+ className="rp-search-hero__subcats"
300
+ role="tablist"
301
+ aria-label={categories.find((c) => c.key === category)?.label}
302
+ >
303
+ {subList.map((item) => (
304
+ <button
305
+ key={item.key}
306
+ type="button"
307
+ role="tab"
308
+ aria-selected={subCategory === item.key}
309
+ onClick={() => {
310
+ setSubCategory(item.key)
311
+ setOpen(true)
312
+ }}
313
+ className={`rp-search-hero__subcat ${subCategory === item.key ? 'is-active' : ''}`}
314
+ >
315
+ {item.label}
316
+ </button>
317
+ ))}
318
+ </div>
319
+ ) : null}
320
+ </div>
321
+ </section>
322
+ )
323
+ }