@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,92 @@
1
+ 'use client'
2
+
3
+ import ArticleRecommend from '@/components/article/ArticleRecommend'
4
+ import CommentSection from '@/components/comment/CommentSection'
5
+ import ReadingContent from '@/components/article/ReadingContent'
6
+ import DoubleColumnLayout from '@/components/layout/DoubleColumnLayout'
7
+ import { PageProvider } from '@/lib/providers/client'
8
+ import { getSuggestionsPageHtml } from '@/lib/reactpress/suggestionsContent'
9
+ import { useLocale } from '@fecommunity/reactpress-toolkit/ui'
10
+ import { Image, ImageViewer } from '@fecommunity/reactpress-toolkit/ui/content'
11
+ import type { IPage } from '@fecommunity/reactpress-toolkit/types'
12
+ import { useEffect } from 'react'
13
+
14
+ interface CmsPageClientProps {
15
+ page: IPage
16
+ }
17
+
18
+ export default function CmsPageClient({ page }: CmsPageClientProps) {
19
+ const { t, locale } = useLocale()
20
+ const isSuggestionsPage = page.path === 'suggestions'
21
+
22
+ useEffect(() => {
23
+ PageProvider.updatePageViews(page.id)
24
+ }, [page.id])
25
+
26
+ if (isSuggestionsPage) {
27
+ const heroSubtitle =
28
+ locale === 'zh'
29
+ ? '欢迎提出想法,帮助我们改进 ReactPress 与主题 Starter。'
30
+ : 'Share your ideas to help us improve ReactPress and this theme starter.'
31
+
32
+ const content = (
33
+ <ImageViewer containerSelector="#js-suggestions-wrapper">
34
+ <div className="rp-article-page">
35
+ <article id="js-suggestions-wrapper" className="rp-article-wrap">
36
+ <div className="rp-article-inner">
37
+ <header className="rp-article-header">
38
+ <h1 className="rp-article-title">{t('suggestions')}</h1>
39
+ <p className="rp-article-lead">{heroSubtitle}</p>
40
+ </header>
41
+ <div className="rp-article-body">
42
+ <ReadingContent key={locale} content={getSuggestionsPageHtml(locale, page.html)} />
43
+ </div>
44
+ </div>
45
+ </article>
46
+ </div>
47
+ </ImageViewer>
48
+ )
49
+
50
+ return <DoubleColumnLayout leftNode={content} fillMinHeight />
51
+ }
52
+
53
+ return (
54
+ <ImageViewer containerSelector="#js-page-wrapper">
55
+ <div id="js-page-wrapper" className="rp-cms-page relative bg-[var(--bg-box)]">
56
+ <div
57
+ className="rp-cms-page-inner border-b border-[var(--border-color)] pt-5"
58
+ style={{
59
+ backgroundColor: 'var(--bg-second)',
60
+ }}
61
+ >
62
+ {page.cover ? (
63
+ <div className="rp-cms-cover mx-auto max-w-3xl px-2 pb-6">
64
+ <Image
65
+ url={page.cover}
66
+ size="large"
67
+ alt={t('articleCover')}
68
+ className="w-full rounded-lg"
69
+ />
70
+ </div>
71
+ ) : null}
72
+ <div className="rp-cms-content mx-auto max-w-3xl px-2 pb-5">
73
+ <ReadingContent content={page.html} />
74
+ </div>
75
+ </div>
76
+
77
+ <div className="rp-comment-wrap mx-auto max-w-3xl px-4 py-4">
78
+ <p className="rp-cms-section-title">{t('comment')}</p>
79
+ <div className="rp-comment-panel">
80
+ <CommentSection hostId={page.id} />
81
+ </div>
82
+ </div>
83
+ <div className="rp-cms-recommend mx-auto max-w-3xl px-4 py-4">
84
+ <p className="rp-cms-section-title">{t('recommendToReading')}</p>
85
+ <div className="overflow-hidden rounded-lg shadow-[var(--box-shadow)]">
86
+ <ArticleRecommend needTitle={false} mode="inline" />
87
+ </div>
88
+ </div>
89
+ </div>
90
+ </ImageViewer>
91
+ )
92
+ }
@@ -0,0 +1,59 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useRef, type ReactNode } from 'react'
4
+
5
+ interface LoadMoreProps {
6
+ hasMore: boolean
7
+ loadMore: (page: number) => void | Promise<void>
8
+ pageStart?: number
9
+ loader?: ReactNode
10
+ children: ReactNode
11
+ }
12
+
13
+ export default function LoadMore({
14
+ hasMore,
15
+ loadMore,
16
+ pageStart = 1,
17
+ loader,
18
+ children,
19
+ }: LoadMoreProps) {
20
+ const sentinelRef = useRef<HTMLDivElement>(null)
21
+ const pageRef = useRef(pageStart)
22
+ const loadingRef = useRef(false)
23
+
24
+ useEffect(() => {
25
+ pageRef.current = pageStart
26
+ }, [pageStart])
27
+
28
+ useEffect(() => {
29
+ const node = sentinelRef.current
30
+ if (!node || !hasMore) return undefined
31
+
32
+ const observer = new IntersectionObserver(
33
+ (entries) => {
34
+ if (!entries[0]?.isIntersecting || loadingRef.current || !hasMore) return
35
+ loadingRef.current = true
36
+ const next = pageRef.current + 1
37
+ Promise.resolve(loadMore(next)).finally(() => {
38
+ pageRef.current = next
39
+ loadingRef.current = false
40
+ })
41
+ },
42
+ { rootMargin: '240px 0px' }
43
+ )
44
+
45
+ observer.observe(node)
46
+ return () => observer.disconnect()
47
+ }, [hasMore, loadMore])
48
+
49
+ return (
50
+ <>
51
+ {children}
52
+ {hasMore ? (
53
+ <div ref={sentinelRef} aria-hidden>
54
+ {loader}
55
+ </div>
56
+ ) : null}
57
+ </>
58
+ )
59
+ }
@@ -0,0 +1,51 @@
1
+ 'use client'
2
+
3
+ import { ContentStylesLoader } from '@/components/layout/ContentStylesLoader'
4
+ import { useLocale } from '@fecommunity/reactpress-toolkit/ui'
5
+ import { HtmlContent } from '@fecommunity/reactpress-toolkit/ui/content'
6
+ import { memo } from 'react'
7
+
8
+ interface ReadingContentProps {
9
+ content?: string
10
+ className?: string
11
+ }
12
+
13
+ const ArticleBody = memo(function ArticleBody({
14
+ content,
15
+ className,
16
+ copyLabel,
17
+ copySuccessLabel,
18
+ }: {
19
+ content?: string
20
+ className: string
21
+ copyLabel: string
22
+ copySuccessLabel: string
23
+ }) {
24
+ return (
25
+ <HtmlContent
26
+ content={content}
27
+ className={className}
28
+ copyLabel={copyLabel}
29
+ copySuccessLabel={copySuccessLabel}
30
+ />
31
+ )
32
+ })
33
+
34
+ export default function ReadingContent({
35
+ content,
36
+ className = 'markdown rp-html-content max-w-none',
37
+ }: ReadingContentProps) {
38
+ const { t } = useLocale()
39
+
40
+ return (
41
+ <>
42
+ <ContentStylesLoader />
43
+ <ArticleBody
44
+ content={content}
45
+ className={className}
46
+ copyLabel={t('copy')}
47
+ copySuccessLabel={t('copySuccess')}
48
+ />
49
+ </>
50
+ )
51
+ }
@@ -0,0 +1,314 @@
1
+ 'use client'
2
+
3
+ import Link from '@/components/shared/Link'
4
+ import { resolveGithubAuthorizeUrl } from '@/lib/auth/githubOAuth'
5
+ import { normalizeSessionUser } from '@/lib/auth/sessionUser'
6
+ import { UserProvider } from '@/lib/providers/client'
7
+ import { isLoggedInUser } from '@/lib/utils/comment'
8
+ import { useLocale } from '@fecommunity/reactpress-toolkit/ui'
9
+ import type { IUser } from '@fecommunity/reactpress-toolkit/types'
10
+ import {
11
+ getCommentEmailError,
12
+ useAsyncLoading,
13
+ useSiteSetting,
14
+ useSiteUser,
15
+ } from '@fecommunity/reactpress-toolkit/theme'
16
+ import { useRouter } from 'next/navigation'
17
+ import { useCallback, useEffect, useMemo, useState } from 'react'
18
+
19
+ type AuthMode = 'login' | 'register'
20
+
21
+ interface AuthPageClientProps {
22
+ mode: AuthMode
23
+ from?: string
24
+ }
25
+
26
+ type FieldErrors = Partial<Record<'name' | 'email' | 'password' | 'confirmPassword', string>>
27
+
28
+ function resolveErrorMessage(error: unknown): string {
29
+ if (error instanceof Error && error.message.trim()) return error.message.trim()
30
+ if (error && typeof error === 'object' && 'message' in error) {
31
+ const message = (error as { message?: unknown }).message
32
+ if (typeof message === 'string' && message.trim()) return message.trim()
33
+ }
34
+ return ''
35
+ }
36
+
37
+ function GithubIcon() {
38
+ return (
39
+ <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden fill="currentColor">
40
+ <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 0 0 24 12c0-6.63-5.37-12-12-12Z" />
41
+ </svg>
42
+ )
43
+ }
44
+
45
+ export default function AuthPageClient({ mode: initialMode, from = '/' }: AuthPageClientProps) {
46
+ const { t, locale } = useLocale()
47
+ const router = useRouter()
48
+ const setting = useSiteSetting()
49
+ const { user, setUser } = useSiteUser()
50
+ const [mode, setMode] = useState<AuthMode>(initialMode)
51
+ const [name, setName] = useState('')
52
+ const [email, setEmail] = useState('')
53
+ const [password, setPassword] = useState('')
54
+ const [confirmPassword, setConfirmPassword] = useState('')
55
+ const [showPassword, setShowPassword] = useState(false)
56
+ const [fieldErrors, setFieldErrors] = useState<FieldErrors>({})
57
+ const [formError, setFormError] = useState('')
58
+ const [successMessage, setSuccessMessage] = useState('')
59
+ const [loginRequest, loggingIn] = useAsyncLoading(UserProvider.login)
60
+ const [registerRequest, registering] = useAsyncLoading(UserProvider.register)
61
+
62
+ const redirectTarget = from.startsWith('/') ? from : '/'
63
+ const loading = loggingIn || registering
64
+ const githubAuthorizeUrl = useMemo(
65
+ () => resolveGithubAuthorizeUrl(redirectTarget),
66
+ [redirectTarget]
67
+ )
68
+
69
+ useEffect(() => {
70
+ setMode(initialMode)
71
+ }, [initialMode])
72
+
73
+ useEffect(() => {
74
+ if (isLoggedInUser(user)) {
75
+ router.replace(redirectTarget)
76
+ }
77
+ }, [redirectTarget, router, user])
78
+
79
+ const validate = useCallback((): boolean => {
80
+ const nextErrors: FieldErrors = {}
81
+ const trimmedName = name.trim()
82
+
83
+ if (!trimmedName) {
84
+ nextErrors.name = t('commentNamespace.userInfoNameValidMsg')
85
+ }
86
+
87
+ if (mode === 'register') {
88
+ const emailError = getCommentEmailError(email, {
89
+ required: t('commentNamespace.userInfoEmailValidMsg'),
90
+ invalid: t('commentNamespace.userInfoIllegalEmailValidMsg'),
91
+ })
92
+ if (emailError) nextErrors.email = emailError
93
+ }
94
+
95
+ if (!password.trim()) {
96
+ nextErrors.password = t('commentNamespace.userInfoPasswordValidMsg')
97
+ }
98
+
99
+ if (mode === 'register') {
100
+ if (!confirmPassword.trim()) {
101
+ nextErrors.confirmPassword = t('commentNamespace.userInfoPleaseEnterConfirmPassword')
102
+ } else if (confirmPassword !== password) {
103
+ nextErrors.confirmPassword = t('commentNamespace.confirmPasswordIsNotMatchTips')
104
+ }
105
+ }
106
+
107
+ setFieldErrors(nextErrors)
108
+ return Object.keys(nextErrors).length === 0
109
+ }, [confirmPassword, email, mode, name, password, t])
110
+
111
+ const completeLogin = useCallback(
112
+ async (payload: { name: string; password: string }) => {
113
+ const response = await loginRequest(payload)
114
+ const sessionUser = normalizeSessionUser(response)
115
+ if (!sessionUser?.token) {
116
+ throw { message: t('commentNamespace.userInfoPasswordValidMsg') }
117
+ }
118
+ setUser?.(sessionUser as IUser & { token: string })
119
+ router.replace(redirectTarget)
120
+ },
121
+ [loginRequest, redirectTarget, router, setUser, t]
122
+ )
123
+
124
+ const handleSubmit = useCallback(
125
+ async (event: React.FormEvent<HTMLFormElement>) => {
126
+ event.preventDefault()
127
+ setFormError('')
128
+ setSuccessMessage('')
129
+ if (!validate()) return
130
+
131
+ const trimmedName = name.trim()
132
+ const trimmedEmail = email.trim()
133
+
134
+ try {
135
+ if (mode === 'register') {
136
+ await registerRequest({
137
+ name: trimmedName,
138
+ email: trimmedEmail,
139
+ password,
140
+ })
141
+ setSuccessMessage(t('commentNamespace.registerSuccessTips'))
142
+ await completeLogin({ name: trimmedName, password })
143
+ return
144
+ }
145
+
146
+ await completeLogin({ name: trimmedName, password })
147
+ } catch (error) {
148
+ setFormError(resolveErrorMessage(error) || t('wrongPasswd'))
149
+ }
150
+ },
151
+ [completeLogin, email, mode, name, password, registerRequest, t, validate]
152
+ )
153
+
154
+ const title =
155
+ mode === 'login' ? t('commentNamespace.userInfoConfirm') : t('commentNamespace.userRegister')
156
+ const subtitle =
157
+ mode === 'login' ? t('loginTipMessage') : setting.systemSubTitle?.trim() || t('loginTipMessage')
158
+
159
+ return (
160
+ <div className="rp-auth-page">
161
+ <div className="rp-auth-card">
162
+ <header className="rp-auth-card__header">
163
+ <h1 className="rp-auth-card__title">{title}</h1>
164
+ <p className="rp-auth-card__subtitle">{subtitle}</p>
165
+ </header>
166
+
167
+ {formError ? (
168
+ <div className="rp-auth-alert is-error" role="alert">
169
+ {formError}
170
+ </div>
171
+ ) : null}
172
+ {successMessage ? (
173
+ <div className="rp-auth-alert is-success" role="status">
174
+ {successMessage}
175
+ </div>
176
+ ) : null}
177
+
178
+ <form className="rp-auth-form" onSubmit={handleSubmit} noValidate>
179
+ <label className="rp-auth-field">
180
+ <span className="rp-auth-field__label">{t('commentNamespace.userInfoName')}</span>
181
+ <input
182
+ type="text"
183
+ name="name"
184
+ autoComplete="username"
185
+ value={name}
186
+ onChange={(event) => setName(event.target.value)}
187
+ className={`rp-auth-input${fieldErrors.name ? 'is-error' : ''}`}
188
+ placeholder={t('commentNamespace.userInfoName')}
189
+ />
190
+ {fieldErrors.name ? (
191
+ <span className="rp-auth-field__error">{fieldErrors.name}</span>
192
+ ) : null}
193
+ </label>
194
+
195
+ {mode === 'register' ? (
196
+ <label className="rp-auth-field">
197
+ <span className="rp-auth-field__label">{t('commentNamespace.userInfoEmail')}</span>
198
+ <input
199
+ type="email"
200
+ name="email"
201
+ autoComplete="email"
202
+ value={email}
203
+ onChange={(event) => setEmail(event.target.value)}
204
+ className={`rp-auth-input${fieldErrors.email ? 'is-error' : ''}`}
205
+ placeholder="name@example.com"
206
+ />
207
+ {fieldErrors.email ? (
208
+ <span className="rp-auth-field__error">{fieldErrors.email}</span>
209
+ ) : null}
210
+ </label>
211
+ ) : null}
212
+
213
+ <label className="rp-auth-field">
214
+ <span className="rp-auth-field__label">{t('commentNamespace.userInfoPassword')}</span>
215
+ <div className="rp-auth-input-wrap">
216
+ <input
217
+ type={showPassword ? 'text' : 'password'}
218
+ name="password"
219
+ autoComplete={mode === 'login' ? 'current-password' : 'new-password'}
220
+ value={password}
221
+ onChange={(event) => setPassword(event.target.value)}
222
+ className={`rp-auth-input${fieldErrors.password ? 'is-error' : ''}`}
223
+ placeholder={t('commentNamespace.userInfoPassword')}
224
+ />
225
+ <button
226
+ type="button"
227
+ className="rp-auth-input-toggle"
228
+ aria-label={
229
+ showPassword
230
+ ? locale === 'zh'
231
+ ? '隐藏密码'
232
+ : 'Hide password'
233
+ : locale === 'zh'
234
+ ? '显示密码'
235
+ : 'Show password'
236
+ }
237
+ onClick={() => setShowPassword((open) => !open)}
238
+ >
239
+ {showPassword
240
+ ? locale === 'zh'
241
+ ? '隐藏'
242
+ : 'Hide'
243
+ : locale === 'zh'
244
+ ? '显示'
245
+ : 'Show'}
246
+ </button>
247
+ </div>
248
+ {fieldErrors.password ? (
249
+ <span className="rp-auth-field__error">{fieldErrors.password}</span>
250
+ ) : null}
251
+ </label>
252
+
253
+ {mode === 'register' ? (
254
+ <label className="rp-auth-field">
255
+ <span className="rp-auth-field__label">
256
+ {t('commentNamespace.userInfoPleaseEnterConfirmPassword')}
257
+ </span>
258
+ <input
259
+ type={showPassword ? 'text' : 'password'}
260
+ name="confirmPassword"
261
+ autoComplete="new-password"
262
+ value={confirmPassword}
263
+ onChange={(event) => setConfirmPassword(event.target.value)}
264
+ className={`rp-auth-input${fieldErrors.confirmPassword ? 'is-error' : ''}`}
265
+ placeholder={t('commentNamespace.userInfoPleaseEnterConfirmPassword')}
266
+ />
267
+ {fieldErrors.confirmPassword ? (
268
+ <span className="rp-auth-field__error">{fieldErrors.confirmPassword}</span>
269
+ ) : null}
270
+ </label>
271
+ ) : null}
272
+
273
+ <button type="submit" className="rp-auth-submit" disabled={loading}>
274
+ {loading
275
+ ? t('loading')
276
+ : mode === 'login'
277
+ ? t('commentNamespace.userInfoConfirm')
278
+ : t('commentNamespace.register')}
279
+ </button>
280
+ </form>
281
+
282
+ {githubAuthorizeUrl ? (
283
+ <>
284
+ <div className="rp-auth-divider">
285
+ <span>{t('commentNamespace.openAuth')}</span>
286
+ </div>
287
+ <a href={githubAuthorizeUrl} className="rp-auth-github">
288
+ <GithubIcon />
289
+ <span>{t('useGithubToLogin')}</span>
290
+ </a>
291
+ </>
292
+ ) : null}
293
+
294
+ <p className="rp-auth-switch">
295
+ {mode === 'login' ? (
296
+ <>
297
+ {t('commentNamespace.areYouNoAccount')}{' '}
298
+ <Link href={`/register?from=${encodeURIComponent(redirectTarget)}`}>
299
+ {t('commentNamespace.registerNow')}
300
+ </Link>
301
+ </>
302
+ ) : (
303
+ <>
304
+ {t('commentNamespace.areYouHasAccount')}{' '}
305
+ <Link href={`/login?from=${encodeURIComponent(redirectTarget)}`}>
306
+ {t('commentNamespace.loginNow')}
307
+ </Link>
308
+ </>
309
+ )}
310
+ </p>
311
+ </div>
312
+ </div>
313
+ )
314
+ }