Преглед на файлове

feat(filesTable): integrate Radix UI Checkbox component and enhance file selection UI

vtugulan преди 6 месеца
родител
ревизия
533086bb9d
променени са 4 файла, в които са добавени 196 реда и са изтрити 186 реда
  1. 134 186
      app/components/filesTable.tsx
  2. 30 0
      components/ui/checkbox.tsx
  3. 31 0
      package-lock.json
  4. 1 0
      package.json

+ 134 - 186
app/components/filesTable.tsx

@@ -12,6 +12,19 @@ import {
   RowSelectionState,
 } from "@tanstack/react-table";
 import { useQuery } 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";
 
 interface FileData {
   id: string;
@@ -26,7 +39,7 @@ interface FilesTableProps {
   onFileAdded?: (file: FileData) => void;
 }
 
-export function FilesTable({ }: FilesTableProps) {
+export function FilesTable({}: FilesTableProps) {
   const [sorting, setSorting] = useState<SortingState>([]);
   const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
   const [files, setFiles] = useState<FileData[]>([]);
@@ -54,24 +67,21 @@ export function FilesTable({ }: FilesTableProps) {
     {
       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>
+        <Checkbox
+          checked={
+            table.getIsAllPageRowsSelected() ||
+            (table.getIsSomePageRowsSelected() && "indeterminate")
+          }
+          onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
+          aria-label="Select all"
+        />
       ),
       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>
+        <Checkbox
+          checked={row.getIsSelected()}
+          onCheckedChange={(value) => row.toggleSelected(!!value)}
+          aria-label="Select row"
+        />
       ),
       enableSorting: false,
       enableHiding: false,
@@ -80,14 +90,14 @@ export function FilesTable({ }: FilesTableProps) {
       accessorKey: "filename",
       header: "File Name",
       cell: ({ row }) => (
-        <div className="font-medium text-gray-900">{row.getValue("filename")}</div>
+        <div className="font-medium">{row.getValue("filename")}</div>
       ),
     },
     {
       accessorKey: "mimetype",
       header: "Type",
       cell: ({ row }) => (
-        <div className="text-sm text-gray-600">{row.getValue("mimetype")}</div>
+        <div className="text-sm text-muted-foreground">{row.getValue("mimetype")}</div>
       ),
     },
     {
@@ -137,44 +147,22 @@ export function FilesTable({ }: FilesTableProps) {
       header: "Actions",
       cell: ({ row }) => (
         <div className="flex gap-2">
-          <button
+          <Button
+            variant="ghost"
+            size="sm"
             onClick={() => handleDownload(row.original.id, row.original.filename)}
-            className="text-green-600 hover:text-green-800 transition-colors"
             title="Download file"
           >
-            <svg
-              className="w-5 h-5"
-              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>
-          </button>
-          <button
+            <Download className="h-4 w-4" />
+          </Button>
+          <Button
+            variant="ghost"
+            size="sm"
             onClick={() => handleDeleteFile(row.original.id, row.original.filename)}
-            className="text-red-600 hover:text-red-800 transition-colors"
             title="Delete file"
           >
-            <svg
-              className="w-5 h-5"
-              fill="none"
-              stroke="currentColor"
-              viewBox="0 0 24 24"
-            >
-              <path
-                strokeLinecap="round"
-                strokeLinejoin="round"
-                strokeWidth={2}
-                d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
-              />
-            </svg>
-          </button>
+            <Trash2 className="h-4 w-4" />
+          </Button>
         </div>
       ),
       enableSorting: false,
@@ -215,15 +203,13 @@ export function FilesTable({ }: FilesTableProps) {
     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
+        }, index * 500);
       });
     }
   };
