From bf7d4b5e6656185d0d255dd7c56d5b4e3d92ea89 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Fri, 17 Jul 2026 23:29:01 +0330 Subject: [PATCH] avatar --- src/components/UserAvatar.tsx | 42 ++++ .../components/ChatMessage.tsx} | 53 ++-- .../components/ChatSection.tsx} | 93 ++++--- src/pages/chat/hooks/useChatData.ts | 23 ++ src/pages/chat/index.ts | 15 ++ src/pages/chat/service/ChatService.ts | 20 ++ src/pages/chat/type/Types.ts | 48 ++++ src/pages/order/OrderDetail.tsx | 10 +- src/pages/order/components/TicketSection.tsx | 235 ------------------ src/pages/order/hooks/useOrderData.ts | 23 +- src/pages/order/service/OrderService.ts | 14 -- src/pages/order/type/Types.ts | 5 +- src/pages/request/RequestDetail.tsx | 10 +- src/pages/request/hooks/useRequestData.ts | 21 -- src/pages/request/service/RequestService.ts | 13 - src/pages/request/type/Types.ts | 28 +-- 16 files changed, 279 insertions(+), 374 deletions(-) create mode 100644 src/components/UserAvatar.tsx rename src/pages/{request/components/TicketMessage.tsx => chat/components/ChatMessage.tsx} (64%) rename src/pages/{request/components/TicketSection.tsx => chat/components/ChatSection.tsx} (72%) create mode 100644 src/pages/chat/hooks/useChatData.ts create mode 100644 src/pages/chat/index.ts create mode 100644 src/pages/chat/service/ChatService.ts create mode 100644 src/pages/chat/type/Types.ts delete mode 100644 src/pages/order/components/TicketSection.tsx diff --git a/src/components/UserAvatar.tsx b/src/components/UserAvatar.tsx new file mode 100644 index 0000000..09fb6ea --- /dev/null +++ b/src/components/UserAvatar.tsx @@ -0,0 +1,42 @@ +import { type FC } from 'react' +import { usePresignedUrl } from '@/pages/uploader/hooks/usePresignedUrl' +import { clx } from '@/helpers/utils' + +type Props = { + src?: string | null + firstName?: string | null + lastName?: string | null + className?: string + textClassName?: string +} + +const UserAvatar: FC = ({ + src, + firstName, + lastName, + className = 'size-10', + textClassName = 'text-xs', +}) => { + const resolvedSrc = usePresignedUrl(src ?? undefined) + const initials = + `${String(firstName ?? '').charAt(0)}${String(lastName ?? '').charAt(0)}`.trim() || + '?' + + return ( +
+ {resolvedSrc ? ( + + ) : ( + initials + )} +
+ ) +} + +export default UserAvatar diff --git a/src/pages/request/components/TicketMessage.tsx b/src/pages/chat/components/ChatMessage.tsx similarity index 64% rename from src/pages/request/components/TicketMessage.tsx rename to src/pages/chat/components/ChatMessage.tsx index 8e02d22..b467f8e 100644 --- a/src/pages/request/components/TicketMessage.tsx +++ b/src/pages/chat/components/ChatMessage.tsx @@ -4,24 +4,31 @@ import { Paperclip2 } from 'iconsax-react' import { getFileNameAndExtensionFromUrl } from '@/config/func' import { getPresignedUrl } from '@/pages/uploader/service/UploaderService' import VoicePlayer from '@/components/VoicePlayer' -import type { AttachmentsType } from '../type/Types' +import UserAvatar from '@/components/UserAvatar' +import type { ChatAttachmentType } from '../type/Types' type Props = { content: string - attachments?: AttachmentsType[] + attachments?: ChatAttachmentType[] senderName?: string senderLabel?: string createdAt?: string isAdmin?: boolean + avatarUrl?: string | null + firstName?: string | null + lastName?: string | null } -const TicketMessage: FC = ({ +const ChatMessage: FC = ({ content, attachments = [], senderName, senderLabel = 'پشتیبان', createdAt, isAdmin = false, + avatarUrl, + firstName, + lastName, }) => { const fileAttachments = attachments.filter((a) => a.type !== 'voice') const voiceAttachments = attachments.filter((a) => a.type === 'voice') @@ -31,22 +38,32 @@ const TicketMessage: FC = ({ window.open(url, '_blank', 'noopener,noreferrer') } + const avatar = ( + + ) + return ( -
+
+ {!isAdmin && avatar}
- {isAdmin && senderName && ( -
- {senderLabel}: - {senderName} -
- )} +
+ {senderLabel}: + {senderName ? {senderName} : null} +
{content && ( -

{content}

+

+ {content} +

)} {fileAttachments.length > 0 && ( @@ -60,7 +77,11 @@ const TicketMessage: FC = ({ > - {getFileNameAndExtensionFromUrl(attach.url).fileName} + { + getFileNameAndExtensionFromUrl( + attach.url, + ).fileName + } ))} @@ -76,13 +97,17 @@ const TicketMessage: FC = ({ )} {createdAt && ( -
+
{moment(createdAt).format('jYYYY/jMM/jDD HH:mm')}
)}
+ {isAdmin && avatar}
) } -export default TicketMessage +export default ChatMessage diff --git a/src/pages/request/components/TicketSection.tsx b/src/pages/chat/components/ChatSection.tsx similarity index 72% rename from src/pages/request/components/TicketSection.tsx rename to src/pages/chat/components/ChatSection.tsx index d6b35aa..77538ee 100644 --- a/src/pages/request/components/TicketSection.tsx +++ b/src/pages/chat/components/ChatSection.tsx @@ -4,19 +4,37 @@ import { Microphone, Play, Pause } from 'iconsax-react' import { useState, type FC } from 'react' import { useVoiceRecorder } from '@/hooks/useVoiceRecorder' import { useMultiUpload, useSingleUpload } from '@/pages/uploader/hooks/useUploader' -import type { AddTicketType } from '../type/Types' -import { useAddTicket, useGetTickets } from '../hooks/useRequestData' -import { useParams } from 'react-router-dom' +import type { AddChatMessageType } from '../type/Types' +import { getChatSenderName } from '../type/Types' +import { useAddChatMessage, useGetChatMessages } from '../hooks/useChatData' import { toast } from '@/shared/toast' import { extractErrorMessage } from '@/config/func' -import TicketMessage from './TicketMessage' +import ChatMessage from './ChatMessage' -const TicketSection: FC = () => { - const { id } = useParams() +export type ChatSectionProps = { + refId: string + title?: string + description?: string + senderLabel?: string + userLabel?: string + submitLabel?: string + uploadLabel?: string +} + +const ChatSection: FC = ({ + refId, + title, + description, + senderLabel = 'پشتیبان', + userLabel = 'شما', + submitLabel = 'ارسال پیام', + uploadLabel = 'فایل‌های ضمیمه', +}) => { const [message, setMessage] = useState('') const [files, setFiles] = useState([]) - const addTicket = useAddTicket() - const { data, refetch, isPending: isLoadingTickets } = useGetTickets(id!) + const addMessage = useAddChatMessage() + const { data, refetch, isPending: isLoadingMessages } = + useGetChatMessages(refId) const singleUpload = useSingleUpload() const multiUpload = useMultiUpload() @@ -33,9 +51,9 @@ const TicketSection: FC = () => { resetRecorder, } = useVoiceRecorder() - const tickets = data?.data ?? [] + const messages = data?.data ?? [] const isSubmitting = - singleUpload.isPending || multiUpload.isPending || addTicket.isPending + singleUpload.isPending || multiUpload.isPending || addMessage.isPending const handleSubmit = async () => { if (!message.trim() && !audioFile && files.length === 0) { @@ -43,7 +61,7 @@ const TicketSection: FC = () => { return } - const params: AddTicketType = { + const params: AddChatMessageType = { attachments: [], content: message.trim(), } @@ -72,8 +90,8 @@ const TicketSection: FC = () => { }) } - addTicket.mutate( - { requestId: id!, params }, + addMessage.mutate( + { refId, params }, { onSuccess: () => { toast('پیام شما با موفقیت ارسال شد') @@ -85,52 +103,65 @@ const TicketSection: FC = () => { onError: (error) => { toast(extractErrorMessage(error), 'error') }, - } + }, ) } return (
-

گفتگو با پشتیبانی

-

- سوالات یا توضیحات تکمیلی خود را با تیم پشتیبانی در میان بگذارید. -

+ {title &&

{title}

} + {description && ( +

{description}

+ )}
- {isLoadingTickets && tickets.length === 0 && ( -

در حال بارگذاری پیام‌ها...

+ {isLoadingMessages && messages.length === 0 && ( +

+ در حال بارگذاری پیام‌ها... +

)} - {!isLoadingTickets && tickets.length === 0 && ( + {!isLoadingMessages && messages.length === 0 && (
-

هنوز پیامی ثبت نشده است.

+

+ هنوز پیامی ثبت نشده است. +

اولین پیام خود را در فرم زیر ارسال کنید.

)} - {tickets.map((item) => { + {messages.map((item) => { if (item.user) { return ( - ) } if (item.admin) { return ( - ) } @@ -188,7 +219,9 @@ const TicketSection: FC = () => { className="w-[2px] rounded-full transition-all" style={{ height: `${Math.random() * 18 + 4}px`, - backgroundColor: passed ? '#0047FF' : '#E5E7EB', + backgroundColor: passed + ? '#0047FF' + : '#E5E7EB', }} /> ) @@ -201,12 +234,12 @@ const TicketSection: FC = () => {
- +
- +
) } diff --git a/src/pages/order/components/TicketSection.tsx b/src/pages/order/components/TicketSection.tsx deleted file mode 100644 index a2e3346..0000000 --- a/src/pages/order/components/TicketSection.tsx +++ /dev/null @@ -1,235 +0,0 @@ -import Button from '@/components/Button' -import UploadBox from '@/components/UploadBox' -import { Microphone, Paperclip2 } from 'iconsax-react' -import { useState, type FC } from 'react' -import { useVoiceRecorder } from '@/hooks/useVoiceRecorder' -import { Play, Pause } from 'iconsax-react' -import { useMultiUpload, useSingleUpload } from '@/pages/uploader/hooks/useUploader' -import type { AddTicketType } from '@/pages/request/type/Types' -import { useAddTicket, useGetTickets } from '../hooks/useOrderData' -import { useParams } from 'react-router-dom' -import { toast } from '@/shared/toast' -import { extractErrorMessage, getFileNameAndExtensionFromUrl } from '@/config/func' -import VoicePlayer from '@/components/VoicePlayer' -import { getPresignedUrl } from '@/pages/uploader/service/UploaderService' - -const TicketSection: FC = () => { - const { id } = useParams() - const [message, setMessage] = useState('') - const [files, setFiles] = useState([]) - const addTicket = useAddTicket() - const { data, refetch } = useGetTickets(id!) - const singleUpload = useSingleUpload() - const multiUpload = useMultiUpload() - - const { - isRecording, - isPlaying, - audioUrl, - audioRef, - togglePlayPause, - startRecording, - stopRecording, - progress, - audioFile, - resetRecorder, - } = useVoiceRecorder() - - const handleSubmit = async () => { - const params: AddTicketType = { - attachments: [], - content: message, - } - if (audioFile) { - await singleUpload.mutateAsync(audioFile, { - onSuccess: (data) => { - params.attachments.push({ - type: 'voice', - url: data?.data?.key, - }) - }, - }) - } - if (files.length) { - await multiUpload.mutateAsync(files, { - onSuccess: (data) => { - data?.data?.map((item) => { - params.attachments.push({ - type: 'uploads_attach', - url: item.key, - }) - }) - }, - }) - } - - addTicket.mutate( - { requestId: id!, params }, - { - onSuccess: () => { - toast('پیام شما با موفقیت ارسال شد') - refetch() - setMessage('') - setFiles([]) - resetRecorder() - }, - onError: (error) => { - toast(extractErrorMessage(error), 'error') - }, - }, - ) - } - - const handleOpenLink = async (key: string) => { - const url = await getPresignedUrl(key) - window.open(url, '_blank', 'noopener,noreferrer') - } - - return ( -
- {data?.data?.map((item) => { - if (item.user) - return ( -
-
- {item.content} -
-
- {item.attachments - ?.filter((a) => a.type !== 'voice') - .map((attach) => ( -
handleOpenLink(attach.url)} - className="flex cursor-pointer items-center gap-1.5 text-[#0047FF]" - > - -
- {getFileNameAndExtensionFromUrl(attach.url).fileName} -
-
- ))} -
- {item.attachments - ?.filter((a) => a.type === 'voice') - .map((voice) => ( - - ))} -
- ) - if (item.admin) - return ( -
-
-
-
طراح :
-
- {item.admin?.firstName + ' ' + item?.admin?.lastName} -
-
-
- {item.content} -
-
- {item.attachments - ?.filter((a) => a.type !== 'voice') - .map((attach) => ( -
handleOpenLink(attach.url)} - className="flex cursor-pointer items-center gap-1.5 text-[#0047FF]" - > - -
- {getFileNameAndExtensionFromUrl(attach.url).fileName} -
-
- ))} -
- {item.attachments - ?.filter((a) => a.type === 'voice') - .map((voice) => ( - - ))} -
-
- ) - })} - -
-
پیام شما
-
-