Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
140 lines
4.3 KiB
TypeScript
140 lines
4.3 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* ChatImagePreviewModal — full-screen-ish preview + download for chat images.
|
|
*
|
|
* Purpose
|
|
* - Opens from `MessageBubble`; uses ConsumerModal. Download URL resolution
|
|
* stays local (guess filename / trusted upload URL).
|
|
*/
|
|
|
|
import Image from 'next/image'
|
|
import { useState } from 'react'
|
|
|
|
import ConsumerButton from '@/components/consumer/ConsumerButton'
|
|
import CloseIcon from '@/components/icons/CloseIcon'
|
|
import CloudDownloadIcon from '@/components/icons/CloudDownloadIcon'
|
|
import ConsumerModal from '@/components/consumer/ConsumerModal'
|
|
import { withBasePath } from '@/constants/images'
|
|
import { texts } from '@/texts'
|
|
import { addToast } from '@/lib/toast'
|
|
|
|
interface ChatImagePreviewModalProps {
|
|
imageUrl: string | null
|
|
isOpen: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
}
|
|
|
|
const guessFileName = (imageUrl: string) => {
|
|
try {
|
|
const pathname = new URL(imageUrl, window.location.origin).pathname
|
|
const base = pathname.split('/').filter(Boolean).at(-1)
|
|
|
|
if (base && /\.[a-z0-9]{2,5}$/i.test(base)) return decodeURIComponent(base)
|
|
} catch {
|
|
// ignore invalid URL
|
|
}
|
|
|
|
return `chat-image-${Date.now()}.jpg`
|
|
}
|
|
|
|
const resolveDownloadUrl = (imageUrl: string) => {
|
|
try {
|
|
const target = new URL(imageUrl, window.location.origin)
|
|
|
|
if (target.origin === window.location.origin) return target.toString()
|
|
} catch {
|
|
return imageUrl
|
|
}
|
|
|
|
// Cross-origin file hosts (e.g. ghabilee.ir from localhost) often omit CORS
|
|
// for /uploads — proxy through Next so the browser sees a same-origin blob.
|
|
return `${withBasePath('/api/download-upload')}?url=${encodeURIComponent(imageUrl)}`
|
|
}
|
|
|
|
const ChatImagePreviewModal = ({ imageUrl, isOpen, onOpenChange }: ChatImagePreviewModalProps) => {
|
|
const [isDownloading, setIsDownloading] = useState(false)
|
|
|
|
const handleDownload = async () => {
|
|
if (!imageUrl || isDownloading) return
|
|
setIsDownloading(true)
|
|
|
|
try {
|
|
const response = await fetch(resolveDownloadUrl(imageUrl))
|
|
|
|
if (!response.ok) throw new Error('download failed')
|
|
|
|
const blob = await response.blob()
|
|
const objectUrl = URL.createObjectURL(blob)
|
|
const anchor = document.createElement('a')
|
|
|
|
anchor.href = objectUrl
|
|
anchor.download = guessFileName(imageUrl)
|
|
document.body.appendChild(anchor)
|
|
anchor.click()
|
|
anchor.remove()
|
|
URL.revokeObjectURL(objectUrl)
|
|
} catch {
|
|
window.open(imageUrl, '_blank', 'noopener,noreferrer')
|
|
addToast({ title: texts.chats.downloadFallbackToast, color: 'warning' })
|
|
} finally {
|
|
setIsDownloading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<ConsumerModal
|
|
hideCloseButton
|
|
hideFooter
|
|
hideHeader
|
|
backdrop="blur"
|
|
bodyClassName="!p-0"
|
|
className="!max-h-[95dvh] border-0 bg-transparent p-0 shadow-none"
|
|
containerClassName="bg-transparent"
|
|
isOpen={isOpen}
|
|
onOpenChange={onOpenChange}
|
|
>
|
|
<div className="relative flex min-h-[40vh] flex-col rounded-t-2xl bg-white">
|
|
<div className="absolute inset-x-0 top-0 z-10 flex items-center justify-between gap-2 p-3">
|
|
<ConsumerButton
|
|
iconOnly
|
|
aria-label={texts.common.close}
|
|
className="min-h-11 min-w-11 bg-consumer-surface-muted text-consumer-text hover:bg-consumer-surface-muted/80"
|
|
fill="none"
|
|
size="sm"
|
|
onClick={() => {
|
|
onOpenChange(false)
|
|
}}
|
|
>
|
|
<CloseIcon className="size-5" />
|
|
</ConsumerButton>
|
|
<ConsumerButton
|
|
aria-label={texts.chats.downloadImageAria}
|
|
className="min-h-11 min-w-11 bg-consumer-surface-muted text-consumer-text hover:bg-consumer-surface-muted/80"
|
|
fill="none"
|
|
iconStart={!isDownloading ? <CloudDownloadIcon className="size-4" /> : undefined}
|
|
isLoading={isDownloading}
|
|
onClick={() => void handleDownload()}
|
|
>
|
|
{texts.chats.download}
|
|
</ConsumerButton>
|
|
</div>
|
|
|
|
<div className="relative min-h-[40vh] w-full flex-1">
|
|
{imageUrl ? (
|
|
<Image
|
|
fill
|
|
alt={texts.chats.chatAttachmentAlt}
|
|
className="object-contain p-3 pt-14"
|
|
sizes="100vw"
|
|
src={imageUrl}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</ConsumerModal>
|
|
)
|
|
}
|
|
|
|
export default ChatImagePreviewModal
|