admin/app/(dashboard)/chat-oversight/[id]/_components/AdminMessageBubble.tsx
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

132 lines
4.9 KiB
TypeScript

'use client'
import { useState } from 'react'
import ChatImagePreviewModal from '@/app/(dashboard)/chat-oversight/[id]/_components/ChatImagePreviewModal'
import Button from '@/components/formElements/Button'
import { formatPersonName } from '@/helpers'
import { cn } from '@/lib/cn'
import { formatIranianMobile, formatPersianDate } from '@/lib/formatters'
import { resolveUserAvatarSrc } from '@/lib/resolveUserAvatarSrc'
import type { AdminChatMessage } from '@/services/adminChat'
interface AdminMessageBubbleProps {
message: AdminChatMessage
side: 'primary' | 'secondary'
showAvatar: boolean
showSenderName: boolean
isClusterStart: boolean
isClusterEnd: boolean
}
const formatTime = (value: string) =>
formatPersianDate(value, {
hour: '2-digit',
minute: '2-digit',
})
const AdminMessageBubble = ({ message, side, showAvatar, showSenderName, isClusterStart, isClusterEnd }: AdminMessageBubbleProps) => {
const [previewImageUrl, setPreviewImageUrl] = useState<string | null>(null)
const isPrimary = side === 'primary'
const senderName = formatPersonName(message.senderFirstName, message.senderLastName, 'کاربر')
const senderMeta = message.senderMobile ? formatIranianMobile(message.senderMobile) : message.senderId
const avatarSrc = resolveUserAvatarSrc({
avatarUrl: message.senderAvatarUrl,
gender: message.senderGender,
})
return (
<div className={cn('flex w-full', isPrimary ? 'justify-start' : 'justify-end', isClusterStart ? 'mt-2' : 'mt-1')}>
<div className={cn('flex max-w-[88%] gap-2 sm:max-w-[75%]', isPrimary ? 'flex-row' : 'flex-row-reverse')}>
<div className="flex w-9 shrink-0 flex-col justify-end">
{showAvatar ? (
<div className="size-9 overflow-hidden rounded-full border border-consumer-border bg-consumer-surface-muted">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
alt=""
className="size-full object-cover"
decoding="async"
loading="lazy"
src={avatarSrc}
/>
</div>
) : (
<span className="size-9" />
)}
</div>
<div
className={cn(
'max-w-full px-3 py-2 text-sm',
isPrimary
? cn(
'bg-primary text-white',
isClusterStart && isClusterEnd
? 'rounded-2xl rounded-br-sm'
: isClusterStart
? 'rounded-2xl rounded-br-md rounded-bl-md'
: isClusterEnd
? 'rounded-2xl rounded-br-sm rounded-tl-md rounded-tr-md'
: 'rounded-md'
)
: cn(
'border border-consumer-border bg-consumer-surface text-consumer-text',
isClusterStart && isClusterEnd
? 'rounded-2xl rounded-bl-sm'
: isClusterStart
? 'rounded-2xl rounded-bl-md rounded-br-md'
: isClusterEnd
? 'rounded-2xl rounded-bl-sm rounded-tl-md rounded-tr-md'
: 'rounded-md'
)
)}
>
{showSenderName ? (
<div className="mb-1 leading-tight">
<p className={cn('text-sm font-bold', isPrimary ? 'text-white' : 'text-primary')}>{senderName}</p>
<p
className={cn('mt-1 text-[10px]', isPrimary ? 'text-primary-100' : 'text-default-400')}
dir="ltr"
>
{senderMeta}
</p>
</div>
) : null}
{message.body ? <p className="whitespace-pre-wrap break-words leading-relaxed">{message.body}</p> : null}
{message.imageUrl ? (
<Button
aria-label="مشاهده تصویر"
className="mt-1 h-auto min-h-0 w-full overflow-hidden rounded-lg p-0"
variant="light"
onClick={() => {
setPreviewImageUrl(message.imageUrl ?? null)
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
alt="پیوست"
className="max-h-52 w-full cursor-zoom-in rounded-lg object-cover"
decoding="async"
loading="lazy"
src={message.imageUrl}
/>
</Button>
) : null}
<div className={cn('mt-1 text-[10px]', isPrimary ? 'text-primary-100' : 'text-default-400')}>{formatTime(message.createdAt)}</div>
</div>
</div>
<ChatImagePreviewModal
imageUrl={previewImageUrl}
isOpen={Boolean(previewImageUrl)}
onOpenChange={(open) => {
if (!open) setPreviewImageUrl(null)
}}
/>
</div>
)
}
export default AdminMessageBubble