Kaynağa Gözat

feat(files): implement file management table with Tanstack Table and refresh functionality

vtugulan 6 ay önce
ebeveyn
işleme
1b7414dadd

+ 95 - 0
IMPLEMENTATION_PLAN.md

@@ -0,0 +1,95 @@
+# Files Page Table Implementation Plan
+
+## Overview
+Add a table to the files page that displays all files in the database using Tanstack Table, with refresh functionality.
+
+## Requirements
+- Display all files from the database in a table format
+- Use Tanstack Table for advanced table features
+- Include refresh functionality without page reload
+- Responsive design with Tailwind CSS
+- Loading states and error handling
+
+## Implementation Steps
+
+### 1. Install Required Dependencies
+```bash
+npm install @tanstack/react-table @tanstack/react-query
+```
+
+### 2. Create Tanstack Table Component
+Create `app/components/filesTable.tsx` with:
+- Tanstack Table implementation
+- Column definitions for file data
+- Sorting and filtering capabilities
+- Responsive design
+
+### 3. Set Up Tanstack Query
+Create query client and provider for data fetching:
+- Configure query client with default options
+- Add query provider to layout or page
+
+### 4. Update Files Page
+Modify `app/files/page.tsx` to:
+- Include the new Tanstack Table component
+- Add refresh button functionality
+- Style the page layout
+
+### 5. API Integration
+Use existing `/api/files` endpoint with Tanstack Query:
+- Fetch files on component mount
+- Implement refresh functionality
+- Handle loading and error states
+
+## File Structure
+```
+app/
+├── components/
+│   └── filesTable.tsx (new)
+├── files/
+│   └── page.tsx (updated)
+└── api/
+    └── files/
+        └── route.ts (existing)
+```
+
+## Component Details
+
+### FilesTable Component
+- **Purpose**: Display files in a Tanstack Table
+- **Features**:
+  - Sortable columns
+  - Pagination
+  - Search/filter
+  - Responsive design
+  - Refresh functionality
+
+### Data Structure
+```typescript
+interface FileData {
+  id: string;
+  filename: string;
+  mimetype: string;
+  size: number;
+  createdAt: string;
+  updatedAt: string;
+}
+```
+
+## Styling
+- Use Tailwind CSS for responsive design
+- Dark/light mode support
+- Loading skeletons
+- Error states
+
+## Testing
+- Test refresh functionality
+- Verify responsive design
+- Check error handling
+
+## Next Steps
+1. Switch to code mode
+2. Install dependencies
+3. Create Tanstack Table component
+4. Update files page
+5. Test implementation

+ 256 - 0
app/components/filesTable.tsx

@@ -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>
+  );
+}

+ 8 - 0
app/files/page.tsx

@@ -1,5 +1,8 @@
+"use client";
+
 import Image from "next/image";
 import { UploadForm } from "../components/uploadForm";
+import { FilesTable } from "../components/filesTable";
 
 export default function FilesPage() {
   return (
@@ -26,6 +29,11 @@ export default function FilesPage() {
           <h2 className="text-xl font-semibold">Upload Files</h2>
           <UploadForm />
         </div>
+
+        <div className="w-full max-w-6xl">
+          <h2 className="text-2xl font-semibold mb-4">Files in Database</h2>
+          <FilesTable />
+        </div>
         
         <div className="flex gap-4 items-center flex-col sm:flex-row">
           <a

+ 7 - 6
app/layout.tsx

@@ -1,6 +1,7 @@
 import type { Metadata } from "next";
 import { Geist, Geist_Mono } from "next/font/google";
 import "./globals.css";
+import { Providers } from "./providers";
 
 const geistSans = Geist({
   variable: "--font-geist-sans",
@@ -13,8 +14,8 @@ const geistMono = Geist_Mono({
 });
 
 export const metadata: Metadata = {
-  title: "Vtor.io",
-  description: "All your solutions in one place.",
+  title: "Create Next App",
+  description: "Generated by create next app",
 };
 
 export default function RootLayout({
@@ -24,10 +25,10 @@ export default function RootLayout({
 }>) {
   return (
     <html lang="en">
-      <body
-        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
-      >
-        {children}
+      <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
+        <Providers>
+          {children}
+        </Providers>
       </body>
     </html>
   );

+ 20 - 0
app/providers.tsx

@@ -0,0 +1,20 @@
+"use client";
+
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { useState } from "react";
+
+export function Providers({ children }: { children: React.ReactNode }) {
+  const [queryClient] = useState(
+    () =>
+      new QueryClient({
+        defaultOptions: {
+          queries: {
+            staleTime: 60 * 1000, // 1 minute
+            refetchOnWindowFocus: false,
+          },
+        },
+      })
+  );
+
+  return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
+}

+ 61 - 0
package-lock.json

@@ -11,6 +11,8 @@
         "@prisma/client": "^6.11.1",
         "@scalar/api-reference-react": "^0.7.33",
         "@scalar/nextjs-api-reference": "^0.8.12",
+        "@tanstack/react-query": "^5.83.0",
+        "@tanstack/react-table": "^8.21.3",
         "next": "^15.4.1",
         "pg": "^8.16.3",
         "prisma": "^6.11.1",
@@ -2161,6 +2163,65 @@
         "tslib": "^2.8.0"
       }
     },
+    "node_modules/@tanstack/query-core": {
+      "version": "5.83.0",
+      "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz",
+      "integrity": "sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/tannerlinsley"
+      }
+    },
+    "node_modules/@tanstack/react-query": {
+      "version": "5.83.0",
+      "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.83.0.tgz",
+      "integrity": "sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@tanstack/query-core": "5.83.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/tannerlinsley"
+      },
+      "peerDependencies": {
+        "react": "^18 || ^19"
+      }
+    },
+    "node_modules/@tanstack/react-table": {
+      "version": "8.21.3",
+      "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz",
+      "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==",
+      "license": "MIT",
+      "dependencies": {
+        "@tanstack/table-core": "8.21.3"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/tannerlinsley"
+      },
+      "peerDependencies": {
+        "react": ">=16.8",
+        "react-dom": ">=16.8"
+      }
+    },
+    "node_modules/@tanstack/table-core": {
+      "version": "8.21.3",
+      "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz",
+      "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/tannerlinsley"
+      }
+    },
     "node_modules/@tanstack/virtual-core": {
       "version": "3.13.12",
       "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",

+ 2 - 0
package.json

@@ -12,6 +12,8 @@
     "@prisma/client": "^6.11.1",
     "@scalar/api-reference-react": "^0.7.33",
     "@scalar/nextjs-api-reference": "^0.8.12",
+    "@tanstack/react-query": "^5.83.0",
+    "@tanstack/react-table": "^8.21.3",
     "next": "^15.4.1",
     "pg": "^8.16.3",
     "prisma": "^6.11.1",