@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,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
|
+
}
|