"use client"; import type React from "react"; import { useState, useCallback, useRef, useEffect } from "react"; import useSWR, { mutate } from "swr"; import { toast } from "sonner"; import { OnlineUsers } from "@/components/online-users"; import { usePresence } from "@/hooks/use-presence"; import { Folder, File, Upload, FolderPlus, Download, Trash2, ChevronRight, Home, Loader2, Search, Grid, List, MoreVertical, CheckSquare, Square, X, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { cn } from "@/lib/utils"; import Image from "next/image"; interface FileItem { id: string; name: string; originalName: string; size: number; compressedSize: number; type: string; folderId: string | null; createdAt: string; } interface FolderItem { id: string; name: string; parentId: string | null; createdAt: string; } const fetcher = (url: string) => fetch(url).then((res) => res.json()); function formatBytes(bytes: number): string { if (bytes === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return ( Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i] ); } function getFileIcon(name: string) { const extension = name.split(".").pop() || ""; const extensionMap = { image: ["png", "jpg", "jpeg", "gif", "webp", "svg"], video: ["mp4", "webm", "ogg", "avi", "mkv"], audio: ["mp3", "wav", "ogg", "flac", "aac"], pdf: ["pdf"], zip: ["zip", "rar", "7z"], text: ["txt", "md", "csv", "json", "xml"], }; if (extensionMap.image.includes(extension)) return "🖼️"; if (extensionMap.video.includes(extension)) return "🎬"; if (extensionMap.audio.includes(extension)) return "🎵"; if (extensionMap.pdf.includes(extension)) return "📄"; if (extensionMap.zip.includes(extension)) return "📦"; if (extensionMap.text.includes(extension)) return "📝"; return "📎"; } export function FileExplorer() { const [currentFolderId, setCurrentFolderId] = useState(null); const [isUploading, setIsUploading] = useState(false); const [newFolderName, setNewFolderName] = useState(""); const [folderDialogOpen, setFolderDialogOpen] = useState(false); const [dragActive, setDragActive] = useState(false); const [viewMode, setViewMode] = useState<"grid" | "list">("grid"); const [searchQuery, setSearchQuery] = useState(""); const fileInputRef = useRef(null); const [selectedFiles, setSelectedFiles] = useState>(new Set()); const [isDownloadingBulk, setIsDownloadingBulk] = useState(false); const { notifyFileUpload, notifyFileDelete, notifyFolderCreate, notifyFolderDelete, socket, } = usePresence(); const { data: files = [], isLoading: filesLoading } = useSWR( `/api/files?folderId=${currentFolderId || ""}`, fetcher ); const { data: folders = [], isLoading: foldersLoading } = useSWR< FolderItem[] >(`/api/folders?parentId=${currentFolderId || ""}`, fetcher); const { data: breadcrumbs = [] } = useSWR( currentFolderId ? `/api/folders?breadcrumbs=${currentFolderId}` : null, fetcher ); const refreshData = useCallback(() => { mutate(`/api/files?folderId=${currentFolderId || ""}`); mutate(`/api/folders?parentId=${currentFolderId || ""}`); }, [currentFolderId]); useEffect(() => { if (!socket) return; const handleFileAdded = () => { refreshData(); toast.info("A file was uploaded by another user"); }; const handleFileRemoved = () => { refreshData(); toast.info("A file was deleted by another user"); }; const handleFolderAdded = () => { refreshData(); toast.info("A folder was created by another user"); }; const handleFolderRemoved = () => { refreshData(); toast.info("A folder was deleted by another user"); }; socket.on("file-added", handleFileAdded); socket.on("file-removed", handleFileRemoved); socket.on("folder-added", handleFolderAdded); socket.on("folder-removed", handleFolderRemoved); return () => { socket.off("file-added", handleFileAdded); socket.off("file-removed", handleFileRemoved); socket.off("folder-added", handleFolderAdded); socket.off("folder-removed", handleFolderRemoved); }; }, [socket, refreshData]); const handleFileUpload = async (fileList: FileList | null) => { if (!fileList || fileList.length === 0) return; setIsUploading(true); for (const file of Array.from(fileList)) { const formData = new FormData(); formData.append("file", file); if (currentFolderId) { formData.append("folderId", currentFolderId); } try { const response = await fetch("/api/files", { method: "POST", body: formData, }); if (!response.ok) throw new Error("Upload failed"); const uploadedFile = await response.json(); toast.success(`Uploaded ${file.name}`); notifyFileUpload(uploadedFile); } catch { toast.error(`Failed to upload ${file.name}`); } } setIsUploading(false); refreshData(); // Reset file input if (fileInputRef.current) { fileInputRef.current.value = ""; } }; const handleCreateFolder = async () => { if (!newFolderName.trim()) return; try { const response = await fetch("/api/folders", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: newFolderName.trim(), parentId: currentFolderId, }), }); if (!response.ok) throw new Error("Failed to create folder"); const createdFolder = await response.json(); toast.success(`Created folder: ${newFolderName}`); setNewFolderName(""); setFolderDialogOpen(false); refreshData(); notifyFolderCreate(createdFolder); } catch { toast.error("Failed to create folder"); } }; const handleDownload = async (file: FileItem) => { try { const response = await fetch(`/api/files/${encodeURIComponent(file.id)}`); if (!response.ok) throw new Error("Download failed"); const blob = await response.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = file.originalName; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); toast.success(`Downloaded ${file.originalName}`); } catch { toast.error("Failed to download file"); } }; const handleDeleteFile = async (file: FileItem) => { try { const response = await fetch( `/api/files/${encodeURIComponent(file.id)}`, { method: "DELETE", } ); if (!response.ok) throw new Error("Delete failed"); toast.success(`Deleted ${file.originalName}`); refreshData(); notifyFileDelete(file.id); } catch { toast.error("Failed to delete file"); } }; const handleDeleteFolder = async (folder: FolderItem) => { try { const response = await fetch(`/api/folders/${folder.id}`, { method: "DELETE", }); if (!response.ok) throw new Error("Delete failed"); toast.success(`Deleted folder: ${folder.name}`); refreshData(); notifyFolderDelete(folder.id); } catch { toast.error("Failed to delete folder"); } }; const handleDrag = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.type === "dragenter" || e.type === "dragover") { setDragActive(true); } else if (e.type === "dragleave") { setDragActive(false); } }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setDragActive(false); handleFileUpload(e.dataTransfer.files); }; const toggleFileSelection = (fileId: string) => { setSelectedFiles((prev) => { const newSet = new Set(prev); if (newSet.has(fileId)) { newSet.delete(fileId); } else { newSet.add(fileId); } return newSet; }); }; const selectAllFiles = () => { setSelectedFiles(new Set(filteredFiles.map((f) => f.id))); }; const clearSelection = () => { setSelectedFiles(new Set()); }; const handleBulkDownload = async () => { if (selectedFiles.size === 0) return; setIsDownloadingBulk(true); try { const response = await fetch("/api/files/bulk-download", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ fileIds: Array.from(selectedFiles) }), }); if (!response.ok) throw new Error("Bulk download failed"); const blob = await response.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `files-${Date.now()}.zip`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); toast.success(`Downloaded ${selectedFiles.size} file(s)`); clearSelection(); } catch { toast.error("Failed to download files"); } finally { setIsDownloadingBulk(false); } }; const handleBulkDelete = async () => { if (selectedFiles.size === 0) return; const count = selectedFiles.size; const confirmed = window.confirm( `Are you sure you want to delete ${count} file${count !== 1 ? "s" : ""}?` ); if (!confirmed) return; try { const deletePromises = Array.from(selectedFiles).map((fileId) => fetch(`/api/files/${encodeURIComponent(fileId)}`, { method: "DELETE" }) ); await Promise.all(deletePromises); toast.success(`Deleted ${count} file${count !== 1 ? "s" : ""}`); clearSelection(); refreshData(); } catch { toast.error("Failed to delete some files"); } }; const filteredFiles = files.filter((f) => f.name.toLowerCase().includes(searchQuery.toLowerCase()) ); const filteredFolders = folders.filter((f) => f.name.toLowerCase().includes(searchQuery.toLowerCase()) ); const isLoading = filesLoading || foldersLoading; return (
LanDropr Logo

LanDropr

setSearchQuery(e.target.value)} />
Create New Folder
setNewFolderName(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleCreateFolder()} />
handleFileUpload(e.target.files)} disabled={isUploading} />
{selectedFiles.size > 0 && (
{selectedFiles.size} file {selectedFiles.size !== 1 ? "s" : ""} selected
{selectedFiles.size < filteredFiles.length && ( )}
)}
fileInputRef.current?.click()} >

Drag and drop files here

or click to browse

{isLoading ? (

Loading content...

) : (
{filteredFolders.length > 0 && (

Folders

{filteredFolders.map((folder) => ( setCurrentFolderId(folder.id)} >
{folder.name}
{ e.stopPropagation(); handleDeleteFolder(folder); }} > Delete
))}
)} {filteredFiles.length > 0 && (

Files

{viewMode === "grid" ? (
{filteredFiles.map((file) => (
{getFileIcon(file.name)}

{file.originalName}

{formatBytes(file.size)}

handleDownload(file)} className="gap-2" > Download handleDeleteFile(file)} className="gap-2 text-destructive focus:text-destructive" > Delete
{new Date(file.createdAt).toLocaleString()}
))}
) : (
{filteredFiles.map((file) => (
{getFileIcon(file.type)}

{file.originalName}

{new Date(file.createdAt).toLocaleString()}
))}
)}
)} {filteredFolders.length === 0 && filteredFiles.length === 0 && (

No files found

{searchQuery ? "No files or folders match your search query." : "This folder is empty. Upload files or create a new folder to get started."}

{!searchQuery && ( )}
)}
)}
); }