diff options
Diffstat (limited to 'frontend/components/FileTable.tsx')
| -rw-r--r-- | frontend/components/FileTable.tsx | 179 |
1 files changed, 0 insertions, 179 deletions
diff --git a/frontend/components/FileTable.tsx b/frontend/components/FileTable.tsx deleted file mode 100644 index 97660f3..0000000 --- a/frontend/components/FileTable.tsx +++ /dev/null | |||
| @@ -1,179 +0,0 @@ | |||
| 1 | 'use client' | ||
| 2 | |||
| 3 | import { useState } from 'react' | ||
| 4 | import { DriveLsEntry } from '@/lib/drive_types' | ||
| 5 | import { Drive_basename } from '@/lib/drive_shared' | ||
| 6 | import { formatSize } from '@/lib/utils' | ||
| 7 | import Link from 'next/link' | ||
| 8 | |||
| 9 | interface FileTableEntry extends DriveLsEntry { | ||
| 10 | isParent?: boolean | ||
| 11 | parentPath?: string | ||
| 12 | } | ||
| 13 | |||
| 14 | interface FileTableProps { | ||
| 15 | entries: DriveLsEntry[] | ||
| 16 | currentPath?: string | ||
| 17 | showParent?: boolean | ||
| 18 | parentPath?: string | ||
| 19 | onSelectedFilesChange?: (selectedFiles: string[]) => void | ||
| 20 | } | ||
| 21 | |||
| 22 | export default function FileTable({ | ||
| 23 | entries, | ||
| 24 | currentPath = '', | ||
| 25 | showParent = false, | ||
| 26 | parentPath, | ||
| 27 | onSelectedFilesChange | ||
| 28 | }: FileTableProps) { | ||
| 29 | const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set()) | ||
| 30 | |||
| 31 | // Create entries with optional parent directory at top | ||
| 32 | const allEntries: FileTableEntry[] = [] | ||
| 33 | if (showParent && parentPath !== undefined) { | ||
| 34 | allEntries.push({ | ||
| 35 | path: '(parent)', | ||
| 36 | type: 'dir' as const, | ||
| 37 | lastmod: 0, | ||
| 38 | blob: null, | ||
| 39 | size: null, | ||
| 40 | author: '', | ||
| 41 | isParent: true, | ||
| 42 | parentPath | ||
| 43 | }) | ||
| 44 | } | ||
| 45 | |||
| 46 | // Sort entries: directories first, then files, both alphabetically | ||
| 47 | const sortedEntries = entries.sort((a, b) => { | ||
| 48 | // First sort by type (directories before files) | ||
| 49 | if (a.type !== b.type) { | ||
| 50 | return a.type === 'dir' ? -1 : 1 | ||
| 51 | } | ||
| 52 | // Then sort alphabetically by path | ||
| 53 | return a.path.localeCompare(b.path) | ||
| 54 | }) | ||
| 55 | |||
| 56 | allEntries.push(...sortedEntries) | ||
| 57 | |||
| 58 | const handleFileSelection = (filePath: string, isSelected: boolean) => { | ||
| 59 | const newSelectedFiles = new Set(selectedFiles) | ||
| 60 | if (isSelected) { | ||
| 61 | newSelectedFiles.add(filePath) | ||
| 62 | } else { | ||
| 63 | newSelectedFiles.delete(filePath) | ||
| 64 | } | ||
| 65 | setSelectedFiles(newSelectedFiles) | ||
| 66 | onSelectedFilesChange?.(Array.from(newSelectedFiles)) | ||
| 67 | } | ||
| 68 | |||
| 69 | const handleSelectAll = (isSelected: boolean) => { | ||
| 70 | if (isSelected) { | ||
| 71 | // Select all files (not directories or parent) | ||
| 72 | const fileEntries = allEntries.filter(entry => | ||
| 73 | entry.type === 'file' && !entry.isParent | ||
| 74 | ) | ||
| 75 | const newSelectedFiles = new Set(fileEntries.map(entry => entry.path)) | ||
| 76 | setSelectedFiles(newSelectedFiles) | ||
| 77 | onSelectedFilesChange?.(Array.from(newSelectedFiles)) | ||
| 78 | } else { | ||
| 79 | setSelectedFiles(new Set()) | ||
| 80 | onSelectedFilesChange?.([]) | ||
| 81 | } | ||
| 82 | } | ||
| 83 | |||
| 84 | const selectableFiles = allEntries.filter(entry => | ||
| 85 | entry.type === 'file' && !entry.isParent | ||
| 86 | ) | ||
| 87 | const allFilesSelected = selectableFiles.length > 0 && | ||
| 88 | selectableFiles.every(entry => selectedFiles.has(entry.path)) | ||
| 89 | |||
| 90 | return ( | ||
| 91 | <div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden"> | ||
| 92 | <div className="overflow-x-auto"> | ||
| 93 | <table className="w-full"> | ||
| 94 | <thead className="bg-gray-50 dark:bg-gray-700"> | ||
| 95 | <tr> | ||
| 96 | <th className="px-4 py-3 text-left"> | ||
| 97 | <input | ||
| 98 | type="checkbox" | ||
| 99 | checked={allFilesSelected} | ||
| 100 | onChange={(e) => handleSelectAll(e.target.checked)} | ||
| 101 | className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" | ||
| 102 | disabled={selectableFiles.length === 0} | ||
| 103 | /> | ||
| 104 | </th> | ||
| 105 | <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"> | ||
| 106 | Name | ||
| 107 | </th> | ||
| 108 | <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"> | ||
| 109 | Size | ||
| 110 | </th> | ||
| 111 | <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"> | ||
| 112 | Author | ||
| 113 | </th> | ||
| 114 | <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"> | ||
| 115 | Modified | ||
| 116 | </th> | ||
| 117 | </tr> | ||
| 118 | </thead> | ||
| 119 | <tbody className="divide-y divide-gray-200 dark:divide-gray-600"> | ||
| 120 | {allEntries.map((entry) => ( | ||
| 121 | <tr | ||
| 122 | key={entry.path} | ||
| 123 | className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors" | ||
| 124 | > | ||
| 125 | <td className="px-4 py-4 whitespace-nowrap"> | ||
| 126 | {entry.type === 'file' && !entry.isParent ? ( | ||
| 127 | <input | ||
| 128 | type="checkbox" | ||
| 129 | checked={selectedFiles.has(entry.path)} | ||
| 130 | onChange={(e) => handleFileSelection(entry.path, e.target.checked)} | ||
| 131 | className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" | ||
| 132 | /> | ||
| 133 | ) : ( | ||
| 134 | <div className="w-4 h-4" /> // Placeholder to maintain alignment | ||
| 135 | )} | ||
| 136 | </td> | ||
| 137 | <td className="px-4 py-4 whitespace-nowrap"> | ||
| 138 | <div className="flex items-center"> | ||
| 139 | <div className="flex-shrink-0 h-5 w-5 mr-3"> | ||
| 140 | {entry.type === 'dir' ? ( | ||
| 141 | <div className="h-5 w-5 text-blue-500">📁</div> | ||
| 142 | ) : ( | ||
| 143 | <div className="h-5 w-5 text-gray-400">📄</div> | ||
| 144 | )} | ||
| 145 | </div> | ||
| 146 | {entry.type === 'dir' ? ( | ||
| 147 | <Link | ||
| 148 | href={entry.isParent ? entry.parentPath! : `/drive${entry.path}`} | ||
| 149 | className="text-sm font-medium text-blue-600 dark:text-blue-400 hover:underline" | ||
| 150 | > | ||
| 151 | {entry.isParent ? '(parent)' : Drive_basename(entry.path)} | ||
| 152 | </Link> | ||
| 153 | ) : ( | ||
| 154 | <Link | ||
| 155 | href={`/blob/${entry.blob}?filename=${encodeURIComponent(Drive_basename(entry.path))}`} | ||
| 156 | className="text-sm font-medium text-blue-600 dark:text-blue-400 hover:underline" | ||
| 157 | > | ||
| 158 | {Drive_basename(entry.path)} | ||
| 159 | </Link> | ||
| 160 | )} | ||
| 161 | </div> | ||
| 162 | </td> | ||
| 163 | <td className="px-4 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300"> | ||
| 164 | {formatSize(entry.size)} | ||
| 165 | </td> | ||
| 166 | <td className="px-4 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300"> | ||
| 167 | {entry.author} | ||
| 168 | </td> | ||
| 169 | <td className="px-4 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300"> | ||
| 170 | {entry.lastmod > 0 ? new Date(entry.lastmod * 1000).toLocaleString() : ''} | ||
| 171 | </td> | ||
| 172 | </tr> | ||
| 173 | ))} | ||
| 174 | </tbody> | ||
| 175 | </table> | ||
| 176 | </div> | ||
| 177 | </div> | ||
| 178 | ) | ||
| 179 | } \ No newline at end of file | ||
