import React, { useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Card, CardContent, CardHeader, CardTitle, CardDescription, } from "@/components/ui/card"; import { EditIcon, Folder, Trash2 } from "lucide-react"; import { apiRequest } from "@/lib/queryClient"; import type { CloudFolder } from "@repo/db/types"; import { getPageNumbers } from "@/utils/pageNumberGenerator"; import { Pagination, PaginationContent, PaginationItem, PaginationPrevious, PaginationLink, PaginationNext, } from "@/components/ui/pagination"; import type { QueryKey } from "@tanstack/react-query"; import { useToast } from "@/hooks/use-toast"; 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"; export const recentTopLevelFoldersQueryKey = (page: number): QueryKey => [ "/api/cloud-storage/folders/recent", page, ]; export type RecentTopLevelFoldersCardProps = { pageSize?: number; initialPage?: number; className?: string; onSelect?: (folderId: number | null) => void; }; export default function RecentTopLevelFoldersCard({ pageSize = 10, initialPage = 1, className, onSelect, }: RecentTopLevelFoldersCardProps) { const [currentPage, setCurrentPage] = useState(initialPage); const [selectedFolderId, setSelectedFolderId] = useState(null); const [isRenameOpen, setIsRenameOpen] = useState(false); const [renameInitialName, setRenameInitialName] = useState(""); const [renameTargetId, setRenameTargetId] = useState(null); const [isDeleteOpen, setIsDeleteOpen] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); const qc = useQueryClient(); const { toast } = useToast(); const { data: recentFoldersData, isLoading: isLoadingRecentFolders, refetch, } = useQuery({ queryKey: recentTopLevelFoldersQueryKey(currentPage), queryFn: async () => { const offset = (currentPage - 1) * pageSize; const res = await apiRequest( "GET", `/api/cloud-storage/folders/recent?limit=${pageSize}&offset=${offset}` ); const json = await res.json(); if (!res.ok) throw new Error(json?.message || "Failed to load recent folders"); const data: CloudFolder[] = Array.isArray(json.data) ? json.data : []; const totalCount = typeof json.totalCount === "number" ? json.totalCount : typeof json.total === "number" ? json.total : data.length; return { data, totalCount }; }, }); const data = recentFoldersData?.data ?? []; const totalCount = recentFoldersData?.totalCount ?? data.length; const totalPages = Math.max(1, Math.ceil(totalCount / pageSize)); const startItem = totalCount === 0 ? 0 : (currentPage - 1) * pageSize + 1; const endItem = Math.min(totalCount, currentPage * pageSize); // toggle selection: select if different, deselect if same function handleTileClick(id: number) { if (selectedFolderId === id) { setSelectedFolderId(null); onSelect?.(null); } else { setSelectedFolderId(id); onSelect?.(id); } // close any open context menu contextMenu.hideAll(); } // show react-contexify menu on right-click function handleContextMenu(e: React.MouseEvent, folder: CloudFolder) { e.preventDefault(); e.stopPropagation(); contextMenu.show({ id: "recent-folder-context-menu", event: e.nativeEvent, props: { folder }, }); } // rename flow function openRename(folder: CloudFolder) { setRenameTargetId(Number(folder.id)); setRenameInitialName(folder.name ?? ""); setIsRenameOpen(true); contextMenu.hideAll(); } async function handleRenameSubmit(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 || "Failed to rename folder"); toast({ title: "Folder renamed" }); setIsRenameOpen(false); setRenameTargetId(null); // refresh current page & first page qc.invalidateQueries({ queryKey: recentTopLevelFoldersQueryKey(currentPage), }); qc.invalidateQueries({ queryKey: recentTopLevelFoldersQueryKey(1) }); await refetch(); } catch (err: any) { toast({ title: "Error", description: err?.message || String(err) }); } } // delete flow function openDelete(folder: CloudFolder) { setDeleteTarget(folder); setIsDeleteOpen(true); contextMenu.hideAll(); } async function handleDeleteConfirm() { if (!deleteTarget) return; const id = deleteTarget.id; try { const res = await apiRequest( "DELETE", `/api/cloud-storage/folders/${id}` ); const json = await res.json(); if (!res.ok) throw new Error(json?.message || "Failed to delete folder"); toast({ title: "Folder deleted" }); setIsDeleteOpen(false); setDeleteTarget(null); // if the deleted folder was selected, deselect it and notify parent if (selectedFolderId === id) { setSelectedFolderId(null); onSelect?.(null); } // refresh pages qc.invalidateQueries({ queryKey: recentTopLevelFoldersQueryKey(currentPage), }); qc.invalidateQueries({ queryKey: recentTopLevelFoldersQueryKey(1) }); await refetch(); } catch (err: any) { toast({ title: "Error", description: err?.message || String(err) }); } } return ( Recent Folders Most recently updated top-level folders. {isLoadingRecentFolders ? (
Loading...
) : ( <>
{data.map((f) => { const isSelected = selectedFolderId === Number(f.id); return (
handleTileClick(Number(f.id))} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") handleTileClick(Number(f.id)); }} onContextMenu={(e) => handleContextMenu(e, f)} className={ "w-full flex items-center gap-3 p-2 rounded-lg hover:bg-gray-100 cursor-pointer focus:outline-none " + (isSelected ? "ring-2 ring-blue-400 bg-blue-50" : "") } style={{ minHeight: 44 }} >
{f.name}
); })}
{totalPages > 1 && (
Showing {startItem}–{endItem} of {totalCount} results
{ e.preventDefault(); if (currentPage > 1) setCurrentPage(currentPage - 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(); if (currentPage < totalPages) setCurrentPage(currentPage + 1); }} className={ currentPage === totalPages ? "pointer-events-none opacity-50" : "" } />
)} )}
{/* react-contexify Menu (single shared menu) */} { const folder: CloudFolder | undefined = props?.folder; if (folder) openRename(folder); }} > Rename { const folder: CloudFolder | undefined = props?.folder; if (folder) openDelete(folder); }} > Delete {/* Rename modal (reuses NewFolderModal) */} { setIsRenameOpen(false); setRenameTargetId(null); }} onSubmit={async (name) => { await handleRenameSubmit(name); }} /> {/* Delete confirmation */} { setIsDeleteOpen(false); setDeleteTarget(null); }} onConfirm={handleDeleteConfirm} />
); }