From 0d346b75cc673ef95b6a2af996f47bfadaec9c66 Mon Sep 17 00:00:00 2001 From: Daniel Luiz Alves Date: Fri, 20 Jun 2025 15:37:00 -0300 Subject: [PATCH] refactor: simplify FilePreviewModal by utilizing useFilePreview hook - Replaced complex state management and effect hooks in FilePreviewModal with a custom useFilePreview hook for improved readability and maintainability. - Integrated FilePreviewRenderer component to handle different file types and rendering logic, enhancing the modularity of the code. - Updated file icon mappings in file-icons.tsx to include additional file types and improve visual representation in the UI. --- .../components/modals/file-preview-modal.tsx | 454 +----------------- .../modals/previews/audio-preview.tsx | 13 + .../modals/previews/default-preview.tsx | 31 ++ .../modals/previews/file-preview-render.tsx | 77 +++ .../modals/previews/image-preview.tsx | 14 + .../src/components/modals/previews/index.ts | 7 + .../modals/previews/pdf-preview.tsx | 54 +++ .../modals/previews/text-preview.tsx | 40 ++ .../modals/previews/video-preview.tsx | 20 + apps/web/src/hooks/use-file-preview.ts | 255 ++++++++++ apps/web/src/utils/file-icons.tsx | 449 ++++++++++++++++- apps/web/src/utils/file-types.ts | 374 +++++++++++++++ 12 files changed, 1341 insertions(+), 447 deletions(-) create mode 100644 apps/web/src/components/modals/previews/audio-preview.tsx create mode 100644 apps/web/src/components/modals/previews/default-preview.tsx create mode 100644 apps/web/src/components/modals/previews/file-preview-render.tsx create mode 100644 apps/web/src/components/modals/previews/image-preview.tsx create mode 100644 apps/web/src/components/modals/previews/index.ts create mode 100644 apps/web/src/components/modals/previews/pdf-preview.tsx create mode 100644 apps/web/src/components/modals/previews/text-preview.tsx create mode 100644 apps/web/src/components/modals/previews/video-preview.tsx create mode 100644 apps/web/src/hooks/use-file-preview.ts create mode 100644 apps/web/src/utils/file-types.ts diff --git a/apps/web/src/components/modals/file-preview-modal.tsx b/apps/web/src/components/modals/file-preview-modal.tsx index eec3e6f..426e18c 100644 --- a/apps/web/src/components/modals/file-preview-modal.tsx +++ b/apps/web/src/components/modals/file-preview-modal.tsx @@ -1,17 +1,13 @@ "use client"; -import { useEffect, useState } from "react"; import { IconDownload } from "@tabler/icons-react"; import { useTranslations } from "next-intl"; -import { toast } from "sonner"; -import { CustomAudioPlayer } from "@/components/audio/custom-audio-player"; -import { AspectRatio } from "@/components/ui/aspect-ratio"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { getDownloadUrl } from "@/http/endpoints"; +import { useFilePreview } from "@/hooks/use-file-preview"; import { getFileIcon } from "@/utils/file-icons"; +import { FilePreviewRenderer } from "./previews"; interface FilePreviewModalProps { isOpen: boolean; @@ -25,435 +21,7 @@ interface FilePreviewModalProps { export function FilePreviewModal({ isOpen, onClose, file }: FilePreviewModalProps) { const t = useTranslations(); - const [previewUrl, setPreviewUrl] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [videoBlob, setVideoBlob] = useState(null); - const [pdfAsBlob, setPdfAsBlob] = useState(false); - const [downloadUrl, setDownloadUrl] = useState(null); - const [pdfLoadFailed, setPdfLoadFailed] = useState(false); - const [isLoadingPreview, setIsLoadingPreview] = useState(false); - const [textContent, setTextContent] = useState(null); - - useEffect(() => { - if (isOpen && file.objectName && !isLoadingPreview) { - setIsLoading(true); - setPreviewUrl(null); - setVideoBlob(null); - setPdfAsBlob(false); - setDownloadUrl(null); - setPdfLoadFailed(false); - setTextContent(null); - loadPreview(); - } - }, [file.objectName, isOpen]); - - useEffect(() => { - return () => { - if (previewUrl && previewUrl.startsWith("blob:")) { - URL.revokeObjectURL(previewUrl); - } - if (videoBlob && videoBlob.startsWith("blob:")) { - URL.revokeObjectURL(videoBlob); - } - }; - }, [previewUrl, videoBlob]); - - useEffect(() => { - if (!isOpen) { - if (previewUrl && previewUrl.startsWith("blob:")) { - URL.revokeObjectURL(previewUrl); - setPreviewUrl(null); - } - if (videoBlob && videoBlob.startsWith("blob:")) { - URL.revokeObjectURL(videoBlob); - setVideoBlob(null); - } - } - }, [isOpen]); - - const loadPreview = async () => { - if (!file.objectName || isLoadingPreview) return; - - setIsLoadingPreview(true); - try { - const encodedObjectName = encodeURIComponent(file.objectName); - const response = await getDownloadUrl(encodedObjectName); - const url = response.data.url; - - setDownloadUrl(url); - - const fileType = getFileType(); - - if (fileType === "video") { - await loadVideoPreview(url); - } else if (fileType === "audio") { - await loadAudioPreview(url); - } else if (fileType === "pdf") { - await loadPdfPreview(url); - } else if (fileType === "text") { - await loadTextPreview(url); - } else { - setPreviewUrl(url); - } - } catch (error) { - console.error("Failed to load preview:", error); - toast.error(t("filePreview.loadError")); - } finally { - setIsLoading(false); - setIsLoadingPreview(false); - } - }; - - const loadVideoPreview = async (url: string) => { - try { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const blob = await response.blob(); - const blobUrl = URL.createObjectURL(blob); - setVideoBlob(blobUrl); - } catch (error) { - console.error("Failed to load video as blob:", error); - setPreviewUrl(url); - } - }; - - const loadAudioPreview = async (url: string) => { - try { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const blob = await response.blob(); - const blobUrl = URL.createObjectURL(blob); - setPreviewUrl(blobUrl); - } catch (error) { - console.error("Failed to load audio as blob:", error); - setPreviewUrl(url); - } - }; - - const loadPdfPreview = async (url: string) => { - try { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const blob = await response.blob(); - const finalBlob = new Blob([blob], { type: "application/pdf" }); - const blobUrl = URL.createObjectURL(finalBlob); - setPreviewUrl(blobUrl); - setPdfAsBlob(true); - } catch (error) { - console.error("Failed to load PDF as blob:", error); - setPreviewUrl(url); - setTimeout(() => { - if (!pdfLoadFailed && !pdfAsBlob) { - handlePdfLoadError(); - } - }, 4000); - } - }; - - const loadTextPreview = async (url: string) => { - try { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const text = await response.text(); - const extension = file.name.split(".").pop()?.toLowerCase(); - - try { - // For JSON files, validate and format - if (extension === "json") { - const parsed = JSON.parse(text); - const formatted = JSON.stringify(parsed, null, 2); - setTextContent(formatted); - } else { - // For other text files, show as-is - setTextContent(text); - } - } catch (jsonError) { - // If JSON parsing fails, show as plain text - setTextContent(text); - } - } catch (error) { - console.error("Failed to load text content:", error); - setTextContent(null); - } - }; - - const handlePdfLoadError = async () => { - if (pdfLoadFailed || pdfAsBlob) return; - - setPdfLoadFailed(true); - - if (downloadUrl) { - setTimeout(() => { - loadPdfPreview(downloadUrl); - }, 500); - } - }; - - const handleDownload = async () => { - try { - let downloadUrlToUse = downloadUrl; - - if (!downloadUrlToUse) { - const encodedObjectName = encodeURIComponent(file.objectName); - const response = await getDownloadUrl(encodedObjectName); - downloadUrlToUse = response.data.url; - } - - const link = document.createElement("a"); - link.href = downloadUrlToUse; - link.download = file.name; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - } catch (error) { - toast.error(t("filePreview.downloadError")); - console.error("Download error:", error); - } - }; - - const getFileType = () => { - const extension = file.name.split(".").pop()?.toLowerCase(); - - if (extension === "pdf") return "pdf"; - if (["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "tiff"].includes(extension || "")) return "image"; - if (["mp3", "wav", "ogg", "m4a", "aac", "flac"].includes(extension || "")) return "audio"; - if (["mp4", "webm", "ogg", "mov", "avi", "mkv", "wmv", "flv", "m4v"].includes(extension || "")) return "video"; - - // Text and code files - if ( - [ - "json", - "txt", - "md", - "markdown", - "log", - "csv", - "xml", - "html", - "htm", - "css", - "scss", - "sass", - "less", - "js", - "jsx", - "ts", - "tsx", - "vue", - "svelte", - "php", - "py", - "rb", - "java", - "c", - "cpp", - "h", - "hpp", - "cs", - "go", - "rs", - "kt", - "swift", - "dart", - "scala", - "clj", - "hs", - "elm", - "f#", - "vb", - "pl", - "r", - "sql", - "sh", - "bash", - "zsh", - "fish", - "ps1", - "bat", - "cmd", - "dockerfile", - "yaml", - "yml", - "toml", - "ini", - "conf", - "config", - "env", - "gitignore", - "gitattributes", - "editorconfig", - "eslintrc", - "prettierrc", - "babelrc", - "tsconfig", - "package", - "composer", - "gemfile", - "requirements", - "makefile", - "rakefile", - "gradle", - "pom", - "build", - ].includes(extension || "") - ) - return "text"; - - return "other"; - }; - - const renderPreview = () => { - const fileType = getFileType(); - const { icon: FileIcon, color } = getFileIcon(file.name); - - if (isLoading) { - return ( -
-
-

{t("filePreview.loading")}

-
- ); - } - - const mediaUrl = fileType === "video" ? videoBlob : previewUrl; - - if (!mediaUrl && (fileType === "video" || fileType === "audio")) { - return ( -
- -

{t("filePreview.notAvailable")}

-

{t("filePreview.downloadToView")}

-
- ); - } - - // For text files, we don't need previewUrl, we use textContent instead - if (fileType === "text" && !textContent && !isLoading) { - return ( -
- -

{t("filePreview.notAvailable")}

-

{t("filePreview.downloadToView")}

-
- ); - } - - if (!previewUrl && fileType !== "video" && fileType !== "text") { - return ( -
- -

{t("filePreview.notAvailable")}

-

{t("filePreview.downloadToView")}

-
- ); - } - - switch (fileType) { - case "pdf": - return ( - -
- {pdfAsBlob ? ( -