Files
clube67_newwhats.local/clube67/newwhats.local/frontend/components/MediaViewer.tsx
T

261 lines
12 KiB
TypeScript

'use client';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { createPortal } from 'react-dom';
import {
X,
Download,
Reply,
Star,
ZoomIn,
ZoomOut,
ChevronLeft,
ChevronRight,
Video,
} from 'lucide-react';
import { Message } from '../types/inboxTypes';
import { getMediaUrl } from '../utils/inboxUtils';
import { format } from 'date-fns';
import { parseSafeDate } from '../utils/inboxUtils';
interface MediaViewerProps {
msgs: Message[]
currentMsgId: string
onClose: () => void
onReply?: (msg: Message) => void
onStar?: (msg: Message) => void
}
const MediaViewer: React.FC<MediaViewerProps> = ({ msgs, currentMsgId, onClose, onReply, onStar }) => {
const [currentIdx, setCurrentIdx] = useState(() => {
const idx = msgs.findIndex(m => m.message_id === currentMsgId)
return idx >= 0 ? idx : 0
})
const [zoom, setZoom] = useState(1)
const filmstripRef = useRef<HTMLDivElement>(null)
const currentMsg = msgs[currentIdx]
const currentUrl = getMediaUrl(currentMsg?.media_url)
const goNext = useCallback(() => {
setCurrentIdx(i => Math.min(i + 1, msgs.length - 1))
setZoom(1)
}, [msgs.length])
const goPrev = useCallback(() => {
setCurrentIdx(i => Math.max(i - 1, 0))
setZoom(1)
}, [])
// Keyboard: ← → Esc
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
if (e.key === 'ArrowRight') goNext()
if (e.key === 'ArrowLeft') goPrev()
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}, [onClose, goNext, goPrev])
// Scroll filmstrip to keep current thumbnail visible
useEffect(() => {
const el = filmstripRef.current
if (!el) return
const thumb = el.children[currentIdx] as HTMLElement
if (thumb) thumb.scrollIntoView({ inline: 'center', block: 'nearest', behavior: 'smooth' })
}, [currentIdx])
if (!currentMsg) return null
const isVideo = currentMsg.type === 'video'
const isImage = currentMsg.type === 'image'
const caption = currentMsg.text?.trim() || null
const timeStr = currentMsg.timestamp ? format(parseSafeDate(currentMsg.timestamp), 'dd/MM/yyyy HH:mm') : ''
const senderLabel = currentMsg.direction === 'out' ? 'Você' : 'Contato'
const viewer = (
<div className="fixed inset-0 z-[9999] flex flex-col bg-[#111b21]">
{/* ── Top bar ─────────────────────────────────────────── */}
<div className="flex items-center justify-between px-2 py-1.5 bg-[#202c33] shrink-0">
{/* Left: back + sender info */}
<div className="flex items-center gap-2">
<button
onClick={onClose}
className="p-2 text-[#aebac1] hover:text-white rounded-full hover:bg-white/10 transition-colors"
title="Fechar"
>
<ChevronLeft className="w-5 h-5" />
</button>
<div className="leading-tight">
<div className="text-white text-[13.5px] font-medium">{senderLabel}</div>
<div className="text-[#aebac1] text-[11px]">{timeStr}</div>
</div>
</div>
{/* Right: actions */}
<div className="flex items-center">
{isImage && (
<>
<button
onClick={() => setZoom(z => Math.min(z + 0.3, 4))}
className="p-2 text-[#aebac1] hover:text-white rounded-full hover:bg-white/10 transition-colors"
title="Aumentar zoom"
>
<ZoomIn className="w-5 h-5" />
</button>
<button
onClick={() => setZoom(z => Math.max(z - 0.3, 0.4))}
className="p-2 text-[#aebac1] hover:text-white rounded-full hover:bg-white/10 transition-colors"
title="Diminuir zoom"
>
<ZoomOut className="w-5 h-5" />
</button>
</>
)}
<button
onClick={() => { onReply?.(currentMsg); onClose(); }}
className="p-2 text-[#aebac1] hover:text-white rounded-full hover:bg-white/10 transition-colors"
title="Responder"
>
<Reply className="w-5 h-5" />
</button>
<button
onClick={() => onStar?.(currentMsg)}
className="p-2 text-[#aebac1] hover:text-white rounded-full hover:bg-white/10 transition-colors"
title="Favoritar"
>
<Star className="w-5 h-5" />
</button>
{currentUrl && (
<a
href={currentUrl}
download
className="p-2 text-[#aebac1] hover:text-white rounded-full hover:bg-white/10 transition-colors"
title="Baixar"
>
<Download className="w-5 h-5" />
</a>
)}
<button
onClick={onClose}
className="p-2 text-[#aebac1] hover:text-white rounded-full hover:bg-white/10 transition-colors"
title="Fechar"
>
<X className="w-5 h-5" />
</button>
</div>
</div>
{/* ── Main content ─────────────────────────────────────── */}
<div
className="flex-1 flex items-center justify-center relative overflow-hidden"
onClick={onClose}
>
{/* Left arrow */}
{currentIdx > 0 && (
<button
onClick={e => { e.stopPropagation(); goPrev() }}
className="absolute left-4 z-10 w-11 h-11 bg-black/50 hover:bg-black/80 text-white rounded-full flex items-center justify-center transition-colors shadow-lg"
>
<ChevronLeft className="w-6 h-6" />
</button>
)}
{/* Media */}
<div className="max-w-full max-h-full flex items-center justify-center" onClick={e => e.stopPropagation()}>
{!currentUrl ? (
<div className="text-[#aebac1] text-sm">Mídia não disponível</div>
) : isVideo ? (
<video
src={currentUrl}
controls
autoPlay
className="max-w-full rounded"
style={{ maxHeight: 'calc(100vh - 160px)' }}
/>
) : (
<img
src={currentUrl}
alt={caption || 'Imagem'}
draggable={false}
className="object-contain select-none rounded"
style={{
maxWidth: '100%',
maxHeight: 'calc(100vh - 160px)',
transform: `scale(${zoom})`,
transition: 'transform 0.18s ease',
cursor: zoom > 1 ? 'zoom-out' : 'default',
}}
onClick={e => { e.stopPropagation(); if (zoom > 1) setZoom(1) }}
/>
)}
</div>
{/* Right arrow */}
{currentIdx < msgs.length - 1 && (
<button
onClick={e => { e.stopPropagation(); goNext() }}
className="absolute right-4 z-10 w-11 h-11 bg-black/50 hover:bg-black/80 text-white rounded-full flex items-center justify-center transition-colors shadow-lg"
>
<ChevronRight className="w-6 h-6" />
</button>
)}
{/* Caption */}
{caption && (
<div className="absolute bottom-2 inset-x-0 flex justify-center pointer-events-none">
<span className="bg-black/60 text-white text-[13px] px-4 py-1.5 rounded-lg max-w-[70%] text-center leading-snug">
{caption}
</span>
</div>
)}
</div>
{/* ── Bottom filmstrip ─────────────────────────────────── */}
{msgs.length > 1 && (
<div className="shrink-0 bg-[#202c33] py-2 px-3">
<div
ref={filmstripRef}
className="flex gap-1.5 overflow-x-auto items-center"
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
>
{msgs.map((m, i) => {
const url = getMediaUrl(m.media_url)
const isVid = m.type === 'video'
const isActive = i === currentIdx
return (
<button
key={m.message_id}
onClick={() => { setCurrentIdx(i); setZoom(1) }}
className={`relative shrink-0 w-[58px] h-[58px] rounded overflow-hidden border-2 transition-all ${
isActive
? 'border-white opacity-100'
: 'border-transparent opacity-50 hover:opacity-80'
}`}
>
{url ? (
isVid ? (
<div className="w-full h-full bg-black flex items-center justify-center">
<Video className="w-5 h-5 text-white fill-white" />
</div>
) : (
<img src={url} alt="" className="w-full h-full object-cover" />
)
) : (
<div className="w-full h-full bg-[#2a3942]" />
)}
</button>
)
})}
</div>
</div>
)}
</div>
)
return typeof document !== 'undefined' ? createPortal(viewer, document.body) : null
}
export default MediaViewer