mirror of
https://github.com/kyantech/Palmr.git
synced 2025-10-23 06:11:58 +00:00
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.
This commit is contained in:
@@ -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<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [videoBlob, setVideoBlob] = useState<string | null>(null);
|
||||
const [pdfAsBlob, setPdfAsBlob] = useState(false);
|
||||
const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
|
||||
const [pdfLoadFailed, setPdfLoadFailed] = useState(false);
|
||||
const [isLoadingPreview, setIsLoadingPreview] = useState(false);
|
||||
const [textContent, setTextContent] = useState<string | null>(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 (
|
||||
<div className="flex flex-col items-center justify-center h-96 gap-4">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
||||
<p className="text-muted-foreground">{t("filePreview.loading")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const mediaUrl = fileType === "video" ? videoBlob : previewUrl;
|
||||
|
||||
if (!mediaUrl && (fileType === "video" || fileType === "audio")) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-96 gap-4">
|
||||
<FileIcon className={`h-12 w-12 ${color}`} />
|
||||
<p className="text-muted-foreground">{t("filePreview.notAvailable")}</p>
|
||||
<p className="text-sm text-muted-foreground">{t("filePreview.downloadToView")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// For text files, we don't need previewUrl, we use textContent instead
|
||||
if (fileType === "text" && !textContent && !isLoading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-96 gap-4">
|
||||
<FileIcon className={`h-12 w-12 ${color}`} />
|
||||
<p className="text-muted-foreground">{t("filePreview.notAvailable")}</p>
|
||||
<p className="text-sm text-muted-foreground">{t("filePreview.downloadToView")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!previewUrl && fileType !== "video" && fileType !== "text") {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-96 gap-4">
|
||||
<FileIcon className={`h-12 w-12 ${color}`} />
|
||||
<p className="text-muted-foreground">{t("filePreview.notAvailable")}</p>
|
||||
<p className="text-sm text-muted-foreground">{t("filePreview.downloadToView")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
switch (fileType) {
|
||||
case "pdf":
|
||||
return (
|
||||
<ScrollArea className="w-full">
|
||||
<div className="w-full min-h-[600px] border rounded-lg overflow-hidden bg-card">
|
||||
{pdfAsBlob ? (
|
||||
<iframe
|
||||
src={`${previewUrl!}#toolbar=0&navpanes=0&scrollbar=0&view=FitH`}
|
||||
className="w-full h-full min-h-[600px]"
|
||||
title={file.name}
|
||||
style={{ border: "none" }}
|
||||
/>
|
||||
) : pdfLoadFailed ? (
|
||||
<div className="flex items-center justify-center h-full min-h-[600px]">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
||||
<p className="text-muted-foreground">{t("filePreview.loadingAlternative")}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full h-full min-h-[600px] relative">
|
||||
<object
|
||||
data={`${previewUrl!}#toolbar=0&navpanes=0&scrollbar=0&view=FitH`}
|
||||
type="application/pdf"
|
||||
className="w-full h-full min-h-[600px]"
|
||||
onError={handlePdfLoadError}
|
||||
>
|
||||
<iframe
|
||||
src={`${previewUrl!}#toolbar=0&navpanes=0&scrollbar=0&view=FitH`}
|
||||
className="w-full h-full min-h-[600px]"
|
||||
title={file.name}
|
||||
style={{ border: "none" }}
|
||||
onError={handlePdfLoadError}
|
||||
/>
|
||||
</object>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
case "text":
|
||||
return (
|
||||
<ScrollArea className="w-full max-h-[600px]">
|
||||
<div className="w-full border rounded-lg overflow-hidden bg-card">
|
||||
{textContent ? (
|
||||
<pre className="p-4 text-sm font-mono whitespace-pre-wrap break-words overflow-x-auto">
|
||||
<code className={`language-${file.name.split(".").pop()?.toLowerCase() || "text"}`}>
|
||||
{textContent}
|
||||
</code>
|
||||
</pre>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-32">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary" />
|
||||
<p className="text-sm text-muted-foreground">{t("filePreview.loading")}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
case "image":
|
||||
return (
|
||||
<AspectRatio ratio={16 / 9} className="bg-muted">
|
||||
<img src={previewUrl!} alt={file.name} className="object-contain w-full h-full rounded-md" />
|
||||
</AspectRatio>
|
||||
);
|
||||
case "audio":
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-6 py-4">
|
||||
<CustomAudioPlayer src={mediaUrl!} />
|
||||
</div>
|
||||
);
|
||||
case "video":
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-4 py-6">
|
||||
<div className="w-full max-w-4xl">
|
||||
<video controls className="w-full rounded-lg" preload="metadata" style={{ maxHeight: "70vh" }}>
|
||||
<source src={mediaUrl!} />
|
||||
{t("filePreview.videoNotSupported")}
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-96 gap-4">
|
||||
<FileIcon className={`text-6xl ${color}`} />
|
||||
<p className="text-muted-foreground">{t("filePreview.notAvailable")}</p>
|
||||
<p className="text-sm text-muted-foreground">{t("filePreview.downloadToView")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
const previewState = useFilePreview({ file, isOpen });
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
@@ -467,12 +35,24 @@ export function FilePreviewModal({ isOpen, onClose, file }: FilePreviewModalProp
|
||||
<span className="truncate">{file.name}</span>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-auto">{renderPreview()}</div>
|
||||
<div className="flex-1 overflow-auto">
|
||||
<FilePreviewRenderer
|
||||
fileType={previewState.fileType}
|
||||
fileName={file.name}
|
||||
previewUrl={previewState.previewUrl}
|
||||
videoBlob={previewState.videoBlob}
|
||||
textContent={previewState.textContent}
|
||||
isLoading={previewState.isLoading}
|
||||
pdfAsBlob={previewState.pdfAsBlob}
|
||||
pdfLoadFailed={previewState.pdfLoadFailed}
|
||||
onPdfLoadError={previewState.handlePdfLoadError}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
{t("common.close")}
|
||||
</Button>
|
||||
<Button onClick={handleDownload}>
|
||||
<Button onClick={previewState.handleDownload}>
|
||||
<IconDownload className="h-4 w-4" />
|
||||
{t("common.download")}
|
||||
</Button>
|
||||
|
13
apps/web/src/components/modals/previews/audio-preview.tsx
Normal file
13
apps/web/src/components/modals/previews/audio-preview.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { CustomAudioPlayer } from "@/components/audio/custom-audio-player";
|
||||
|
||||
interface AudioPreviewProps {
|
||||
src: string;
|
||||
}
|
||||
|
||||
export function AudioPreview({ src }: AudioPreviewProps) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-6 py-4">
|
||||
<CustomAudioPlayer src={src} />
|
||||
</div>
|
||||
);
|
||||
}
|
31
apps/web/src/components/modals/previews/default-preview.tsx
Normal file
31
apps/web/src/components/modals/previews/default-preview.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { getFileIcon } from "@/utils/file-icons";
|
||||
|
||||
interface DefaultPreviewProps {
|
||||
fileName: string;
|
||||
isLoading?: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export function DefaultPreview({ fileName, isLoading, message }: DefaultPreviewProps) {
|
||||
const t = useTranslations();
|
||||
const { icon: FileIcon, color } = getFileIcon(fileName);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-96 gap-4">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
||||
<p className="text-muted-foreground">{t("filePreview.loading")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-96 gap-4">
|
||||
<FileIcon className={`h-12 w-12 ${color}`} />
|
||||
<p className="text-muted-foreground">{message || t("filePreview.notAvailable")}</p>
|
||||
<p className="text-sm text-muted-foreground">{t("filePreview.downloadToView")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -0,0 +1,77 @@
|
||||
import { type FileType } from "@/utils/file-types";
|
||||
import { AudioPreview } from "./audio-preview";
|
||||
import { DefaultPreview } from "./default-preview";
|
||||
import { ImagePreview } from "./image-preview";
|
||||
import { PdfPreview } from "./pdf-preview";
|
||||
import { TextPreview } from "./text-preview";
|
||||
import { VideoPreview } from "./video-preview";
|
||||
|
||||
interface FilePreviewRendererProps {
|
||||
fileType: FileType;
|
||||
fileName: string;
|
||||
previewUrl: string | null;
|
||||
videoBlob: string | null;
|
||||
textContent: string | null;
|
||||
isLoading: boolean;
|
||||
pdfAsBlob: boolean;
|
||||
pdfLoadFailed: boolean;
|
||||
onPdfLoadError: () => void;
|
||||
}
|
||||
|
||||
export function FilePreviewRenderer({
|
||||
fileType,
|
||||
fileName,
|
||||
previewUrl,
|
||||
videoBlob,
|
||||
textContent,
|
||||
isLoading,
|
||||
pdfAsBlob,
|
||||
pdfLoadFailed,
|
||||
onPdfLoadError,
|
||||
}: FilePreviewRendererProps) {
|
||||
if (isLoading) {
|
||||
return <DefaultPreview fileName={fileName} isLoading />;
|
||||
}
|
||||
|
||||
const mediaUrl = fileType === "video" ? videoBlob : previewUrl;
|
||||
|
||||
if (!mediaUrl && (fileType === "video" || fileType === "audio")) {
|
||||
return <DefaultPreview fileName={fileName} />;
|
||||
}
|
||||
|
||||
if (fileType === "text" && !textContent) {
|
||||
return <DefaultPreview fileName={fileName} />;
|
||||
}
|
||||
|
||||
if (!previewUrl && fileType !== "video" && fileType !== "text") {
|
||||
return <DefaultPreview fileName={fileName} />;
|
||||
}
|
||||
|
||||
switch (fileType) {
|
||||
case "pdf":
|
||||
return (
|
||||
<PdfPreview
|
||||
src={previewUrl!}
|
||||
fileName={fileName}
|
||||
pdfAsBlob={pdfAsBlob}
|
||||
pdfLoadFailed={pdfLoadFailed}
|
||||
onLoadError={onPdfLoadError}
|
||||
/>
|
||||
);
|
||||
|
||||
case "text":
|
||||
return <TextPreview content={textContent} fileName={fileName} />;
|
||||
|
||||
case "image":
|
||||
return <ImagePreview src={previewUrl!} alt={fileName} />;
|
||||
|
||||
case "audio":
|
||||
return <AudioPreview src={mediaUrl!} />;
|
||||
|
||||
case "video":
|
||||
return <VideoPreview src={mediaUrl!} />;
|
||||
|
||||
default:
|
||||
return <DefaultPreview fileName={fileName} />;
|
||||
}
|
||||
}
|
14
apps/web/src/components/modals/previews/image-preview.tsx
Normal file
14
apps/web/src/components/modals/previews/image-preview.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { AspectRatio } from "@/components/ui/aspect-ratio";
|
||||
|
||||
interface ImagePreviewProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
}
|
||||
|
||||
export function ImagePreview({ src, alt }: ImagePreviewProps) {
|
||||
return (
|
||||
<AspectRatio ratio={16 / 9} className="bg-muted">
|
||||
<img src={src} alt={alt} className="object-contain w-full h-full rounded-md" />
|
||||
</AspectRatio>
|
||||
);
|
||||
}
|
7
apps/web/src/components/modals/previews/index.ts
Normal file
7
apps/web/src/components/modals/previews/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export { ImagePreview } from "./image-preview";
|
||||
export { VideoPreview } from "./video-preview";
|
||||
export { AudioPreview } from "./audio-preview";
|
||||
export { PdfPreview } from "./pdf-preview";
|
||||
export { TextPreview } from "./text-preview";
|
||||
export { DefaultPreview } from "./default-preview";
|
||||
export { FilePreviewRenderer } from "./file-preview-render";
|
54
apps/web/src/components/modals/previews/pdf-preview.tsx
Normal file
54
apps/web/src/components/modals/previews/pdf-preview.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
|
||||
interface PdfPreviewProps {
|
||||
src: string;
|
||||
fileName: string;
|
||||
pdfAsBlob: boolean;
|
||||
pdfLoadFailed: boolean;
|
||||
onLoadError: () => void;
|
||||
}
|
||||
|
||||
export function PdfPreview({ src, fileName, pdfAsBlob, pdfLoadFailed, onLoadError }: PdfPreviewProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<ScrollArea className="w-full">
|
||||
<div className="w-full min-h-[600px] border rounded-lg overflow-hidden bg-card">
|
||||
{pdfAsBlob ? (
|
||||
<iframe
|
||||
src={`${src}#toolbar=0&navpanes=0&scrollbar=0&view=FitH`}
|
||||
className="w-full h-full min-h-[600px]"
|
||||
title={fileName}
|
||||
style={{ border: "none" }}
|
||||
/>
|
||||
) : pdfLoadFailed ? (
|
||||
<div className="flex items-center justify-center h-full min-h-[600px]">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
||||
<p className="text-muted-foreground">{t("filePreview.loadingAlternative")}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full h-full min-h-[600px] relative">
|
||||
<object
|
||||
data={`${src}#toolbar=0&navpanes=0&scrollbar=0&view=FitH`}
|
||||
type="application/pdf"
|
||||
className="w-full h-full min-h-[600px]"
|
||||
onError={onLoadError}
|
||||
>
|
||||
<iframe
|
||||
src={`${src}#toolbar=0&navpanes=0&scrollbar=0&view=FitH`}
|
||||
className="w-full h-full min-h-[600px]"
|
||||
title={fileName}
|
||||
style={{ border: "none" }}
|
||||
onError={onLoadError}
|
||||
/>
|
||||
</object>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
40
apps/web/src/components/modals/previews/text-preview.tsx
Normal file
40
apps/web/src/components/modals/previews/text-preview.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { getFileExtension } from "@/utils/file-types";
|
||||
|
||||
interface TextPreviewProps {
|
||||
content: string | null;
|
||||
fileName: string;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function TextPreview({ content, fileName, isLoading }: TextPreviewProps) {
|
||||
const t = useTranslations();
|
||||
const extension = getFileExtension(fileName);
|
||||
|
||||
if (isLoading || !content) {
|
||||
return (
|
||||
<ScrollArea className="w-full max-h-[600px]">
|
||||
<div className="w-full border rounded-lg overflow-hidden bg-card">
|
||||
<div className="flex items-center justify-center h-32">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary" />
|
||||
<p className="text-sm text-muted-foreground">{t("filePreview.loading")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollArea className="w-full max-h-[600px]">
|
||||
<div className="w-full border rounded-lg overflow-hidden bg-card">
|
||||
<pre className="p-4 text-sm font-mono whitespace-pre-wrap break-words overflow-x-auto">
|
||||
<code className={`language-${extension || "text"}`}>{content}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
20
apps/web/src/components/modals/previews/video-preview.tsx
Normal file
20
apps/web/src/components/modals/previews/video-preview.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface VideoPreviewProps {
|
||||
src: string;
|
||||
}
|
||||
|
||||
export function VideoPreview({ src }: VideoPreviewProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-4 py-6">
|
||||
<div className="w-full max-w-4xl">
|
||||
<video controls className="w-full rounded-lg" preload="metadata" style={{ maxHeight: "70vh" }}>
|
||||
<source src={src} />
|
||||
{t("filePreview.videoNotSupported")}
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
255
apps/web/src/hooks/use-file-preview.ts
Normal file
255
apps/web/src/hooks/use-file-preview.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { getDownloadUrl } from "@/http/endpoints";
|
||||
import { getFileExtension, getFileType, type FileType } from "@/utils/file-types";
|
||||
|
||||
interface FilePreviewState {
|
||||
previewUrl: string | null;
|
||||
videoBlob: string | null;
|
||||
textContent: string | null;
|
||||
downloadUrl: string | null;
|
||||
isLoading: boolean;
|
||||
isLoadingPreview: boolean;
|
||||
pdfAsBlob: boolean;
|
||||
pdfLoadFailed: boolean;
|
||||
}
|
||||
|
||||
interface UseFilePreviewProps {
|
||||
file: {
|
||||
name: string;
|
||||
objectName: string;
|
||||
type?: string;
|
||||
};
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export function useFilePreview({ file, isOpen }: UseFilePreviewProps) {
|
||||
const t = useTranslations();
|
||||
const [state, setState] = useState<FilePreviewState>({
|
||||
previewUrl: null,
|
||||
videoBlob: null,
|
||||
textContent: null,
|
||||
downloadUrl: null,
|
||||
isLoading: true,
|
||||
isLoadingPreview: false,
|
||||
pdfAsBlob: false,
|
||||
pdfLoadFailed: false,
|
||||
});
|
||||
|
||||
const fileType: FileType = getFileType(file.name);
|
||||
|
||||
// Reset state when file changes or modal opens
|
||||
useEffect(() => {
|
||||
if (isOpen && file.objectName && !state.isLoadingPreview) {
|
||||
resetState();
|
||||
loadPreview();
|
||||
}
|
||||
}, [file.objectName, isOpen]);
|
||||
|
||||
// Cleanup blob URLs
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cleanupBlobUrls();
|
||||
};
|
||||
}, [state.previewUrl, state.videoBlob]);
|
||||
|
||||
// Cleanup when modal closes
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
cleanupBlobUrls();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const resetState = () => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
previewUrl: null,
|
||||
videoBlob: null,
|
||||
textContent: null,
|
||||
downloadUrl: null,
|
||||
pdfAsBlob: false,
|
||||
pdfLoadFailed: false,
|
||||
isLoading: true,
|
||||
}));
|
||||
};
|
||||
|
||||
const cleanupBlobUrls = () => {
|
||||
if (state.previewUrl && state.previewUrl.startsWith("blob:")) {
|
||||
URL.revokeObjectURL(state.previewUrl);
|
||||
}
|
||||
if (state.videoBlob && state.videoBlob.startsWith("blob:")) {
|
||||
URL.revokeObjectURL(state.videoBlob);
|
||||
}
|
||||
};
|
||||
|
||||
const loadPreview = async () => {
|
||||
if (!file.objectName || state.isLoadingPreview) return;
|
||||
|
||||
setState((prev) => ({ ...prev, isLoadingPreview: true }));
|
||||
|
||||
try {
|
||||
const encodedObjectName = encodeURIComponent(file.objectName);
|
||||
const response = await getDownloadUrl(encodedObjectName);
|
||||
const url = response.data.url;
|
||||
|
||||
setState((prev) => ({ ...prev, downloadUrl: url }));
|
||||
|
||||
switch (fileType) {
|
||||
case "video":
|
||||
await loadVideoPreview(url);
|
||||
break;
|
||||
case "audio":
|
||||
await loadAudioPreview(url);
|
||||
break;
|
||||
case "pdf":
|
||||
await loadPdfPreview(url);
|
||||
break;
|
||||
case "text":
|
||||
await loadTextPreview(url);
|
||||
break;
|
||||
default:
|
||||
setState((prev) => ({ ...prev, previewUrl: url }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load preview:", error);
|
||||
toast.error(t("filePreview.loadError"));
|
||||
} finally {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
isLoadingPreview: 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);
|
||||
setState((prev) => ({ ...prev, videoBlob: blobUrl }));
|
||||
} catch (error) {
|
||||
console.error("Failed to load video as blob:", error);
|
||||
setState((prev) => ({ ...prev, previewUrl: 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);
|
||||
setState((prev) => ({ ...prev, previewUrl: blobUrl }));
|
||||
} catch (error) {
|
||||
console.error("Failed to load audio as blob:", error);
|
||||
setState((prev) => ({ ...prev, previewUrl: 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);
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
previewUrl: blobUrl,
|
||||
pdfAsBlob: true,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("Failed to load PDF as blob:", error);
|
||||
setState((prev) => ({ ...prev, previewUrl: url }));
|
||||
setTimeout(() => {
|
||||
if (!state.pdfLoadFailed && !state.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 = getFileExtension(file.name);
|
||||
|
||||
try {
|
||||
// For JSON files, validate and format
|
||||
if (extension === "json") {
|
||||
const parsed = JSON.parse(text);
|
||||
const formatted = JSON.stringify(parsed, null, 2);
|
||||
setState((prev) => ({ ...prev, textContent: formatted }));
|
||||
} else {
|
||||
// For other text files, show as-is
|
||||
setState((prev) => ({ ...prev, textContent: text }));
|
||||
}
|
||||
} catch (jsonError) {
|
||||
// If JSON parsing fails, show as plain text
|
||||
setState((prev) => ({ ...prev, textContent: text }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load text content:", error);
|
||||
setState((prev) => ({ ...prev, textContent: null }));
|
||||
}
|
||||
};
|
||||
|
||||
const handlePdfLoadError = async () => {
|
||||
if (state.pdfLoadFailed || state.pdfAsBlob) return;
|
||||
|
||||
setState((prev) => ({ ...prev, pdfLoadFailed: true }));
|
||||
|
||||
if (state.downloadUrl) {
|
||||
setTimeout(() => {
|
||||
loadPdfPreview(state.downloadUrl!);
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
let downloadUrlToUse = state.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);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...state,
|
||||
fileType,
|
||||
handleDownload,
|
||||
handlePdfLoadError,
|
||||
};
|
||||
}
|
@@ -1,5 +1,30 @@
|
||||
import {
|
||||
Icon,
|
||||
IconApi,
|
||||
IconAtom,
|
||||
IconBook,
|
||||
IconBrandCss3,
|
||||
IconBrandDocker,
|
||||
IconBrandGit,
|
||||
IconBrandGolang,
|
||||
IconBrandHtml5,
|
||||
IconBrandJavascript,
|
||||
IconBrandKotlin,
|
||||
IconBrandNpm,
|
||||
IconBrandPhp,
|
||||
IconBrandPython,
|
||||
IconBrandReact,
|
||||
IconBrandRust,
|
||||
IconBrandSass,
|
||||
IconBrandSwift,
|
||||
IconBrandTypescript,
|
||||
IconBrandVue,
|
||||
IconBrandYarn,
|
||||
IconBug,
|
||||
IconCloud,
|
||||
IconCode,
|
||||
IconDatabase,
|
||||
IconDeviceDesktop,
|
||||
IconFile,
|
||||
IconFileCode,
|
||||
IconFileDescription,
|
||||
@@ -8,8 +33,16 @@ import {
|
||||
IconFileText,
|
||||
IconFileTypePdf,
|
||||
IconFileZip,
|
||||
IconKey,
|
||||
IconLock,
|
||||
IconMarkdown,
|
||||
IconMath,
|
||||
IconPalette,
|
||||
IconPhoto,
|
||||
IconPresentation,
|
||||
IconSettings,
|
||||
IconTerminal,
|
||||
IconTool,
|
||||
IconVideo,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
@@ -20,56 +53,452 @@ interface FileIconMapping {
|
||||
}
|
||||
|
||||
const fileIcons: FileIconMapping[] = [
|
||||
// Images
|
||||
{
|
||||
extensions: ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"],
|
||||
extensions: ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg", "tiff", "ico", "heic", "avif"],
|
||||
icon: IconPhoto,
|
||||
color: "text-blue-500",
|
||||
},
|
||||
|
||||
// Documents
|
||||
{
|
||||
extensions: ["pdf"],
|
||||
icon: IconFileTypePdf,
|
||||
color: "text-red-500",
|
||||
},
|
||||
{
|
||||
extensions: ["doc", "docx"],
|
||||
extensions: ["doc", "docx", "odt", "rtf"],
|
||||
icon: IconFileText,
|
||||
color: "text-blue-600",
|
||||
},
|
||||
{
|
||||
extensions: ["xls", "xlsx", "csv"],
|
||||
extensions: ["xls", "xlsx", "ods", "csv"],
|
||||
icon: IconFileSpreadsheet,
|
||||
color: "text-green-600",
|
||||
},
|
||||
{
|
||||
extensions: ["ppt", "pptx"],
|
||||
extensions: ["ppt", "pptx", "odp"],
|
||||
icon: IconPresentation,
|
||||
color: "text-orange-500",
|
||||
},
|
||||
|
||||
// Media
|
||||
{
|
||||
extensions: ["mp3", "wav", "ogg", "m4a"],
|
||||
extensions: ["mp3", "wav", "ogg", "m4a", "aac", "flac", "wma", "opus"],
|
||||
icon: IconFileMusic,
|
||||
color: "text-purple-500",
|
||||
},
|
||||
{
|
||||
extensions: ["mp4", "avi", "mov", "wmv", "mkv"],
|
||||
extensions: ["mp4", "avi", "mov", "wmv", "mkv", "webm", "flv", "m4v", "3gp"],
|
||||
icon: IconVideo,
|
||||
color: "text-pink-500",
|
||||
},
|
||||
|
||||
// Archives
|
||||
{
|
||||
extensions: ["zip", "rar", "7z", "tar", "gz"],
|
||||
extensions: ["zip", "rar", "7z", "tar", "gz", "bz2", "xz", "lz", "cab", "deb", "rpm"],
|
||||
icon: IconFileZip,
|
||||
color: "text-yellow-600",
|
||||
},
|
||||
|
||||
// JavaScript/TypeScript
|
||||
{
|
||||
extensions: ["html", "css", "js", "ts", "jsx", "tsx", "json", "xml"],
|
||||
icon: IconFileCode,
|
||||
extensions: ["js", "mjs", "cjs"],
|
||||
icon: IconBrandJavascript,
|
||||
color: "text-yellow-500",
|
||||
},
|
||||
{
|
||||
extensions: ["ts", "tsx"],
|
||||
icon: IconBrandTypescript,
|
||||
color: "text-blue-600",
|
||||
},
|
||||
{
|
||||
extensions: ["jsx"],
|
||||
icon: IconBrandReact,
|
||||
color: "text-cyan-500",
|
||||
},
|
||||
{
|
||||
extensions: ["vue"],
|
||||
icon: IconBrandVue,
|
||||
color: "text-green-500",
|
||||
},
|
||||
|
||||
// Web Technologies
|
||||
{
|
||||
extensions: ["html", "htm", "xhtml"],
|
||||
icon: IconBrandHtml5,
|
||||
color: "text-orange-600",
|
||||
},
|
||||
{
|
||||
extensions: ["css"],
|
||||
icon: IconBrandCss3,
|
||||
color: "text-blue-600",
|
||||
},
|
||||
{
|
||||
extensions: ["scss", "sass"],
|
||||
icon: IconBrandSass,
|
||||
color: "text-pink-600",
|
||||
},
|
||||
{
|
||||
extensions: ["less", "stylus"],
|
||||
icon: IconPalette,
|
||||
color: "text-purple-600",
|
||||
},
|
||||
|
||||
// Programming Languages
|
||||
{
|
||||
extensions: ["py", "pyw", "pyc", "pyo", "pyd"],
|
||||
icon: IconBrandPython,
|
||||
color: "text-yellow-600",
|
||||
},
|
||||
{
|
||||
extensions: ["php", "phtml"],
|
||||
icon: IconBrandPhp,
|
||||
color: "text-purple-700",
|
||||
},
|
||||
{
|
||||
extensions: ["go"],
|
||||
icon: IconBrandGolang,
|
||||
color: "text-cyan-600",
|
||||
},
|
||||
{
|
||||
extensions: ["rs"],
|
||||
icon: IconBrandRust,
|
||||
color: "text-orange-700",
|
||||
},
|
||||
{
|
||||
extensions: ["swift"],
|
||||
icon: IconBrandSwift,
|
||||
color: "text-orange-500",
|
||||
},
|
||||
{
|
||||
extensions: ["kt", "kts"],
|
||||
icon: IconBrandKotlin,
|
||||
color: "text-purple-600",
|
||||
},
|
||||
{
|
||||
extensions: ["java", "class", "jar"],
|
||||
icon: IconCode,
|
||||
color: "text-red-600",
|
||||
},
|
||||
{
|
||||
extensions: ["c", "h"],
|
||||
icon: IconCode,
|
||||
color: "text-blue-700",
|
||||
},
|
||||
{
|
||||
extensions: ["cpp", "cxx", "cc", "hpp", "hxx"],
|
||||
icon: IconCode,
|
||||
color: "text-blue-800",
|
||||
},
|
||||
{
|
||||
extensions: ["cs"],
|
||||
icon: IconCode,
|
||||
color: "text-purple-700",
|
||||
},
|
||||
{
|
||||
extensions: ["rb", "rbw", "rake"],
|
||||
icon: IconCode,
|
||||
color: "text-red-500",
|
||||
},
|
||||
{
|
||||
extensions: ["scala", "sc"],
|
||||
icon: IconCode,
|
||||
color: "text-red-700",
|
||||
},
|
||||
{
|
||||
extensions: ["clj", "cljs", "cljc", "edn"],
|
||||
icon: IconCode,
|
||||
color: "text-green-700",
|
||||
},
|
||||
{
|
||||
extensions: ["hs", "lhs"],
|
||||
icon: IconCode,
|
||||
color: "text-purple-800",
|
||||
},
|
||||
{
|
||||
extensions: ["elm"],
|
||||
icon: IconCode,
|
||||
color: "text-blue-700",
|
||||
},
|
||||
{
|
||||
extensions: ["dart"],
|
||||
icon: IconCode,
|
||||
color: "text-blue-600",
|
||||
},
|
||||
{
|
||||
extensions: ["lua"],
|
||||
icon: IconCode,
|
||||
color: "text-blue-800",
|
||||
},
|
||||
{
|
||||
extensions: ["r", "rmd"],
|
||||
icon: IconMath,
|
||||
color: "text-blue-700",
|
||||
},
|
||||
{
|
||||
extensions: ["matlab", "m"],
|
||||
icon: IconMath,
|
||||
color: "text-orange-600",
|
||||
},
|
||||
{
|
||||
extensions: ["julia", "jl"],
|
||||
icon: IconMath,
|
||||
color: "text-purple-600",
|
||||
},
|
||||
|
||||
// Shell Scripts
|
||||
{
|
||||
extensions: ["sh", "bash", "zsh", "fish"],
|
||||
icon: IconTerminal,
|
||||
color: "text-green-600",
|
||||
},
|
||||
{
|
||||
extensions: ["ps1", "psm1", "psd1"],
|
||||
icon: IconTerminal,
|
||||
color: "text-blue-700",
|
||||
},
|
||||
{
|
||||
extensions: ["bat", "cmd"],
|
||||
icon: IconTerminal,
|
||||
color: "text-gray-600",
|
||||
},
|
||||
|
||||
// Database
|
||||
{
|
||||
extensions: ["sql", "mysql", "pgsql", "sqlite", "db"],
|
||||
icon: IconDatabase,
|
||||
color: "text-blue-700",
|
||||
},
|
||||
|
||||
// Configuration Files
|
||||
{
|
||||
extensions: ["json", "json5"],
|
||||
icon: IconCode,
|
||||
color: "text-yellow-700",
|
||||
},
|
||||
{
|
||||
extensions: ["yaml", "yml"],
|
||||
icon: IconSettings,
|
||||
color: "text-purple-600",
|
||||
},
|
||||
{
|
||||
extensions: ["toml"],
|
||||
icon: IconSettings,
|
||||
color: "text-orange-600",
|
||||
},
|
||||
{
|
||||
extensions: ["xml", "xsd", "xsl", "xslt"],
|
||||
icon: IconCode,
|
||||
color: "text-orange-700",
|
||||
},
|
||||
{
|
||||
extensions: ["ini", "cfg", "conf", "config"],
|
||||
icon: IconSettings,
|
||||
color: "text-gray-600",
|
||||
},
|
||||
{
|
||||
extensions: ["txt", "md", "rtf"],
|
||||
extensions: ["env", "dotenv"],
|
||||
icon: IconKey,
|
||||
color: "text-green-700",
|
||||
},
|
||||
{
|
||||
extensions: ["properties"],
|
||||
icon: IconSettings,
|
||||
color: "text-blue-600",
|
||||
},
|
||||
|
||||
// Docker & DevOps
|
||||
{
|
||||
extensions: ["dockerfile", "containerfile"],
|
||||
icon: IconBrandDocker,
|
||||
color: "text-blue-600",
|
||||
},
|
||||
{
|
||||
extensions: ["tf", "tfvars", "hcl"],
|
||||
icon: IconCloud,
|
||||
color: "text-purple-600",
|
||||
},
|
||||
{
|
||||
extensions: ["k8s", "kubernetes"],
|
||||
icon: IconCloud,
|
||||
color: "text-blue-700",
|
||||
},
|
||||
{
|
||||
extensions: ["ansible", "playbook"],
|
||||
icon: IconTool,
|
||||
color: "text-red-600",
|
||||
},
|
||||
|
||||
// Package Managers
|
||||
{
|
||||
extensions: ["package"],
|
||||
icon: IconBrandNpm,
|
||||
color: "text-red-600",
|
||||
},
|
||||
{
|
||||
extensions: ["yarn"],
|
||||
icon: IconBrandYarn,
|
||||
color: "text-blue-600",
|
||||
},
|
||||
{
|
||||
extensions: ["cargo"],
|
||||
icon: IconBrandRust,
|
||||
color: "text-orange-700",
|
||||
},
|
||||
{
|
||||
extensions: ["gemfile"],
|
||||
icon: IconCode,
|
||||
color: "text-red-500",
|
||||
},
|
||||
{
|
||||
extensions: ["composer"],
|
||||
icon: IconBrandPhp,
|
||||
color: "text-purple-700",
|
||||
},
|
||||
{
|
||||
extensions: ["requirements", "pipfile", "poetry"],
|
||||
icon: IconBrandPython,
|
||||
color: "text-yellow-600",
|
||||
},
|
||||
{
|
||||
extensions: ["gradle", "build.gradle"],
|
||||
icon: IconTool,
|
||||
color: "text-green-700",
|
||||
},
|
||||
{
|
||||
extensions: ["pom"],
|
||||
icon: IconCode,
|
||||
color: "text-orange-600",
|
||||
},
|
||||
{
|
||||
extensions: ["makefile", "cmake"],
|
||||
icon: IconTool,
|
||||
color: "text-blue-700",
|
||||
},
|
||||
|
||||
// Git
|
||||
{
|
||||
extensions: ["gitignore", "gitattributes", "gitmodules", "gitconfig"],
|
||||
icon: IconBrandGit,
|
||||
color: "text-orange-600",
|
||||
},
|
||||
|
||||
// Documentation
|
||||
{
|
||||
extensions: ["md", "markdown"],
|
||||
icon: IconMarkdown,
|
||||
color: "text-gray-700",
|
||||
},
|
||||
{
|
||||
extensions: ["rst", "txt"],
|
||||
icon: IconFileDescription,
|
||||
color: "text-gray-500",
|
||||
},
|
||||
{
|
||||
extensions: ["adoc", "asciidoc"],
|
||||
icon: IconBook,
|
||||
color: "text-blue-600",
|
||||
},
|
||||
{
|
||||
extensions: ["tex", "latex"],
|
||||
icon: IconMath,
|
||||
color: "text-green-700",
|
||||
},
|
||||
{
|
||||
extensions: ["log"],
|
||||
icon: IconBug,
|
||||
color: "text-yellow-600",
|
||||
},
|
||||
|
||||
// Templates
|
||||
{
|
||||
extensions: ["hbs", "handlebars", "mustache"],
|
||||
icon: IconCode,
|
||||
color: "text-orange-600",
|
||||
},
|
||||
{
|
||||
extensions: ["twig"],
|
||||
icon: IconCode,
|
||||
color: "text-green-600",
|
||||
},
|
||||
{
|
||||
extensions: ["liquid"],
|
||||
icon: IconCode,
|
||||
color: "text-blue-600",
|
||||
},
|
||||
{
|
||||
extensions: ["ejs", "pug", "jade"],
|
||||
icon: IconCode,
|
||||
color: "text-brown-600",
|
||||
},
|
||||
|
||||
// Data Formats
|
||||
{
|
||||
extensions: ["graphql", "gql"],
|
||||
icon: IconApi,
|
||||
color: "text-pink-600",
|
||||
},
|
||||
{
|
||||
extensions: ["proto", "protobuf"],
|
||||
icon: IconApi,
|
||||
color: "text-blue-700",
|
||||
},
|
||||
|
||||
// Security & Certificates
|
||||
{
|
||||
extensions: ["pem", "crt", "cer", "key", "p12", "pfx"],
|
||||
icon: IconLock,
|
||||
color: "text-green-800",
|
||||
},
|
||||
|
||||
// Web Assembly
|
||||
{
|
||||
extensions: ["wasm", "wat"],
|
||||
icon: IconAtom,
|
||||
color: "text-purple-700",
|
||||
},
|
||||
|
||||
// Shaders
|
||||
{
|
||||
extensions: ["glsl", "hlsl", "vert", "frag", "geom"],
|
||||
icon: IconDeviceDesktop,
|
||||
color: "text-cyan-700",
|
||||
},
|
||||
|
||||
// Specialized
|
||||
{
|
||||
extensions: ["vim", "vimrc"],
|
||||
icon: IconCode,
|
||||
color: "text-green-800",
|
||||
},
|
||||
{
|
||||
extensions: ["eslintrc", "prettierrc", "babelrc"],
|
||||
icon: IconSettings,
|
||||
color: "text-yellow-700",
|
||||
},
|
||||
{
|
||||
extensions: ["tsconfig", "jsconfig"],
|
||||
icon: IconSettings,
|
||||
color: "text-blue-700",
|
||||
},
|
||||
{
|
||||
extensions: ["webpack", "rollup", "vite"],
|
||||
icon: IconTool,
|
||||
color: "text-cyan-600",
|
||||
},
|
||||
{
|
||||
extensions: ["lock", "sum"],
|
||||
icon: IconLock,
|
||||
color: "text-gray-600",
|
||||
},
|
||||
|
||||
// Fallback for general text/code files
|
||||
{
|
||||
extensions: ["svelte", "astro", "erb", "haml", "slim"],
|
||||
icon: IconFileCode,
|
||||
color: "text-gray-600",
|
||||
},
|
||||
];
|
||||
|
||||
export function getFileIcon(filename: string): { icon: Icon; color: string } {
|
||||
|
374
apps/web/src/utils/file-types.ts
Normal file
374
apps/web/src/utils/file-types.ts
Normal file
@@ -0,0 +1,374 @@
|
||||
export type FileType = "pdf" | "image" | "audio" | "video" | "text" | "other";
|
||||
|
||||
export function getFileType(fileName: string): FileType {
|
||||
const extension = fileName.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";
|
||||
}
|
||||
|
||||
const textExtensions = [
|
||||
// Data formats
|
||||
"json",
|
||||
"json5",
|
||||
"jsonp",
|
||||
"txt",
|
||||
"csv",
|
||||
"xml",
|
||||
"svg",
|
||||
"toml",
|
||||
"yaml",
|
||||
"yml",
|
||||
"ini",
|
||||
"conf",
|
||||
"config",
|
||||
"env",
|
||||
"properties",
|
||||
|
||||
// Documentation
|
||||
"md",
|
||||
"markdown",
|
||||
"adoc",
|
||||
"asciidoc",
|
||||
"rst",
|
||||
"textile",
|
||||
"wiki",
|
||||
"log",
|
||||
|
||||
// Web technologies
|
||||
"html",
|
||||
"htm",
|
||||
"xhtml",
|
||||
"css",
|
||||
"scss",
|
||||
"sass",
|
||||
"less",
|
||||
"stylus",
|
||||
|
||||
// JavaScript ecosystem
|
||||
"js",
|
||||
"jsx",
|
||||
"ts",
|
||||
"tsx",
|
||||
"mjs",
|
||||
"cjs",
|
||||
"vue",
|
||||
"svelte",
|
||||
"coffee",
|
||||
"coffeescript",
|
||||
|
||||
// Programming languages
|
||||
"php",
|
||||
"py",
|
||||
"pyw",
|
||||
"rb",
|
||||
"java",
|
||||
"kt",
|
||||
"kts",
|
||||
"scala",
|
||||
"clj",
|
||||
"cljs",
|
||||
"cljc",
|
||||
"hs",
|
||||
"elm",
|
||||
"f#",
|
||||
"fs",
|
||||
"fsx",
|
||||
"vb",
|
||||
"vba",
|
||||
"c",
|
||||
"cpp",
|
||||
"cxx",
|
||||
"cc",
|
||||
"h",
|
||||
"hpp",
|
||||
"hxx",
|
||||
"cs",
|
||||
"go",
|
||||
"rs",
|
||||
"swift",
|
||||
"dart",
|
||||
"r",
|
||||
"rmd",
|
||||
"pl",
|
||||
"pm",
|
||||
|
||||
// Shell scripts
|
||||
"sh",
|
||||
"bash",
|
||||
"zsh",
|
||||
"fish",
|
||||
"ps1",
|
||||
"bat",
|
||||
"cmd",
|
||||
|
||||
// Database
|
||||
"sql",
|
||||
"plsql",
|
||||
"psql",
|
||||
"mysql",
|
||||
"sqlite",
|
||||
|
||||
// Configuration files
|
||||
"dockerfile",
|
||||
"containerfile",
|
||||
"gitignore",
|
||||
"gitattributes",
|
||||
"gitmodules",
|
||||
"gitconfig",
|
||||
"editorconfig",
|
||||
"eslintrc",
|
||||
"prettierrc",
|
||||
"stylelintrc",
|
||||
"babelrc",
|
||||
"browserslistrc",
|
||||
"tsconfig",
|
||||
"jsconfig",
|
||||
"webpack",
|
||||
"rollup",
|
||||
"vite",
|
||||
"astro",
|
||||
|
||||
// Package managers
|
||||
"package",
|
||||
"composer",
|
||||
"gemfile",
|
||||
"podfile",
|
||||
"pipfile",
|
||||
"poetry",
|
||||
"pyproject",
|
||||
"requirements",
|
||||
"cargo",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"sbt",
|
||||
"build.gradle",
|
||||
"build.sbt",
|
||||
"pom",
|
||||
"build",
|
||||
|
||||
// Build tools
|
||||
"makefile",
|
||||
"cmake",
|
||||
"rakefile",
|
||||
"gradle",
|
||||
"gulpfile",
|
||||
"gruntfile",
|
||||
"justfile",
|
||||
|
||||
// Templates
|
||||
"hbs",
|
||||
"handlebars",
|
||||
"mustache",
|
||||
"twig",
|
||||
"jinja",
|
||||
"jinja2",
|
||||
"liquid",
|
||||
"ejs",
|
||||
"pug",
|
||||
"jade",
|
||||
|
||||
// Data serialization
|
||||
"proto",
|
||||
"protobuf",
|
||||
"avro",
|
||||
"thrift",
|
||||
"graphql",
|
||||
"gql",
|
||||
|
||||
// Markup & styling
|
||||
"tex",
|
||||
"latex",
|
||||
"bibtex",
|
||||
"rtf",
|
||||
"org",
|
||||
"pod",
|
||||
|
||||
// Specialized formats
|
||||
"vim",
|
||||
"vimrc",
|
||||
"tmux",
|
||||
"nginx",
|
||||
"apache",
|
||||
"htaccess",
|
||||
"robots",
|
||||
"sitemap",
|
||||
"webmanifest",
|
||||
"lock",
|
||||
"sum",
|
||||
"mod",
|
||||
"workspace",
|
||||
"solution",
|
||||
"sln",
|
||||
"csproj",
|
||||
"vcxproj",
|
||||
"xcodeproj",
|
||||
|
||||
// Additional programming languages
|
||||
"lua",
|
||||
"rb",
|
||||
"php",
|
||||
"asp",
|
||||
"aspx",
|
||||
"jsp",
|
||||
"erb",
|
||||
"haml",
|
||||
"slim",
|
||||
"perl",
|
||||
"awk",
|
||||
"sed",
|
||||
"tcl",
|
||||
"groovy",
|
||||
"scala",
|
||||
"rust",
|
||||
"zig",
|
||||
"nim",
|
||||
"crystal",
|
||||
"julia",
|
||||
"matlab",
|
||||
"octave",
|
||||
"wolfram",
|
||||
"mathematica",
|
||||
"sage",
|
||||
"maxima",
|
||||
"fortran",
|
||||
"cobol",
|
||||
"ada",
|
||||
"pascal",
|
||||
"delphi",
|
||||
"basic",
|
||||
"vb6",
|
||||
"assembly",
|
||||
"asm",
|
||||
"s",
|
||||
"nasm",
|
||||
"gas",
|
||||
"lisp",
|
||||
"scheme",
|
||||
"racket",
|
||||
"clojure",
|
||||
"erlang",
|
||||
"elixir",
|
||||
"haskell",
|
||||
"ocaml",
|
||||
"fsharp",
|
||||
"prolog",
|
||||
"mercury",
|
||||
"curry",
|
||||
"clean",
|
||||
"idris",
|
||||
"agda",
|
||||
"coq",
|
||||
"lean",
|
||||
"smalltalk",
|
||||
"forth",
|
||||
"factor",
|
||||
"postscript",
|
||||
"tcl",
|
||||
"tk",
|
||||
"expect",
|
||||
"applescript",
|
||||
"powershell",
|
||||
"autohotkey",
|
||||
"ahk",
|
||||
"autoit",
|
||||
"nsis",
|
||||
|
||||
// Web assembly and low level
|
||||
"wasm",
|
||||
"wat",
|
||||
"wast",
|
||||
"wit",
|
||||
"wai",
|
||||
|
||||
// Shaders
|
||||
"glsl",
|
||||
"hlsl",
|
||||
"cg",
|
||||
"fx",
|
||||
"fxh",
|
||||
"vsh",
|
||||
"fsh",
|
||||
"vert",
|
||||
"frag",
|
||||
"geom",
|
||||
"tesc",
|
||||
"tese",
|
||||
"comp",
|
||||
|
||||
// Game development
|
||||
"gdscript",
|
||||
"gd",
|
||||
"cs",
|
||||
"boo",
|
||||
"unityscript",
|
||||
"mel",
|
||||
"maxscript",
|
||||
"haxe",
|
||||
"as",
|
||||
"actionscript",
|
||||
|
||||
// DevOps & Infrastructure
|
||||
"tf",
|
||||
"tfvars",
|
||||
"hcl",
|
||||
"nomad",
|
||||
"consul",
|
||||
"vault",
|
||||
"packer",
|
||||
"ansible",
|
||||
"puppet",
|
||||
"chef",
|
||||
"salt",
|
||||
"k8s",
|
||||
"kubernetes",
|
||||
"helm",
|
||||
"kustomize",
|
||||
"skaffold",
|
||||
"tilt",
|
||||
"buildkite",
|
||||
"circleci",
|
||||
"travis",
|
||||
"jenkins",
|
||||
"github",
|
||||
"gitlab",
|
||||
"bitbucket",
|
||||
"azure",
|
||||
"aws",
|
||||
"gcp",
|
||||
"terraform",
|
||||
"cloudformation",
|
||||
|
||||
// Documentation generators
|
||||
"jsdoc",
|
||||
"javadoc",
|
||||
"godoc",
|
||||
"rustdoc",
|
||||
"sphinx",
|
||||
"mkdocs",
|
||||
"gitbook",
|
||||
"jekyll",
|
||||
"hugo",
|
||||
"gatsby",
|
||||
];
|
||||
|
||||
if (textExtensions.includes(extension || "")) {
|
||||
return "text";
|
||||
}
|
||||
|
||||
return "other";
|
||||
}
|
||||
|
||||
export function getFileExtension(fileName: string): string {
|
||||
return fileName.split(".").pop()?.toLowerCase() || "";
|
||||
}
|
Reference in New Issue
Block a user