"use client" import type React from "react" import { useState, useRef } from "react" import Link from "next/link" import { ChevronRight, File, Folder, Upload, Trash2, Move, MoreHorizontal, Edit, Link as LinkIcon, Info, FolderPlus, } from "lucide-react" import { Button } from "@/components/ui/button" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator, } from "@/components/ui/dropdown-menu" import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Checkbox } from "@/components/ui/checkbox" import { toast } from "@/hooks/use-toast" import { DriveLsEntry } from "@/lib/drive_types" import { UPLOAD_MAX_FILE_SIZE, UPLOAD_MAX_FILES } from "@/lib/constants" import { formatFileSize } from "@/lib/utils" import { DriveMoveDialog } from "./DriveMoveDialog" import { StorageUsage } from "./StorageUsage" import type { StorageData } from "@/lib/storage" import type { UserAuth } from "@/lib/auth_types" function formatDate(timestamp: number): string { return new Date(timestamp * 1000).toISOString().split('T')[0] } function formatDateTime(timestamp: number): string { const date = new Date(timestamp * 1000) const dateStr = date.toISOString().split('T')[0] const timeStr = date.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }) return `${dateStr} at ${timeStr}` } interface Breadcrumb { name: string path: string } interface DriveDirectoryClientProps { path: string files: DriveLsEntry[] breadcrumbs: Breadcrumb[] storageData: StorageData userAuth: UserAuth } export function DriveDirectoryClient({ path, files, breadcrumbs, storageData, userAuth }: DriveDirectoryClientProps) { const [selectedFiles, setSelectedFiles] = useState>(new Set()) const [renameDialogOpen, setRenameDialogOpen] = useState(false) const [infoDialogOpen, setInfoDialogOpen] = useState(false) const [moveDialogOpen, setMoveDialogOpen] = useState(false) const [createFolderDialogOpen, setCreateFolderDialogOpen] = useState(false) const [currentItem, setCurrentItem] = useState(null) const [newName, setNewName] = useState("") const [newFolderName, setNewFolderName] = useState("") const fileInputRef = useRef(null) const [uploading, setUploading] = useState(false) const toggleFileSelection = (filePath: string) => { const newSelected = new Set(selectedFiles) if (newSelected.has(filePath)) { newSelected.delete(filePath) } else { newSelected.add(filePath) } setSelectedFiles(newSelected) } const deselectAll = () => { setSelectedFiles(new Set()) } const openRenameDialog = (item: DriveLsEntry) => { setCurrentItem(item) setNewName(item.path.split('/').pop() || '') setRenameDialogOpen(true) } const openInfoDialog = (item: DriveLsEntry) => { setCurrentItem(item) setInfoDialogOpen(true) } const openMoveDialog = () => { setMoveDialogOpen(true) } const copyPermalink = (item: DriveLsEntry) => { if (!item.blob) { toast({ title: "Cannot copy permalink", description: "This item does not have a blob ID", variant: "destructive" }) return } const filename = item.path.split('/').pop() || 'download' const permalink = `${window.location.origin}/blob/${item.blob}?filename=${encodeURIComponent(filename)}` navigator.clipboard.writeText(permalink).then(() => { toast({ title: "Permalink copied!", description: "Permanent blob link has been copied to clipboard", }) }) } const handleRename = async () => { if (!currentItem || !newName.trim()) return try { // Calculate the new full path by replacing the filename const pathParts = currentItem.path.split('/') pathParts[pathParts.length - 1] = newName.trim() const newPath = pathParts.join('/') const response = await fetch('/api/rename', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ oldPath: currentItem.path, newPath: newPath }) }) const result = await response.json() if (response.ok) { setRenameDialogOpen(false) setCurrentItem(null) setNewName("") toast({ title: "Renamed successfully", description: result.message, }) window.location.reload() } else { throw new Error(result.error || `Rename failed with status ${response.status}`) } } catch (error) { console.error('Rename error:', error) toast({ title: "Rename failed", description: error instanceof Error ? error.message : 'Unknown error occurred', variant: "destructive" }) } } const handleFileUpload = async (event: React.ChangeEvent) => { const uploadedFiles = event.target.files if (!uploadedFiles || uploadedFiles.length === 0) return // Validate file count if (uploadedFiles.length > UPLOAD_MAX_FILES) { toast({ title: "Too many files", description: `You can only upload up to ${UPLOAD_MAX_FILES} files at once`, variant: "destructive" }) return } // Validate file sizes const oversizedFiles = Array.from(uploadedFiles).filter(file => file.size > UPLOAD_MAX_FILE_SIZE) if (oversizedFiles.length > 0) { toast({ title: "Files too large", description: `Maximum file size is ${formatFileSize(UPLOAD_MAX_FILE_SIZE)}. Found ${oversizedFiles.length} oversized file(s)`, variant: "destructive" }) return } setUploading(true) let successCount = 0 let errorCount = 0 try { // Upload files sequentially to the current directory for (const file of Array.from(uploadedFiles)) { try { const formData = new FormData() formData.append('file', file) // Use the new simple upload endpoint with path as query parameter const response = await fetch(`/api/upload?path=${encodeURIComponent(path)}`, { method: 'POST', body: formData }) if (!response.ok) { const error = await response.json() throw new Error(error.error || `Upload failed with status ${response.status}`) } successCount++ } catch (error) { console.error(`Failed to upload ${file.name}:`, error) errorCount++ } } // Show results if (successCount > 0) { toast({ title: "Upload successful", description: `${successCount} file(s) uploaded successfully${errorCount > 0 ? `, ${errorCount} failed` : ''}` }) window.location.reload() } if (errorCount > 0 && successCount === 0) { toast({ title: "Upload failed", description: `All ${errorCount} file(s) failed to upload`, variant: "destructive" }) } } catch (error) { console.error('Upload error:', error) toast({ title: "Upload failed", description: error instanceof Error ? error.message : 'Unknown error occurred', variant: "destructive" }) } finally { setUploading(false) // Reset the input event.target.value = '' } } const handleDelete = async (itemPaths: string[]) => { try { const response = await fetch('/api/delete', { method: 'DELETE', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ paths: itemPaths }) }) const result = await response.json() if (response.ok) { setSelectedFiles(new Set()) toast({ title: "Deleted successfully", description: result.message, }) window.location.reload() } else { throw new Error(result.error || `Delete failed with status ${response.status}`) } } catch (error) { console.error('Delete error:', error) toast({ title: "Delete failed", description: error instanceof Error ? error.message : 'Unknown error occurred', variant: "destructive" }) } } const handleMove = async (destinationPath: string) => { const filesToMove = Array.from(selectedFiles) let successCount = 0 let errorCount = 0 const errors: string[] = [] try { // Move files sequentially using the rename API for (const filePath of filesToMove) { try { // Extract filename from the full path const fileName = filePath.split('/').pop() || '' // Construct new path: destination + filename const newPath = destinationPath.endsWith('/') ? `${destinationPath}${fileName}` : `${destinationPath}/${fileName}` const response = await fetch('/api/rename', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ oldPath: filePath, newPath: newPath }) }) if (!response.ok) { const result = await response.json() throw new Error(result.error || `Move failed with status ${response.status}`) } successCount++ } catch (error) { console.error(`Failed to move ${filePath}:`, error) errorCount++ errors.push(`${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`) } } // Clear selection and close dialog setSelectedFiles(new Set()) setMoveDialogOpen(false) // Show results if (successCount > 0) { toast({ title: "Move completed", description: `${successCount} item(s) moved successfully${errorCount > 0 ? `, ${errorCount} failed` : ''}`, }) window.location.reload() } if (errorCount > 0 && successCount === 0) { toast({ title: "Move failed", description: `All ${errorCount} item(s) failed to move. ${errors[0] || ''}`, variant: "destructive" }) } else if (errorCount > 0) { toast({ title: "Partial move failure", description: `${errorCount} item(s) failed to move. Check console for details.`, variant: "destructive" }) console.error('Move errors:', errors) } } catch (error) { console.error('Move error:', error) setSelectedFiles(new Set()) setMoveDialogOpen(false) toast({ title: "Move failed", description: error instanceof Error ? error.message : 'Unknown error occurred', variant: "destructive" }) } } const handleCreateFolder = async () => { if (!newFolderName.trim()) return try { const response = await fetch('/api/mkdir', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ path: path, name: newFolderName.trim() }) }) const result = await response.json() if (response.ok) { setCreateFolderDialogOpen(false) setNewFolderName("") toast({ title: "Folder created", description: result.message, }) window.location.reload() } else { throw new Error(result.error || `Failed to create folder`) } } catch (error) { console.error('Create folder error:', error) toast({ title: "Failed to create folder", description: error instanceof Error ? error.message : 'Unknown error occurred', variant: "destructive" }) } } const handleUploadButtonClick = () => { if (!userAuth.isLoggedIn) { toast({ title: "Authentication required", description: "User is not authenticated. Please log in to upload files.", variant: "destructive" }) return } fileInputRef.current?.click() } const handleCreateFolderButtonClick = () => { if (!userAuth.isLoggedIn) { toast({ title: "Authentication required", description: "User is not authenticated. Please log in to create folders.", variant: "destructive" }) return } setCreateFolderDialogOpen(true) } return (
{/* Storage Info */} {/* Navigation and Actions */}
{/* Breadcrumbs */}
{/* Bulk Actions */} {selectedFiles.size > 0 && (
{selectedFiles.size} item{selectedFiles.size !== 1 ? "s" : ""} selected
)} {/* File Table */}
Name Size Modified Actions {files.length === 0 ? ( This directory is empty ) : ( files.map((file) => { const isSelected = selectedFiles.has(file.path) const fileName = file.path.split('/').pop() || file.path return ( e.stopPropagation()}> toggleFileSelection(file.path)} />
{file.type === "dir" ? ( <>
{fileName}
) : ( <>
{file.blob ? ( {fileName} ) : ( {fileName} )}
)}
{formatFileSize(file.size || 0)} {formatDate(file.lastmod)} e.stopPropagation()}> openRenameDialog(file)}> Rename copyPermalink(file)}> Copy Permalink openInfoDialog(file)}> Info handleDelete([file.path])} className="text-red-600" > Delete
) }) )}
{/* Rename Dialog */} Rename {currentItem?.type === "dir" ? "Folder" : "File"}
setNewName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { handleRename() } }} placeholder="Enter new name" />
{/* Info Dialog */} {currentItem?.type === "dir" ? ( ) : ( )} {currentItem?.type === "dir" ? "Folder" : "File"} Information {currentItem && (

{currentItem.path.split('/').pop()}

{formatFileSize(currentItem.size || 0)}

{formatDateTime(currentItem.lastmod)}

{currentItem.author}

{currentItem.type}

{currentItem.path}

)}
{/* Move Dialog */} {/* Create Folder Dialog */} Create New Folder
setNewFolderName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { handleCreateFolder() } }} placeholder="Enter folder name" />
) }