| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338 |
- "use client";
- import { useState } from "react";
- import {
- ColumnDef,
- flexRender,
- getCoreRowModel,
- getPaginationRowModel,
- getSortedRowModel,
- useReactTable,
- SortingState,
- RowSelectionState,
- } from "@tanstack/react-table";
- import { useQuery } from "@tanstack/react-query";
- interface FileData {
- id: string;
- filename: string;
- mimetype: string;
- size: number;
- createdAt: string;
- updatedAt: string;
- }
- const columns: ColumnDef<FileData>[] = [
- {
- id: "select",
- header: ({ table }) => (
- <div className="px-6 py-3">
- <input
- type="checkbox"
- checked={table.getIsAllPageRowsSelected()}
- onChange={(e) => table.toggleAllPageRowsSelected(!!e.target.checked)}
- className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
- />
- </div>
- ),
- cell: ({ row }) => (
- <div className="px-6 py-4">
- <input
- type="checkbox"
- checked={row.getIsSelected()}
- onChange={(e) => row.toggleSelected(!!e.target.checked)}
- className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
- />
- </div>
- ),
- enableSorting: false,
- enableHiding: false,
- },
- {
- accessorKey: "filename",
- header: "File Name",
- cell: ({ row }) => (
- <div className="font-medium text-gray-900">{row.getValue("filename")}</div>
- ),
- },
- {
- accessorKey: "mimetype",
- header: "Type",
- cell: ({ row }) => (
- <div className="text-sm text-gray-600">{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",
- });
- },
- },
- ];
- export function FilesTable() {
- const [sorting, setSorting] = useState<SortingState>([]);
- const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
- 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[];
- },
- });
- const table = useReactTable({
- data: data || [],
- columns,
- state: {
- sorting,
- rowSelection,
- },
- onSortingChange: setSorting,
- onRowSelectionChange: setRowSelection,
- getCoreRowModel: getCoreRowModel(),
- getPaginationRowModel: getPaginationRowModel(),
- getSortedRowModel: getSortedRowModel(),
- enableRowSelection: true,
- });
- 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) {
- // Single file download
- const selected = selectedRows[0];
- handleDownload(selected.original.id, selected.original.filename);
- } else {
- // Multiple files - download each one
- selectedRows.forEach((row, index) => {
- setTimeout(() => {
- handleDownload(row.original.id, row.original.filename);
- }, index * 500); // Stagger downloads by 500ms to avoid browser blocking
- });
- }
- };
- if (isLoading) {
- return (
- <div className="bg-white rounded-lg shadow-sm border border-gray-200">
- <div className="p-6">
- <div className="flex justify-between items-center mb-4">
- <h2 className="text-xl font-semibold text-gray-900">Files in Database</h2>
- <div className="h-8 w-24 bg-gray-200 rounded animate-pulse"></div>
- </div>
- <div className="space-y-3">
- {[...Array(5)].map((_, i) => (
- <div key={i} className="h-12 bg-gray-100 rounded animate-pulse"></div>
- ))}
- </div>
- </div>
- </div>
- );
- }
- if (isError) {
- return (
- <div className="bg-red-50 border border-red-200 rounded-lg p-6">
- <div className="flex items-center">
- <svg className="w-5 h-5 text-red-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
- <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
- </svg>
- <span className="text-red-700">Error: {error?.message || "Failed to load files"}</span>
- </div>
- <button
- onClick={handleRefresh}
- className="mt-3 px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors"
- >
- Retry
- </button>
- </div>
- );
- }
- if (!data || data.length === 0) {
- return (
- <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
- <div className="text-center">
- <svg className="mx-auto h-12 w-12 text-gray-400" 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>
- <h3 className="mt-2 text-sm font-medium text-gray-900">No files found</h3>
- <p className="mt-1 text-sm text-gray-500">Upload some files to get started.</p>
- </div>
- </div>
- );
- }
- return (
- <div className="bg-white rounded-lg shadow-sm border border-gray-200">
- <div className="p-6">
- <div className="flex justify-between items-center mb-4">
- <h2 className="text-xl font-semibold text-gray-900">Files in Database</h2>
- <div className="flex gap-2">
- <button
- onClick={handleDownloadSelected}
- disabled={table.getSelectedRowModel().rows.length === 0 || isLoading}
- className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center"
- >
- <svg
- className="w-4 h-4 mr-2"
- fill="none"
- stroke="currentColor"
- viewBox="0 0 24 24"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- strokeWidth={2}
- d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
- />
- </svg>
- Download Selected
- </button>
- <button
- onClick={handleRefresh}
- disabled={isLoading}
- className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center"
- >
- <svg
- className={`w-4 h-4 mr-2 ${isLoading ? "animate-spin" : ""}`}
- fill="none"
- stroke="currentColor"
- viewBox="0 0 24 24"
- >
- <path
- strokeLinecap="round"
- strokeLinejoin="round"
- strokeWidth={2}
- d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
- />
- </svg>
- Refresh
- </button>
- </div>
- </div>
- <div className="overflow-x-auto">
- <table className="min-w-full divide-y divide-gray-200">
- <thead className="bg-gray-50">
- {table.getHeaderGroups().map((headerGroup) => (
- <tr key={headerGroup.id}>
- {headerGroup.headers.map((header) => (
- <th
- key={header.id}
- className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
- onClick={header.column.getToggleSortingHandler()}
- >
- <div className="flex items-center">
- {flexRender(header.column.columnDef.header, header.getContext())}
- {header.column.getIsSorted() && (
- <span className="ml-1">
- {header.column.getIsSorted() === "asc" ? "↑" : "↓"}
- </span>
- )}
- </div>
- </th>
- ))}
- </tr>
- ))}
- </thead>
- <tbody className="bg-white divide-y divide-gray-200">
- {table.getRowModel().rows.map((row) => (
- <tr
- key={row.id}
- className={`hover:bg-gray-50 ${row.getIsSelected() ? 'bg-blue-50' : ''}`}
- >
- {row.getVisibleCells().map((cell) => (
- <td key={cell.id} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
- </td>
- ))}
- </tr>
- ))}
- </tbody>
- </table>
- </div>
- {/* Pagination */}
- <div className="flex items-center justify-between mt-4">
- <div className="text-sm text-gray-700">
- Showing {table.getRowModel().rows.length} of {data.length} results
- </div>
- <div className="flex gap-2">
- <button
- onClick={() => table.previousPage()}
- disabled={!table.getCanPreviousPage()}
- className="px-3 py-1 text-sm bg-gray-100 text-gray-700 rounded hover:bg-gray-200 disabled:opacity-50"
- >
- Previous
- </button>
- <button
- onClick={() => table.nextPage()}
- disabled={!table.getCanNextPage()}
- className="px-3 py-1 text-sm bg-gray-100 text-gray-700 rounded hover:bg-gray-200 disabled:opacity-50"
- >
- Next
- </button>
- </div>
- </div>
- </div>
- </div>
- );
- }
|