@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,127 @@
1
+ 'use client'
2
+
3
+ import DoubleColumnLayout from '@/components/layout/DoubleColumnLayout'
4
+ import KnowledgeList from '@/components/knowledge/KnowledgeList'
5
+ import Link from '@/components/shared/Link'
6
+ import { useLocale } from '@fecommunity/reactpress-toolkit/ui'
7
+ import { Image, LocaleTime } from '@fecommunity/reactpress-toolkit/ui/content'
8
+ import type { IKnowledge } from '@fecommunity/reactpress-toolkit/types'
9
+ import { useCallback, useMemo } from 'react'
10
+
11
+ type KnowledgeBook = IKnowledge & { children?: IKnowledge[] }
12
+
13
+ interface KnowledgeBookClientProps {
14
+ pId: string
15
+ book: KnowledgeBook
16
+ otherBooks: KnowledgeBook[]
17
+ }
18
+
19
+ export default function KnowledgeBookClient({
20
+ pId,
21
+ book,
22
+ otherBooks = [],
23
+ }: KnowledgeBookClientProps) {
24
+ const { t, locale } = useLocale()
25
+ const chapters = useMemo(() => book.children ?? [], [book])
26
+
27
+ const startReading = useCallback(() => {
28
+ const chapter = chapters[0]
29
+ if (chapter) {
30
+ window.location.href = `/knowledge/${pId}/${chapter.id}`
31
+ }
32
+ }, [chapters, pId])
33
+
34
+ return (
35
+ <DoubleColumnLayout
36
+ topNode={
37
+ <nav className="mb-4 text-sm text-[var(--second-text-color)]">
38
+ <Link
39
+ href="/knowledge"
40
+ className="text-[var(--primary-color)] no-underline hover:underline"
41
+ >
42
+ {t('knowledgeBooks')}
43
+ </Link>
44
+ <span className="mx-2">/</span>
45
+ <span className="text-[var(--main-text-color)]">{book.title}</span>
46
+ </nav>
47
+ }
48
+ leftNode={
49
+ <div className="overflow-hidden rounded-lg bg-[var(--bg-box)] shadow-[var(--box-shadow)]">
50
+ <header className="border-b border-[var(--border-color)] px-4 py-4 text-lg font-semibold text-[var(--main-text-color)]">
51
+ {book.title}
52
+ </header>
53
+ <main className="p-4">
54
+ <section className="flex flex-col gap-4 sm:flex-row">
55
+ {book.cover ? (
56
+ <div className="h-40 w-full shrink-0 overflow-hidden rounded-lg sm:w-48">
57
+ <Image url={book.cover} size="medium" alt="cover" />
58
+ </div>
59
+ ) : null}
60
+ <div className="min-w-0 flex-1">
61
+ <h1 className="m-0 text-xl font-semibold text-[var(--main-text-color)]">
62
+ {book.title}
63
+ </h1>
64
+ {book.summary ? (
65
+ <p className="mt-2 text-sm leading-relaxed text-[var(--second-text-color)]">
66
+ {book.summary}
67
+ </p>
68
+ ) : null}
69
+ <p className="mt-3 text-xs text-[var(--second-text-color)]">
70
+ <span>
71
+ {book.views} {t('readingCount')}
72
+ </span>
73
+ <span className="mx-2">·</span>
74
+ <LocaleTime date={book.publishAt} locale={locale} />
75
+ </p>
76
+ <button
77
+ type="button"
78
+ onClick={startReading}
79
+ disabled={!chapters.length}
80
+ className="mt-4 rounded-lg bg-[var(--primary-color)] px-5 py-2 text-sm text-white disabled:cursor-not-allowed disabled:opacity-50"
81
+ >
82
+ {t('startReading')}
83
+ </button>
84
+ </div>
85
+ </section>
86
+
87
+ {chapters.length ? (
88
+ <ul className="m-0 mt-6 list-none border-t border-[var(--border-color)] p-0 pt-2">
89
+ {chapters.map((chapter) => (
90
+ <li
91
+ key={chapter.id}
92
+ className="border-b border-[var(--border-color)] last:border-b-0"
93
+ >
94
+ <Link
95
+ href={`/knowledge/${pId}/${chapter.id}`}
96
+ className="flex items-center justify-between gap-3 px-2 py-3 text-sm no-underline hover:bg-[var(--bg-second)]"
97
+ >
98
+ <span className="min-w-0 truncate text-[var(--main-text-color)]">
99
+ {chapter.title}
100
+ </span>
101
+ <span className="flex shrink-0 items-center gap-2 text-xs text-[var(--second-text-color)]">
102
+ <LocaleTime date={chapter.createAt} locale={locale} />
103
+ <span aria-hidden>›</span>
104
+ </span>
105
+ </Link>
106
+ </li>
107
+ ))}
108
+ </ul>
109
+ ) : (
110
+ <div className="mt-6 py-8 text-center text-sm text-[var(--second-text-color)]">
111
+ {t('pleaseWait')}
112
+ </div>
113
+ )}
114
+ </main>
115
+ </div>
116
+ }
117
+ rightNode={
118
+ <div className="rp-sidebar-sticky sticky mb-4 w-72 overflow-hidden rounded-lg bg-[var(--bg-box)] shadow-[var(--box-shadow)]">
119
+ <header className="border-b border-[var(--border-color)] px-4 py-3 font-semibold text-[var(--main-text-color)]">
120
+ {t('otherKnowledges')}
121
+ </header>
122
+ <KnowledgeList knowledges={otherBooks} small />
123
+ </div>
124
+ }
125
+ />
126
+ )
127
+ }
@@ -0,0 +1,175 @@
1
+ 'use client'
2
+
3
+ import CommentSection from '@/components/comment/CommentSection'
4
+ import DoubleColumnLayout from '@/components/layout/DoubleColumnLayout'
5
+ import ArticleCopyrightBar from '@/components/article/ArticleCopyrightBar'
6
+ import ReadingContent from '@/components/article/ReadingContent'
7
+ import Link from '@/components/shared/Link'
8
+ import { KnowledgeProvider } from '@/lib/providers/client'
9
+ import { useLocale } from '@fecommunity/reactpress-toolkit/ui'
10
+ import {
11
+ ArticleToc,
12
+ ImageViewer,
13
+ LocaleTime,
14
+ parseArticleToc,
15
+ } from '@fecommunity/reactpress-toolkit/ui/content'
16
+ import type { IKnowledge } from '@fecommunity/reactpress-toolkit/types'
17
+ import { useEffect, useMemo } from 'react'
18
+
19
+ type KnowledgeNode = IKnowledge & { children?: IKnowledge[] }
20
+
21
+ interface KnowledgeChapterClientProps {
22
+ pId: string
23
+ id: string
24
+ book: KnowledgeNode
25
+ chapter: KnowledgeNode
26
+ }
27
+
28
+ export default function KnowledgeChapterClient({
29
+ pId,
30
+ id,
31
+ book,
32
+ chapter,
33
+ }: KnowledgeChapterClientProps) {
34
+ const { t, locale } = useLocale()
35
+ const chapters = useMemo(() => book.children ?? [], [book.children])
36
+ const tocs = parseArticleToc(chapter.toc ?? '')
37
+ const idx = chapters.findIndex((item) => item.id === chapter.id)
38
+
39
+ const prev = useMemo(() => (idx > 0 ? chapters[idx - 1] : null), [chapters, idx])
40
+ const next = useMemo(
41
+ () => (idx >= 0 && idx < chapters.length - 1 ? chapters[idx + 1] : null),
42
+ [chapters, idx]
43
+ )
44
+
45
+ useEffect(() => {
46
+ KnowledgeProvider.updateKnowledgeViews(pId)
47
+ KnowledgeProvider.updateKnowledgeViews(id)
48
+ }, [pId, id])
49
+
50
+ useEffect(() => {
51
+ const el = document.getElementById(`js-toc-item-wrapper-${id}`)
52
+ el?.scrollIntoView({ block: 'nearest' })
53
+ }, [id])
54
+
55
+ return (
56
+ <DoubleColumnLayout
57
+ likesProps={{
58
+ defaultCount: chapter.likes,
59
+ id: chapter.id,
60
+ api: (chapterId, type) =>
61
+ KnowledgeProvider.updateKnowledgeLikes(chapterId, type).then((res) => res.likes),
62
+ }}
63
+ showComment={book.isCommentable}
64
+ leftNode={
65
+ <>
66
+ <nav className="mb-4 text-sm text-[var(--second-text-color)]">
67
+ <Link
68
+ href="/knowledge"
69
+ className="text-[var(--primary-color)] no-underline hover:underline"
70
+ >
71
+ {t('knowledgeBooks')}
72
+ </Link>
73
+ <span className="mx-2">/</span>
74
+ <Link
75
+ href={`/knowledge/${pId}`}
76
+ className="text-[var(--primary-color)] no-underline hover:underline"
77
+ >
78
+ {book.title}
79
+ </Link>
80
+ <span className="mx-2">/</span>
81
+ <span className="text-[var(--main-text-color)]">{chapter.title}</span>
82
+ </nav>
83
+ <ImageViewer containerSelector="#js-knowledge-content">
84
+ <div
85
+ id="js-knowledge-content"
86
+ className="overflow-hidden rounded-lg bg-[var(--bg-box)] p-4 shadow-[var(--box-shadow)] md:p-6"
87
+ >
88
+ <article>
89
+ <div className="mb-6 border-b border-[var(--border-color)] pb-4">
90
+ <h1 className="m-0 text-2xl font-semibold text-[var(--main-text-color)]">
91
+ {chapter.title}
92
+ </h1>
93
+ <p className="mt-2 text-sm text-[var(--second-text-color)]">
94
+ <span>
95
+ {t('publishAt')}
96
+ <LocaleTime date={chapter.publishAt} locale={locale} />
97
+ </span>
98
+ <span> • </span>
99
+ <span>
100
+ {t('readings')} {chapter.views}
101
+ </span>
102
+ </p>
103
+ </div>
104
+ <ReadingContent content={chapter.html || chapter.content} />
105
+ <ArticleCopyrightBar publishAt={chapter.publishAt} />
106
+ <div className="mt-6 flex flex-wrap gap-4">
107
+ {prev ? (
108
+ <Link
109
+ href={`/knowledge/${pId}/${prev.id}`}
110
+ className="flex min-w-0 items-center gap-2 rounded-lg border border-[var(--border-color)] px-4 py-3 text-sm no-underline hover:border-[var(--primary-color)]"
111
+ style={{ width: next ? '45%' : '100%' }}
112
+ >
113
+ <span aria-hidden>‹</span>
114
+ <span className="truncate text-[var(--main-text-color)]">{prev.title}</span>
115
+ </Link>
116
+ ) : null}
117
+ {next ? (
118
+ <Link
119
+ href={`/knowledge/${pId}/${next.id}`}
120
+ className="ml-auto flex min-w-0 items-center justify-end gap-2 rounded-lg border border-[var(--border-color)] px-4 py-3 text-sm no-underline hover:border-[var(--primary-color)]"
121
+ style={{ width: prev ? '45%' : '100%' }}
122
+ >
123
+ <span className="truncate text-[var(--main-text-color)]">{next.title}</span>
124
+ <span aria-hidden>›</span>
125
+ </Link>
126
+ ) : null}
127
+ </div>
128
+ </article>
129
+ {book.isCommentable ? (
130
+ <div className="rp-comment-wrap mt-8 border-t border-[var(--border-color)] pt-6">
131
+ <p className="rp-cms-section-title">{t('comment')}</p>
132
+ <div className="rp-comment-panel">
133
+ <CommentSection hostId={chapter.id} />
134
+ </div>
135
+ </div>
136
+ ) : null}
137
+ </div>
138
+ </ImageViewer>
139
+ </>
140
+ }
141
+ rightNode={
142
+ <div className="rp-sidebar-sticky sticky mb-4 w-72 overflow-hidden rounded-lg bg-[var(--bg-box)] shadow-[var(--box-shadow)]">
143
+ <header className="border-b border-[var(--border-color)] px-4 py-3 font-semibold text-[var(--main-text-color)]">
144
+ {book.title}
145
+ </header>
146
+ <ul className="m-0 max-h-[70vh] list-none overflow-y-auto p-0">
147
+ {chapters.map((item) => (
148
+ <li
149
+ key={item.id}
150
+ id={`js-toc-item-wrapper-${item.id}`}
151
+ className="border-b border-[var(--border-color)] last:border-b-0"
152
+ >
153
+ <Link
154
+ href={`/knowledge/${pId}/${item.id}`}
155
+ className={`block px-4 py-2.5 text-sm no-underline ${
156
+ item.id === id
157
+ ? 'bg-[var(--bg-second)] font-medium text-[var(--primary-color)]'
158
+ : 'text-[var(--main-text-color)] hover:bg-[var(--bg-second)]'
159
+ }`}
160
+ >
161
+ {item.title}
162
+ </Link>
163
+ {item.id === id && tocs.length ? (
164
+ <div className="border-t border-[var(--border-color)] px-2 py-2">
165
+ <ArticleToc items={tocs} showTitle={false} maxHeight="40vh" />
166
+ </div>
167
+ ) : null}
168
+ </li>
169
+ ))}
170
+ </ul>
171
+ </div>
172
+ }
173
+ />
174
+ )
175
+ }
@@ -0,0 +1,69 @@
1
+ 'use client'
2
+
3
+ import ArticleRecommend from '@/components/article/ArticleRecommend'
4
+ import CategoriesWidget from '@/components/widgets/CategoriesWidget'
5
+ import DoubleColumnLayout from '@/components/layout/DoubleColumnLayout'
6
+ import KnowledgeList from '@/components/knowledge/KnowledgeList'
7
+ import LoadMore from '@/components/article/LoadMore'
8
+ import { KnowledgeProvider } from '@/lib/providers/client'
9
+ import { useLocale } from '@fecommunity/reactpress-toolkit/ui'
10
+ import { useSiteCatalog } from '@fecommunity/reactpress-toolkit/theme'
11
+ import type { IKnowledge } from '@fecommunity/reactpress-toolkit/types'
12
+ import { useCallback, useEffect, useState } from 'react'
13
+
14
+ interface KnowledgeClientProps {
15
+ initialBooks: Array<IKnowledge & { children?: IKnowledge[] }>
16
+ total: number
17
+ }
18
+
19
+ const pageSize = 12
20
+
21
+ export default function KnowledgeClient({ initialBooks = [], total = 0 }: KnowledgeClientProps) {
22
+ const { t } = useLocale()
23
+ const { categories } = useSiteCatalog()
24
+ const [page, setPage] = useState(1)
25
+ const [books, setBooks] = useState(initialBooks)
26
+
27
+ useEffect(() => {
28
+ setBooks(initialBooks)
29
+ setPage(1)
30
+ }, [initialBooks])
31
+
32
+ const loadMore = useCallback((nextPage: number) => {
33
+ return KnowledgeProvider.getKnowledges({
34
+ page: nextPage,
35
+ pageSize,
36
+ status: 'publish',
37
+ }).then((res) => {
38
+ setPage(nextPage)
39
+ setBooks((prev) => [...prev, ...res[0]])
40
+ })
41
+ }, [])
42
+
43
+ return (
44
+ <DoubleColumnLayout
45
+ leftNode={
46
+ <LoadMore
47
+ pageStart={1}
48
+ loadMore={loadMore}
49
+ hasMore={page * pageSize < total}
50
+ loader={
51
+ <div className="py-6 text-center text-sm text-[var(--second-text-color)]">
52
+ {t('gettingKnowledge')}
53
+ </div>
54
+ }
55
+ >
56
+ <KnowledgeList knowledges={books} />
57
+ </LoadMore>
58
+ }
59
+ rightNode={
60
+ <div className="rp-sidebar-sticky sticky mb-4 w-72">
61
+ <ArticleRecommend mode="inline" deferFetch />
62
+ <CategoriesWidget
63
+ categories={categories as Parameters<typeof CategoriesWidget>[0]['categories']}
64
+ />
65
+ </div>
66
+ }
67
+ />
68
+ )
69
+ }
@@ -0,0 +1,80 @@
1
+ 'use client'
2
+
3
+ import AuthPageClient from '@/components/auth/AuthPageClient'
4
+ import { normalizeSessionUser } from '@/lib/auth/sessionUser'
5
+ import { UserProvider } from '@/lib/providers/client'
6
+ import { useLocale } from '@fecommunity/reactpress-toolkit/ui'
7
+ import type { IUser } from '@fecommunity/reactpress-toolkit/types'
8
+ import { useSiteSetting, useSiteUser } from '@fecommunity/reactpress-toolkit/theme'
9
+ import { useRouter } from 'next/navigation'
10
+ import { useEffect, useState } from 'react'
11
+
12
+ interface LoginClientProps {
13
+ code?: string
14
+ from?: string
15
+ }
16
+
17
+ export default function LoginClient({ code = '', from = '/' }: LoginClientProps) {
18
+ const { t } = useLocale()
19
+ const { setUser } = useSiteUser()
20
+ const setting = useSiteSetting()
21
+ const router = useRouter()
22
+ const [githubError, setGithubError] = useState('')
23
+
24
+ useEffect(() => {
25
+ if (!code) return
26
+
27
+ UserProvider.loginWithGithub(code)
28
+ .then((response) => {
29
+ const sessionUser = normalizeSessionUser(response)
30
+ if (!sessionUser?.token) {
31
+ throw new Error(t('wrongPasswd'))
32
+ }
33
+ setUser?.(sessionUser as IUser & { token: string })
34
+ router.replace(from || '/')
35
+ })
36
+ .catch((error: unknown) => {
37
+ const message =
38
+ error && typeof error === 'object' && 'message' in error
39
+ ? String((error as { message?: unknown }).message || '')
40
+ : ''
41
+ setGithubError(message || t('wrongPasswd'))
42
+ })
43
+ }, [code, from, router, setUser, t])
44
+
45
+ if (code) {
46
+ if (githubError) {
47
+ return (
48
+ <div className="rp-auth-page">
49
+ <div className="rp-auth-card">
50
+ <header className="rp-auth-card__header">
51
+ <p className="rp-auth-card__eyebrow">{setting.systemTitle}</p>
52
+ <h1 className="rp-auth-card__title">{t('pageTitleLogin')}</h1>
53
+ </header>
54
+ <div className="rp-auth-alert is-error" role="alert">
55
+ {githubError}
56
+ </div>
57
+ <a
58
+ href={`/login?from=${encodeURIComponent(from || '/')}`}
59
+ className="rp-auth-submit rp-auth-submit--link"
60
+ >
61
+ {t('commentNamespace.loginNow')}
62
+ </a>
63
+ </div>
64
+ </div>
65
+ )
66
+ }
67
+
68
+ return (
69
+ <div className="rp-auth-page">
70
+ <div className="rp-auth-card rp-auth-card--loading">
71
+ <div className="rp-auth-spinner" role="status" aria-label={t('logingWithGithub')} />
72
+ <p className="rp-auth-loading-text">{t('logingWithGithub')}</p>
73
+ <p className="rp-auth-loading-subtext">{setting.systemTitle}</p>
74
+ </div>
75
+ </div>
76
+ )
77
+ }
78
+
79
+ return <AuthPageClient mode="login" from={from} />
80
+ }
@@ -0,0 +1,177 @@
1
+ 'use client'
2
+
3
+ import ArticleRecommend from '@/components/article/ArticleRecommend'
4
+ import DoubleColumnLayout from '@/components/layout/DoubleColumnLayout'
5
+ import Link from '@/components/shared/Link'
6
+ import ThemeImage from '@/components/shared/ThemeImage'
7
+ import SectionHeading from '@/components/shared/SectionHeading'
8
+ import { ChevronLeftIcon, ChevronRightIcon } from '@/lib/utils/icons'
9
+ import { useLocale } from '@fecommunity/reactpress-toolkit/ui'
10
+ import { useSiteCatalog } from '@fecommunity/reactpress-toolkit/theme'
11
+ import { useRouter } from 'next/navigation'
12
+ import { useCallback, useMemo, useState } from 'react'
13
+
14
+ interface NavChild {
15
+ key: string
16
+ label: string
17
+ description?: string
18
+ url?: string
19
+ icon?: string
20
+ }
21
+
22
+ interface NavDetailClientProps {
23
+ siteKey: string
24
+ navConfig?: {
25
+ urlConfig?: Array<{ children?: NavChild[] }>
26
+ }
27
+ }
28
+
29
+ function getIconUrl(item: { icon?: string; url?: string }) {
30
+ if (item?.icon?.trim()) return item.icon.trim()
31
+ if (item?.url) return `${item.url.replace(/\/$/, '')}/favicon.ico`
32
+ return ''
33
+ }
34
+
35
+ function plainDescription(value?: string) {
36
+ if (!value?.trim()) return ''
37
+ return value
38
+ .replace(/<[^>]+>/g, ' ')
39
+ .replace(/\s+/g, ' ')
40
+ .trim()
41
+ }
42
+
43
+ function SiteIcon({ src, label }: { src: string; label: string }) {
44
+ const [failed, setFailed] = useState(false)
45
+
46
+ if (!src || failed) {
47
+ return (
48
+ <span className="rp-nav-detail__icon-fallback" aria-hidden>
49
+ {label.slice(0, 1)}
50
+ </span>
51
+ )
52
+ }
53
+
54
+ return (
55
+ <ThemeImage
56
+ src={src}
57
+ alt=""
58
+ width={56}
59
+ height={56}
60
+ className="rp-nav-detail__icon-img"
61
+ onError={() => setFailed(true)}
62
+ />
63
+ )
64
+ }
65
+
66
+ export default function NavDetailClient({ siteKey, navConfig }: NavDetailClientProps) {
67
+ const { t, locale } = useLocale()
68
+ const router = useRouter()
69
+ const { siteConfig, globalSetting } = useSiteCatalog()
70
+ const backLabel = locale === 'zh' ? '返回' : 'Back'
71
+
72
+ const handleBack = useCallback(() => {
73
+ if (typeof window !== 'undefined' && window.history.length > 1) {
74
+ router.back()
75
+ return
76
+ }
77
+ router.push('/search')
78
+ }, [router])
79
+
80
+ const article = useMemo(() => {
81
+ const urlConfig =
82
+ navConfig?.urlConfig ??
83
+ siteConfig?.nav?.urlConfig ??
84
+ (globalSetting as { globalConfig?: { urlConfig?: Array<{ children?: NavChild[] }> } })
85
+ ?.globalConfig?.urlConfig ??
86
+ []
87
+ const urlItem = urlConfig
88
+ .flatMap((item) => item.children ?? [])
89
+ .find((item) => item.key === siteKey)
90
+ return {
91
+ id: siteKey,
92
+ title: urlItem?.label ?? t('unknownTitle'),
93
+ cover: getIconUrl(urlItem ?? {}),
94
+ summary: plainDescription(urlItem?.description),
95
+ url: urlItem?.url ?? '',
96
+ }
97
+ }, [navConfig?.urlConfig, siteConfig?.nav?.urlConfig, globalSetting, siteKey, t])
98
+
99
+ const displayUrl = article.url.replace(/^https?:\/\//, '').replace(/\/$/, '')
100
+
101
+ return (
102
+ <DoubleColumnLayout
103
+ className="rp-nav-detail-page"
104
+ topNode={
105
+ <nav className="rp-nav-detail__crumb" aria-label="breadcrumb">
106
+ <Link href="/search" className="rp-nav-detail__crumb-link">
107
+ {t('searchArticle')}
108
+ </Link>
109
+ <ChevronRightIcon size={14} className="rp-nav-detail__crumb-sep" aria-hidden />
110
+ <span className="rp-nav-detail__crumb-current">{article.title}</span>
111
+ </nav>
112
+ }
113
+ leftNode={
114
+ <div className="rp-nav-detail">
115
+ <article className="rp-nav-detail__card">
116
+ <header className="rp-nav-detail__header">
117
+ <div className="rp-nav-detail__header-main">
118
+ <div className="rp-nav-detail__icon">
119
+ <SiteIcon src={article.cover} label={article.title} />
120
+ </div>
121
+ <div className="rp-nav-detail__meta">
122
+ <h1 className="rp-nav-detail__title">{article.title}</h1>
123
+ {displayUrl ? (
124
+ <p className="rp-nav-detail__url" title={article.url}>
125
+ {displayUrl}
126
+ </p>
127
+ ) : null}
128
+ </div>
129
+ </div>
130
+ <div className="rp-nav-detail__actions">
131
+ {article.url ? (
132
+ <button
133
+ type="button"
134
+ onClick={() => window.open(article.url, '_blank', 'noopener,noreferrer')}
135
+ className="rp-nav-detail__btn rp-nav-detail__btn--primary"
136
+ >
137
+ {t('navOpenSite')}
138
+ </button>
139
+ ) : null}
140
+ <button
141
+ type="button"
142
+ onClick={handleBack}
143
+ className="rp-nav-detail__btn rp-nav-detail__btn--ghost"
144
+ >
145
+ <ChevronLeftIcon size={14} aria-hidden />
146
+ {backLabel}
147
+ </button>
148
+ </div>
149
+ </header>
150
+
151
+ {article.summary ? <p className="rp-nav-detail__summary">{article.summary}</p> : null}
152
+
153
+ <div className="rp-nav-detail__disclaimer" role="note">
154
+ {t('navPreviewDisclaimer')}
155
+ </div>
156
+
157
+ {article.url ? (
158
+ <div className="rp-nav-detail__preview">
159
+ <iframe
160
+ title={article.title}
161
+ src={article.url}
162
+ className="rp-nav-detail__iframe"
163
+ allowFullScreen
164
+ />
165
+ </div>
166
+ ) : null}
167
+ </article>
168
+
169
+ <section className="rp-nav-detail__recommend">
170
+ <SectionHeading>{t('recommendToReading')}</SectionHeading>
171
+ <ArticleRecommend articleId={article.id} needTitle={false} />
172
+ </section>
173
+ </div>
174
+ }
175
+ />
176
+ )
177
+ }