File managers are complex UI components. Here is how to build one with full navigation and uploads.
Types
export interface FileItem {
id: string;
name: string;
type: "file" | "folder";
size?: number;
mimeType?: string;
parentId: string | null;
createdAt: string;
updatedAt: string;
}
export type ViewMode = "grid" | "list";
export type SortBy = "name" | "size" | "updatedAt";
File Manager Hook
"use client";
import { useState, useCallback, useMemo } from "react";
import type { FileItem, SortBy } from "./types";
interface UseFileManagerOptions {
initialFiles: FileItem[];
onUpload?: (files: File[], parentId: string | null) => Promise<FileItem[]>;
onDelete?: (ids: string[]) => Promise<void>;
onRename?: (id: string, name: string) => Promise<void>;
onMove?: (ids: string[], targetFolderId: string | null) => Promise<void>;
}
export function useFileManager({
initialFiles,
onUpload,
onDelete,
onRename,
onMove,
}: UseFileManagerOptions) {
const [files, setFiles] = useState<FileItem[]>(initialFiles);
const [currentFolder, setCurrentFolder] = useState<string | null>(null);
const [selected, setSelected] = useState<Set<string>>(new Set());
const [sortBy, setSortBy] = useState<SortBy>("name");
const [sortAsc, setSortAsc] = useState(true);
// Current folder contents
const currentFiles = useMemo(() => {
const filtered = files.filter((f) => f.parentId === currentFolder);
return filtered.sort((a, b) => {
// Folders first
if (a.type !== b.type) return a.type === "folder" ? -1 : 1;
let comparison = 0;
switch (sortBy) {
case "name":
comparison = a.name.localeCompare(b.name);
break;
case "size":
comparison = (a.size ?? 0) - (b.size ?? 0);
break;
case "updatedAt":
comparison = new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime();
break;
}
return sortAsc ? comparison : -comparison;
});
}, [files, currentFolder, sortBy, sortAsc]);
// Breadcrumb path
const breadcrumbs = useMemo(() => {
const path: FileItem[] = [];
let folderId = currentFolder;
while (folderId) {
const folder = files.find((f) => f.id === folderId);
if (folder) {
path.unshift(folder);
folderId = folder.parentId;
} else {
break;
}
}
return path;
}, [files, currentFolder]);
const navigateTo = useCallback((folderId: string | null) => {
setCurrentFolder(folderId);
setSelected(new Set());
}, []);
const toggleSelect = useCallback((id: string, multi: boolean) => {
setSelected((prev) => {
const next = multi ? new Set(prev) : new Set<string>();
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
}, []);
const selectAll = useCallback(() => {
setSelected(new Set(currentFiles.map((f) => f.id)));
}, [currentFiles]);
const uploadFiles = useCallback(
async (fileList: File[]) => {
if (!onUpload) return;
const newFiles = await onUpload(fileList, currentFolder);
setFiles((prev) => [...prev, ...newFiles]);
},
[currentFolder, onUpload],
);
const deleteSelected = useCallback(async () => {
if (!onDelete || selected.size === 0) return;
const ids = Array.from(selected);
await onDelete(ids);
setFiles((prev) => prev.filter((f) => !ids.includes(f.id)));
setSelected(new Set());
}, [selected, onDelete]);
const renameFile = useCallback(
async (id: string, newName: string) => {
if (!onRename) return;
await onRename(id, newName);
setFiles((prev) =>
prev.map((f) => (f.id === id ? { ...f, name: newName } : f)),
);
},
[onRename],
);
const moveFiles = useCallback(
async (targetFolderId: string | null) => {
if (!onMove || selected.size === 0) return;
const ids = Array.from(selected);
await onMove(ids, targetFolderId);
setFiles((prev) =>
prev.map((f) =>
ids.includes(f.id) ? { ...f, parentId: targetFolderId } : f,
),
);
setSelected(new Set());
},
[selected, onMove],
);
const createFolder = useCallback(
(name: string) => {
const folder: FileItem = {
id: crypto.randomUUID(),
name,
type: "folder",
parentId: currentFolder,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
setFiles((prev) => [...prev, folder]);
},
[currentFolder],
);
return {
files: currentFiles,
breadcrumbs,
currentFolder,
selected,
sortBy,
sortAsc,
navigateTo,
toggleSelect,
selectAll,
uploadFiles,
deleteSelected,
renameFile,
moveFiles,
createFolder,
setSortBy,
setSortAsc,
};
}
Drop Zone
"use client";
import { useState, useCallback } from "react";
interface DropZoneProps {
onDrop: (files: File[]) => void;
children: React.ReactNode;
className?: string;
}
export function DropZone({ onDrop, children, className }: DropZoneProps) {
const [dragging, setDragging] = useState(false);
const [dragCount, setDragCount] = useState(0);
const handleDragEnter = useCallback((e: React.DragEvent) => {
e.preventDefault();
setDragCount((c) => c + 1);
setDragging(true);
}, []);
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault();
setDragCount((c) => {
const next = c - 1;
if (next === 0) setDragging(false);
return next;
});
}, []);
const handleDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault();
setDragging(false);
setDragCount(0);
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) {
onDrop(files);
}
},
[onDrop],
);
return (
<div
className={`${className} ${dragging ? "ring-2 ring-primary ring-dashed bg-primary/5" : ""}`}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={(e) => e.preventDefault()}
onDrop={handleDrop}
>
{children}
{dragging && (
<div className="absolute inset-0 flex items-center justify-center bg-primary/5 z-10 pointer-events-none">
<p className="text-primary font-medium">Drop files here</p>
</div>
)}
</div>
);
}
File Grid and List Views
"use client";
import type { FileItem, ViewMode } from "./types";
interface FileListProps {
files: FileItem[];
viewMode: ViewMode;
selected: Set<string>;
onSelect: (id: string, multi: boolean) => void;
onOpen: (item: FileItem) => void;
}
export function FileList({
files,
viewMode,
selected,
onSelect,
onOpen,
}: FileListProps) {
if (files.length === 0) {
return (
<div className="flex flex-col items-center justify-center py-16 text-muted-foreground">
<p>This folder is empty</p>
<p className="text-sm">Drop files here or click upload</p>
</div>
);
}
if (viewMode === "grid") {
return (
<div className="grid grid-cols-[repeat(auto-fill,minmax(140px,1fr))] gap-2 p-4">
{files.map((file) => (
<button
key={file.id}
className={`flex flex-col items-center gap-1 p-3 rounded-lg text-center hover:bg-muted cursor-pointer ${
selected.has(file.id) ? "bg-primary/10 ring-1 ring-primary" : ""
}`}
onClick={(e) => onSelect(file.id, e.ctrlKey || e.metaKey)}
onDoubleClick={() => onOpen(file)}
>
<span className="text-3xl">
{file.type === "folder" ? "π" : getFileIcon(file.mimeType)}
</span>
<span className="text-xs truncate w-full">{file.name}</span>
{file.size && (
<span className="text-[10px] text-muted-foreground">
{formatFileSize(file.size)}
</span>
)}
</button>
))}
</div>
);
}
return (
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left text-muted-foreground">
<th className="px-4 py-2 font-medium">Name</th>
<th className="px-4 py-2 font-medium">Size</th>
<th className="px-4 py-2 font-medium">Modified</th>
</tr>
</thead>
<tbody>
{files.map((file) => (
<tr
key={file.id}
className={`border-b cursor-pointer hover:bg-muted ${
selected.has(file.id) ? "bg-primary/10" : ""
}`}
onClick={(e) => onSelect(file.id, e.ctrlKey || e.metaKey)}
onDoubleClick={() => onOpen(file)}
>
<td className="px-4 py-2 flex items-center gap-2">
<span>{file.type === "folder" ? "π" : getFileIcon(file.mimeType)}</span>
{file.name}
</td>
<td className="px-4 py-2 text-muted-foreground">
{file.size ? formatFileSize(file.size) : "-"}
</td>
<td className="px-4 py-2 text-muted-foreground">
{new Date(file.updatedAt).toLocaleDateString()}
</td>
</tr>
))}
</tbody>
</table>
);
}
function getFileIcon(mimeType?: string): string {
if (!mimeType) return "π";
if (mimeType.startsWith("image/")) return "πΌοΈ";
if (mimeType.startsWith("video/")) return "π¬";
if (mimeType.startsWith("audio/")) return "π΅";
if (mimeType === "application/pdf") return "π";
return "π";
}
function formatFileSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
Need Custom File Management?
We build file management systems tailored to your workflow. Contact us to get started.