import { useState, useRef } from 'react' import { createFileRoute } from '@tanstack/react-router' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { storageFolderTreeOptions, storageFolderDetailOptions, storageFolderMutations, storageFolderKeys, storageFileListOptions, storageFileMutations, storageFileKeys, } from '@/api/storage' import { usePagination } from '@/hooks/use-pagination' import { FolderTree } from '@/components/storage/folder-tree' import { FileIcon, formatFileSize } from '@/components/storage/file-icons' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Badge } from '@/components/ui/badge' import { Skeleton } from '@/components/ui/skeleton' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' import { Label } from '@/components/ui/label' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { FolderPlus, Upload, Search, Folder, ChevronRight, MoreVertical, Trash2, Download } from 'lucide-react' import { toast } from 'sonner' import { useAuthStore } from '@/stores/auth.store' import type { StorageFolder, StorageFile } from '@/types/storage' export const Route = createFileRoute('/_authenticated/files/')({ validateSearch: (search: Record) => ({ page: Number(search.page) || 1, limit: Number(search.limit) || 50, q: (search.q as string) || undefined, sort: (search.sort as string) || undefined, order: (search.order as 'asc' | 'desc') || 'asc', }), component: FileManagerPage, }) function FileManagerPage() { const queryClient = useQueryClient() const token = useAuthStore((s) => s.token) const [selectedFolderId, setSelectedFolderId] = useState(null) const [newFolderOpen, setNewFolderOpen] = useState(false) const [newFolderName, setNewFolderName] = useState('') const fileInputRef = useRef(null) const { params } = usePagination() const { data: treeData, isLoading: treeLoading } = useQuery(storageFolderTreeOptions()) const { data: folderDetail } = useQuery(storageFolderDetailOptions(selectedFolderId ?? '')) const { data: filesData, isLoading: filesLoading } = useQuery( storageFileListOptions(selectedFolderId ?? '', { ...params, limit: 50 }), ) const { data: subFoldersData } = useQuery({ queryKey: storageFolderKeys.children(selectedFolderId), queryFn: () => { const allFolders = treeData?.data ?? [] return { data: allFolders.filter((f) => f.parentId === selectedFolderId) } }, enabled: !!treeData, }) const createFolderMutation = useMutation({ mutationFn: storageFolderMutations.create, onSuccess: () => { queryClient.invalidateQueries({ queryKey: storageFolderKeys.all }) toast.success('Folder created') setNewFolderOpen(false) setNewFolderName('') }, onError: (err) => toast.error(err.message), }) const deleteFolderMutation = useMutation({ mutationFn: storageFolderMutations.delete, onSuccess: () => { queryClient.invalidateQueries({ queryKey: storageFolderKeys.all }) setSelectedFolderId(null) toast.success('Folder deleted') }, onError: (err) => toast.error(err.message), }) const deleteFileMutation = useMutation({ mutationFn: storageFileMutations.delete, onSuccess: () => { if (selectedFolderId) queryClient.invalidateQueries({ queryKey: storageFileKeys.all(selectedFolderId) }) toast.success('File deleted') }, onError: (err) => toast.error(err.message), }) async function handleFileUpload(e: React.ChangeEvent) { if (!selectedFolderId || !e.target.files?.length) return const files = Array.from(e.target.files) for (const file of files) { const formData = new FormData() formData.append('file', file) try { const res = await fetch(`/v1/storage/folders/${selectedFolderId}/files`, { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: formData, }) if (!res.ok) { const err = await res.json().catch(() => ({})) toast.error(`Upload failed: ${(err as any).error?.message ?? file.name}`) } } catch { toast.error(`Upload failed: ${file.name}`) } } queryClient.invalidateQueries({ queryKey: storageFileKeys.all(selectedFolderId) }) toast.success(`${files.length} file(s) uploaded`) e.target.value = '' } async function handleDownload(file: StorageFile) { try { const { url } = await storageFileMutations.getSignedUrl(file.id) window.open(url, '_blank') } catch { toast.error('Download failed') } } function handleCreateFolder(e: React.FormEvent) { e.preventDefault() if (!newFolderName.trim()) return createFolderMutation.mutate({ name: newFolderName.trim(), parentId: selectedFolderId ?? undefined }) } const folders = treeData?.data ?? [] const files = filesData?.data ?? [] const subFolders = subFoldersData?.data ?? [] const breadcrumbs = folderDetail?.breadcrumbs ?? [] return (
{/* Left Panel — Folder Tree */}

Folders

New Folder
setNewFolderName(e.target.value)} placeholder="e.g. HR Documents" autoFocus />
{selectedFolderId && (

Creating inside: {breadcrumbs.map((b) => b.name).join(' / ') || 'Root'}

)}
{treeLoading ? (
{Array.from({ length: 5 }).map((_, i) => )}
) : ( )}
{/* Right Panel — Files */}
{/* Toolbar */}
{/* Breadcrumbs */}
{breadcrumbs.map((crumb) => ( ))}
{selectedFolderId && ( <> )}
{/* Content */}
{!selectedFolderId ? (

Select a folder to view files

Or create a new folder to get started

) : filesLoading ? (
{Array.from({ length: 8 }).map((_, i) => )}
) : ( <> {/* Sub-folders */} {subFolders.length > 0 && (

Folders

{subFolders.map((folder) => ( ))}
)} {/* Files */} {files.length > 0 && (
{subFolders.length > 0 &&

Files

}
{files.map((file) => (
handleDownload(file)}> Download deleteFileMutation.mutate(file.id)}> Delete
))}
)} {files.length === 0 && subFolders.length === 0 && (

This folder is empty

Upload files or create sub-folders

)} )}
) }