@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,170 @@
1
+ 'use client'
2
+
3
+ import Link from '@/components/shared/Link'
4
+ import Logo from '@/components/shared/logo.svg'
5
+ import { getColorFromNumber, getTagStyle } from '@/lib/utils/colors'
6
+ import { ClockIcon, EyeIcon, FolderIcon, HeartIcon } from '@/lib/utils/icons'
7
+ import { useLocale } from '@fecommunity/reactpress-toolkit/ui'
8
+ import { Image, LocaleTime } from '@fecommunity/reactpress-toolkit/ui/content'
9
+ import { useSiteCatalog } from '@fecommunity/reactpress-toolkit/theme'
10
+ import { useEffect, useMemo, useRef } from 'react'
11
+
12
+ interface Article {
13
+ id: string
14
+ title: string
15
+ cover?: string
16
+ summary: string
17
+ category?: { value: string; label: string }
18
+ likes: number
19
+ views: number
20
+ publishAt: string
21
+ }
22
+
23
+ interface ArticleListProps {
24
+ articles: Article[]
25
+ /** Tighter spacing for recommend blocks. */
26
+ density?: 'feed' | 'compact'
27
+ }
28
+
29
+ const COVER_WIDTH = 200
30
+ const COVER_HEIGHT = 114
31
+
32
+ function ArticleCard({
33
+ article,
34
+ categoryIndex,
35
+ index,
36
+ animate = false,
37
+ compact = false,
38
+ }: {
39
+ article: Article
40
+ categoryIndex: number
41
+ index: number
42
+ animate?: boolean
43
+ compact?: boolean
44
+ }) {
45
+ const { locale } = useLocale()
46
+ const eager = index < 3
47
+
48
+ return (
49
+ <div
50
+ className={`rp-article-card group/card rp-surface relative flex w-full justify-between overflow-hidden rounded-xl border border-transparent ring-1 ring-black/5 transition-[box-shadow,border-color] duration-300 ease-out hover:border-[color-mix(in_srgb,var(--border-color)_70%,transparent)] hover:shadow-[0_8px_24px_color-mix(in_srgb,var(--main-text-color)_10%,transparent),var(--box-shadow)] dark:ring-white/5 hover:[&_header_.title]:text-[var(--primary-color)] ${
51
+ compact ? 'p-3' : 'p-4'
52
+ } ${animate ? 'rp-home-card-enter' : ''}`}
53
+ >
54
+ <span
55
+ className="absolute top-5 left-0 h-6 w-1 rounded-r bg-[var(--primary-color)] opacity-90 shadow-sm transition-opacity duration-300 group-hover/card:opacity-100"
56
+ aria-hidden
57
+ />
58
+ <div className="rp-cover-zoom-host mr-2.5 h-[114px] w-[200px] shrink-0 overflow-hidden rounded-lg bg-[var(--bg-second)] max-md:h-20 max-md:w-[140px]">
59
+ {article.cover ? (
60
+ <Link href={`/article/${article.id}`} className="block h-full w-full overflow-hidden">
61
+ <Image
62
+ url={article.cover}
63
+ size="thumb"
64
+ alt={`${article.title} cover`}
65
+ width={COVER_WIDTH}
66
+ height={COVER_HEIGHT}
67
+ loading={eager ? 'eager' : 'lazy'}
68
+ fetchPriority={index === 0 ? 'high' : undefined}
69
+ decoding={index === 0 ? 'sync' : 'async'}
70
+ className="rp-article-cover-img rp-cover-zoom h-full w-full object-contain object-center"
71
+ />
72
+ </Link>
73
+ ) : (
74
+ <div className="flex h-full w-full items-center justify-center bg-[var(--bg-second)]">
75
+ <Logo className="h-12 w-auto max-w-[155px] object-contain" aria-label="ReactPress" />
76
+ </div>
77
+ )}
78
+ </div>
79
+ <div className="min-w-0 flex-1">
80
+ <header className="flex items-start gap-1">
81
+ <Link
82
+ href={`/article/${article.id}`}
83
+ className="title line-clamp-2 min-w-0 flex-1 text-base leading-snug font-semibold text-[var(--main-text-color)] no-underline transition-colors duration-300"
84
+ >
85
+ {article.title}
86
+ </Link>
87
+ {article.category && categoryIndex >= 0 ? (
88
+ <Link
89
+ href={`/category/${article.category.value}`}
90
+ className="rp-category-tag ml-1 inline-flex shrink-0 items-center gap-1 rounded px-2 py-0.5 text-xs no-underline transition-opacity duration-200 hover:opacity-80"
91
+ style={getTagStyle(getColorFromNumber(categoryIndex))}
92
+ >
93
+ <FolderIcon size={12} className="opacity-90" />
94
+ <span>{article.category.label}</span>
95
+ </Link>
96
+ ) : null}
97
+ </header>
98
+ <Link href={`/article/${article.id}`} className="mt-2 block no-underline">
99
+ <main className="flex flex-col justify-between">
100
+ <div
101
+ className="rp-rich-text line-clamp-2 text-sm text-[var(--second-text-color)] max-md:line-clamp-1"
102
+ dangerouslySetInnerHTML={{ __html: article.summary }}
103
+ />
104
+ <div className="mt-3 flex items-center justify-between text-sm whitespace-nowrap text-[#8590a6]">
105
+ <div className="flex items-center gap-2">
106
+ <span className="inline-flex items-center text-[var(--second-text-color)]">
107
+ <HeartIcon size={14} />
108
+ <span className="ml-1.5">{article.likes}</span>
109
+ </span>
110
+ <span>·</span>
111
+ <span className="inline-flex items-center text-[var(--second-text-color)]">
112
+ <EyeIcon size={14} />
113
+ <span className="ml-1.5">{article.views}</span>
114
+ </span>
115
+ </div>
116
+ <span className="inline-flex items-center max-md:hidden">
117
+ <ClockIcon size={14} />
118
+ <span className="ml-1.5">
119
+ <LocaleTime date={article.publishAt} format="yyyy-MM-dd" locale={locale} />
120
+ </span>
121
+ </span>
122
+ </div>
123
+ </main>
124
+ </Link>
125
+ </div>
126
+ </div>
127
+ )
128
+ }
129
+
130
+ export default function ArticleList({ articles = [], density = 'feed' }: ArticleListProps) {
131
+ const { t } = useLocale()
132
+ const { categories } = useSiteCatalog()
133
+ const compact = density === 'compact'
134
+ const animatedCountRef = useRef(0)
135
+
136
+ useEffect(() => {
137
+ animatedCountRef.current = articles.length
138
+ }, [articles])
139
+
140
+ const categoryIndices = useMemo(
141
+ () =>
142
+ articles.map((article) =>
143
+ categories?.findIndex((category) => category?.value === article?.category?.value)
144
+ ),
145
+ [articles, categories]
146
+ )
147
+
148
+ const animateFromIndex = animatedCountRef.current
149
+
150
+ return (
151
+ <div className="rp-article-list rp-home-stagger flex w-full flex-col gap-4">
152
+ {articles.length ? (
153
+ articles.map((article, index) => (
154
+ <ArticleCard
155
+ key={article.id}
156
+ article={article}
157
+ categoryIndex={categoryIndices[index]}
158
+ index={index}
159
+ animate={index >= animateFromIndex}
160
+ compact={compact}
161
+ />
162
+ ))
163
+ ) : (
164
+ <div className="rounded-xl bg-[var(--bg-box)] p-8 text-center text-[var(--second-text-color)]">
165
+ {t('empty')}
166
+ </div>
167
+ )}
168
+ </div>
169
+ )
170
+ }
@@ -0,0 +1,104 @@
1
+ 'use client'
2
+
3
+ import type { ReactNode } from 'react'
4
+
5
+ type ReadingChromeMode = 'side' | 'mobile' | 'toc' | 'all'
6
+
7
+ interface ArticleReadingChromeProps {
8
+ actions: ReactNode
9
+ mobileActions: ReactNode
10
+ showToc: boolean
11
+ tocExpanded: boolean
12
+ onToggleToc: () => void
13
+ tocPanel: ReactNode
14
+ collapseLabel: string
15
+ expandLabel: string
16
+ tocLabel: string
17
+ mode?: ReadingChromeMode
18
+ }
19
+
20
+ function TocIcon() {
21
+ return (
22
+ <svg
23
+ viewBox="0 0 24 24"
24
+ width="20"
25
+ height="20"
26
+ fill="none"
27
+ stroke="currentColor"
28
+ strokeWidth="1.8"
29
+ aria-hidden
30
+ >
31
+ <path d="M4 6h16M4 12h10M4 18h14" strokeLinecap="round" />
32
+ </svg>
33
+ )
34
+ }
35
+
36
+ export default function ArticleReadingChrome({
37
+ actions,
38
+ mobileActions,
39
+ showToc,
40
+ tocExpanded,
41
+ onToggleToc,
42
+ tocPanel,
43
+ collapseLabel,
44
+ expandLabel,
45
+ tocLabel,
46
+ mode = 'all',
47
+ }: ArticleReadingChromeProps) {
48
+ const showSide = mode === 'side' || mode === 'all'
49
+ const showMobile = mode === 'mobile' || mode === 'all'
50
+ const showTocDrawer = mode === 'toc' || mode === 'all'
51
+
52
+ return (
53
+ <>
54
+ {showToc && showTocDrawer ? (
55
+ <>
56
+ <button
57
+ type="button"
58
+ className={`rp-toc-toggle${tocExpanded ? 'is-open' : ''}`}
59
+ aria-label={tocExpanded ? collapseLabel : expandLabel}
60
+ aria-expanded={tocExpanded}
61
+ aria-controls="rp-toc-fixed-drawer"
62
+ onClick={onToggleToc}
63
+ >
64
+ <TocIcon />
65
+ </button>
66
+ <aside
67
+ id="rp-toc-fixed-drawer"
68
+ className={`rp-toc-fixed-drawer${tocExpanded ? '' : 'is-collapsed'}`}
69
+ aria-hidden={!tocExpanded}
70
+ >
71
+ <div className="rp-toc-fixed-drawer__inner">{tocPanel}</div>
72
+ </aside>
73
+ </>
74
+ ) : null}
75
+
76
+ {showSide && actions ? (
77
+ <nav className="rp-reading-dock rp-reading-dock--side" aria-label="Reading actions">
78
+ <div className="rp-reading-dock__actions">{actions}</div>
79
+ </nav>
80
+ ) : null}
81
+
82
+ {showMobile ? (
83
+ <nav className="rp-reading-dock rp-reading-dock--bottom" aria-label="Reading actions">
84
+ <div className="rp-reading-dock__inner rp-reading-dock__inner--bottom">
85
+ {showToc ? (
86
+ <button
87
+ type="button"
88
+ className={`rp-reading-dock__toc rp-reading-dock__toc--mobile${tocExpanded ? 'is-open' : ''}`}
89
+ aria-label={tocExpanded ? collapseLabel : expandLabel}
90
+ aria-expanded={tocExpanded}
91
+ aria-controls="rp-toc-fixed-drawer"
92
+ onClick={onToggleToc}
93
+ >
94
+ <TocIcon />
95
+ <span className="rp-reading-dock__toc-label">{tocLabel}</span>
96
+ </button>
97
+ ) : null}
98
+ {mobileActions}
99
+ </div>
100
+ </nav>
101
+ ) : null}
102
+ </>
103
+ )
104
+ }
@@ -0,0 +1,75 @@
1
+ 'use client'
2
+
3
+ import { useHeaderTop } from '@/lib/utils/headerPosition'
4
+ import { useEffect, useState } from 'react'
5
+ import { createPortal } from 'react-dom'
6
+
7
+ const ARTICLE_SELECTOR = '#js-article-wrapper'
8
+
9
+ function measureProgress(): number {
10
+ const article = document.querySelector(ARTICLE_SELECTOR)
11
+ if (!article) return 0
12
+
13
+ const rect = article.getBoundingClientRect()
14
+ const articleHeight = rect.height
15
+ const viewportHeight = window.innerHeight
16
+ const scrollable = articleHeight - viewportHeight
17
+
18
+ if (scrollable <= 0) {
19
+ return rect.top <= 0 ? 100 : 0
20
+ }
21
+
22
+ const scrolled = -rect.top
23
+ return Math.min(100, Math.max(0, (scrolled / scrollable) * 100))
24
+ }
25
+
26
+ export default function ArticleReadingProgress() {
27
+ const headerTop = useHeaderTop()
28
+ const [progress, setProgress] = useState(0)
29
+ const [mounted, setMounted] = useState(false)
30
+
31
+ useEffect(() => {
32
+ setMounted(true)
33
+ }, [])
34
+
35
+ useEffect(() => {
36
+ let frame = 0
37
+
38
+ const update = () => {
39
+ frame = 0
40
+ setProgress(measureProgress())
41
+ }
42
+
43
+ const onScroll = () => {
44
+ if (frame) return
45
+ frame = window.requestAnimationFrame(update)
46
+ }
47
+
48
+ update()
49
+ window.addEventListener('scroll', onScroll, { passive: true })
50
+ window.addEventListener('resize', onScroll, { passive: true })
51
+
52
+ return () => {
53
+ if (frame) window.cancelAnimationFrame(frame)
54
+ window.removeEventListener('scroll', onScroll)
55
+ window.removeEventListener('resize', onScroll)
56
+ }
57
+ }, [])
58
+
59
+ if (!mounted) return null
60
+
61
+ return createPortal(
62
+ <div
63
+ className="rp-reading-progress"
64
+ style={{ top: headerTop }}
65
+ role="progressbar"
66
+ aria-valuenow={Math.round(progress)}
67
+ aria-valuemin={0}
68
+ aria-valuemax={100}
69
+ aria-label="Reading progress"
70
+ >
71
+ <div className="rp-reading-progress__bar" style={{ width: `${progress}%` }} />
72
+ </div>,
73
+ document.body
74
+ )
75
+ }