FauziIsyrinApridal
update download all to zip
d81bdc0
"use client";
import { useEffect, useState } from "react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import { DownloadIcon, EyeOpenIcon, TrashIcon } from "@radix-ui/react-icons";
import { ChevronLeft, ChevronRight, Loader2 } from "lucide-react";
import { formatDatetime } from "@/utils/formatDatetime";
import { createClient } from "@/utils/supabase/client";
import { toast } from "@/hooks/use-toast";
interface FileTableProps {
fetchData: () => Promise<void>;
ragData: any[];
}
export default function FileTable({ fetchData, ragData }: FileTableProps) {
const supabase = createClient();
const [loadingMap, setLoadingMap] = useState<Record<string, boolean>>({});
const [currentPage, setCurrentPage] = useState(1);
const [sortedData, setSortedData] = useState<any[]>([]);
const itemsPerPage = 10; // Adjust as needed
const pagesToShow = 2; // Number of page buttons to display at once
useEffect(() => {
// Sort data by created_at in descending order (newest first)
if (ragData && ragData.length > 0) {
const sorted = [...ragData].sort((a, b) => {
return (
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
);
});
setSortedData(sorted);
} else {
setSortedData([]);
}
}, [ragData]);
const downloadAllFiles = async () => {
try {
const res = await fetch("/api/download-all");
if (!res.ok) {
throw new Error("Gagal mengunduh file ZIP");
}
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "all-files.zip";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
} catch (error) {
toast({
title: "Gagal",
description: "Tidak dapat mengunduh semua file.",
variant: "destructive",
});
}
};
const deleteAllFiles = async () => {
const confirmed = window.confirm("Yakin ingin menghapus SEMUA file?");
if (!confirmed) return;
try {
const fileNames = sortedData.map((item) => item.name);
setLoadingMap(
fileNames.reduce((acc, name) => ({ ...acc, [name]: true }), {}),
);
const { error } = await supabase.storage
.from("pnp-bot-storage")
.remove(fileNames);
if (error) {
toast({
title: "Gagal menghapus semua file",
description: error.message,
variant: "destructive",
});
} else {
toast({
title: "Semua file berhasil dihapus",
description: `${fileNames.length} file telah dihapus.`,
});
fetchData(); // refresh data
}
} catch (err) {
console.error("Gagal menghapus semua file:", err);
toast({
title: "Terjadi kesalahan",
description: "Tidak dapat menghapus semua file.",
variant: "destructive",
});
} finally {
setLoadingMap({});
}
};
const deleteItem = async (fileName: string) => {
const confirmed = window.confirm(
`Yakin ingin menghapus file "${fileName}"?`,
);
if (!confirmed) return;
try {
setLoadingMap((prev) => ({ ...prev, [fileName]: true }));
const { error } = await supabase.storage
.from("pnp-bot-storage")
.remove([fileName]);
if (error) {
toast({
title: "Gagal menghapus file",
description: error.message,
variant: "destructive",
});
} else {
toast({
title: "File berhasil dihapus",
description: `File "${fileName}" telah dihapus.`,
});
fetchData(); // refresh daftar file
}
} catch (err) {
console.error("Gagal menghapus:", err);
toast({
title: "Terjadi kesalahan",
description: "Tidak dapat menghapus file.",
variant: "destructive",
});
} finally {
setLoadingMap((prev) => ({ ...prev, [fileName]: false }));
}
};
// Lihat File (Open in New Tab)
const inspectItem = (fileName: string) => {
const { data } = supabase.storage
.from("pnp-bot-storage")
.getPublicUrl(fileName);
if (!data?.publicUrl) {
toast({
title: "Gagal membuka file",
description: `File "${fileName}" tidak memiliki URL publik.`,
variant: "destructive",
});
return;
}
window.open(data.publicUrl, "_blank");
};
// Unduh File
const downloadItem = async (fileName: string) => {
try {
// Retrieve the file as a blob using the download method
const { data, error } = await supabase.storage
.from("pnp-bot-storage") // Use your bucket name
.download(fileName);
if (error) {
toast({
title: "Gagal mengunduh file",
description: error.message || "Terjadi kesalahan saat mengunduh.",
variant: "destructive",
});
return;
}
// Create a link element to download the file
const url = URL.createObjectURL(data);
const link = document.createElement("a");
link.href = url;
link.download = fileName;
// Programmatically trigger the download
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Clean up the object URL
URL.revokeObjectURL(url);
toast({
title: "Unduh berhasil",
description: `File "${fileName}" berhasil diunduh.`,
duration: 2000,
});
} catch (err) {
console.error("Gagal mengunduh:", err);
toast({
title: "Terjadi kesalahan",
description: "Tidak dapat mengunduh file.",
variant: "destructive",
});
}
};
// Calculate pagination
const totalPages = Math.ceil(sortedData.length / itemsPerPage);
const paginatedData = sortedData.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage,
);
const goToNextPage = () => {
if (currentPage < totalPages) {
setCurrentPage(currentPage + 1);
}
};
const goToPrevPage = () => {
if (currentPage > 1) {
setCurrentPage(currentPage - 1);
}
};
const goToPage = (page: number) => {
setCurrentPage(page);
};
// Calculate the range of page numbers to display
const startPage = Math.max(1, currentPage - Math.floor(pagesToShow / 2));
const endPage = Math.min(totalPages, startPage + pagesToShow - 1);
const pageNumbers = Array.from(
{ length: endPage - startPage + 1 },
(_, index) => startPage + index,
);
return (
<div className="space-y-4">
<Table>
<TableHeader className="bg-slate-100">
<TableRow>
<TableHead className="text-center">#</TableHead>
<TableHead className="min-w-[240px]">Name</TableHead>
<TableHead>Uploaded At</TableHead>
<TableHead>File Size</TableHead>
<TableHead className="text-center">
<div className="flex justify-center gap-2">
<Button
size="sm"
variant="outline"
className="hover:bg-blue-600 hover:text-white"
onClick={downloadAllFiles}
>
<DownloadIcon className="mr-1 h-4 w-4" />
All
</Button>
<Button
size="sm"
variant="destructive"
className="hover:bg-red-800"
onClick={deleteAllFiles}
>
<TrashIcon className="mr-1 h-4 w-4" />
All
</Button>
</div>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{paginatedData && paginatedData.length > 0 ? (
paginatedData.map((item: any, index: number) => (
<TableRow key={index}>
<TableCell className="text-center">
{(currentPage - 1) * itemsPerPage + index + 1}
</TableCell>
<TableCell className="min-w-[240px] font-medium">
{item.name}
</TableCell>
<TableCell>{formatDatetime(item.created_at)}</TableCell>
<TableCell>{item.metadata.size}</TableCell>
<TableCell className="flex justify-center gap-2">
<Button
variant={"secondary"}
className="hover:bg-neutral-500 hover:text-white"
onClick={() => inspectItem(item.name)}
>
<EyeOpenIcon className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="hover:bg-blue-600 hover:text-white"
onClick={() => downloadItem(item.name)}
>
<DownloadIcon className="h-4 w-4" />
</Button>
<Button
variant={"destructive"}
className="hover:bg-red-800"
onClick={() => deleteItem(item.name)}
>
{loadingMap[item.name] ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<TrashIcon className="h-4 w-4" />
)}
</Button>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={5} className="py-4 text-center">
No Data Available
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
{/* Pagination Controls */}
{sortedData.length > 0 && (
<div className="flex items-center justify-between px-4 py-3">
<div className="text-sm text-muted-foreground">
Showing{" "}
<span className="font-medium">
{(currentPage - 1) * itemsPerPage + 1}
</span>{" "}
to{" "}
<span className="font-medium">
{Math.min(currentPage * itemsPerPage, sortedData.length)}
</span>{" "}
of <span className="font-medium">{sortedData.length}</span> files
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={goToPrevPage}
disabled={currentPage === 1}
className="px-3"
>
<ChevronLeft className="h-4 w-4" />
</Button>
{/* Always show first page */}
<Button
variant="outline"
size="sm"
onClick={() => goToPage(1)}
className={currentPage === 1 ? "bg-blue-500 text-white" : ""}
disabled={currentPage === 1}
>
1
</Button>
{/* Show "..." if current page is far from start */}
{currentPage > 3 && <span className="px-2">...</span>}
{/* Dynamic page numbers (middle range) */}
<div className="flex space-x-1">
{Array.from({ length: Math.min(3, totalPages - 2) }, (_, i) => {
let page;
if (currentPage <= 2)
page = i + 2; // Near start: 2, 3, 4
else if (currentPage >= totalPages - 1)
page = totalPages - 2 + i; // Near end
else page = currentPage - 1 + i; // Middle range
if (page > 1 && page < totalPages) {
return (
<Button
key={page}
variant="outline"
size="sm"
className={
currentPage === page ? "bg-blue-500 text-white" : ""
}
onClick={() => goToPage(page)}
>
{page}
</Button>
);
}
return null;
})}
</div>
{/* Show "..." if current page is far from end */}
{currentPage < totalPages - 2 && <span className="px-2">...</span>}
{/* Always show last page (if different from first) */}
{totalPages > 1 && (
<Button
variant="outline"
size="sm"
onClick={() => goToPage(totalPages)}
disabled={currentPage === totalPages}
className={
currentPage === totalPages ? "bg-blue-500 text-white" : ""
}
>
{totalPages}
</Button>
)}
<Button
variant="outline"
size="sm"
onClick={goToNextPage}
disabled={currentPage === totalPages || totalPages === 0}
className="px-3"
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
)}
</div>
);
}