import React, { useEffect, 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 { Folder as FolderIcon, Plus, Trash2, Edit3 as EditIcon, } from "lucide-react"; import { apiRequest } from "@/lib/queryClient"; import type { CloudFolder } from "@repo/db/types"; import { NewFolderModal } from "@/components/cloud-storage/new-folder-modal"; 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 { recentTopLevelFoldersQueryKey } from "./recent-top-level-folder-modal"; import { toast } from "@/hooks/use-toast"; export type FolderSectionProps = { parentId: number | null; pageSize?: number; className?: string; onSelect?: (folderId: number | null) => void; }; export default function FolderSection({ parentId, pageSize = 10, className, onSelect, }: FolderSectionProps) { 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); const [isNewOpen, setIsNewOpen] = useState(false); const [isRenameOpen, setIsRenameOpen] = useState(false); const [renameInitial, setRenameInitial] = useState(""); const [renameTargetId, setRenameTargetId] = useState(null); const [isDeleteOpen, setIsDeleteOpen] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); const [selectedId, setSelectedId] = useState(null); // reset selectedId and page when parent changes useEffect(() => { setSelectedId(null); setCurrentPage(1); }, [parentId]); // load page 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 pid = parentId === null ? "null" : String(parentId); const res = await apiRequest( "GET", `/api/cloud-storage/items/folders?parentId=${encodeURIComponent( pid )}&limit=${pageSize}&offset=${offset}` ); const json = await res.json(); if (!res.ok) throw new Error(json?.message || "Failed to load folders"); const rows: CloudFolder[] = Array.isArray(json.data) ? json.data : []; setData(rows); const t = typeof json.total === "number" ? json.total : typeof json.totalCount === "number" ? json.totalCount : rows.length; setTotal(t); } catch (err: any) { setData([]); setTotal(0); toast({ title: "Failed to load folders", description: err?.message ?? String(err), variant: "destructive", }); } finally { setIsLoading(false); } } // tile click toggles selection function handleTileClick(id: number) { const next = selectedId === id ? null : id; setSelectedId(next); onSelect?.(next); contextMenu.hideAll(); } // right-click menu via react-contexify function showMenu(e: React.MouseEvent, folder: CloudFolder) { e.preventDefault(); e.stopPropagation(); contextMenu.show({ id: `folder-section-menu`, event: e.nativeEvent, props: { folder }, }); } // create folder async function handleCreate(name: string) { if (!userId) { toast({ title: "Not signed in", description: "Please sign in to create folders.", variant: "destructive", }); return; // caller should ensure auth } try { const res = await apiRequest("POST", "/api/cloud-storage/folders", { userId, name, parentId, }); const json = await res.json(); if (!res.ok) throw new Error(json?.message || "Create failed"); setIsNewOpen(false); toast({ title: "Folder created" }); // refresh this page and top-level recent loadPage(1); setCurrentPage(1); qc.invalidateQueries({ queryKey: recentTopLevelFoldersQueryKey(1) }); } catch (err: any) { toast({ title: "Create failed", description: err?.message ?? String(err), variant: "destructive", }); } } // rename function openRename(folder: CloudFolder) { setRenameTargetId(Number(folder.id)); setRenameInitial(folder.name ?? ""); setIsRenameOpen(true); contextMenu.hideAll(); } async function submitRename(newName: string) { if (!renameTargetId) return; try { const res = await apiRequest( "PUT", `/api/cloud-storage/folders/${renameTargetId}`, { name: newName } ); const json = await res.json(); if (!res.ok) throw new Error(json?.message || "Rename failed"); setIsRenameOpen(false); setRenameTargetId(null); toast({ title: "Folder renamed" }); loadPage(currentPage); qc.invalidateQueries({ queryKey: recentTopLevelFoldersQueryKey(1) }); } catch (err: any) { toast({ title: "Rename failed", description: err?.message ?? String(err), variant: "destructive", }); } } // delete function openDelete(folder: CloudFolder) { setDeleteTarget(folder); setIsDeleteOpen(true); contextMenu.hideAll(); } async function confirmDelete() { if (!deleteTarget) return; try { const res = await apiRequest( "DELETE", `/api/cloud-storage/folders/${deleteTarget.id}` ); const json = await res.json(); if (!res.ok) throw new Error(json?.message || "Delete failed"); // deselect if needed if (selectedId === deleteTarget.id) { setSelectedId(null); onSelect?.(null); } setIsDeleteOpen(false); setDeleteTarget(null); toast({ title: "Folder deleted" }); // reload current page (if empty page and not first, move back) const maybePage = Math.max(1, currentPage); loadPage(maybePage); qc.invalidateQueries({ queryKey: recentTopLevelFoldersQueryKey(1) }); } catch (err: any) { toast({ title: "Delete failed", description: err?.message ?? String(err), variant: "destructive", }); } } 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); return (
Folders Manage all its Child folders
{isLoading ? (
Loading...
) : ( <>
{data.map((f) => { const isSelected = selectedId === f.id; return (
handleTileClick(Number(f.id))} onContextMenu={(e) => showMenu(e, f)} className={ "w-full flex items-center gap-3 p-2 rounded-lg hover:bg-gray-100 cursor-pointer " + (isSelected ? "ring-2 ring-blue-400 bg-blue-50" : "") } >
{f.name}
); })}
{/* Pagination inside card */} {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" : "" } />
)} )}
{/* react-contexify menu */} openRename(props.folder)}> Rename openDelete(props.folder)}> Delete {/* Modals */} setIsNewOpen(false)} onSubmit={handleCreate} /> { setIsRenameOpen(false); setRenameTargetId(null); }} onSubmit={submitRename} /> { setIsDeleteOpen(false); setDeleteTarget(null); }} onConfirm={confirmDelete} />
); }