From 17537249fdede12a3482e7a396dc04f3ffecc85f Mon Sep 17 00:00:00 2001 From: VPS 4 Deploy Agent Date: Mon, 11 May 2026 08:07:34 +0200 Subject: [PATCH] =?UTF-8?q?fix(inbox):=20FilePreviewModal=20via=20createPo?= =?UTF-8?q?rtal=20=E2=80=94=20full-screen=20real=20+=20zoom=20+=20melhoria?= =?UTF-8?q?s=20visuais?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../frontend/components/FilePreviewModal.tsx | 263 ++++++++++-------- 1 file changed, 148 insertions(+), 115 deletions(-) diff --git a/clube67/newwhats.local/frontend/components/FilePreviewModal.tsx b/clube67/newwhats.local/frontend/components/FilePreviewModal.tsx index bfe28ed..baaea09 100644 --- a/clube67/newwhats.local/frontend/components/FilePreviewModal.tsx +++ b/clube67/newwhats.local/frontend/components/FilePreviewModal.tsx @@ -1,8 +1,9 @@ 'use client'; import React, { useEffect, useRef, useState, useCallback } from 'react'; +import { createPortal } from 'react-dom'; import { motion, AnimatePresence } from 'framer-motion'; -import { X, Send, FileText, Film, Music, Archive, File } from 'lucide-react'; +import { X, Send, FileText, Film, Music, Archive, File, ZoomIn, ZoomOut } from 'lucide-react'; interface FilePreviewModalProps { file: File | null; @@ -17,160 +18,192 @@ function formatFileSize(bytes: number): string { } function getFileIcon(type: string) { - if (type.startsWith('video/')) return ; - if (type.startsWith('audio/')) return ; + if (type.startsWith('video/')) return ; + if (type.startsWith('audio/')) return ; if (type.includes('pdf') || type.includes('document') || type.includes('word')) - return ; + return ; if (type.includes('zip') || type.includes('compressed') || type.includes('tar')) - return ; - return ; + return ; + return ; } function getFileLabel(type: string): string { if (type.startsWith('image/')) return 'Imagem'; if (type.startsWith('video/')) return 'Vídeo'; if (type.startsWith('audio/')) return 'Áudio'; - if (type.includes('pdf')) return 'PDF'; + if (type.includes('pdf')) return 'PDF'; return 'Arquivo'; } -export default function FilePreviewModal({ file, onConfirm, onCancel }: FilePreviewModalProps) { - const [caption, setCaption] = useState(''); - const [sending, setSending] = useState(false); +function ModalContent({ file, onConfirm, onCancel }: FilePreviewModalProps & { file: File }) { + const [caption, setCaption] = useState(''); + const [sending, setSending] = useState(false); const [objectUrl, setObjectUrl] = useState(null); + const [zoomed, setZoomed] = useState(false); const captionRef = useRef(null); - const isImage = file?.type.startsWith('image/'); - const isVideo = file?.type.startsWith('video/'); + const isImage = file.type.startsWith('image/'); + const isVideo = file.type.startsWith('video/'); - // Create / revoke object URL useEffect(() => { - if (!file) { setObjectUrl(null); return; } const url = URL.createObjectURL(file); setObjectUrl(url); setCaption(''); setSending(false); - setTimeout(() => captionRef.current?.focus(), 200); + setZoomed(false); + setTimeout(() => captionRef.current?.focus(), 250); return () => URL.revokeObjectURL(url); }, [file]); - // ESC cancels useEffect(() => { - const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onCancel(); }; + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') onCancel(); + }; document.addEventListener('keydown', onKey); return () => document.removeEventListener('keydown', onKey); }, [onCancel]); const handleSend = useCallback(async () => { - if (!file || sending) return; + if (sending) return; setSending(true); - try { - await onConfirm(file, caption.trim()); - } finally { - setSending(false); - } + try { await onConfirm(file, caption.trim()); } + finally { setSending(false); } }, [file, caption, sending, onConfirm]); const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - handleSend(); - } + if (e.key === 'Enter') { e.preventDefault(); handleSend(); } }; return ( - - {file && ( - + {/* ── Top bar ── */} +
+ - - {file ? getFileLabel(file.type) : ''} - -
{/* spacer */} -
+ + - {/* ── Preview area ── */} -
- {isImage && objectUrl && ( - - )} +
+

{file.name}

+

{getFileLabel(file.type)} · {formatFileSize(file.size)}

+
- {isVideo && objectUrl && ( - - )} + {isImage && ( + + )} +
- {!isImage && !isVideo && file && ( - - {getFileIcon(file.type)} -
-

- {file.name} -

-

{formatFileSize(file.size)}

-
-
- )} -
+ {/* ── Preview area ── */} +
isImage && setZoomed(z => !z)} + style={{ cursor: isImage ? (zoomed ? 'zoom-out' : 'zoom-in') : 'default' }} + > + {isImage && objectUrl && ( + + )} - {/* ── Caption + Send bar ── */} -
- setCaption(e.target.value)} - onKeyDown={handleKeyDown} - disabled={sending} - className="flex-1 bg-[#2a3942] text-[#e9edef] placeholder:text-[#8696a0] text-sm rounded-full px-4 py-2.5 outline-none border-none focus:ring-0 disabled:opacity-50" - /> - -
- - )} - + {isVideo && objectUrl && ( + + )} + + {!isImage && !isVideo && ( + +
+ {getFileIcon(file.type)} +
+
+

{file.name}

+

{formatFileSize(file.size)}

+
+
+ )} +
+ + {/* ── Caption + Send bar ── */} +
+ setCaption(e.target.value)} + onKeyDown={handleKeyDown} + disabled={sending} + maxLength={1024} + className="flex-1 bg-[#2a3942] text-[#e9edef] placeholder:text-[#8696a0] text-sm rounded-full px-5 py-2.5 outline-none border-none focus:ring-0 disabled:opacity-50 transition-opacity" + /> + +
+
+ ); +} + +export default function FilePreviewModal({ file, onConfirm, onCancel }: FilePreviewModalProps) { + const [mounted, setMounted] = useState(false); + useEffect(() => { setMounted(true); }, []); + if (!mounted) return null; + + return createPortal( + + {file && ( + + )} + , + document.body ); }