import React, { useEffect, useRef, useState } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { Card, CardContent, CardHeader, CardTitle, CardDescription, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Plus, File as FileIcon, FileText, Image as ImageIcon, Trash2, Download, Edit3 as EditIcon, } from "lucide-react"; import { apiRequest } from "@/lib/queryClient"; import type { CloudFile } from "@repo/db/types"; import { MultipleFileUploadZone } from "@/components/file-upload/multiple-file-upload-zone"; import { DeleteConfirmationDialog } from "@/components/ui/deleteDialog"; import { Menu, Item, contextMenu } from "react-contexify"; import "react-contexify/dist/ReactContexify.css"; import { useAuth } from "@/hooks/use-auth"; import { Pagination, PaginationContent, PaginationItem, PaginationPrevious, PaginationLink, PaginationNext, } from "@/components/ui/pagination"; import { getPageNumbers } from "@/utils/pageNumberGenerator"; import { NewFolderModal } from "./new-folder-modal"; import { toast } from "@/hooks/use-toast"; import FilePreviewModal from "./file-preview-modal"; export type FilesSectionProps = { parentId: number | null; pageSize?: number; className?: string; onFileOpen?: (fileId: number) => void; }; const FILES_LIMIT_DEFAULT = 20; const MAX_FILE_MB = 10; const MAX_FILE_BYTES = MAX_FILE_MB * 1024 * 1024; function fileIcon(mime?: string) { if (!mime) return ; if (mime.startsWith("image/")) return ; if (mime === "application/pdf" || mime.endsWith("/pdf")) return ; return ; } export default function FilesSection({ parentId, pageSize = FILES_LIMIT_DEFAULT, className, onFileOpen, }: FilesSectionProps) { const qc = useQueryClient(); const { user } = useAuth(); const userId = user?.id; const [data, setData] = useState([]); const [total, setTotal] = useState(0); const [currentPage, setCurrentPage] = useState(1); const [isLoading, setIsLoading] = useState(false); // upload modal and ref const [uploading, setUploading] = useState(false); const [isUploadOpen, setIsUploadOpen] = useState(false); const uploadRef = useRef(null); // rename/delete const [isRenameOpen, setIsRenameOpen] = useState(false); const [renameTargetId, setRenameTargetId] = useState(null); const [renameInitial, setRenameInitial] = useState(""); // delete dialog const [isDeleteOpen, setIsDeleteOpen] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); // preview modal const [isPreviewOpen, setIsPreviewOpen] = useState(false); const [previewFileId, setPreviewFileId] = useState(null); useEffect(() => { loadPage(currentPage); // eslint-disable-next-line react-hooks/exhaustive-deps }, [parentId, currentPage]); async function loadPage(page: number) { setIsLoading(true); try { const offset = (page - 1) * pageSize; const fid = parentId === null ? "null" : String(parentId); const res = await apiRequest( "GET", `/api/cloud-storage/items/files?parentId=${encodeURIComponent( fid )}&limit=${pageSize}&offset=${offset}` ); const json = await res.json(); if (!res.ok) throw new Error(json?.message || "Failed to load files"); const rows: CloudFile[] = Array.isArray(json.data) ? json.data : []; setData(rows); const t = typeof json.totalCount === "number" ? json.totalCount : typeof json.total === "number" ? json.total : rows.length; setTotal(t); } catch (err: any) { setData([]); setTotal(0); toast({ title: "Failed to load files", description: err?.message ?? String(err), variant: "destructive", }); } finally { setIsLoading(false); } } function showMenu(e: React.MouseEvent, file: CloudFile) { e.preventDefault(); e.stopPropagation(); contextMenu.show({ id: "files-section-menu", event: e.nativeEvent, props: { file }, }); } // rename function openRename(file: CloudFile) { setRenameTargetId(Number(file.id)); setRenameInitial(file.name ?? ""); setIsRenameOpen(true); contextMenu.hideAll(); } async function submitRename(newName: string) { if (!renameTargetId) return; try { const res = await apiRequest( "PUT", `/api/cloud-storage/files/${renameTargetId}`, { name: newName } ); const json = await res.json(); if (!res.ok) throw new Error(json?.message || "Rename failed"); setIsRenameOpen(false); setRenameTargetId(null); toast({ title: "File renamed" }); loadPage(currentPage); qc.invalidateQueries({ queryKey: ["/api/cloud-storage/folders/recent", 1], }); } catch (err: any) { toast({ title: "Rename failed", description: err?.message ?? String(err), variant: "destructive", }); } } // delete function openDelete(file: CloudFile) { setDeleteTarget(file); setIsDeleteOpen(true); contextMenu.hideAll(); } async function confirmDelete() { if (!deleteTarget) return; try { const res = await apiRequest( "DELETE", `/api/cloud-storage/files/${deleteTarget.id}` ); const json = await res.json(); if (!res.ok) throw new Error(json?.message || "Delete failed"); setIsDeleteOpen(false); setDeleteTarget(null); toast({ title: "File deleted" }); // reload current page (ensure page index valid) loadPage(currentPage); qc.invalidateQueries({ queryKey: ["/api/cloud-storage/folders/recent", 1], }); } catch (err: any) { toast({ title: "Delete failed", description: err?.message ?? String(err), variant: "destructive", }); } } // download (context menu) - (fetch bytes from backend host via wrapper) async function handleDownload(file: CloudFile) { try { const res = await apiRequest( "GET", `/api/cloud-storage/files/${file.id}/download` ); if (!res.ok) { const j = await res.json().catch(() => ({})); throw new Error(j?.message || `Download failed (${res.status})`); } const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = file.name ?? `file-${file.id}`; document.body.appendChild(a); a.click(); a.remove(); // revoke after a bit setTimeout(() => URL.revokeObjectURL(url), 5000); contextMenu.hideAll(); } catch (err: any) { toast({ title: "Download failed", description: err?.message ?? String(err), variant: "destructive", }); } } // upload: get files from MultipleFileUploadZone (imperative handle) async function handleUploadSubmit() { const files: File[] = uploadRef.current?.getFiles?.() ?? []; if (!files.length) { toast({ title: "No files selected", description: "Please choose files to upload before clicking Upload.", variant: "destructive", }); return; } setUploading(true); // pre-check all files and show errors / skip too-large files const oversized = files.filter((f) => f.size > MAX_FILE_BYTES); if (oversized.length) { oversized.slice(0, 5).forEach((f) => toast({ title: "File too large", description: `${f.name} is ${Math.round(f.size / 1024 / 1024)} MB — max ${MAX_FILE_MB} MB allowed.`, variant: "destructive", }) ); // Remove oversized files from the upload list (upload the rest) } const toUpload = files.filter((f) => f.size <= MAX_FILE_BYTES); if (toUpload.length === 0) { // nothing to upload return; } try { for (const f of toUpload) { const fid = parentId === null ? "null" : String(parentId); const initRes = await apiRequest( "POST", `/api/cloud-storage/folders/${encodeURIComponent(fid)}/files`, { userId, name: f.name, mimeType: f.type || null, expectedSize: f.size, totalChunks: 1, } ); const initJson = await initRes.json(); const created = initJson?.data; if (!created || typeof created.id !== "number") throw new Error("Init failed"); const raw = await f.arrayBuffer(); // upload chunk await apiRequest( "POST", `/api/cloud-storage/files/${created.id}/chunks?seq=0`, raw ); // finalize await apiRequest( "POST", `/api/cloud-storage/files/${created.id}/complete`, {} ); toast({ title: "Upload complete", description: f.name }); } setIsUploadOpen(false); loadPage(currentPage); qc.invalidateQueries({ queryKey: ["/api/cloud-storage/folders/recent", 1], }); } catch (err: any) { toast({ title: "Upload failed", description: err?.message ?? String(err), variant: "destructive", }); } finally { setUploading(false); } } // Pagination const totalPages = Math.max(1, Math.ceil(total / pageSize)); const startItem = total === 0 ? 0 : (currentPage - 1) * pageSize + 1; const endItem = Math.min(total, currentPage * pageSize); // open preview (single click) function openPreview(file: CloudFile) { setPreviewFileId(Number(file.id)); setIsPreviewOpen(true); contextMenu.hideAll(); } return (
Files Manage Files in this folder
{isLoading ? (
Loading...
) : ( <>
{data.map((file) => (
showMenu(e, file)} onClick={() => openPreview(file)} >
{fileIcon((file as any).mimeType)}
{file.name}
{((file as any).fileSize ?? 0).toString()} bytes
))}
{/* pagination */} {totalPages > 1 && (
Showing {startItem}–{endItem} of {total} results
{ e.preventDefault(); setCurrentPage((p) => Math.max(1, p - 1)); }} className={ currentPage === 1 ? "pointer-events-none opacity-50" : "" } /> {getPageNumbers(currentPage, totalPages).map( (page, idx) => ( {page === "..." ? ( ... ) : ( { e.preventDefault(); setCurrentPage(page as number); }} isActive={currentPage === page} > {page} )} ) )} { e.preventDefault(); setCurrentPage((p) => Math.min(totalPages, p + 1)); }} className={ currentPage === totalPages ? "pointer-events-none opacity-50" : "" } />
)} )}
{/* context menu */} openRename(props.file)}> Rename handleDownload(props.file)}> Download openDelete(props.file)}> Delete {/* upload modal using MultipleFileUploadZone (imperative handle) */} {isUploadOpen && (

Upload files

)} {/* rename modal (reusing NewFolderModal for simplicity) */} { setIsRenameOpen(false); setRenameTargetId(null); }} onSubmit={submitRename} /> {/* FIle Preview Modal */} { setIsPreviewOpen(false); setPreviewFileId(null); }} /> {/* delete confirm */} { setIsDeleteOpen(false); setDeleteTarget(null); }} onConfirm={confirmDelete} />
); }