@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,100 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import ArticleList from '@/components/article/ArticleList'
|
|
4
|
+
import DoubleColumnLayout from '@/components/layout/DoubleColumnLayout'
|
|
5
|
+
import SearchHero from '@/components/search/SearchHero'
|
|
6
|
+
import SearchQuickLinks from '@/components/search/SearchQuickLinks'
|
|
7
|
+
import { useFeedFooterPlacement } from '@/lib/reactpress/useFeedFooterPlacement'
|
|
8
|
+
import { useSiteNavConfig } from '@/lib/reactpress/useSiteNavConfig'
|
|
9
|
+
import { useLocale } from '@fecommunity/reactpress-toolkit/ui'
|
|
10
|
+
import type { ListArticle } from '@fecommunity/reactpress-toolkit/theme'
|
|
11
|
+
import { useRouter } from 'next/navigation'
|
|
12
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
13
|
+
|
|
14
|
+
interface SearchClientProps {
|
|
15
|
+
keyword: string
|
|
16
|
+
articles: ListArticle[]
|
|
17
|
+
navConfig?: {
|
|
18
|
+
urlConfig?: unknown[]
|
|
19
|
+
searchCategories?: {
|
|
20
|
+
categories?: Array<{ key: string; label: string }>
|
|
21
|
+
subCategories?: Record<string, Array<{ key: string; label: string; url?: string }>>
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default function SearchClient({
|
|
27
|
+
keyword: initialKeyword = '',
|
|
28
|
+
articles: initialArticles = [],
|
|
29
|
+
navConfig,
|
|
30
|
+
}: SearchClientProps) {
|
|
31
|
+
const { t, locale } = useLocale()
|
|
32
|
+
const router = useRouter()
|
|
33
|
+
const [articles, setArticles] = useState(initialArticles)
|
|
34
|
+
const hasSearchResults = Boolean(initialKeyword.trim())
|
|
35
|
+
const { footerAtBottom } = useFeedFooterPlacement({
|
|
36
|
+
itemCount: hasSearchResults ? articles.length : 0,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const { urlConfig, searchCategories } = useSiteNavConfig(navConfig)
|
|
40
|
+
const quickLinks = urlConfig as Parameters<typeof SearchQuickLinks>[0]['dataSource']
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
setArticles(initialArticles)
|
|
44
|
+
}, [initialArticles])
|
|
45
|
+
|
|
46
|
+
const runSearch = useCallback(
|
|
47
|
+
(value: string) => {
|
|
48
|
+
const trimmed = value.trim()
|
|
49
|
+
if (!trimmed) {
|
|
50
|
+
router.push('/search')
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
router.push(`/search?keyword=${encodeURIComponent(trimmed)}`)
|
|
54
|
+
},
|
|
55
|
+
[router]
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<DoubleColumnLayout
|
|
60
|
+
className="rp-search-page"
|
|
61
|
+
fillMinHeight={footerAtBottom}
|
|
62
|
+
topNode={
|
|
63
|
+
<SearchHero
|
|
64
|
+
key={locale}
|
|
65
|
+
searchCategories={searchCategories}
|
|
66
|
+
initialKeyword={initialKeyword}
|
|
67
|
+
onLocalSearch={runSearch}
|
|
68
|
+
compact={hasSearchResults}
|
|
69
|
+
/>
|
|
70
|
+
}
|
|
71
|
+
leftNode={
|
|
72
|
+
hasSearchResults ? (
|
|
73
|
+
<div className="rp-search-results">
|
|
74
|
+
<div className="rp-search-results__summary">
|
|
75
|
+
<p className="m-0 text-sm text-[var(--second-text-color)]">
|
|
76
|
+
{t('totalSearch')}{' '}
|
|
77
|
+
<span className="font-semibold text-[var(--primary-color)]">{articles.length}</span>{' '}
|
|
78
|
+
{t('piece')}
|
|
79
|
+
</p>
|
|
80
|
+
<p className="m-0 mt-1 text-base font-medium text-[var(--main-text-color)]">
|
|
81
|
+
“{initialKeyword}”
|
|
82
|
+
</p>
|
|
83
|
+
</div>
|
|
84
|
+
<main className="rp-search-results__list">
|
|
85
|
+
{articles.length ? (
|
|
86
|
+
<ArticleList articles={articles} />
|
|
87
|
+
) : (
|
|
88
|
+
<div className="rounded-xl bg-[var(--bg-box)] p-10 text-center text-[var(--second-text-color)] ring-1 ring-black/5 dark:ring-white/5">
|
|
89
|
+
{t('empty')}
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
</main>
|
|
93
|
+
</div>
|
|
94
|
+
) : (
|
|
95
|
+
<SearchQuickLinks key={locale} dataSource={quickLinks} />
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
/>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import ArchiveBanner from '@/components/article/ArchiveBanner'
|
|
4
|
+
import ArticleFeedSection from '@/components/article/ArticleFeedSection'
|
|
5
|
+
import ArticleList from '@/components/article/ArticleList'
|
|
6
|
+
import DoubleColumnLayout from '@/components/layout/DoubleColumnLayout'
|
|
7
|
+
import HomeSidebar from '@/components/widgets/HomeSidebar'
|
|
8
|
+
import TagsWidget from '@/components/widgets/TagsWidget'
|
|
9
|
+
import { FEED_PAGE_SIZE } from '@/lib/reactpress/feedFooterPlacement'
|
|
10
|
+
import { useFeedFooterPlacement } from '@/lib/reactpress/useFeedFooterPlacement'
|
|
11
|
+
import { ArticleProvider } from '@/lib/providers/client'
|
|
12
|
+
import { getArchiveBannerImage } from '@/lib/utils/archiveBanner'
|
|
13
|
+
import { useLocale } from '@fecommunity/reactpress-toolkit/ui'
|
|
14
|
+
import {
|
|
15
|
+
slimArticlesForList,
|
|
16
|
+
useSiteCatalog,
|
|
17
|
+
type ListArticle,
|
|
18
|
+
} from '@fecommunity/reactpress-toolkit/theme'
|
|
19
|
+
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
20
|
+
|
|
21
|
+
interface TagArchiveClientProps {
|
|
22
|
+
initialArticles: ListArticle[]
|
|
23
|
+
total: number
|
|
24
|
+
tag: { value: string; label: string }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const pageSize = FEED_PAGE_SIZE
|
|
28
|
+
|
|
29
|
+
export default function TagArchiveClient({
|
|
30
|
+
initialArticles = [],
|
|
31
|
+
total = 0,
|
|
32
|
+
tag,
|
|
33
|
+
}: TagArchiveClientProps) {
|
|
34
|
+
const { t } = useLocale()
|
|
35
|
+
const { tags } = useSiteCatalog()
|
|
36
|
+
const [page, setPage] = useState(1)
|
|
37
|
+
const [articles, setArticles] = useState(initialArticles)
|
|
38
|
+
const banner = useMemo(() => getArchiveBannerImage(articles), [articles])
|
|
39
|
+
const hasMore = page * pageSize < total
|
|
40
|
+
const { footerInSidebar, footerAtBottom } = useFeedFooterPlacement({
|
|
41
|
+
hasMore,
|
|
42
|
+
itemCount: articles.length,
|
|
43
|
+
pageSize,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
setArticles(initialArticles)
|
|
48
|
+
setPage(1)
|
|
49
|
+
}, [initialArticles])
|
|
50
|
+
|
|
51
|
+
const getArticles = useCallback(
|
|
52
|
+
(nextPage: number) => {
|
|
53
|
+
return ArticleProvider.getArticlesByTag(tag.value, {
|
|
54
|
+
page: nextPage,
|
|
55
|
+
pageSize,
|
|
56
|
+
status: 'publish',
|
|
57
|
+
}).then((res) => {
|
|
58
|
+
setPage(nextPage)
|
|
59
|
+
setArticles((prev) => [...prev, ...slimArticlesForList(res[0])])
|
|
60
|
+
})
|
|
61
|
+
},
|
|
62
|
+
[tag.value]
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<DoubleColumnLayout
|
|
67
|
+
fillMinHeight={footerAtBottom}
|
|
68
|
+
leftNode={
|
|
69
|
+
<>
|
|
70
|
+
<ArchiveBanner
|
|
71
|
+
imageUrl={banner.url}
|
|
72
|
+
isBrandFallback={banner.isBrandFallback}
|
|
73
|
+
title={
|
|
74
|
+
<>
|
|
75
|
+
{t('yu')} <span>{tag.label}</span> {t('tagRelativeArticles')}
|
|
76
|
+
</>
|
|
77
|
+
}
|
|
78
|
+
subtitle={
|
|
79
|
+
<>
|
|
80
|
+
{t('totalSearch')} <span>{total}</span> {t('piece')}
|
|
81
|
+
</>
|
|
82
|
+
}
|
|
83
|
+
/>
|
|
84
|
+
<TagsWidget tags={tags as Parameters<typeof TagsWidget>[0]['tags']} />
|
|
85
|
+
<ArticleFeedSection
|
|
86
|
+
showCategoryMenu={false}
|
|
87
|
+
pageStart={1}
|
|
88
|
+
loadMore={getArticles}
|
|
89
|
+
hasMore={hasMore}
|
|
90
|
+
>
|
|
91
|
+
<ArticleList articles={articles} />
|
|
92
|
+
</ArticleFeedSection>
|
|
93
|
+
</>
|
|
94
|
+
}
|
|
95
|
+
rightNode={
|
|
96
|
+
<HomeSidebar
|
|
97
|
+
showTags={false}
|
|
98
|
+
showCategories
|
|
99
|
+
showAboutUs={footerInSidebar}
|
|
100
|
+
deferRecommend={false}
|
|
101
|
+
/>
|
|
102
|
+
}
|
|
103
|
+
/>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import DoubleColumnLayout from '@/components/layout/DoubleColumnLayout'
|
|
4
|
+
import HomeSidebar from '@/components/widgets/HomeSidebar'
|
|
5
|
+
import TagsWidget from '@/components/widgets/TagsWidget'
|
|
6
|
+
import { FEED_PAGE_SIZE } from '@/lib/reactpress/feedFooterPlacement'
|
|
7
|
+
import { useFeedFooterPlacement } from '@/lib/reactpress/useFeedFooterPlacement'
|
|
8
|
+
import { useLocale } from '@fecommunity/reactpress-toolkit/ui'
|
|
9
|
+
import type { ITag } from '@fecommunity/reactpress-toolkit/types'
|
|
10
|
+
|
|
11
|
+
interface TagsClientProps {
|
|
12
|
+
tags: ITag[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function TagsClient({ tags }: TagsClientProps) {
|
|
16
|
+
const { t } = useLocale()
|
|
17
|
+
const { footerInSidebar, footerAtBottom } = useFeedFooterPlacement({
|
|
18
|
+
itemCount: tags.length,
|
|
19
|
+
pageSize: FEED_PAGE_SIZE,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<DoubleColumnLayout
|
|
24
|
+
fillMinHeight={footerAtBottom}
|
|
25
|
+
leftNode={
|
|
26
|
+
<div className="overflow-hidden rounded-lg bg-[var(--bg-box)] p-6 shadow-[var(--box-shadow)]">
|
|
27
|
+
<h1 className="m-0 text-2xl font-semibold text-[var(--main-text-color)]">
|
|
28
|
+
{t('tagTitle')}
|
|
29
|
+
</h1>
|
|
30
|
+
<p className="mt-2 text-sm text-[var(--second-text-color)]">{tags.length} tags</p>
|
|
31
|
+
<div className="mt-6">
|
|
32
|
+
<TagsWidget tags={tags} needTitle={false} />
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
}
|
|
36
|
+
rightNode={
|
|
37
|
+
<HomeSidebar
|
|
38
|
+
showTags={false}
|
|
39
|
+
showCategories
|
|
40
|
+
showAboutUs={footerInSidebar}
|
|
41
|
+
deferRecommend={false}
|
|
42
|
+
/>
|
|
43
|
+
}
|
|
44
|
+
/>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import ThemeImage from '@/components/shared/ThemeImage'
|
|
4
|
+
import type { ReactNode } from 'react'
|
|
5
|
+
import { useLocale } from '@fecommunity/reactpress-toolkit/ui'
|
|
6
|
+
import { useSiteSetting } from '@fecommunity/reactpress-toolkit/theme'
|
|
7
|
+
|
|
8
|
+
function trimUrl(value?: string) {
|
|
9
|
+
return (value ?? '').trim()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function QrPopover({
|
|
13
|
+
imageUrl,
|
|
14
|
+
label,
|
|
15
|
+
children,
|
|
16
|
+
}: {
|
|
17
|
+
imageUrl: string
|
|
18
|
+
label: string
|
|
19
|
+
children: ReactNode
|
|
20
|
+
}) {
|
|
21
|
+
return (
|
|
22
|
+
<span className="group relative inline-flex">
|
|
23
|
+
{children}
|
|
24
|
+
<span
|
|
25
|
+
role="tooltip"
|
|
26
|
+
className="rp-glass pointer-events-none absolute bottom-full left-1/2 z-20 mb-2 hidden -translate-x-1/2 rounded-xl p-2 shadow-lg group-hover:block"
|
|
27
|
+
>
|
|
28
|
+
<ThemeImage
|
|
29
|
+
src={imageUrl}
|
|
30
|
+
alt={label}
|
|
31
|
+
width={300}
|
|
32
|
+
height={200}
|
|
33
|
+
className="h-auto max-h-[200px] w-auto max-w-[300px]"
|
|
34
|
+
/>
|
|
35
|
+
</span>
|
|
36
|
+
</span>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ContactInfo() {
|
|
41
|
+
const { t } = useLocale()
|
|
42
|
+
const setting = useSiteSetting()
|
|
43
|
+
const githubUrl = trimUrl(setting?.aboutUsGithubUrl)
|
|
44
|
+
const commentQr = trimUrl(setting?.aboutUsCommentQr)
|
|
45
|
+
const wechatQr = trimUrl(setting?.aboutUsWechatQr)
|
|
46
|
+
|
|
47
|
+
const items = [
|
|
48
|
+
<a
|
|
49
|
+
key="rss"
|
|
50
|
+
href="/rss"
|
|
51
|
+
target="_blank"
|
|
52
|
+
rel="noopener noreferrer"
|
|
53
|
+
aria-label="rss"
|
|
54
|
+
title={t('rssSubscribe')}
|
|
55
|
+
className="inline-flex text-[var(--main-text-color)] hover:text-[var(--primary-color)]"
|
|
56
|
+
>
|
|
57
|
+
<svg viewBox="0 0 1024 1024" width="24" height="24" fill="currentColor" aria-hidden>
|
|
58
|
+
<path d="M512 0C230.4 0 0 230.4 0 512s230.4 512 512 512 512-230.4 512-512S793.6 0 512 0z m-182.4 768C288 768 256 736 256 694.4s32-73.6 73.6-73.6 73.6 32 73.6 73.6-32 73.6-73.6 73.6z m185.6 0c0-144-115.2-259.2-259.2-259.2v-80c185.6 0 339.2 150.4 339.2 339.2h-80z m172.8 0c0-240-195.2-432-432-432V256c281.6 0 512 230.4 512 512h-80z" />
|
|
59
|
+
</svg>
|
|
60
|
+
</a>,
|
|
61
|
+
githubUrl ? (
|
|
62
|
+
<a
|
|
63
|
+
key="github"
|
|
64
|
+
href={githubUrl}
|
|
65
|
+
target="_blank"
|
|
66
|
+
rel="noopener noreferrer nofollow"
|
|
67
|
+
aria-label="Github"
|
|
68
|
+
title="Github"
|
|
69
|
+
className="inline-flex text-[var(--main-text-color)] hover:text-[var(--primary-color)]"
|
|
70
|
+
>
|
|
71
|
+
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" aria-hidden>
|
|
72
|
+
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
|
|
73
|
+
</svg>
|
|
74
|
+
</a>
|
|
75
|
+
) : null,
|
|
76
|
+
commentQr ? (
|
|
77
|
+
<QrPopover key="comment" imageUrl={commentQr} label="comment">
|
|
78
|
+
<span
|
|
79
|
+
role="img"
|
|
80
|
+
aria-label="comment"
|
|
81
|
+
title={t('comment')}
|
|
82
|
+
className="inline-flex cursor-pointer text-2xl text-[var(--main-text-color)] hover:text-[var(--primary-color)]"
|
|
83
|
+
>
|
|
84
|
+
💬
|
|
85
|
+
</span>
|
|
86
|
+
</QrPopover>
|
|
87
|
+
) : null,
|
|
88
|
+
wechatQr ? (
|
|
89
|
+
<QrPopover key="wechat" imageUrl={wechatQr} label="wechat">
|
|
90
|
+
<span
|
|
91
|
+
role="img"
|
|
92
|
+
aria-label="wechat"
|
|
93
|
+
title="WeChat"
|
|
94
|
+
className="inline-flex cursor-pointer text-2xl text-[var(--main-text-color)] hover:text-[var(--primary-color)]"
|
|
95
|
+
>
|
|
96
|
+
💚
|
|
97
|
+
</span>
|
|
98
|
+
</QrPopover>
|
|
99
|
+
) : null,
|
|
100
|
+
].filter(Boolean)
|
|
101
|
+
|
|
102
|
+
if (!items.length) return null
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div className="mt-4 border-t border-dashed border-[var(--border-color)] pt-4">
|
|
106
|
+
<ul className="m-0 flex list-none items-center justify-between p-0">
|
|
107
|
+
{items.map((item, index) => (
|
|
108
|
+
<li
|
|
109
|
+
key={index}
|
|
110
|
+
className={index > 0 ? 'ml-3 border-l border-[var(--border-color)] pl-3' : undefined}
|
|
111
|
+
>
|
|
112
|
+
{item}
|
|
113
|
+
</li>
|
|
114
|
+
))}
|
|
115
|
+
</ul>
|
|
116
|
+
</div>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface AboutUsProps {
|
|
121
|
+
variant?: 'sidebar' | 'default' | 'bottom'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function AboutUsBody() {
|
|
125
|
+
const { t } = useLocale()
|
|
126
|
+
const setting = useSiteSetting()
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<>
|
|
130
|
+
{setting?.systemSubTitle ? (
|
|
131
|
+
<p className="mt-0 mb-3 leading-relaxed">{setting.systemSubTitle}</p>
|
|
132
|
+
) : null}
|
|
133
|
+
{setting?.systemFooterInfo ? (
|
|
134
|
+
<div
|
|
135
|
+
className="rp-rich-text leading-relaxed"
|
|
136
|
+
dangerouslySetInnerHTML={{ __html: setting.systemFooterInfo }}
|
|
137
|
+
/>
|
|
138
|
+
) : (
|
|
139
|
+
<p className="m-0 leading-relaxed">{t('aboutUsFallback')}</p>
|
|
140
|
+
)}
|
|
141
|
+
<ContactInfo />
|
|
142
|
+
</>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export default function AboutUs({ variant = 'default' }: AboutUsProps) {
|
|
147
|
+
const { t } = useLocale()
|
|
148
|
+
const isSidebar = variant === 'sidebar'
|
|
149
|
+
const isBottom = variant === 'bottom'
|
|
150
|
+
|
|
151
|
+
if (isBottom) {
|
|
152
|
+
return (
|
|
153
|
+
<div className="rp-widget-panel overflow-hidden rounded-xl bg-[var(--bg-box)] text-[var(--second-text-color)] shadow-[var(--box-shadow)] ring-1 ring-black/5 dark:ring-white/5">
|
|
154
|
+
<div className="flex flex-col gap-4 p-4 md:flex-row md:items-start md:gap-8 md:p-6">
|
|
155
|
+
<h3 className="m-0 shrink-0 text-base font-bold text-[var(--main-text-color)] md:w-40">
|
|
156
|
+
{t('aboutUs')}
|
|
157
|
+
</h3>
|
|
158
|
+
<div className="min-w-0 flex-1 text-sm">
|
|
159
|
+
<AboutUsBody />
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<div
|
|
168
|
+
className={`rp-widget-panel overflow-hidden rounded-xl bg-[var(--bg-box)] shadow-[var(--box-shadow)] ring-1 ring-black/5 dark:ring-white/5 ${
|
|
169
|
+
isSidebar ? 'border-t-0 text-[var(--second-text-color)]' : ''
|
|
170
|
+
}`}
|
|
171
|
+
>
|
|
172
|
+
<div className="border-b border-[var(--border-color)] p-4 font-bold text-[var(--main-text-color)]">
|
|
173
|
+
{t('aboutUs')}
|
|
174
|
+
</div>
|
|
175
|
+
<div className="p-4 text-sm text-[var(--second-text-color)]">
|
|
176
|
+
<AboutUsBody />
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from 'react'
|
|
4
|
+
|
|
5
|
+
export type ArticleActionVariant = 'floating' | 'inline' | 'mobile'
|
|
6
|
+
|
|
7
|
+
interface ArticleActionButtonProps {
|
|
8
|
+
active?: boolean
|
|
9
|
+
disabled?: boolean
|
|
10
|
+
label: string
|
|
11
|
+
count?: number
|
|
12
|
+
onClick: () => void
|
|
13
|
+
variant?: ArticleActionVariant
|
|
14
|
+
activeClassName?: string
|
|
15
|
+
children: ReactNode
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function ArticleActionButton({
|
|
19
|
+
active = false,
|
|
20
|
+
disabled = false,
|
|
21
|
+
label,
|
|
22
|
+
count,
|
|
23
|
+
onClick,
|
|
24
|
+
variant = 'floating',
|
|
25
|
+
activeClassName = 'is-liked',
|
|
26
|
+
children,
|
|
27
|
+
}: ArticleActionButtonProps) {
|
|
28
|
+
const className = [
|
|
29
|
+
'rp-action-btn',
|
|
30
|
+
variant === 'inline' ? 'rp-action-btn--inline' : '',
|
|
31
|
+
variant === 'mobile' ? 'rp-action-btn--mobile' : '',
|
|
32
|
+
active ? activeClassName : '',
|
|
33
|
+
disabled ? 'is-disabled' : '',
|
|
34
|
+
]
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.join(' ')
|
|
37
|
+
|
|
38
|
+
if (variant === 'inline') {
|
|
39
|
+
return (
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
aria-label={label}
|
|
43
|
+
aria-pressed={active}
|
|
44
|
+
disabled={disabled}
|
|
45
|
+
onClick={onClick}
|
|
46
|
+
className={className}
|
|
47
|
+
>
|
|
48
|
+
<span className="rp-action-btn__icon" aria-hidden>
|
|
49
|
+
{children}
|
|
50
|
+
</span>
|
|
51
|
+
<span className="rp-action-btn__label">{label}</span>
|
|
52
|
+
{count != null && count > 0 ? <span className="rp-action-btn__count">{count}</span> : null}
|
|
53
|
+
</button>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (variant === 'mobile') {
|
|
58
|
+
return (
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
aria-label={label}
|
|
62
|
+
aria-pressed={active}
|
|
63
|
+
disabled={disabled}
|
|
64
|
+
onClick={onClick}
|
|
65
|
+
className={className}
|
|
66
|
+
>
|
|
67
|
+
<span className="rp-action-btn__icon" aria-hidden>
|
|
68
|
+
{children}
|
|
69
|
+
</span>
|
|
70
|
+
<span className="rp-action-btn__mobile-label">{label}</span>
|
|
71
|
+
{count != null && count > 0 ? (
|
|
72
|
+
<span className="rp-action-btn__mobile-count">{count}</span>
|
|
73
|
+
) : null}
|
|
74
|
+
</button>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<button
|
|
80
|
+
type="button"
|
|
81
|
+
aria-label={label}
|
|
82
|
+
aria-pressed={active}
|
|
83
|
+
disabled={disabled}
|
|
84
|
+
onClick={onClick}
|
|
85
|
+
className={className}
|
|
86
|
+
>
|
|
87
|
+
<span className="rp-action-btn__icon" aria-hidden>
|
|
88
|
+
{children}
|
|
89
|
+
</span>
|
|
90
|
+
{count != null ? (
|
|
91
|
+
<span className="rp-action-btn__count-below">{count > 0 ? count : ''}</span>
|
|
92
|
+
) : null}
|
|
93
|
+
</button>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import ArticleActionButton, {
|
|
4
|
+
type ArticleActionVariant,
|
|
5
|
+
} from '@/components/widgets/ArticleActionButton'
|
|
6
|
+
import { useSiteCatalog } from '@fecommunity/reactpress-toolkit/theme'
|
|
7
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
8
|
+
|
|
9
|
+
const STORAGE_KEY = 'BOOKMARKS'
|
|
10
|
+
|
|
11
|
+
interface BookmarkWidgetProps {
|
|
12
|
+
id: string
|
|
13
|
+
variant?: ArticleActionVariant
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readBookmarks(): string[] {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(window.localStorage.getItem(STORAGE_KEY) || '[]') as string[]
|
|
19
|
+
} catch {
|
|
20
|
+
return []
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function BookmarkWidget({ id, variant = 'floating' }: BookmarkWidgetProps) {
|
|
25
|
+
const { locale } = useSiteCatalog()
|
|
26
|
+
const [bookmarked, setBookmarked] = useState(false)
|
|
27
|
+
const label = locale === 'zh' ? '收藏' : 'Bookmark'
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
setBookmarked(readBookmarks().includes(id))
|
|
31
|
+
}, [id])
|
|
32
|
+
|
|
33
|
+
const toggle = useCallback(() => {
|
|
34
|
+
const stored = readBookmarks()
|
|
35
|
+
const nextStored = bookmarked ? stored.filter((item) => item !== id) : [...stored, id]
|
|
36
|
+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(nextStored))
|
|
37
|
+
setBookmarked(!bookmarked)
|
|
38
|
+
}, [bookmarked, id])
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<ArticleActionButton
|
|
42
|
+
active={bookmarked}
|
|
43
|
+
label={label}
|
|
44
|
+
onClick={toggle}
|
|
45
|
+
variant={variant}
|
|
46
|
+
activeClassName="is-bookmarked"
|
|
47
|
+
>
|
|
48
|
+
<svg
|
|
49
|
+
viewBox="0 0 24 24"
|
|
50
|
+
width="20"
|
|
51
|
+
height="20"
|
|
52
|
+
fill="none"
|
|
53
|
+
stroke="currentColor"
|
|
54
|
+
strokeWidth="1.8"
|
|
55
|
+
aria-hidden
|
|
56
|
+
>
|
|
57
|
+
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
|
58
|
+
</svg>
|
|
59
|
+
</ArticleActionButton>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import Link from '@/components/shared/Link'
|
|
4
|
+
import { FolderIcon } from '@/lib/utils/icons'
|
|
5
|
+
import { useLocale } from '@fecommunity/reactpress-toolkit/ui'
|
|
6
|
+
|
|
7
|
+
interface Category {
|
|
8
|
+
value: string
|
|
9
|
+
label: string
|
|
10
|
+
articleCount?: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface CategoriesWidgetProps {
|
|
14
|
+
categories: Category[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function CategoriesWidget({ categories = [] }: CategoriesWidgetProps) {
|
|
18
|
+
const { t } = useLocale()
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="rp-widget-panel mb-5 overflow-hidden rounded-xl bg-[var(--bg-box)] leading-snug shadow-[var(--box-shadow)] ring-1 ring-black/5 dark:ring-white/5">
|
|
22
|
+
<div className="border-b border-[var(--border-color)] p-4 font-bold text-[var(--main-text-color)]">
|
|
23
|
+
<FolderIcon size={16} className="mr-2 inline-block text-[var(--primary-color)]" />
|
|
24
|
+
<span>{t('categoryTitle')}</span>
|
|
25
|
+
</div>
|
|
26
|
+
<ul className="m-0 list-none p-4">
|
|
27
|
+
{categories.map((category) => (
|
|
28
|
+
<li
|
|
29
|
+
key={category.value}
|
|
30
|
+
className="rp-category-item rounded-lg px-2 py-2 text-[var(--second-text-color)]"
|
|
31
|
+
>
|
|
32
|
+
<Link
|
|
33
|
+
href={`/category/${category.value}`}
|
|
34
|
+
aria-label={category.label}
|
|
35
|
+
className="flex w-full items-center justify-between text-sm no-underline hover:text-inherit"
|
|
36
|
+
>
|
|
37
|
+
<span className="text-[var(--main-text-color)]">{category.label}</span>
|
|
38
|
+
<span>
|
|
39
|
+
{t('total')} {category.articleCount ?? 0} {t('articleCountTemplate')}
|
|
40
|
+
</span>
|
|
41
|
+
</Link>
|
|
42
|
+
</li>
|
|
43
|
+
))}
|
|
44
|
+
</ul>
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import AboutUs from '@/components/widgets/AboutUs'
|
|
4
|
+
import ArticleRecommend from '@/components/article/ArticleRecommend'
|
|
5
|
+
import CategoriesWidget from '@/components/widgets/CategoriesWidget'
|
|
6
|
+
import TagsWidget from '@/components/widgets/TagsWidget'
|
|
7
|
+
import { useSiteCatalog } from '@fecommunity/reactpress-toolkit/theme'
|
|
8
|
+
|
|
9
|
+
interface HomeSidebarProps {
|
|
10
|
+
showTags?: boolean
|
|
11
|
+
showCategories?: boolean
|
|
12
|
+
showRecommend?: boolean
|
|
13
|
+
showAboutUs?: boolean
|
|
14
|
+
deferRecommend?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function HomeSidebar({
|
|
18
|
+
showTags = true,
|
|
19
|
+
showCategories = false,
|
|
20
|
+
showRecommend = true,
|
|
21
|
+
showAboutUs = true,
|
|
22
|
+
deferRecommend = true,
|
|
23
|
+
}: HomeSidebarProps) {
|
|
24
|
+
const { tags, categories } = useSiteCatalog()
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="rp-sidebar-enter rp-sidebar-sticky sticky mb-4 w-72">
|
|
28
|
+
{showRecommend ? <ArticleRecommend mode="inline" deferFetch={deferRecommend} /> : null}
|
|
29
|
+
{showTags ? (
|
|
30
|
+
<TagsWidget tags={tags as Parameters<typeof TagsWidget>[0]['tags']} animationMode />
|
|
31
|
+
) : null}
|
|
32
|
+
{showCategories ? (
|
|
33
|
+
<CategoriesWidget
|
|
34
|
+
categories={categories as Parameters<typeof CategoriesWidget>[0]['categories']}
|
|
35
|
+
/>
|
|
36
|
+
) : null}
|
|
37
|
+
{showAboutUs ? <AboutUs variant="sidebar" /> : null}
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
}
|