diff options
| author | diogo464 <[email protected]> | 2025-08-12 16:28:33 +0100 |
|---|---|---|
| committer | diogo464 <[email protected]> | 2025-08-12 16:28:33 +0100 |
| commit | 70738d871decbcdec4f5535a7b6f57de26de7d2a (patch) | |
| tree | 6402994fefa3bed8c352e24d20b53cb59a75afa0 /frontend/components/drive/DriveDirectoryClient.tsx | |
| parent | 507d9ee9ae524edd4e39942b735d987aa5d48359 (diff) | |
Clean up old UI code and rename V2 to Drive
- Remove old UI components: file-drive.tsx, history-view.tsx
- Remove unused API endpoints: /api/tree, /api/log, /api/fs/route.ts
- Rename /v2 routes to /drive routes for cleaner URLs
- Rename V2* components to Drive* components (V2DirectoryView -> DriveDirectoryView, etc.)
- Update all breadcrumb and navigation references from /v2 to /drive
- Redirect root path to /drive instead of old UI
- Keep /api/fs/[...path] and /api/directories for uploads and move functionality
- Preserve Drive_* server functions for potential future use
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <[email protected]>
Diffstat (limited to 'frontend/components/drive/DriveDirectoryClient.tsx')
| -rw-r--r-- | frontend/components/drive/DriveDirectoryClient.tsx | 591 |
1 files changed, 591 insertions, 0 deletions
diff --git a/frontend/components/drive/DriveDirectoryClient.tsx b/frontend/components/drive/DriveDirectoryClient.tsx new file mode 100644 index 0000000..548773a --- /dev/null +++ b/frontend/components/drive/DriveDirectoryClient.tsx | |||
| @@ -0,0 +1,591 @@ | |||
| 1 | "use client" | ||
| 2 | |||
| 3 | import type React from "react" | ||
| 4 | import { useState, useRef } from "react" | ||
| 5 | import Link from "next/link" | ||
| 6 | import { | ||
| 7 | ChevronRight, | ||
| 8 | File, | ||
| 9 | Folder, | ||
| 10 | Upload, | ||
| 11 | Trash2, | ||
| 12 | Move, | ||
| 13 | MoreHorizontal, | ||
| 14 | HardDrive, | ||
| 15 | Edit, | ||
| 16 | Link as LinkIcon, | ||
| 17 | Info, | ||
| 18 | LogIn, | ||
| 19 | LogOut, | ||
| 20 | } from "lucide-react" | ||
| 21 | import { Button } from "@/components/ui/button" | ||
| 22 | import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" | ||
| 23 | import { Progress } from "@/components/ui/progress" | ||
| 24 | import { | ||
| 25 | DropdownMenu, | ||
| 26 | DropdownMenuContent, | ||
| 27 | DropdownMenuItem, | ||
| 28 | DropdownMenuTrigger, | ||
| 29 | DropdownMenuSeparator, | ||
| 30 | } from "@/components/ui/dropdown-menu" | ||
| 31 | import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" | ||
| 32 | import { Input } from "@/components/ui/input" | ||
| 33 | import { Label } from "@/components/ui/label" | ||
| 34 | import { Checkbox } from "@/components/ui/checkbox" | ||
| 35 | import { toast } from "@/hooks/use-toast" | ||
| 36 | import { DriveLsEntry } from "@/lib/drive_types" | ||
| 37 | import { UPLOAD_MAX_FILE_SIZE, UPLOAD_MAX_FILES } from "@/lib/constants" | ||
| 38 | import { DriveMoveDialog } from "./DriveMoveDialog" | ||
| 39 | |||
| 40 | function formatFileSize(bytes: number): string { | ||
| 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 Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i] | ||
| 46 | } | ||
| 47 | |||
| 48 | function formatDate(timestamp: number): string { | ||
| 49 | return new Date(timestamp * 1000).toISOString().split('T')[0] | ||
| 50 | } | ||
| 51 | |||
| 52 | function formatDateTime(timestamp: number): string { | ||
| 53 | const date = new Date(timestamp * 1000) | ||
| 54 | const dateStr = date.toISOString().split('T')[0] | ||
| 55 | const timeStr = date.toLocaleTimeString('en-US', { | ||
| 56 | hour12: false, | ||
| 57 | hour: '2-digit', | ||
| 58 | minute: '2-digit', | ||
| 59 | second: '2-digit' | ||
| 60 | }) | ||
| 61 | return `${dateStr} at ${timeStr}` | ||
| 62 | } | ||
| 63 | |||
| 64 | interface Breadcrumb { | ||
| 65 | name: string | ||
| 66 | path: string | ||
| 67 | } | ||
| 68 | |||
| 69 | interface DriveDirectoryClientProps { | ||
| 70 | path: string | ||
| 71 | files: DriveLsEntry[] | ||
| 72 | breadcrumbs: Breadcrumb[] | ||
| 73 | } | ||
| 74 | |||
| 75 | export function DriveDirectoryClient({ path, files, breadcrumbs }: DriveDirectoryClientProps) { | ||
| 76 | const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set()) | ||
| 77 | const [renameDialogOpen, setRenameDialogOpen] = useState(false) | ||
| 78 | const [infoDialogOpen, setInfoDialogOpen] = useState(false) | ||
| 79 | const [moveDialogOpen, setMoveDialogOpen] = useState(false) | ||
| 80 | const [currentItem, setCurrentItem] = useState<DriveLsEntry | null>(null) | ||
| 81 | const [newName, setNewName] = useState("") | ||
| 82 | const fileInputRef = useRef<HTMLInputElement>(null) | ||
| 83 | const [uploading, setUploading] = useState(false) | ||
| 84 | |||
| 85 | const [isLoggedIn, setIsLoggedIn] = useState(true) // Mock logged in state | ||
| 86 | |||
| 87 | const maxStorage = 1073741824 // 1GB | ||
| 88 | const usedStorage = 0 // TODO: Calculate from files if needed | ||
| 89 | const storagePercentage = (usedStorage / maxStorage) * 100 | ||
| 90 | |||
| 91 | const toggleFileSelection = (filePath: string) => { | ||
| 92 | const newSelected = new Set(selectedFiles) | ||
| 93 | if (newSelected.has(filePath)) { | ||
| 94 | newSelected.delete(filePath) | ||
| 95 | } else { | ||
| 96 | newSelected.add(filePath) | ||
| 97 | } | ||
| 98 | setSelectedFiles(newSelected) | ||
| 99 | } | ||
| 100 | |||
| 101 | const selectAll = () => { | ||
| 102 | setSelectedFiles(new Set(files.map(file => file.path))) | ||
| 103 | } | ||
| 104 | |||
| 105 | const deselectAll = () => { | ||
| 106 | setSelectedFiles(new Set()) | ||
| 107 | } | ||
| 108 | |||
| 109 | const openRenameDialog = (item: DriveLsEntry) => { | ||
| 110 | setCurrentItem(item) | ||
| 111 | setNewName(item.path.split('/').pop() || '') | ||
| 112 | setRenameDialogOpen(true) | ||
| 113 | } | ||
| 114 | |||
| 115 | const openInfoDialog = (item: DriveLsEntry) => { | ||
| 116 | setCurrentItem(item) | ||
| 117 | setInfoDialogOpen(true) | ||
| 118 | } | ||
| 119 | |||
| 120 | const openMoveDialog = () => { | ||
| 121 | setMoveDialogOpen(true) | ||
| 122 | } | ||
| 123 | |||
| 124 | const copyPermalink = (item: DriveLsEntry) => { | ||
| 125 | const permalink = `${window.location.origin}/drive/file/${item.path}` | ||
| 126 | navigator.clipboard.writeText(permalink).then(() => { | ||
| 127 | toast({ | ||
| 128 | title: "Link copied!", | ||
| 129 | description: "Permalink has been copied to clipboard", | ||
| 130 | }) | ||
| 131 | }) | ||
| 132 | } | ||
| 133 | |||
| 134 | const handleRename = () => { | ||
| 135 | if (currentItem && newName.trim()) { | ||
| 136 | // TODO: Implement actual rename API call | ||
| 137 | setRenameDialogOpen(false) | ||
| 138 | setCurrentItem(null) | ||
| 139 | setNewName("") | ||
| 140 | toast({ | ||
| 141 | title: "Renamed successfully", | ||
| 142 | description: `Item renamed to "${newName.trim()}"`, | ||
| 143 | }) | ||
| 144 | // Refresh page to show changes | ||
| 145 | window.location.reload() | ||
| 146 | } | ||
| 147 | } | ||
| 148 | |||
| 149 | const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => { | ||
| 150 | const uploadedFiles = event.target.files | ||
| 151 | if (!uploadedFiles || uploadedFiles.length === 0) return | ||
| 152 | |||
| 153 | // Validate file count | ||
| 154 | if (uploadedFiles.length > UPLOAD_MAX_FILES) { | ||
| 155 | toast({ | ||
| 156 | title: "Too many files", | ||
| 157 | description: `You can only upload up to ${UPLOAD_MAX_FILES} files at once`, | ||
| 158 | variant: "destructive" | ||
| 159 | }) | ||
| 160 | return | ||
| 161 | } | ||
| 162 | |||
| 163 | // Validate file sizes | ||
| 164 | const oversizedFiles = Array.from(uploadedFiles).filter(file => file.size > UPLOAD_MAX_FILE_SIZE) | ||
| 165 | if (oversizedFiles.length > 0) { | ||
| 166 | toast({ | ||
| 167 | title: "Files too large", | ||
| 168 | description: `Maximum file size is ${formatFileSize(UPLOAD_MAX_FILE_SIZE)}. Found ${oversizedFiles.length} oversized file(s)`, | ||
| 169 | variant: "destructive" | ||
| 170 | }) | ||
| 171 | return | ||
| 172 | } | ||
| 173 | |||
| 174 | setUploading(true) | ||
| 175 | let successCount = 0 | ||
| 176 | let errorCount = 0 | ||
| 177 | |||
| 178 | try { | ||
| 179 | // Upload files sequentially to the current directory | ||
| 180 | for (const file of Array.from(uploadedFiles)) { | ||
| 181 | try { | ||
| 182 | const formData = new FormData() | ||
| 183 | formData.append('file', file) | ||
| 184 | |||
| 185 | // Construct the upload path (current path + filename) | ||
| 186 | const uploadPath = path === '/' ? file.name : `${path.slice(1)}/${file.name}` | ||
| 187 | // Encode each path segment for the URL - Next.js will decode it back for the API | ||
| 188 | const encodedPath = uploadPath.split('/').map(encodeURIComponent).join('/') | ||
| 189 | |||
| 190 | const response = await fetch(`/api/fs/${encodedPath}`, { | ||
| 191 | method: 'PUT', | ||
| 192 | headers: { | ||
| 193 | 'AUTH': '1' // Development auth header | ||
| 194 | }, | ||
| 195 | body: formData | ||
| 196 | }) | ||
| 197 | |||
| 198 | if (!response.ok) { | ||
| 199 | const error = await response.json() | ||
| 200 | throw new Error(error.error || `Upload failed with status ${response.status}`) | ||
| 201 | } | ||
| 202 | |||
| 203 | successCount++ | ||
| 204 | } catch (error) { | ||
| 205 | console.error(`Failed to upload ${file.name}:`, error) | ||
| 206 | errorCount++ | ||
| 207 | } | ||
| 208 | } | ||
| 209 | |||
| 210 | // Show results | ||
| 211 | if (successCount > 0) { | ||
| 212 | toast({ | ||
| 213 | title: "Upload successful", | ||
| 214 | description: `${successCount} file(s) uploaded successfully${errorCount > 0 ? `, ${errorCount} failed` : ''}` | ||
| 215 | }) | ||
| 216 | |||
| 217 | // Refresh page to show changes | ||
| 218 | window.location.reload() | ||
| 219 | } | ||
| 220 | |||
| 221 | if (errorCount > 0 && successCount === 0) { | ||
| 222 | toast({ | ||
| 223 | title: "Upload failed", | ||
| 224 | description: `All ${errorCount} file(s) failed to upload`, | ||
| 225 | variant: "destructive" | ||
| 226 | }) | ||
| 227 | } | ||
| 228 | |||
| 229 | } catch (error) { | ||
| 230 | console.error('Upload error:', error) | ||
| 231 | toast({ | ||
| 232 | title: "Upload failed", | ||
| 233 | description: error instanceof Error ? error.message : 'Unknown error occurred', | ||
| 234 | variant: "destructive" | ||
| 235 | }) | ||
| 236 | } finally { | ||
| 237 | setUploading(false) | ||
| 238 | // Reset the input | ||
| 239 | event.target.value = '' | ||
| 240 | } | ||
| 241 | } | ||
| 242 | |||
| 243 | const handleDelete = async (itemPaths: string[]) => { | ||
| 244 | // TODO: Implement actual delete API calls | ||
| 245 | setSelectedFiles(new Set()) | ||
| 246 | |||
| 247 | toast({ | ||
| 248 | title: "Deleted successfully", | ||
| 249 | description: `${itemPaths.length} item(s) deleted`, | ||
| 250 | }) | ||
| 251 | |||
| 252 | // Refresh page to show changes | ||
| 253 | window.location.reload() | ||
| 254 | } | ||
| 255 | |||
| 256 | const handleLogin = () => { | ||
| 257 | // Redirect to external auth page (configured via env var) | ||
| 258 | const authUrl = process.env.NEXT_PUBLIC_AUTH_URL || "/auth/login" | ||
| 259 | window.location.href = authUrl | ||
| 260 | } | ||
| 261 | |||
| 262 | const handleLogout = () => { | ||
| 263 | // Handle logout (would typically clear tokens, etc.) | ||
| 264 | setIsLoggedIn(false) | ||
| 265 | // Could also redirect to logout endpoint | ||
| 266 | } | ||
| 267 | |||
| 268 | const handleMove = async (destinationPath: string) => { | ||
| 269 | // TODO: Implement actual move API calls | ||
| 270 | console.log('Moving files:', Array.from(selectedFiles), 'to:', destinationPath) | ||
| 271 | setSelectedFiles(new Set()) | ||
| 272 | setMoveDialogOpen(false) | ||
| 273 | |||
| 274 | toast({ | ||
| 275 | title: "Moved successfully", | ||
| 276 | description: `${selectedFiles.size} item(s) moved to ${destinationPath}`, | ||
| 277 | }) | ||
| 278 | |||
| 279 | // Refresh page to show changes | ||
| 280 | window.location.reload() | ||
| 281 | } | ||
| 282 | |||
| 283 | return ( | ||
| 284 | <div className="container mx-auto p-6 space-y-6"> | ||
| 285 | {/* Header with Breadcrumbs */} | ||
| 286 | <div className="flex items-center justify-between"> | ||
| 287 | <div className="flex items-center gap-4"> | ||
| 288 | <div className="flex items-center gap-2"> | ||
| 289 | <HardDrive className="h-6 w-6" /> | ||
| 290 | <h1 className="text-2xl font-bold">Drive</h1> | ||
| 291 | </div> | ||
| 292 | |||
| 293 | {/* Breadcrumbs */} | ||
| 294 | <nav className="flex items-center gap-1 text-sm text-muted-foreground"> | ||
| 295 | {breadcrumbs.map((crumb, index) => ( | ||
| 296 | <div key={crumb.path} className="flex items-center gap-1"> | ||
| 297 | {index > 0 && <ChevronRight className="h-3 w-3" />} | ||
| 298 | {index === breadcrumbs.length - 1 ? ( | ||
| 299 | <span className="text-foreground font-medium">{crumb.name}</span> | ||
| 300 | ) : ( | ||
| 301 | <Link | ||
| 302 | href={crumb.path} | ||
| 303 | className="hover:text-foreground transition-colors" | ||
| 304 | > | ||
| 305 | {crumb.name} | ||
| 306 | </Link> | ||
| 307 | )} | ||
| 308 | </div> | ||
| 309 | ))} | ||
| 310 | </nav> | ||
| 311 | </div> | ||
| 312 | |||
| 313 | <div className="flex items-center gap-2"> | ||
| 314 | <Button | ||
| 315 | onClick={() => fileInputRef.current?.click()} | ||
| 316 | disabled={uploading} | ||
| 317 | > | ||
| 318 | <Upload className="mr-2 h-4 w-4" /> | ||
| 319 | {uploading ? "Uploading..." : "Upload Files"} | ||
| 320 | </Button> | ||
| 321 | {isLoggedIn ? ( | ||
| 322 | <Button variant="outline" onClick={handleLogout}> | ||
| 323 | <LogOut className="mr-2 h-4 w-4" /> | ||
| 324 | Logout | ||
| 325 | </Button> | ||
| 326 | ) : ( | ||
| 327 | <Button onClick={handleLogin}> | ||
| 328 | <LogIn className="mr-2 h-4 w-4" /> | ||
| 329 | Login | ||
| 330 | </Button> | ||
| 331 | )} | ||
| 332 | </div> | ||
| 333 | </div> | ||
| 334 | |||
| 335 | {/* Storage Info */} | ||
| 336 | <div className="bg-card rounded-lg border p-4"> | ||
| 337 | <div className="flex items-center justify-between mb-2"> | ||
| 338 | <span className="text-sm font-medium">Storage Usage</span> | ||
| 339 | <span className="text-sm text-muted-foreground"> | ||
| 340 | {formatFileSize(usedStorage)} of {formatFileSize(maxStorage)} used | ||
| 341 | </span> | ||
| 342 | </div> | ||
| 343 | <Progress value={storagePercentage} className="h-2" /> | ||
| 344 | <div className="flex justify-between text-xs text-muted-foreground mt-1"> | ||
| 345 | <span>{storagePercentage.toFixed(1)}% used</span> | ||
| 346 | <span>{formatFileSize(maxStorage - usedStorage)} available</span> | ||
| 347 | </div> | ||
| 348 | </div> | ||
| 349 | |||
| 350 | {/* Bulk Actions */} | ||
| 351 | {selectedFiles.size > 0 && ( | ||
| 352 | <div className="bg-blue-50 border border-blue-200 rounded-lg p-4 flex items-center justify-between"> | ||
| 353 | <div className="flex items-center gap-4"> | ||
| 354 | <span className="text-sm font-medium text-blue-900"> | ||
| 355 | {selectedFiles.size} item{selectedFiles.size !== 1 ? "s" : ""} selected | ||
| 356 | </span> | ||
| 357 | <Button variant="outline" size="sm" onClick={deselectAll}> | ||
| 358 | Deselect All | ||
| 359 | </Button> | ||
| 360 | </div> | ||
| 361 | <div className="flex items-center gap-2"> | ||
| 362 | <Button | ||
| 363 | variant="outline" | ||
| 364 | size="sm" | ||
| 365 | onClick={openMoveDialog} | ||
| 366 | > | ||
| 367 | <Move className="mr-2 h-4 w-4" /> | ||
| 368 | Move Selected | ||
| 369 | </Button> | ||
| 370 | <Button | ||
| 371 | variant="outline" | ||
| 372 | size="sm" | ||
| 373 | onClick={() => handleDelete(Array.from(selectedFiles))} | ||
| 374 | className="text-red-600 hover:text-red-700" | ||
| 375 | > | ||
| 376 | <Trash2 className="mr-2 h-4 w-4" /> | ||
| 377 | Delete Selected | ||
| 378 | </Button> | ||
| 379 | </div> | ||
| 380 | </div> | ||
| 381 | )} | ||
| 382 | |||
| 383 | {/* File Table */} | ||
| 384 | <div className="border rounded-lg"> | ||
| 385 | <Table> | ||
| 386 | <TableHeader> | ||
| 387 | <TableRow> | ||
| 388 | <TableHead className="w-[40px]"></TableHead> | ||
| 389 | <TableHead>Name</TableHead> | ||
| 390 | <TableHead>Size</TableHead> | ||
| 391 | <TableHead>Modified</TableHead> | ||
| 392 | <TableHead className="w-[50px]">Actions</TableHead> | ||
| 393 | </TableRow> | ||
| 394 | </TableHeader> | ||
| 395 | <TableBody> | ||
| 396 | {files.length === 0 ? ( | ||
| 397 | <TableRow> | ||
| 398 | <TableCell colSpan={5} className="text-center py-8 text-muted-foreground"> | ||
| 399 | This directory is empty | ||
| 400 | </TableCell> | ||
| 401 | </TableRow> | ||
| 402 | ) : ( | ||
| 403 | files.map((file) => { | ||
| 404 | const isSelected = selectedFiles.has(file.path) | ||
| 405 | const fileName = file.path.split('/').pop() || file.path | ||
| 406 | |||
| 407 | return ( | ||
| 408 | <TableRow | ||
| 409 | key={file.path} | ||
| 410 | className={`hover:bg-muted/50 ${isSelected ? "bg-muted/30" : ""}`} | ||
| 411 | > | ||
| 412 | <TableCell className="w-[40px]" onClick={(e) => e.stopPropagation()}> | ||
| 413 | <Checkbox | ||
| 414 | checked={isSelected} | ||
| 415 | onCheckedChange={() => toggleFileSelection(file.path)} | ||
| 416 | /> | ||
| 417 | </TableCell> | ||
| 418 | <TableCell className="font-medium"> | ||
| 419 | <div className="flex items-center gap-2"> | ||
| 420 | {file.type === "dir" ? ( | ||
| 421 | <> | ||
| 422 | <Folder className="h-4 w-4 text-blue-500" /> | ||
| 423 | <Link | ||
| 424 | href={`/drive${file.path}`} | ||
| 425 | className="text-blue-600 hover:text-blue-800 hover:underline cursor-pointer" | ||
| 426 | > | ||
| 427 | {fileName} | ||
| 428 | </Link> | ||
| 429 | </> | ||
| 430 | ) : ( | ||
| 431 | <> | ||
| 432 | <File className="h-4 w-4 text-gray-500" /> | ||
| 433 | {file.blob ? ( | ||
| 434 | <a | ||
| 435 | href={`/blob/${file.blob}?filename=${encodeURIComponent(fileName)}`} | ||
| 436 | className="text-blue-600 hover:text-blue-800 hover:underline cursor-pointer" | ||
| 437 | target="_blank" | ||
| 438 | rel="noopener noreferrer" | ||
| 439 | > | ||
| 440 | {fileName} | ||
| 441 | </a> | ||
| 442 | ) : ( | ||
| 443 | <span>{fileName}</span> | ||
| 444 | )} | ||
| 445 | </> | ||
| 446 | )} | ||
| 447 | </div> | ||
| 448 | </TableCell> | ||
| 449 | <TableCell>{formatFileSize(file.size || 0)}</TableCell> | ||
| 450 | <TableCell>{formatDate(file.lastmod)}</TableCell> | ||
| 451 | <TableCell onClick={(e) => e.stopPropagation()}> | ||
| 452 | <DropdownMenu> | ||
| 453 | <DropdownMenuTrigger asChild> | ||
| 454 | <Button variant="ghost" size="sm" className="h-8 w-8 p-0"> | ||
| 455 | <MoreHorizontal className="h-4 w-4" /> | ||
| 456 | </Button> | ||
| 457 | </DropdownMenuTrigger> | ||
| 458 | <DropdownMenuContent align="end"> | ||
| 459 | <DropdownMenuItem onClick={() => openRenameDialog(file)}> | ||
| 460 | <Edit className="mr-2 h-4 w-4" /> | ||
| 461 | Rename | ||
| 462 | </DropdownMenuItem> | ||
| 463 | <DropdownMenuItem onClick={() => copyPermalink(file)}> | ||
| 464 | <LinkIcon className="mr-2 h-4 w-4" /> | ||
| 465 | Copy Permalink | ||
| 466 | </DropdownMenuItem> | ||
| 467 | <DropdownMenuItem onClick={() => openInfoDialog(file)}> | ||
| 468 | <Info className="mr-2 h-4 w-4" /> | ||
| 469 | Info | ||
| 470 | </DropdownMenuItem> | ||
| 471 | <DropdownMenuSeparator /> | ||
| 472 | <DropdownMenuItem | ||
| 473 | onClick={() => handleDelete([file.path])} | ||
| 474 | className="text-red-600" | ||
| 475 | > | ||
| 476 | <Trash2 className="mr-2 h-4 w-4" /> | ||
| 477 | Delete | ||
| 478 | </DropdownMenuItem> | ||
| 479 | </DropdownMenuContent> | ||
| 480 | </DropdownMenu> | ||
| 481 | </TableCell> | ||
| 482 | </TableRow> | ||
| 483 | ) | ||
| 484 | }) | ||
| 485 | )} | ||
| 486 | </TableBody> | ||
| 487 | </Table> | ||
| 488 | </div> | ||
| 489 | |||
| 490 | {/* Rename Dialog */} | ||
| 491 | <Dialog open={renameDialogOpen} onOpenChange={setRenameDialogOpen}> | ||
| 492 | <DialogContent> | ||
| 493 | <DialogHeader> | ||
| 494 | <DialogTitle>Rename {currentItem?.type === "dir" ? "Folder" : "File"}</DialogTitle> | ||
| 495 | </DialogHeader> | ||
| 496 | <div className="space-y-4"> | ||
| 497 | <div> | ||
| 498 | <Label htmlFor="newName">New Name</Label> | ||
| 499 | <Input | ||
| 500 | id="newName" | ||
| 501 | value={newName} | ||
| 502 | onChange={(e) => setNewName(e.target.value)} | ||
| 503 | onKeyDown={(e) => { | ||
| 504 | if (e.key === "Enter") { | ||
| 505 | handleRename() | ||
| 506 | } | ||
| 507 | }} | ||
| 508 | placeholder="Enter new name" | ||
| 509 | /> | ||
| 510 | </div> | ||
| 511 | <div className="flex justify-end gap-2"> | ||
| 512 | <Button variant="outline" onClick={() => setRenameDialogOpen(false)}> | ||
| 513 | Cancel | ||
| 514 | </Button> | ||
| 515 | <Button onClick={handleRename} disabled={!newName.trim()}> | ||
| 516 | Rename | ||
| 517 | </Button> | ||
| 518 | </div> | ||
| 519 | </div> | ||
| 520 | </DialogContent> | ||
| 521 | </Dialog> | ||
| 522 | |||
| 523 | {/* Info Dialog */} | ||
| 524 | <Dialog open={infoDialogOpen} onOpenChange={setInfoDialogOpen}> | ||
| 525 | <DialogContent className="max-w-md"> | ||
| 526 | <DialogHeader> | ||
| 527 | <DialogTitle className="flex items-center gap-2"> | ||
| 528 | {currentItem?.type === "dir" ? ( | ||
| 529 | <Folder className="h-5 w-5 text-blue-500" /> | ||
| 530 | ) : ( | ||
| 531 | <File className="h-5 w-5 text-gray-500" /> | ||
| 532 | )} | ||
| 533 | {currentItem?.type === "dir" ? "Folder" : "File"} Information | ||
| 534 | </DialogTitle> | ||
| 535 | </DialogHeader> | ||
| 536 | {currentItem && ( | ||
| 537 | <div className="space-y-4"> | ||
| 538 | <div className="space-y-3"> | ||
| 539 | <div> | ||
| 540 | <Label className="text-sm font-medium text-muted-foreground">Name</Label> | ||
| 541 | <p className="text-sm break-words">{currentItem.path.split('/').pop()}</p> | ||
| 542 | </div> | ||
| 543 | <div> | ||
| 544 | <Label className="text-sm font-medium text-muted-foreground">Size</Label> | ||
| 545 | <p className="text-sm">{formatFileSize(currentItem.size || 0)}</p> | ||
| 546 | </div> | ||
| 547 | <div> | ||
| 548 | <Label className="text-sm font-medium text-muted-foreground">Modified</Label> | ||
| 549 | <p className="text-sm">{formatDateTime(currentItem.lastmod)}</p> | ||
| 550 | </div> | ||
| 551 | <div> | ||
| 552 | <Label className="text-sm font-medium text-muted-foreground">Modified By</Label> | ||
| 553 | <p className="text-sm">{currentItem.author}</p> | ||
| 554 | </div> | ||
| 555 | <div> | ||
| 556 | <Label className="text-sm font-medium text-muted-foreground">Type</Label> | ||
| 557 | <p className="text-sm capitalize">{currentItem.type}</p> | ||
| 558 | </div> | ||
| 559 | <div> | ||
| 560 | <Label className="text-sm font-medium text-muted-foreground">Path</Label> | ||
| 561 | <p className="text-sm font-mono text-xs">{currentItem.path}</p> | ||
| 562 | </div> | ||
| 563 | </div> | ||
| 564 | <div className="flex justify-end"> | ||
| 565 | <Button variant="outline" onClick={() => setInfoDialogOpen(false)}> | ||
| 566 | Close | ||
| 567 | </Button> | ||
| 568 | </div> | ||
| 569 | </div> | ||
| 570 | )} | ||
| 571 | </DialogContent> | ||
| 572 | </Dialog> | ||
| 573 | |||
| 574 | {/* Move Dialog */} | ||
| 575 | <DriveMoveDialog | ||
| 576 | open={moveDialogOpen} | ||
| 577 | onOpenChange={setMoveDialogOpen} | ||
| 578 | selectedCount={selectedFiles.size} | ||
| 579 | onMove={handleMove} | ||
| 580 | /> | ||
| 581 | |||
| 582 | <input | ||
| 583 | ref={fileInputRef} | ||
| 584 | type="file" | ||
| 585 | multiple | ||
| 586 | className="hidden" | ||
| 587 | onChange={handleFileUpload} | ||
| 588 | /> | ||
| 589 | </div> | ||
| 590 | ) | ||
| 591 | } \ No newline at end of file | ||