@@ -239,12 +225,6 @@ export function FilesTable({ }: FilesTableProps) {
     if (!confirmDelete) return;
 
     try {
-      const fileIds = selectedRows.map(row => row.original.id);
-      const fileNames = selectedRows.map(row => row.original.filename);
-      
-      console.log(`Deleting ${fileIds.length} files:`, fileNames);
-      
-      // Process deletions sequentially to avoid race conditions
       const results = [];
       for (const row of selectedRows) {
         try {
@@ -254,14 +234,11 @@ export function FilesTable({ }: FilesTableProps) {
           
           if (!response.ok) {
             const errorText = await response.text();
-            console.error(`Failed to delete ${row.original.filename}:`, errorText);
             results.push({ id: row.original.id, success: false, error: errorText });
           } else {
-            console.log(`Successfully deleted: ${row.original.filename}`);
             results.push({ id: row.original.id, success: true });
           }
         } catch (error) {
-          console.error(`Error deleting ${row.original.filename}:`, error);
           results.push({ id: row.original.id, success: false, error: String(error) });
         }
       }
@@ -270,12 +247,11 @@ export function FilesTable({ }: FilesTableProps) {
       const failed = results.filter(r => !r.success).length;
       
       if (failed > 0) {
-        alert(`${successful} file(s) deleted successfully, ${failed} failed. Check console for details.`);
+        alert(`${successful} file(s) deleted successfully, ${failed} failed.`);
       } else {
         alert(`${successful} file(s) deleted successfully.`);
       }
       
-      // Clear selection and refresh the table
       setRowSelection({});
       refetch();
       
@@ -311,188 +287,160 @@ export function FilesTable({ }: FilesTableProps) {
 
   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>
+      <Card>
+        <CardHeader>
+          <CardTitle>Files in Database</CardTitle>
+        </CardHeader>
+        <CardContent>
           <div className="space-y-3">
             {[...Array(5)].map((_, i) => (
-              <div key={i} className="h-12 bg-gray-100 rounded animate-pulse"></div>
+              <div key={i} className="flex items-center space-x-4">
+                <Skeleton className="h-12 w-full" />
+              </div>
             ))}
           </div>
-        </div>
-      </div>
+        </CardContent>
+      </Card>
     );
   }
 
   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>
+      <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 (
-      <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>
+      <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 (
-    <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>
+    <Card>
+      <CardHeader>
+        <div className="flex justify-between items-center">
+          <CardTitle>Files in Database</CardTitle>
           <div className="flex gap-2">
-            <button
+            <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"
+              variant="outline"
+              size="sm"
             >
-              <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 className="h-4 w-4 mr-2" />
               Download Selected
-            </button>
-            <button
+            </Button>
+            <Button
               onClick={handleDeleteSelected}
               disabled={table.getSelectedRowModel().rows.length === 0 || isLoading}
-              className="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center"
+              variant="destructive"
+              size="sm"
             >
-              <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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
-                />
-              </svg>
+              <Trash2 className="h-4 w-4 mr-2" />
               Delete Selected
-            </button>
-            <button
+            </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"
+              variant="outline"
+              size="sm"
             >
-              <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>
+              <RefreshCw className={`h-4 w-4 mr-2 ${isLoading ? "animate-spin" : ""}`} />
               Refresh
-            </button>
+            </Button>
           </div>
         </div>
-
-        <div className="overflow-x-auto">
-          <table className="min-w-full divide-y divide-gray-200">
-            <thead className="bg-gray-50">
+      </CardHeader>
+      <CardContent>
+        <div className="rounded-md border">
+          <Table>
+            <TableHeader>
               {table.getHeaderGroups().map((headerGroup) => (
-                <tr key={headerGroup.id}>
+                <TableRow key={headerGroup.id}>
                   {headerGroup.headers.map((header) => (
-                    <th
+                    <TableHead
                       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()}
+                      className="cursor-pointer"
                     >
-                      <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>
+                      {flexRender(
+                        header.column.columnDef.header,
+                        header.getContext()
+                      )}
+                      {header.column.getIsSorted() && (
+                        <span className="ml-1">
+                          {header.column.getIsSorted() === "asc" ? "↑" : "↓"}
+                        </span>
+                      )}
+                    </TableHead>
                   ))}
-                </tr>
+                </TableRow>
               ))}
-            </thead>
-            <tbody className="bg-white divide-y divide-gray-200">
+            </TableHeader>
+            <TableBody>
               {table.getRowModel().rows.map((row) => (
-                <tr
+                <TableRow
                   key={row.id}
-                  className={`hover:bg-gray-50 ${row.getIsSelected() ? 'bg-blue-50' : ''}`}
+                  data-state={row.getIsSelected() && "selected"}
                 >
                   {row.getVisibleCells().map((cell) => (
-                    <td key={cell.id} className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
+                    <TableCell key={cell.id}>
                       {flexRender(cell.column.columnDef.cell, cell.getContext())}
-                    </td>
+                    </TableCell>
                   ))}
-                </tr>
+                </TableRow>
               ))}
-            </tbody>
-          </table>
+            </TableBody>
+          </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 {files.length} results
+          <div className="text-sm text-muted-foreground">
+            {table.getRowModel().rows.length} of {files.length} row(s) selected
           </div>
           <div className="flex gap-2">
-            <button
+            <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"
+              variant="outline"
+              size="sm"
             >
               Previous
-            </button>
-            <button
+            </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"
+              variant="outline"
+              size="sm"
             >
               Next
-            </button>
+            </Button>
           </div>
         </div>
-      </div>
-    </div>
+      </CardContent>
+    </Card>
   );
 }

+ 30 - 0
components/ui/checkbox.tsx

@@ -0,0 +1,30 @@
+"use client"
+
+import * as React from "react"
+import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
+import { Check } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Checkbox = React.forwardRef<
+  React.ElementRef<typeof CheckboxPrimitive.Root>,
+  React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
+>(({ className, ...props }, ref) => (
+  <CheckboxPrimitive.Root
+    ref={ref}
+    className={cn(
+      "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
+      className
+    )}
+    {...props}
+  >
+    <CheckboxPrimitive.Indicator
+      className={cn("flex items-center justify-center text-current")}
+    >
+      <Check className="h-4 w-4" />
+    </CheckboxPrimitive.Indicator>
+  </CheckboxPrimitive.Root>
+))
+Checkbox.displayName = CheckboxPrimitive.Root.displayName
+
+export { Checkbox }

+ 31 - 0
package-lock.json

@@ -13,6 +13,7 @@
         "@prisma/client": "^6.11.1",
         "@radix-ui/react-alert-dialog": "^1.1.14",
         "@radix-ui/react-avatar": "^1.1.10",
+        "@radix-ui/react-checkbox": "^1.3.2",
         "@radix-ui/react-dropdown-menu": "^2.1.15",
         "@radix-ui/react-label": "^2.1.7",
         "@radix-ui/react-navigation-menu": "^1.2.13",
@@ -1721,6 +1722,36 @@
         }
       }
     },
