Skip to content

Commit 35b61db

Browse files
committed
feat: comment ui
Signed-off-by: Innei <[email protected]>
1 parent f8902a6 commit 35b61db

16 files changed

+282
-12
lines changed

src/app/notes/[id]/layout.tsx

+9-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { headers } from 'next/dist/client/components/headers'
22
import type { Metadata } from 'next'
33

4+
import { NotSupport } from '~/components/common/NotSupport'
45
import { BottomToUpTransitionView } from '~/components/ui/transition/BottomToUpTransitionView'
5-
import { Comments } from '~/components/widgets/comment/Comments'
6+
import { CommentRoot } from '~/components/widgets/comment/CommentRoot'
67
import { NoteMainContainer } from '~/components/widgets/note/NoteMainContainer'
7-
import { REQUEST_QUERY } from '~/constants/system'
8+
import { REQUEST_GEO, REQUEST_QUERY } from '~/constants/system'
89
import { attachUA } from '~/lib/attach-ua'
910
import { getSummaryFromMd } from '~/lib/markdown'
1011
import {
@@ -65,13 +66,17 @@ export default async (
6566
}>,
6667
) => {
6768
attachUA()
68-
const searchParams = new URLSearchParams(headers().get(REQUEST_QUERY) || '')
69+
const header = headers()
70+
const searchParams = new URLSearchParams(header.get(REQUEST_QUERY) || '')
6971
const id = props.params.id
7072
const query = queries.note.byNid(
7173
id,
7274
searchParams.get('password') || undefined,
7375
)
7476
const data = await getQueryClient().fetchQuery(query)
77+
const geo = header.get(REQUEST_GEO)
78+
79+
const isCN = geo === 'CN'
7580

7681
return (
7782
<>
@@ -81,7 +86,7 @@ export default async (
8186

8287
<BottomToUpTransitionView className="min-w-0">
8388
<Paper as={NoteMainContainer}>{props.children}</Paper>
84-
<Comments refId={id} />
89+
{isCN ? <NotSupport /> : <CommentRoot refId={data.data.id} />}
8590
</BottomToUpTransitionView>
8691
</>
8792
)

src/components/common/Lazyload.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
'use client'
2+
13
import React, { useEffect } from 'react'
24
import { useInView } from 'react-intersection-observer'
35
import type { FC, PropsWithChildren } from 'react'

src/components/common/NotSupport.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const NotSupport = () => {
2+
return (
3+
<div className="flex h-[100px] items-center justify-center text-lg font-medium">
4+
您当前所在地区暂不支持此功能
5+
</div>
6+
)
7+
}

src/components/ui/link-card/LinkCard.module.css

+2
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@
6161
}
6262

6363
.skeleton {
64+
@apply !cursor-auto;
65+
6466
& .title,
6567
& .desc {
6668
border-radius: 99px;

src/components/ui/link-card/LinkCard.tsx

+21-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { FC } from 'react'
88

99
import { simpleCamelcaseKeys as camelcaseKeys } from '@mx-space/api-client'
1010

11+
import { useIsClient } from '~/hooks/common/use-is-client'
1112
import { useIsUnMounted } from '~/hooks/common/use-is-unmounted'
1213
import { useSafeSetState } from '~/hooks/common/use-safe-setState'
1314
import { apiClient } from '~/utils/request'
@@ -20,7 +21,14 @@ export interface LinkCardProps {
2021
source?: LinkCardSource
2122
className?: string
2223
}
23-
export const LinkCard: FC<LinkCardProps> = (props) => {
24+
25+
export const LinkCard = (props: LinkCardProps) => {
26+
const isClient = useIsClient()
27+
if (!isClient) return <LinkCardSkeleton />
28+
29+
return <LinkCardImpl {...props} />
30+
}
31+
export const LinkCardImpl: FC<LinkCardProps> = (props) => {
2432
const { id, source = 'self', className } = props
2533
const isUnMounted = useIsUnMounted()
2634

@@ -181,3 +189,15 @@ export const LinkCard: FC<LinkCardProps> = (props) => {
181189
</LinkComponent>
182190
)
183191
}
192+
193+
const LinkCardSkeleton = () => {
194+
return (
195+
<span className={clsx(styles['card-grid'], styles['skeleton'])}>
196+
<span className={styles['contents']}>
197+
<span className={styles['title']} />
198+
<span className={styles['desc']} />
199+
</span>
200+
<span className={styles['image']} />
201+
</span>
202+
)
203+
}
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { clsxm } from '~/utils/helper'
2+
3+
export const Skeleton: Component = ({ className }) => {
4+
return (
5+
<div className={clsxm('flex animate-pulse flex-col gap-3', className)}>
6+
<div className="h-6 w-full rounded-lg bg-gray-200 dark:bg-zinc-800/80" />
7+
<div className="h-6 w-full rounded-lg bg-gray-200 dark:bg-zinc-800/80" />
8+
<div className="h-6 w-full rounded-lg bg-gray-200 dark:bg-zinc-800/80" />
9+
<div className="h-6 w-full rounded-lg bg-gray-200 dark:bg-zinc-800/80" />
10+
<span className="sr-only">Loading...</span>
11+
</div>
12+
)
13+
}

src/components/ui/skeleton/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './Skeleton'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
.comment__message {
2+
* {
3+
@apply leading-6;
4+
}
5+
6+
h1,
7+
h2,
8+
h3,
9+
h4,
10+
h5,
11+
h6 {
12+
@apply font-semibold tracking-tight;
13+
}
14+
15+
h1 {
16+
@apply text-lg font-bold;
17+
}
18+
19+
h2 {
20+
font-size: 1.065rem;
21+
line-height: 1.75rem;
22+
@apply font-bold;
23+
}
24+
25+
hr {
26+
@apply my-1.5 border-zinc-400 opacity-20;
27+
}
28+
29+
ul {
30+
@apply list-disc pl-4;
31+
}
32+
33+
ol {
34+
@apply list-decimal pl-4;
35+
}
36+
37+
blockquote {
38+
@apply my-1 border-l-4 border-zinc-400 pl-2;
39+
}
40+
41+
img,
42+
video {
43+
@apply rounded-md;
44+
max-height: 350px;
45+
}
46+
47+
pre {
48+
@apply my-1.5 whitespace-break-spaces;
49+
}
50+
51+
pre,
52+
code:not([class^='language-']) {
53+
@apply rounded bg-zinc-700/10 px-1 py-0.5 text-zinc-900;
54+
}
55+
56+
pre > code {
57+
@apply bg-transparent px-0 py-0 !important;
58+
}
59+
}
60+
61+
.dark .comment__message {
62+
hr {
63+
@apply border-zinc-100 opacity-20;
64+
}
65+
66+
blockquote {
67+
@apply border-zinc-50/50;
68+
}
69+
70+
pre,
71+
code:not([class^='language-']) {
72+
@apply bg-zinc-200/20 text-zinc-50;
73+
}
74+
75+
pre > code {
76+
@apply bg-transparent !important;
77+
}
78+
}
+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import clsx from 'clsx'
2+
import Markdown from 'markdown-to-jsx'
3+
import Image from 'next/image'
4+
import type { CommentModel } from '@mx-space/api-client'
5+
6+
import { RelativeTime } from '~/components/ui/relative-time'
7+
8+
import styles from './Comment.module.css'
9+
10+
export const Comment: Component<{ comment: CommentModel }> = (props) => {
11+
const { comment, className } = props
12+
const { id: cid, avatar, author, text } = comment
13+
const parentId =
14+
typeof comment.parent === 'string' ? comment.parent : comment.parent?.id
15+
return (
16+
<li data-comment-id={cid} data-parent-id={parentId} className={className}>
17+
<div className="flex w-full items-stretch gap-2">
18+
<div className="flex w-9 shrink-0 items-end">
19+
<Image
20+
src={avatar}
21+
alt=""
22+
className="h-9 w-9 select-none rounded-full"
23+
width={24}
24+
height={24}
25+
unoptimized
26+
/>
27+
</div>
28+
<div className={clsx('flex flex-1 flex-col', 'items-start')}>
29+
<span
30+
className={clsx(
31+
'flex items-center gap-2 font-semibold text-zinc-800 dark:text-zinc-200',
32+
'mb-2',
33+
)}
34+
>
35+
<span className="ml-2">{author}</span>
36+
<span className="inline-flex select-none text-[10px] font-medium opacity-40">
37+
<RelativeTime date={comment.created} />
38+
</span>
39+
</span>
40+
41+
<div
42+
className={clsx(
43+
styles['comment__message'],
44+
'group relative inline-block rounded-xl px-2 py-1 text-zinc-800 dark:text-zinc-200',
45+
'rounded-bl-sm bg-zinc-600/5 dark:bg-zinc-500/20',
46+
)}
47+
>
48+
<Markdown>{text}</Markdown>
49+
</div>
50+
</div>
51+
</div>
52+
</li>
53+
)
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { FC } from 'react'
2+
import type { CommentBaseProps } from './types'
3+
4+
export const CommentBox: FC<CommentBaseProps> = (props) => {
5+
return <div>CommentBox WIP, RefId: {props.refId}</div>
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { FC } from 'react'
2+
import type { CommentBaseProps } from './types'
3+
4+
import { LazyLoad } from '~/components/common/Lazyload'
5+
import { Loading } from '~/components/ui/loading'
6+
7+
import { CommentBox } from './CommentBox'
8+
import { Comments } from './Comments'
9+
10+
const LoadingElement = <Loading loadingText="评论区加载中..." />
11+
export const CommentRoot: FC<CommentBaseProps> = (props) => {
12+
return (
13+
<LazyLoad placeholder={LoadingElement}>
14+
<div className="mt-12">
15+
<CommentBox refId={props.refId} />
16+
17+
<div className="h-12" />
18+
<Comments refId={props.refId} />
19+
</div>
20+
</LazyLoad>
21+
)
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { clsxm } from '~/utils/helper'
2+
3+
export const CommentSkeleton: Component = ({ className }) => {
4+
return (
5+
<div className={clsxm('flex animate-pulse flex-col gap-3', className)}>
6+
<div className="h-6 w-full rounded-lg bg-gray-200 dark:bg-zinc-800/80" />
7+
<div className="h-6 w-full rounded-lg bg-gray-200 dark:bg-zinc-800/80" />
8+
<div className="h-6 w-full rounded-lg bg-gray-200 dark:bg-zinc-800/80" />
9+
<div className="h-6 w-full rounded-lg bg-gray-200 dark:bg-zinc-800/80" />
10+
<span className="sr-only">Loading...</span>
11+
</div>
12+
)
13+
}
+39-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,44 @@
1+
'use client'
2+
3+
import { useQuery } from '@tanstack/react-query'
4+
import { useState } from 'react'
15
import type { FC } from 'react'
6+
import type { CommentBaseProps } from './types'
7+
8+
import { apiClient } from '~/utils/request'
9+
10+
import { Comment } from './Comment'
11+
import { CommentSkeleton } from './CommentSkeleton'
212

3-
export const Comments: FC<{
4-
refId: string
5-
}> = ({ refId }) => {
13+
export const Comments: FC<CommentBaseProps> = ({ refId }) => {
14+
const [page, setPage] = useState(1)
15+
16+
const { data, isLoading } = useQuery(
17+
['comments', refId],
18+
async ({ queryKey, meta }) => {
19+
const { page } = meta as { page: number }
20+
const [, refId] = queryKey as [string, string]
21+
const data = await apiClient.comment.getByRefId(refId, {
22+
page,
23+
})
24+
return data.$serialized
25+
},
26+
27+
{
28+
meta: {
29+
page,
30+
},
31+
},
32+
)
33+
if (isLoading) {
34+
return <CommentSkeleton />
35+
}
36+
if (!data) return null
637
return (
7-
<div className="relative mb-[60px] mt-[120px] min-h-[100px]">
8-
Comments WIP, RefId: {refId}
9-
</div>
38+
<ul className="list-none space-y-4">
39+
{data.data.map((comment) => {
40+
return <Comment comment={comment} key={comment.id} />
41+
})}
42+
</ul>
1043
)
1144
}
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export interface CommentBaseProps {
2+
refId: string
3+
}

src/middleware.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import type { NextRequest } from 'next/server'
55
import countries from '~/data/countries.json'
66
import { kvKeys, redis } from '~/lib/redis.server'
77

8-
import { REQUEST_PATHNAME, REQUEST_QUERY } from './constants/system'
8+
import {
9+
REQUEST_GEO,
10+
REQUEST_PATHNAME,
11+
REQUEST_QUERY,
12+
} from './constants/system'
913

1014
export default async function middleware(req: NextRequest) {
1115
const { pathname, search } = req.nextUrl
@@ -26,6 +30,7 @@ export default async function middleware(req: NextRequest) {
2630
const requestHeaders = new Headers(req.headers)
2731
requestHeaders.set(REQUEST_PATHNAME, pathname)
2832
requestHeaders.set(REQUEST_QUERY, search)
33+
requestHeaders.set(REQUEST_GEO, geo?.country || 'unknown')
2934

3035
const isApi = pathname.startsWith('/api/')
3136

src/providers/internal/createDataProvider.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ export const createDataProvider = <Model,>() => {
2525
jotaiStore.set(currentDataAtom, data)
2626
}, [data])
2727

28+
useEffect(() => {
29+
return () => {
30+
jotaiStore.set(currentDataAtom, null)
31+
}
32+
}, [])
33+
2834
return children
2935
})
3036
const useCurrentDataSelector = <T,>(

0 commit comments

Comments
 (0)