Skip to content

Commit

Permalink
feat: comment box
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <i@innei.in>
  • Loading branch information
Innei committed Jun 28, 2023
1 parent 50a7735 commit 8f99190
Show file tree
Hide file tree
Showing 10 changed files with 178 additions and 29 deletions.
3 changes: 3 additions & 0 deletions src/app/posts/(post-detail)/[category]/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ClientOnly } from '~/components/common/ClientOnly'
import { ReadIndicator } from '~/components/common/ReadIndicator'
import { useSetHeaderMetaInfo } from '~/components/layout/header/hooks'
import { Markdown } from '~/components/ui/markdown'
import { CommentAreaRoot } from '~/components/widgets/comment'
import { PostActionAside } from '~/components/widgets/post/PostActionAside'
import { PostCopyright } from '~/components/widgets/post/PostCopyright'
import { PostMetaBar } from '~/components/widgets/post/PostMetaBar'
Expand Down Expand Up @@ -70,6 +71,8 @@ const PostPage = () => {
<SubscribeBell defaultType="post_c" />
<XLogInfoForPost />
</ClientOnly>

<CommentAreaRoot refId={id} />
</div>
)
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/common/ToastCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const ToastCard: FC<{
layout="position"
className={clsx(
'relative w-full overflow-hidden rounded-xl shadow-md shadow-slate-200 dark:shadow-stone-800',
'my-4 mr-4 px-4 py-5',
'my-4 mr-4 px-4 py-5 pr-7',
'bg-slate-50/90 backdrop-blur-sm dark:bg-neutral-900/90',
'border border-slate-100/80 dark:border-neutral-900/80',
'space-x-4',
Expand Down
17 changes: 14 additions & 3 deletions src/components/widgets/comment/CommentBox/CommentAuthedInput.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
'use client'

import { useRef } from 'react'
import { useEffect, useRef } from 'react'
import clsx from 'clsx'
import Image from 'next/image'

import { useUser } from '@clerk/nextjs'

import { getRandomPlaceholder } from '../constants'
import { CommentAuthedInputSkeleton } from './CommentAuthedInputSkeleton'
import { CommentBoxActionBar } from './CommentBoxActionBar'
import {
useCommentBoxTextValue,
useSetCommentBoxValues,
} from './CommentBoxProvider'
import { getRandomPlaceholder } from './constants'

export const CommentAuthedInput = () => {
const { user } = useUser()
const setter = useSetCommentBoxValues()

useEffect(() => {
if (!user) return
setter(
'author',
user.fullName || user.lastName || user.firstName || 'Anonymous',
)
setter('avatar', user.profileImageUrl)
setter('mail', user.primaryEmailAddress?.emailAddress || '')
}, [user])

const TextArea = useRef(function Textarea() {
const placeholder = useRef(getRandomPlaceholder()).current
Expand All @@ -29,7 +40,7 @@ export const CommentAuthedInput = () => {
}}
placeholder={placeholder}
className={clsx(
'h-full w-full bg-transparent',
'h-full w-full resize-none bg-transparent',
'overflow-auto px-3 py-4',
'text-neutral-900/80 dark:text-slate-100/80',
)}
Expand Down
81 changes: 73 additions & 8 deletions src/components/widgets/comment/CommentBox/CommentBoxActionBar.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
'use client'

import { useMutation, useQueryClient } from '@tanstack/react-query'
import clsx from 'clsx'
import { AnimatePresence, motion } from 'framer-motion'
import { produce } from 'immer'
import type {
CommentModel,
PaginateResult,
RequestError,
} from '@mx-space/api-client'
import type { InfiniteData } from '@tanstack/react-query'

import { useIsLogged } from '~/atoms'
import { TiltedSendIcon } from '~/components/icons/TiltedSendIcon'
import { FloatPopover } from '~/components/ui/float-popover'
import { MLink } from '~/components/ui/markdown/renderers/link'
import { jotaiStore } from '~/lib/store'
import { toast } from '~/lib/toast'
import { clsxm } from '~/utils/helper'
import { apiClient, getErrorMessageFromRequestError } from '~/utils/request'

import { buildQueryKey } from '../Comments'
import { MAX_COMMENT_TEXT_LENGTH } from '../constants'
import {
useCommentBoxHasText,
useCommentBoxRefIdValue,
useCommentBoxTextIsOversize,
useCommentBoxTextValue,
useGetCommentBoxAtomValues,
} from './CommentBoxProvider'
import { MAX_COMMENT_TEXT_LENGTH } from './constants'

const TextLengthIndicator = () => {
const isTextOversize = useCommentBoxTextIsOversize()
Expand Down Expand Up @@ -61,9 +75,7 @@ export const CommentBoxActionBar: Component = ({ className }) => {
>
<TextLengthIndicator />

<FloatPopover type="tooltip" TriggerComponent={SubmitButton}>
发送
</FloatPopover>
<SubmitButton />
</motion.aside>
)}
</AnimatePresence>
Expand All @@ -72,18 +84,71 @@ export const CommentBoxActionBar: Component = ({ className }) => {
}

const SubmitButton = () => {
const isLoading = false
const onClickSend = () => {}
const commentRefId = useCommentBoxRefIdValue()
const {
text: textAtom,
author: authorAtom,
mail: mailAtom,
} = useGetCommentBoxAtomValues()
const isLogged = useIsLogged()
const queryClient = useQueryClient()
const { isLoading, mutate } = useMutation({
mutationFn: async (refId: string) => {
const text = jotaiStore.get(textAtom)
const author = jotaiStore.get(authorAtom)
const mail = jotaiStore.get(mailAtom)

if (isLogged) {
return apiClient.comment.proxy.master
.comment(refId)
.post<CommentModel>({
params: {
ts: Date.now(),
},
data: { text },
})
}
return apiClient.comment.comment(refId, { text, author, mail })
},
mutationKey: [commentRefId, 'comment'],
onError(error: RequestError) {
toast.error(getErrorMessageFromRequestError(error))
},
onSuccess(data) {
toast.success('感谢你的评论!')
jotaiStore.set(textAtom, '')
queryClient.setQueryData<
InfiniteData<
PaginateResult<
CommentModel & {
ref: string
}
>
>
>(buildQueryKey(commentRefId), (oldData) => {
if (!oldData) return oldData
return produce(oldData, (draft) => {
draft.pages[0].data.unshift(data)
})
})
},
})
const onClickSend = () => {
mutate(commentRefId)
}
return (
<motion.button
className="appearance-none"
className="flex appearance-none items-center space-x-1 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
type="button"
disabled={isLoading}
onClick={onClickSend}
>
<TiltedSendIcon className="h-5 w-5 text-zinc-800 dark:text-zinc-200" />
<motion.span className="text-sm" layout="size">
{isLoading ? '送信...' : '送信'}
</motion.span>
</motion.button>
)
}
29 changes: 25 additions & 4 deletions src/components/widgets/comment/CommentBox/CommentBoxProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,33 @@ import type { PropsWithChildren } from 'react'

import { jotaiStore } from '~/lib/store'

import { MAX_COMMENT_TEXT_LENGTH } from './constants'
import { MAX_COMMENT_TEXT_LENGTH } from '../constants'

const createInitialValue = () => ({
text: atom(''),
refId: atom(''),

text: atom(''),
author: atom(''),
mail: atom(''),
url: atom(''),

avatar: atom(''),
source: atom(''),
})
const CommentBoxContext = createContext(createInitialValue())
export const CommentBoxProvider = (props: PropsWithChildren) => {
export const CommentBoxProvider = (
props: PropsWithChildren & { refId: string },
) => {
return (
<CommentBoxContext.Provider value={useRef(createInitialValue()).current}>
<CommentBoxContext.Provider
key={props.refId}
value={
useRef({
...createInitialValue(),
refId: atom(props.refId),
}).current
}
>
{props.children}
</CommentBoxContext.Provider>
)
Expand All @@ -29,6 +46,10 @@ export const useCommentBoxTextValue = () =>
export const useCommentBoxRefIdValue = () =>
useAtomValue(useContext(CommentBoxContext).refId)

export const useGetCommentBoxAtomValues = () => {
return useContext(CommentBoxContext)
}

export const useCommentBoxHasText = () =>
useAtomValue(
selectAtom(
Expand Down
3 changes: 2 additions & 1 deletion src/components/widgets/comment/CommentBox/CommentBoxRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ const enum CommentBoxMode {
}

export const CommentBoxRoot: FC<CommentBaseProps> = (props) => {
const { refId } = props
const [mode, setMode] = useState<CommentBoxMode>(CommentBoxMode['with-auth'])
return (
<CommentBoxProvider>
<CommentBoxProvider refId={refId}>
<div className="relative w-full">
{mode === CommentBoxMode.legacy ? (
<CommentBoxLegacy />
Expand Down
5 changes: 4 additions & 1 deletion src/components/widgets/comment/Comments.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client'

import { useInfiniteQuery } from '@tanstack/react-query'
import { useMemo } from 'react'
import { useInView } from 'react-intersection-observer'
import type { FC } from 'react'
import type { CommentBaseProps } from './types'
Expand All @@ -12,9 +13,11 @@ import { apiClient } from '~/utils/request'
import { Comment } from './Comment'
import { CommentSkeleton } from './CommentSkeleton'

export const buildQueryKey = (refId: string) => ['comments', refId]
export const Comments: FC<CommentBaseProps> = ({ refId }) => {
const key = useMemo(() => buildQueryKey(refId), [refId])
const { data, isLoading, fetchNextPage, hasNextPage } = useInfiniteQuery(
['comments', refId],
key,
async ({ queryKey, pageParam }) => {
const page = pageParam
// const { page } = meta as { page: number }
Expand Down
File renamed without changes.
45 changes: 36 additions & 9 deletions src/lib/toast.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,38 @@
import { createElement } from 'react'
import { toast as Toast } from 'react-toastify'
import type { ToastOptions, TypeOptions } from 'react-toastify'
import type { Id, ToastOptions, TypeOptions } from 'react-toastify'

import { ToastCard } from '~/components/common/ToastCard'

export const toast = (
const baseConfig = {
position: 'bottom-right',
autoClose: 3000,
pauseOnHover: true,
hideProgressBar: true,

closeOnClick: true,
closeButton: false,
} satisfies ToastOptions

interface ToastCustom {
(
message: string,
type?: TypeOptions,
options?: ToastOptions & {
iconElement?: JSX.Element
},
): Id
}

interface ToastCustom {
success(message: string, options?: ToastOptions): Id
info(message: string, options?: ToastOptions): Id
warn(message: string, options?: ToastOptions): Id
error(message: string, options?: ToastOptions): Id
}

// @ts-ignore
export const toast: ToastCustom = (
message: string,
type?: TypeOptions,
options?: ToastOptions & {
Expand All @@ -14,13 +42,12 @@ export const toast = (
const { iconElement, ...rest } = options || {}
return Toast(createElement(ToastCard, { message, iconElement }), {
type,
position: 'bottom-right',
autoClose: 3000,
pauseOnHover: true,
hideProgressBar: true,

closeOnClick: true,
closeButton: false,
...baseConfig,
...rest,
})
}
;['success', 'info', 'warn', 'error'].forEach((type) => {
// @ts-ignore
toast[type] = (message: string, options?: ToastOptions) =>
toast(message, type as TypeOptions, options)
})
22 changes: 20 additions & 2 deletions src/utils/request.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type { AxiosInstance } from 'axios'
import type { AxiosError, AxiosInstance } from 'axios'

import { allControllers, createClient } from '@mx-space/api-client'
import {
allControllers,
createClient,
RequestError,
} from '@mx-space/api-client'
import { axiosAdaptor } from '@mx-space/api-client/dist/adaptors/axios'

import { API_URL } from '~/constants/env'
Expand Down Expand Up @@ -46,3 +50,17 @@ $axios.interceptors.request.use((config) => {

return config
})

export const getErrorMessageFromRequestError = (error: RequestError) => {
if (!(error instanceof RequestError)) return (error as Error).message
const axiosError = error.raw as AxiosError
const messagesOrMessage = (axiosError.response?.data as any)?.message
const bizMessage =
typeof messagesOrMessage === 'string'
? messagesOrMessage
: Array.isArray(messagesOrMessage)
? messagesOrMessage[0]
: undefined

return bizMessage || axiosError.message
}

1 comment on commit 8f99190

@vercel
Copy link

@vercel vercel bot commented on 8f99190 Jun 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

springtide – ./

springtide-git-main-innei.vercel.app
springtide-innei.vercel.app
springtide.vercel.app
innei.in

Please sign in to comment.