+    "node_modules/@radix-ui/react-checkbox": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.2.tgz",
+      "integrity": "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==",
+      "license": "MIT",
+      "dependencies": {
+        "@radix-ui/primitive": "1.1.2",
+        "@radix-ui/react-compose-refs": "1.1.2",
+        "@radix-ui/react-context": "1.1.2",
+        "@radix-ui/react-presence": "1.1.4",
+        "@radix-ui/react-primitive": "2.1.3",
+        "@radix-ui/react-use-controllable-state": "1.2.2",
+        "@radix-ui/react-use-previous": "1.1.1",
+        "@radix-ui/react-use-size": "1.1.1"
+      },
+      "peerDependencies": {
+        "@types/react": "*",
+        "@types/react-dom": "*",
+        "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+        "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "@types/react-dom": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/@radix-ui/react-collection": {
       "version": "1.1.7",
       "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",

+ 1 - 0
package.json

@@ -14,6 +14,7 @@
     "@prisma/client": "^6.11.1",
     "@radix-ui/react-alert-dialog": "^1.1.14",
     "@radix-ui/react-avatar": "^1.1.10",
+    "@radix-ui/react-checkbox": "^1.3.2",
     "@radix-ui/react-dropdown-menu": "^2.1.15",
     "@radix-ui/react-label": "^2.1.7",
     "@radix-ui/react-navigation-menu": "^1.2.13",