mirror of
https://github.com/Afilmory/afilmory
synced 2026-05-01 18:26:41 +00:00
feat: enhance comments functionality with social sign-in and user management
- Introduced social sign-in options for comments, allowing users to authenticate via various providers. - Updated comments API to handle user data, including fetching user information and managing comment relations. - Refactored comment components to improve user experience, including a new user avatar component and enhanced comment input handling. - Added loading states and error handling for comment submissions and reactions. - Implemented a relative time display for comment timestamps to improve readability. Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
@@ -1,67 +1,98 @@
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { clsxm as cn } from '@afilmory/utils'
|
||||
import { useAtom, useAtomValue } from 'jotai'
|
||||
import { selectAtom } from 'jotai/utils'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
import { sessionUserAtom } from '~/atoms/session'
|
||||
import type { Comment } from '~/lib/api/comments'
|
||||
import { useMobile } from '~/hooks/useMobile'
|
||||
|
||||
interface CommentInputProps {
|
||||
isMobile: boolean
|
||||
import { useCommentsContext } from './context'
|
||||
import { UserAvatar } from './UserAvatar'
|
||||
|
||||
replyTo: Comment | null
|
||||
setReplyTo: (comment: Comment | null) => void
|
||||
newComment: string
|
||||
setNewComment: (value: string) => void
|
||||
onSubmit: (content: string) => void
|
||||
}
|
||||
|
||||
export const CommentInput = ({
|
||||
isMobile,
|
||||
|
||||
replyTo,
|
||||
setReplyTo,
|
||||
newComment,
|
||||
setNewComment,
|
||||
onSubmit,
|
||||
}: CommentInputProps) => {
|
||||
export const CommentInput = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { atoms, methods } = useCommentsContext()
|
||||
const [newComment, setNewComment] = useAtom(atoms.newCommentAtom)
|
||||
const sessionUser = useAtomValue(sessionUserAtom)
|
||||
const submitError = useAtomValue(atoms.submitErrorAtom)
|
||||
const status = useAtomValue(atoms.statusAtom)
|
||||
|
||||
const [replyTo, setReplyTo] = useAtom(atoms.replyToAtom)
|
||||
const isMobile = useMobile()
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setNewComment(e.target.value)
|
||||
if (submitError) {
|
||||
methods.clearSubmitError()
|
||||
}
|
||||
},
|
||||
[setNewComment, submitError, methods],
|
||||
)
|
||||
|
||||
const replyUserName = useAtomValue(
|
||||
useMemo(
|
||||
() => selectAtom(atoms.usersAtom, (users) => (replyTo?.userId ? users[replyTo.userId]?.name : null)),
|
||||
[atoms.usersAtom, replyTo?.userId],
|
||||
),
|
||||
)
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
onSubmit(newComment)
|
||||
methods.submit(newComment)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-accent/10 shrink-0 border-t p-4">
|
||||
{replyTo ? (
|
||||
<div className="border-accent/20 bg-accent/5 mb-3 flex items-center justify-between rounded-lg border px-3 py-2 text-xs text-white/80">
|
||||
{submitError && (
|
||||
<div className="animate-shake mb-3 flex items-center gap-2 rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-400">
|
||||
<i className="i-lucide-alert-circle shrink-0" />
|
||||
<span>{t(submitError.message as any)}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="ml-auto text-red-400/60 transition hover:text-red-400"
|
||||
onClick={() => methods.clearSubmitError()}
|
||||
>
|
||||
<i className="i-lucide-x" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{replyTo && !submitError ? (
|
||||
<div className="border-accent/20 bg-accent/5 mb-3 flex items-center justify-between rounded-lg border px-3 py-2 text-xs text-white/80 select-none">
|
||||
<div className="flex items-center gap-2">
|
||||
<i className="i-mingcute-reply-line text-accent" />
|
||||
<i className="i-lucide-reply text-accent" />
|
||||
<span>
|
||||
{t('comments.replyingTo', {
|
||||
user: replyTo.userId.slice(-6),
|
||||
})}
|
||||
<Trans
|
||||
i18nKey="comments.replyingTo"
|
||||
components={{ strong: <b className="font-medium" /> }}
|
||||
values={{ user: replyUserName }}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<button type="button" className="text-white/50 transition hover:text-white" onClick={() => setReplyTo(null)}>
|
||||
{t('comments.cancelReply')}
|
||||
<i className="i-lucide-x" />
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<form onSubmit={handleSubmit} className="flex items-end gap-2">
|
||||
<div className="flex size-9 shrink-0 items-center justify-center rounded-full bg-white/10 text-sm font-semibold text-white/80">
|
||||
{(sessionUser?.name || sessionUser?.id || 'G')[0]}
|
||||
</div>
|
||||
<UserAvatar image={sessionUser?.image} name={sessionUser?.name || sessionUser?.id} fallback="G" size={36} />
|
||||
|
||||
<div className="flex-1">
|
||||
<textarea
|
||||
value={newComment}
|
||||
onChange={(e) => setNewComment(e.target.value)}
|
||||
onChange={handleInputChange}
|
||||
placeholder={t('comments.placeholder')}
|
||||
rows={isMobile ? 2 : 1}
|
||||
className="bg-material-medium focus:ring-accent/50 w-full resize-none rounded-lg border-0 px-3 py-2 text-sm text-white placeholder:text-white/40 focus:ring-1 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60"
|
||||
disabled={status.isLoading}
|
||||
className={cn(
|
||||
'bg-material-medium w-full resize-none rounded-lg border px-3 py-2 text-sm text-white transition-colors placeholder:text-white/40 focus:ring-1 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60',
|
||||
submitError
|
||||
? 'border-red-500/50 focus:border-red-500/50 focus:ring-red-500/50'
|
||||
: 'focus:ring-accent/50 focus:border-accent/50 border-transparent',
|
||||
)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
@@ -73,14 +104,20 @@ export const CommentInput = ({
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!newComment.trim()}
|
||||
className="bg-accent shadow-accent/20 flex size-9 shrink-0 items-center justify-center rounded-lg text-white shadow-lg transition disabled:cursor-not-allowed disabled:opacity-40"
|
||||
disabled={!newComment.trim() || status.isLoading}
|
||||
className={cn(
|
||||
'flex size-9 shrink-0 items-center justify-center rounded-lg text-white shadow-lg transition disabled:cursor-not-allowed disabled:opacity-40',
|
||||
status.isLoading ? 'bg-accent/50' : 'bg-accent shadow-accent/20',
|
||||
)}
|
||||
>
|
||||
<i className="i-mingcute-send-line" />
|
||||
{status.isLoading ? (
|
||||
<i className="i-mingcute-loading-line animate-spin" />
|
||||
) : (
|
||||
<i className="i-mingcute-send-line" />
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
<p className="mt-2 text-xs text-white/40">{t('comments.hint')}</p>
|
||||
{!sessionUser && <p className="mt-1 text-xs text-white/50">{t('comments.loginRequired')}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user