| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493 |
- "use client";
- import React, { useState, useEffect } from "react";
- import {
- ColumnDef,
- flexRender,
- getCoreRowModel,
- getPaginationRowModel,
- getSortedRowModel,
- useReactTable,
- SortingState,
- RowSelectionState,
- } from "@tanstack/react-table";
- import { useQuery, useQueryClient } from "@tanstack/react-query";
- import { Button } from "@/components/ui/button";
- import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
- import { Checkbox } from "@/components/ui/checkbox";
- import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
- } from "@/components/ui/table";
- import { Skeleton } from "@/components/ui/skeleton";
- import { AlertCircle, Download, Trash2, RefreshCw } from "lucide-react";
- import { Input } from "@/components/ui/input";
- interface FileData {
- id: string;
- filename: string;
- mimetype: string;
- size: number;
- createdAt: string;
- updatedAt: string;
- }
- export function FilesTable() {
- const [sorting, setSorting] = useState<SortingState>([]);
- const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
- const [files, setFiles] = useState<FileData[]>([]);
- const [isUploading, setIsUploading] = useState(false);
- const queryClient = useQueryClient();
- const { data, isLoading, isError, error, refetch } = useQuery({
- queryKey: ["files"],
- queryFn: async () => {
- const response = await fetch("/api/files");
- if (!response.ok) {
- throw new Error("Failed to fetch files");
- }
- const data = await response.json();
- return data.files as FileData[];
- },
- });
- // Update local files state when data changes
- useEffect(() => {
- if (data) {
- setFiles(data);
- }
- }, [data]);
- const columns: ColumnDef<FileData>[] = [
- {
- id: "select",
- header: ({ table }) => (
- <Checkbox
- checked={
- table.getIsAllPageRowsSelected() ||
- (table.getIsSomePageRowsSelected() && "indeterminate")
- }
- onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
- aria-label="Select all"
- />
- ),
- cell: ({ row }) => (
- <Checkbox
- checked={row.getIsSelected()}
- onCheckedChange={(value) => row.toggleSelected(!!value)}
- aria-label="Select row"
- />
- ),
- enableSorting: false,
- enableHiding: false,
- },
- {
- accessorKey: "filename",
- header: "File Name",
- cell: ({ row }) => (
- <div className="font-medium">{row.getValue("filename")}</div>
- ),
- },
- {
- accessorKey: "mimetype",
- header: "Type",
- cell: ({ row }) => (
- <div className="text-sm text-muted-foreground">{row.getValue("mimetype")}</div>
- ),
- },
- {
- accessorKey: "size",
- header: "Size",
- cell: ({ row }) => {
- const bytes = row.getValue("size") as number;
- if (bytes === 0) return "0 Bytes";
-
- const k = 1024;
- const sizes = ["Bytes", "KB", "MB", "GB"];
- const i = Math.floor(Math.log(bytes) / Math.log(k));
-
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
- },
- },
- {
- accessorKey: "createdAt",
- header: "Created",
- cell: ({ row }) => {
- const date = new Date(row.getValue("createdAt"));
- return date.toLocaleDateString("en-US", {
- year: "numeric",
- month: "short",
- day: "numeric",
- hour: "2-digit",
- minute: "2-digit",
- });
- },
- },
- {
- accessorKey: "updatedAt",
- header: "Updated",
- cell: ({ row }) => {
- const date = new Date(row.getValue("updatedAt"));
- return date.toLocaleDateString("en-US", {
- year: "numeric",
- month: "short",
- day: "numeric",
- hour: "2-digit",
- minute: "2-digit",
- });
- },
- },
- {
- id: "actions",
- header: "Actions",
- cell: ({ row }) => (
- <div className="flex gap-2">
- <Button
- variant="ghost"
- size="sm"
- onClick={() => handleDownload(row.original.id, row.original.filename)}
- title="Download file"
- >
- <Download className="h-4 w-4" />
- </Button>
- <Button
- variant="ghost"
- size="sm"
- onClick={() => handleDeleteFile(row.original.id, row.original.filename)}
- title="Delete file"
- >
- <Trash2 className="h-4 w-4" />
- </Button>
- </div>
- ),
- enableSorting: false,
- },
- ];
- const table = useReactTable({
- data: files,
- columns,
- state: {
- sorting,
- rowSelection,
- },
- onSortingChange: setSorting,
- onRowSelectionChange: setRowSelection,
- getCoreRowModel: getCoreRowModel(),
- getPaginationRowModel: getPaginationRowModel(),
- getSortedRowModel: getSortedRowModel(),
- enableRowSelection: true,
- });
- const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
- const file = event.target.files?.[0];
- if (!file) return;
- setIsUploading(true);
- const formData = new FormData();
- formData.append("file", file);
- try {
- const response = await fetch("/api/upload", {
- method: "POST",
- body: formData,
- });
- const result = await response.json();
-
- if (result.success) {
- // Add new file to the beginning of the list
- setFiles(prevFiles => [result.file, ...prevFiles]);
-
- // Invalidate and refetch to ensure consistency
- await queryClient.invalidateQueries({ queryKey: ["files"] });
-
- // Reset file input
- event.target.value = '';
- } else {
- alert(`Upload failed: ${result.error || 'Unknown error'}`);
- }
- } catch (error) {
- console.error("Upload error:", error);
- alert("Failed to upload file");
- } finally {
- setIsUploading(false);
- }
- };
- const handleRefresh = () => {
- refetch();
- };
- const handleDownload = (fileId: string, filename: string) => {
- const downloadUrl = `/api/files/${fileId}`;
- const link = document.createElement('a');
- link.href = downloadUrl;
- link.download = filename;
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- };
- const handleDownloadSelected = () => {
- const selectedRows = table.getSelectedRowModel().rows;
- if (selectedRows.length === 0) return;
-
- if (selectedRows.length === 1) {
- const selected = selectedRows[0];
- handleDownload(selected.original.id, selected.original.filename);
- } else {
- selectedRows.forEach((row, index) => {
- setTimeout(() => {
- handleDownload(row.original.id, row.original.filename);
- }, index * 500);
- });
- }
- };
- const handleDeleteSelected = async () => {
- const selectedRows = table.getSelectedRowModel().rows;
- if (selectedRows.length === 0) return;
- const confirmDelete = window.confirm(
- `Are you sure you want to delete ${selectedRows.length} file${selectedRows.length > 1 ? 's' : ''}? This action cannot be undone.`
- );
-
- if (!confirmDelete) return;
- try {
- const results = [];
- for (const row of selectedRows) {
- try {
- const response = await fetch(`/api/files/${row.original.id}`, {
- method: 'DELETE',
- });
-
- if (!response.ok) {
- const errorText = await response.text();
- results.push({ id: row.original.id, success: false, error: errorText });
- } else {
- results.push({ id: row.original.id, success: true });
- }
- } catch (error) {
- results.push({ id: row.original.id, success: false, error: String(error) });
- }
- }
- const successful = results.filter(r => r.success).length;
- const failed = results.filter(r => !r.success).length;
-
- if (failed > 0) {
- alert(`${successful} file(s) deleted successfully, ${failed} failed.`);
- } else {
- alert(`${successful} file(s) deleted successfully.`);
- }
-
- setRowSelection({});
- refetch();
-
- } catch (error) {
- console.error('Error in delete process:', error);
- alert('Failed to delete files. Please try again.');
- }
- };
- const handleDeleteFile = async (fileId: string, filename: string) => {
- const confirmDelete = window.confirm(
- `Are you sure you want to delete "${filename}"? This action cannot be undone.`
- );
-
- if (!confirmDelete) return;
- try {
- const response = await fetch(`/api/files/${fileId}`, {
- method: 'DELETE',
- });
-
- if (!response.ok) {
- throw new Error('Failed to delete file');
- }
-
- refetch();
-
- } catch (error) {
- console.error('Error deleting file:', error);
- alert('Failed to delete file. Please try again.');
- }
- };
- if (isLoading) {
- return (
- <Card>
- <CardHeader>
- <CardTitle>Files in Database</CardTitle>
- </CardHeader>
- <CardContent>
- <div className="space-y-3">
- {[...Array(5)].map((_, i) => (
- <div key={i} className="flex items-center space-x-4">
- <Skeleton className="h-12 w-full" />
- </div>
- ))}
- </div>
- </CardContent>
- </Card>
- );
- }
- if (isError) {
- return (
- <Card className="border-red-200">
- <CardContent className="pt-6">
- <div className="flex items-center text-red-700">
- <AlertCircle className="h-5 w-5 mr-2" />
- <span>Error: {error?.message || "Failed to load files"}</span>
- </div>
- <Button onClick={handleRefresh} className="mt-3" variant="outline">
- Retry
- </Button>
- </CardContent>
- </Card>
- );
- }
- if (!data || data.length === 0) {
- return (
- <Card>
- <CardContent className="pt-6">
- <div className="text-center">
- <div className="mx-auto h-12 w-12 text-gray-400">
- <svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z" />
- </svg>
- </div>
- <h3 className="mt-2 text-sm font-medium">No files found</h3>
- <p className="mt-1 text-sm text-muted-foreground">Upload some files to get started.</p>
- </div>
- </CardContent>
- </Card>
- );
- }
- return (
- <Card>
- <CardHeader>
- <div className="flex justify-between items-center">
- <CardTitle>Files in Database</CardTitle>
- <div className="flex gap-2">
- <div className="flex items-center gap-2">
- <Input
- type="file"
- onChange={handleFileUpload}
- disabled={isUploading}
- className="w-64 text-sm"
- />
- {isUploading && (
- <span className="text-sm text-muted-foreground">Uploading...</span>
- )}
- </div>
- <Button
- onClick={handleDownloadSelected}
- disabled={table.getSelectedRowModel().rows.length === 0 || isLoading}
- variant="outline"
- size="sm"
- >
- <Download className="h-4 w-4 mr-2" />
- Download Selected
- </Button>
- <Button
- onClick={handleDeleteSelected}
- disabled={table.getSelectedRowModel().rows.length === 0 || isLoading}
- variant="destructive"
- size="sm"
- >
- <Trash2 className="h-4 w-4 mr-2" />
- Delete Selected
- </Button>
- <Button
- onClick={handleRefresh}
- disabled={isLoading}
- variant="outline"
- size="sm"
- >
- <RefreshCw className={`h-4 w-4 mr-2 ${isLoading ? "animate-spin" : ""}`} />
- Refresh
- </Button>
- </div>
- </div>
- </CardHeader>
- <CardContent>
- <div className="rounded-md border">
- <Table>
- <TableHeader>
- {table.getHeaderGroups().map((headerGroup) => (
- <TableRow key={headerGroup.id}>
- {headerGroup.headers.map((header) => (
- <TableHead
- key={header.id}
- onClick={header.column.getToggleSortingHandler()}
- className="cursor-pointer"
- >
- {flexRender(
- header.column.columnDef.header,
- header.getContext()
- )}
- {header.column.getIsSorted() && (
- <span className="ml-1">
- {header.column.getIsSorted() === "asc" ? "↑" : "↓"}
- </span>
- )}
- </TableHead>
- ))}
- </TableRow>
- ))}
- </TableHeader>
- <TableBody>
- {table.getRowModel().rows.map((row) => (
- <TableRow
- key={row.id}
- data-state={row.getIsSelected() && "selected"}
- >
- {row.getVisibleCells().map((cell) => (
- <TableCell key={cell.id}>
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
- </TableCell>
- ))}
- </TableRow>
- ))}
- </TableBody>
- </Table>
- </div>
- <div className="flex items-center justify-between mt-4">
- <div className="text-sm text-muted-foreground">
- {table.getSelectedRowModel().rows.length} of {files.length} row(s) selected
- </div>
- <div className="flex gap-2">
- <Button
- onClick={() => table.previousPage()}
- disabled={!table.getCanPreviousPage()}
- variant="outline"
- size="sm"
- >
- Previous
- </Button>
- <Button
- onClick={() => table.nextPage()}
- disabled={!table.getCanNextPage()}
- variant="outline"
- size="sm"
- >
- Next
- </Button>
- </div>
- </div>
- </CardContent>
- </Card>
- );
- }
|