Files
afilmory/apps/web/src/modules/social/comments/CommentInput.tsx
Innei 2a95dcecae 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>
2025-11-26 23:23:09 +08:00

124 lines
4.5 KiB
TypeScript

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 { useMobile } from '~/hooks/useMobile'
import { useCommentsContext } from './context'
import { UserAvatar } from './UserAvatar'
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()
methods.submit(newComment)
}
return (
<div className="border-accent/10 shrink-0 border-t p-4">
{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-lucide-reply text-accent" />
<span>
<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)}>
<i className="i-lucide-x" />
</button>
</div>
) : null}
<form onSubmit={handleSubmit} className="flex items-end gap-2">
<UserAvatar image={sessionUser?.image} name={sessionUser?.name || sessionUser?.id} fallback="G" size={36} />
<div className="flex-1">
<textarea
value={newComment}
onChange={handleInputChange}
placeholder={t('comments.placeholder')}
rows={isMobile ? 2 : 1}
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()
handleSubmit(e)
}
}}
/>
</div>
<button
type="submit"
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',
)}
>
{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>
</div>
)
}