filesTable.tsx 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. "use client";
  2. import { useState } from "react";
  3. import {
  4. ColumnDef,
  5. flexRender,
  6. getCoreRowModel,
  7. getPaginationRowModel,
  8. getSortedRowModel,
  9. useReactTable,
  10. SortingState,
  11. } from "@tanstack/react-table";
  12. import { useQuery, useQueryClient } from "@tanstack/react-query";
  13. interface FileData {
  14. id: string;
  15. filename: string;
  16. mimetype: string;
  17. size: number;
  18. createdAt: string;
  19. updatedAt: string;
  20. }
  21. const columns: ColumnDef<FileData>[] = [
  22. {
  23. accessorKey: "filename",
  24. header: "File Name",
  25. cell: ({ row }) => (
  26. <div className="font-medium text-gray-900">{row.getValue("filename")}</div>
  27. ),
  28. },
  29. {
  30. accessorKey: "mimetype",
  31. header: "Type",
  32. cell: ({ row }) => (
  33. <div className="text-sm text-gray-600">{row.getValue("mimetype")}</div>
  34. ),
  35. },
  36. {
  37. accessorKey: "size",
  38. header: "Size",
  39. cell: ({ row }) => {
  40. const bytes = row.getValue("size") as number;
  41. if (bytes === 0) return "0 Bytes";
  42. const k = 1024;
  43. const sizes = ["Bytes", "KB", "MB", "GB"];
  44. const i = Math.floor(Math.log(bytes) / Math.log(k));
  45. return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
  46. },
  47. },
  48. {
  49. accessorKey: "createdAt",
  50. header: "Created",
  51. cell: ({ row }) => {
  52. const date = new Date(row.getValue("createdAt"));
  53. return date.toLocaleDateString("en-US", {
  54. year: "numeric",
  55. month: "short",
  56. day: "numeric",
  57. hour: "2-digit",
  58. minute: "2-digit",
  59. });
  60. },
  61. },
  62. {
  63. accessorKey: "updatedAt",
  64. header: "Updated",
  65. cell: ({ row }) => {
  66. const date = new Date(row.getValue("updatedAt"));
  67. return date.toLocaleDateString("en-US", {
  68. year: "numeric",
  69. month: "short",
  70. day: "numeric",
  71. hour: "2-digit",
  72. minute: "2-digit",
  73. });
  74. },
  75. },
  76. ];
  77. export function FilesTable() {
  78. const [sorting, setSorting] = useState<SortingState>([]);
  79. const queryClient = useQueryClient();
  80. const { data, isLoading, isError, error, refetch } = useQuery({
  81. queryKey: ["files"],
  82. queryFn: async () => {
  83. const response = await fetch("/api/files");
  84. if (!response.ok) {
  85. throw new Error("Failed to fetch files");
  86. }
  87. const data = await response.json();
  88. return data.files as FileData[];
  89. },
  90. });
  91. const table = useReactTable({
  92. data: data || [],
  93. columns,
  94. state: {
  95. sorting,
  96. },
  97. onSortingChange: setSorting,
  98. getCoreRowModel: getCoreRowModel(),
  99. getPaginationRowModel: getPaginationRowModel(),
  100. getSortedRowModel: getSortedRowModel(),
  101. });
  102. const handleRefresh = () => {
  103. refetch();
  104. };
  105. if (isLoading) {
  106. return (
  107. <div className="bg-white rounded-lg shadow-sm border border-gray-200">
  108. <div className="p-6">
  109. <div className="flex justify-between items-center mb-4">
  110. <h2 className="text-xl font-semibold text-gray-900">Files in Database</h2>
  111. <div className="h-8 w-24 bg-gray-200 rounded animate-pulse"></div>
  112. </div>
  113. <div className="space-y-3">
  114. {[...Array(5)].map((_, i) => (
  115. <div key={i} className="h-12 bg-gray-100 rounded animate-pulse"></div>
  116. ))}
  117. </div>
  118. </div>
  119. </div>
  120. );
  121. }
  122. if (isError) {
  123. return (
  124. <div className="bg-red-50 border border-red-200 rounded-lg p-6">
  125. <div className="flex items-center">
  126. <svg className="w-5 h-5 text-red-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
  127. <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" />
  128. </svg>
  129. <span className="text-red-700">Error: {error?.message || "Failed to load files"}</span>
  130. </div>
  131. <button
  132. onClick={handleRefresh}
  133. className="mt-3 px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors"
  134. >
  135. Retry
  136. </button>
  137. </div>
  138. );
  139. }
  140. if (!data || data.length === 0) {
  141. return (
  142. <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
  143. <div className="text-center">
  144. <svg className="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
  145. <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" />
  146. </svg>
  147. <h3 className="mt-2 text-sm font-medium text-gray-900">No files found</h3>
  148. <p className="mt-1 text-sm text-gray-500">Upload some files to get started.</p>
  149. </div>
  150. </div>
  151. );
  152. }
  153. return (
  154. <div className="bg-white rounded-lg shadow-sm border border-gray-200">
  155. <div className="p-6">
  156. <div className="flex justify-between items-center mb-4">
  157. <h2 className="text-xl font-semibold text-gray-900">Files in Database</h2>
  158. <button
  159. onClick={handleRefresh}
  160. disabled={isLoading}
  161. 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"
  162. >
  163. <svg
  164. className={`w-4 h-4 mr-2 ${isLoading ? "animate-spin" : ""}`}
  165. fill="none"
  166. stroke="currentColor"
  167. viewBox="0 0 24 24"
  168. >
  169. <path
  170. strokeLinecap="round"
  171. strokeLinejoin="round"
  172. strokeWidth={2}
  173. 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"
  174. />
  175. </svg>
  176. Refresh
  177. </button>
  178. </div>
  179. <div className="overflow-x-auto">
  180. <table className="min-w-full divide-y divide-gray-200">
  181. <thead className="bg-gray-50">
  182. {table.getHeaderGroups().map((headerGroup) => (
  183. <tr key={headerGroup.id}>
  184. {headerGroup.headers.map((header) => (
  185. <th
  186. key={header.id}
  187. className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
  188. onClick={header.column.getToggleSortingHandler()}
  189. >
  190. <div className="flex items-center">
  191. {flexRender(header.column.columnDef.header, header.getContext())}
  192. {header.column.getIsSorted() && (
  193. <span className="ml-1">
  194. {header.column.getIsSorted() === "asc" ? "↑" : "↓"}
  195. </span>
  196. )}
  197. </div>
  198. </th>
  199. ))}
  200. </tr>
  201. ))}
  202. </thead>
  203. <tbody className="bg-white divide-y divide-gray-200">
  204. {table.getRowModel().rows.map((row) => (
  205. <tr key={row.id} className="hover:bg-gray-50">
  206. {row.getVisibleCells().map((cell) => (
  207. <td key={cell.id} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
  208. {flexRender(cell.column.columnDef.cell, cell.getContext())}
  209. </td>
  210. ))}
  211. </tr>
  212. ))}
  213. </tbody>
  214. </table>
  215. </div>
  216. {/* Pagination */}
  217. <div className="flex items-center justify-between mt-4">
  218. <div className="text-sm text-gray-700">
  219. Showing {table.getRowModel().rows.length} of {data.length} results
  220. </div>
  221. <div className="flex gap-2">
  222. <button
  223. onClick={() => table.previousPage()}
  224. disabled={!table.getCanPreviousPage()}
  225. className="px-3 py-1 text-sm bg-gray-100 text-gray-700 rounded hover:bg-gray-200 disabled:opacity-50"
  226. >
  227. Previous
  228. </button>
  229. <button
  230. onClick={() => table.nextPage()}
  231. disabled={!table.getCanNextPage()}
  232. className="px-3 py-1 text-sm bg-gray-100 text-gray-700 rounded hover:bg-gray-200 disabled:opacity-50"
  233. >
  234. Next
  235. </button>
  236. </div>
  237. </div>
  238. </div>
  239. </div>
  240. );
  241. }