Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
132 lines
4.9 KiB
TypeScript
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
|