summaryrefslogtreecommitdiff
path: root/frontend/components/drive/DriveDirectoryClient.tsx
diff options
context:
space:
mode:
authordiogo464 <[email protected]>2025-08-12 16:28:33 +0100
committerdiogo464 <[email protected]>2025-08-12 16:28:33 +0100
commit70738d871decbcdec4f5535a7b6f57de26de7d2a (patch)
tree6402994fefa3bed8c352e24d20b53cb59a75afa0 /frontend/components/drive/DriveDirectoryClient.tsx
parent507d9ee9ae524edd4e39942b735d987aa5d48359 (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.tsx591
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
3import type React from "react"
4import { useState, useRef } from "react"
5import Link from "next/link"
6import {
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"
21import { Button } from "@/components/ui/button"
22import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
23import { Progress } from "@/components/ui/progress"
24import {
25 DropdownMenu,
26 DropdownMenuContent,
27 DropdownMenuItem,
28 DropdownMenuTrigger,
29 DropdownMenuSeparator,
30} from "@/components/ui/dropdown-menu"
31import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
32import { Input } from "@/components/ui/input"
33import { Label } from "@/components/ui/label"
34import { Checkbox } from "@/components/ui/checkbox"
35import { toast } from "@/hooks/use-toast"
36import { DriveLsEntry } from "@/lib/drive_types"
37import { UPLOAD_MAX_FILE_SIZE, UPLOAD_MAX_FILES } from "@/lib/constants"
38import { DriveMoveDialog } from "./DriveMoveDialog"
39
40function 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
48function formatDate(timestamp: number): string {
49 return new Date(timestamp * 1000).toISOString().split('T')[0]
50}
51
52function 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
64interface Breadcrumb {
65 name: string
66 path: string
67}
68
69interface DriveDirectoryClientProps {
70 path: string
71 files: DriveLsEntry[]
72 breadcrumbs: Breadcrumb[]
73}
74
75export 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