|
|
@@ -0,0 +1,256 @@
|
|
|
+"use client";
|
|
|
+
|
|
|
+import { useState } from "react";
|
|
|
+import {
|
|
|
+ ColumnDef,
|
|
|
+ flexRender,
|
|
|
+ getCoreRowModel,
|
|
|
+ getPaginationRowModel,
|
|
|
+ getSortedRowModel,
|
|
|
+ useReactTable,
|
|
|
+ SortingState,
|
|
|
+} from "@tanstack/react-table";
|
|
|
+import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
|
+
|
|
|
+interface FileData {
|
|
|
+ id: string;
|
|
|
+ filename: string;
|
|
|
+ mimetype: string;
|
|
|
+ size: number;
|
|
|
+ createdAt: string;
|
|
|
+ updatedAt: string;
|
|
|
+}
|
|
|
+
|
|
|
+const columns: ColumnDef<FileData>[] = [
|
|
|
+ {
|
|
|
+ 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 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[];
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ const table = useReactTable({
|
|
|
+ data: data || [],
|
|
|
+ columns,
|
|
|
+ state: {
|
|
|
+ sorting,
|
|
|
+ },
|
|
|
+ onSortingChange: setSorting,
|
|
|
+ getCoreRowModel: getCoreRowModel(),
|
|
|
+ getPaginationRowModel: getPaginationRowModel(),
|
|
|
+ getSortedRowModel: getSortedRowModel(),
|
|
|
+ });
|
|
|
+
|
|
|
+ const handleRefresh = () => {
|
|
|
+ refetch();
|
|
|
+ };
|
|
|
+
|
|
|
+ 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>
|
|
|
+ <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 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.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>
|
|
|
+ );
|
|
|
+}
|