@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.
- package/.env.example +22 -0
- package/LICENSE +21 -0
- package/README.md +286 -0
- package/app/api/[[...path]]/route.ts +69 -0
- package/app/archives/page.tsx +28 -0
- package/app/article/[id]/page.tsx +92 -0
- package/app/category/[category]/page.tsx +60 -0
- package/app/globals.css +4969 -0
- package/app/knowledge/[pId]/[id]/page.tsx +54 -0
- package/app/knowledge/[pId]/page.tsx +47 -0
- package/app/knowledge/page.tsx +29 -0
- package/app/layout.tsx +76 -0
- package/app/llms.txt/route.ts +26 -0
- package/app/login/page.tsx +17 -0
- package/app/nav/[id]/page.tsx +44 -0
- package/app/nav/page.tsx +5 -0
- package/app/not-found.tsx +21 -0
- package/app/page/[id]/page.tsx +57 -0
- package/app/page.tsx +61 -0
- package/app/register/page.tsx +17 -0
- package/app/robots.ts +14 -0
- package/app/rss/route.ts +82 -0
- package/app/search/page.tsx +46 -0
- package/app/sitemap.ts +38 -0
- package/app/suggestions/page.tsx +47 -0
- package/app/tag/[tag]/page.tsx +58 -0
- package/app/tags/page.tsx +18 -0
- package/app/template.tsx +6 -0
- package/components/article/ArchiveBanner.tsx +40 -0
- package/components/article/ArticleActionsBar.tsx +53 -0
- package/components/article/ArticleCarousel.tsx +237 -0
- package/components/article/ArticleCopyrightBar.tsx +128 -0
- package/components/article/ArticleEndBar.tsx +19 -0
- package/components/article/ArticleFeedSection.tsx +59 -0
- package/components/article/ArticleList.tsx +170 -0
- package/components/article/ArticleReadingChrome.tsx +104 -0
- package/components/article/ArticleReadingProgress.tsx +75 -0
- package/components/article/ArticleRecommend.tsx +362 -0
- package/components/article/ArticleTocPanel.tsx +19 -0
- package/components/article/ArticleViewClient.tsx +221 -0
- package/components/article/CategoryMenu.tsx +67 -0
- package/components/article/CmsPageClient.tsx +92 -0
- package/components/article/LoadMore.tsx +59 -0
- package/components/article/ReadingContent.tsx +51 -0
- package/components/auth/AuthPageClient.tsx +314 -0
- package/components/comment/CommentEditor.tsx +290 -0
- package/components/comment/CommentEmoji.tsx +110 -0
- package/components/comment/CommentIconButton.tsx +37 -0
- package/components/comment/CommentItem.tsx +135 -0
- package/components/comment/CommentSection.tsx +123 -0
- package/components/comment/emojis.ts +152 -0
- package/components/knowledge/KnowledgeList.tsx +70 -0
- package/components/layout/BackToTop.tsx +27 -0
- package/components/layout/ConditionalSiteFooter.tsx +29 -0
- package/components/layout/ContentStylesLoader.tsx +9 -0
- package/components/layout/DoubleColumnLayout.tsx +171 -0
- package/components/layout/HeaderSearch.tsx +23 -0
- package/components/layout/LocaleSwitcher.tsx +24 -0
- package/components/layout/PageContainer.tsx +11 -0
- package/components/layout/PageLoadProgress.tsx +169 -0
- package/components/layout/SiteFooter.tsx +75 -0
- package/components/layout/SiteHeader.tsx +312 -0
- package/components/layout/SystemNotification.tsx +176 -0
- package/components/layout/ThemeSwitch.tsx +19 -0
- package/components/layout/UserAuth.tsx +130 -0
- package/components/search/SearchHero.tsx +323 -0
- package/components/search/SearchQuickLinks.tsx +285 -0
- package/components/shared/DevChunkRecovery.tsx +8 -0
- package/components/shared/Link.tsx +30 -0
- package/components/shared/SectionHeading.tsx +38 -0
- package/components/shared/ThemeImage.tsx +13 -0
- package/components/shared/logo.svg +7 -0
- package/components/views/ArchivesClient.tsx +137 -0
- package/components/views/CategoryClient.tsx +87 -0
- package/components/views/HomeClient.tsx +79 -0
- package/components/views/KnowledgeBookClient.tsx +127 -0
- package/components/views/KnowledgeChapterClient.tsx +175 -0
- package/components/views/KnowledgeClient.tsx +69 -0
- package/components/views/LoginClient.tsx +80 -0
- package/components/views/NavDetailClient.tsx +177 -0
- package/components/views/SearchClient.tsx +100 -0
- package/components/views/TagArchiveClient.tsx +105 -0
- package/components/views/TagsClient.tsx +46 -0
- package/components/widgets/AboutUs.tsx +180 -0
- package/components/widgets/ArticleActionButton.tsx +95 -0
- package/components/widgets/BookmarkWidget.tsx +61 -0
- package/components/widgets/CategoriesWidget.tsx +47 -0
- package/components/widgets/HomeSidebar.tsx +40 -0
- package/components/widgets/LikesWidget.tsx +77 -0
- package/components/widgets/ShareWidget.tsx +120 -0
- package/components/widgets/TagCloud.tsx +94 -0
- package/components/widgets/TagsWidget.tsx +160 -0
- package/eslint.config.mjs +80 -0
- package/lib/auth/githubOAuth.ts +26 -0
- package/lib/auth/sessionUser.ts +41 -0
- package/lib/mock-api/auth.ts +129 -0
- package/lib/mock-api/config.ts +11 -0
- package/lib/mock-api/cors.ts +53 -0
- package/lib/mock-api/data.ts +998 -0
- package/lib/mock-api/match-route.ts +202 -0
- package/lib/mock-api/proxy.ts +28 -0
- package/lib/providers/client.ts +58 -0
- package/lib/providers/materializeProvider.ts +9 -0
- package/lib/providers/server.ts +8 -0
- package/lib/reactpress/appearance.ts +69 -0
- package/lib/reactpress/bootstrap.ts +48 -0
- package/lib/reactpress/coercePublishedPage.ts +12 -0
- package/lib/reactpress/contentSeo.ts +123 -0
- package/lib/reactpress/env.ts +31 -0
- package/lib/reactpress/feedFooterPlacement.ts +16 -0
- package/lib/reactpress/fetchSetting.ts +19 -0
- package/lib/reactpress/homeFooter.tsx +27 -0
- package/lib/reactpress/layout-shell.tsx +19 -0
- package/lib/reactpress/materializeBootstrapCatalog.ts +31 -0
- package/lib/reactpress/normalizeApiResponse.ts +25 -0
- package/lib/reactpress/providers.tsx +199 -0
- package/lib/reactpress/resolveSiteNav.ts +56 -0
- package/lib/reactpress/seoMetadata.ts +82 -0
- package/lib/reactpress/serverLocale.ts +43 -0
- package/lib/reactpress/siteMetadata.ts +151 -0
- package/lib/reactpress/staticParams.ts +46 -0
- package/lib/reactpress/suggestionsContent.ts +48 -0
- package/lib/reactpress/useFeedFooterPlacement.ts +22 -0
- package/lib/reactpress/useSiteNavConfig.ts +39 -0
- package/lib/utils/archiveBanner.ts +8 -0
- package/lib/utils/colors.ts +119 -0
- package/lib/utils/comment.ts +46 -0
- package/lib/utils/headerPosition.ts +55 -0
- package/lib/utils/icons.tsx +238 -0
- package/lib/utils/reading.ts +16 -0
- package/lib/utils/scroll.ts +8 -0
- package/lib/utils/tagCloud.ts +222 -0
- package/next.config.js +103 -0
- package/package.json +98 -0
- package/postcss.config.js +5 -0
- package/prettier.config.js +10 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/cli.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/home-dark.png +0 -0
- package/public/home-light.png +0 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/logo-400.png +0 -0
- package/public/logo.png +0 -0
- package/public/mobile.png +0 -0
- package/public/pad.png +0 -0
- package/public/site.webmanifest +19 -0
- package/scripts/build-mock.mjs +139 -0
- package/scripts/dev.mjs +221 -0
- package/scripts/ensure-env.mjs +100 -0
- package/scripts/remote-dev.mjs +143 -0
- package/scripts/resolve-api-env.mjs +114 -0
- package/scripts/set-publish-version.mjs +31 -0
- package/scripts/smoke-pages.mjs +113 -0
- package/scripts/start-mock.mjs +79 -0
- package/scripts/verify-toolkit.mjs +37 -0
- package/svg.d.ts +6 -0
- package/theme.json +123 -0
- package/theme.manifest.schema.json +265 -0
- package/tsconfig.json +41 -0
- 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
|
+
}
|