gbclient/app/drive/page.tsx

568 lines
18 KiB
TypeScript
Raw Normal View History

"use client";
import React, { useState, useMemo } from 'react';
import {
Search, Plus, Upload, Download, Trash2, Share, Star,
Grid, List, MoreVertical, Home, ChevronRight,
Folder, File, Image, Video, Music, FileText, Code, Database,
Package, Archive, Clock, Users, Eye, Edit3
} from 'lucide-react';
import { cn } from "@/lib/utils";
// Simple date formatting functions
const formatDate = (dateString) => {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
};
const formatDateTime = (dateString) => {
const date = new Date(dateString);
return date.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true
});
};
const formatDistanceToNow = (date) => {
const now = new Date();
const diffMs = now - new Date(date);
const diffMinutes = Math.floor(diffMs / (1000 * 60));
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffMinutes < 1) return 'now';
if (diffMinutes < 60) return `${diffMinutes}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays < 7) return `${diffDays}d ago`;
return formatDate(date);
};
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
TooltipProvider,
} from "@/components/ui/tooltip";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
// File system data
const fileSystemData = {
"": {
id: "root",
name: "My Drive",
path: "",
is_dir: true,
children: ["projects", "documents", "media", "shared"]
},
"projects": {
id: "projects",
name: "Projects",
path: "projects",
is_dir: true,
modified: "2025-01-15T10:30:00Z",
starred: true,
shared: false,
children: ["web-apps", "mobile-apps", "ai-research"]
},
"projects/web-apps": {
id: "web-apps",
name: "Web Applications",
path: "projects/web-apps",
is_dir: true,
modified: "2025-01-14T16:45:00Z",
starred: false,
shared: true,
children: ["dashboard-pro", "package.json", "README.md"]
},
"projects/web-apps/package.json": {
id: "package-json",
name: "package.json",
path: "projects/web-apps/package.json",
is_dir: false,
size: 2048,
type: "json",
modified: "2025-01-13T14:20:00Z",
starred: false,
shared: false
},
"projects/web-apps/README.md": {
id: "readme-md",
name: "README.md",
path: "projects/web-apps/README.md",
is_dir: false,
size: 5120,
type: "markdown",
modified: "2025-01-12T09:30:00Z",
starred: false,
shared: true
},
"documents": {
id: "documents",
name: "Documents",
path: "documents",
is_dir: true,
modified: "2025-01-14T12:00:00Z",
starred: false,
shared: false,
children: ["proposals", "Q1-Strategy.pdf", "Budget-2025.xlsx"]
},
"documents/Q1-Strategy.pdf": {
id: "q1-strategy",
name: "Q1 Strategy.pdf",
path: "documents/Q1-Strategy.pdf",
is_dir: false,
size: 1048576,
type: "pdf",
modified: "2025-01-10T15:30:00Z",
starred: true,
shared: true
},
"media": {
id: "media",
name: "Media",
path: "media",
is_dir: true,
modified: "2025-01-13T18:45:00Z",
starred: false,
shared: false,
children: ["photos", "videos", "vacation-2024.jpg"]
},
"media/vacation-2024.jpg": {
id: "vacation-photo",
name: "vacation-2024.jpg",
path: "media/vacation-2024.jpg",
is_dir: false,
size: 3145728,
type: "image",
modified: "2024-12-25T20:00:00Z",
starred: true,
shared: false
},
"shared": {
id: "shared",
name: "Shared",
path: "shared",
is_dir: true,
modified: "2025-01-12T11:20:00Z",
starred: false,
shared: true,
children: ["team-resources", "client-files"]
}
};
const getFileIcon = (item) => {
if (item.is_dir) {
return <Folder className="w-4 h-4 text-blue-600" />;
}
const iconMap = {
pdf: <FileText className="w-4 h-4 text-red-500" />,
xlsx: <Database className="w-4 h-4 text-green-600" />,
json: <Code className="w-4 h-4 text-yellow-600" />,
markdown: <Edit3 className="w-4 h-4 text-purple-500" />,
md: <Edit3 className="w-4 h-4 text-purple-500" />,
jpg: <Image className="w-4 h-4 text-pink-500" />,
jpeg: <Image className="w-4 h-4 text-pink-500" />,
png: <Image className="w-4 h-4 text-pink-500" />,
mp4: <Video className="w-4 h-4 text-red-600" />,
mp3: <Music className="w-4 h-4 text-green-600" />
};
return iconMap[item.type] || <File className="w-4 h-4 text-gray-500" />;
};
const formatFileSize = (bytes) => {
if (!bytes) return '';
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
};
const FolderTree = ({ onSelect, selectedPath, isCollapsed }) => {
const [expanded, setExpanded] = useState({ "": true, "projects": true });
const toggleExpand = (path) => {
setExpanded(prev => ({ ...prev, [path]: !prev[path] }));
onSelect(path);
};
const navLinks = [
{ title: "My Drive", path: "", icon: Home },
{ title: "Shared", path: "shared", icon: Users },
{ title: "Starred", path: "starred", icon: Star },
{ title: "Recent", path: "recent", icon: Clock },
{ title: "Trash", path: "trash", icon: Trash2 },
];
const renderTreeItem = (path, level = 0) => {
const item = fileSystemData[path];
if (!item || !item.is_dir) return null;
const isExpanded = expanded[path];
const isSelected = selectedPath === path;
return (
<div key={item.id}>
<button
onClick={() => toggleExpand(path)}
className={cn(
"flex items-center w-full px-2 py-1.5 text-sm rounded-md hover:bg-accent transition-colors",
isSelected && "bg-muted",
isCollapsed ? "justify-center" : "justify-start"
)}
>
{!isCollapsed && (
<>
<ChevronRight className={cn("w-4 h-4 mr-1 transition-transform", isExpanded && "rotate-90")} />
{getFileIcon(item)}
<span className="ml-2 truncate">{item.name}</span>
{item.starred && <Star className="w-3 h-3 ml-auto text-yellow-500 fill-current" />}
</>
)}
{isCollapsed && getFileIcon(item)}
</button>
{!isCollapsed && isExpanded && item.children && (
<div className="ml-4">
{item.children.map(childPath => {
const fullPath = path ? `${path}/${childPath}` : childPath;
return renderTreeItem(fullPath, level + 1);
})}
</div>
)}
</div>
);
};
return (
<div className="flex flex-col h-full">
<div className={cn("p-2", isCollapsed ? "px-1" : "")}>
<nav className="space-y-1">
{navLinks.map((link) =>
isCollapsed ? (
<Tooltip key={link.path} delayDuration={0}>
<TooltipTrigger asChild>
<Button
variant={selectedPath === link.path ? "default" : "ghost"}
size="icon"
className="w-9 h-9"
onClick={() => onSelect(link.path)}
>
<link.icon className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="right">{link.title}</TooltipContent>
</Tooltip>
) : (
<Button
key={link.path}
variant={selectedPath === link.path ? "default" : "ghost"}
className="w-full justify-start"
onClick={() => onSelect(link.path)}
>
<link.icon className="mr-2 h-4 w-4" />
{link.title}
</Button>
)
)}
</nav>
</div>
{!isCollapsed && (
<>
<Separator />
<ScrollArea className="flex-1 p-2">
{renderTreeItem("")}
</ScrollArea>
</>
)}
</div>
);
};
const FileList = ({ path, searchTerm, filterType }) => {
const [selectedFile, setSelectedFile] = useState(null);
const files = useMemo(() => {
const currentItem = fileSystemData[path];
if (!currentItem || !currentItem.is_dir || !currentItem.children) return [];
let items = currentItem.children.map(childName => {
const childPath = path ? `${path}/${childName}` : childName;
return fileSystemData[childPath];
}).filter(Boolean);
if (searchTerm) {
items = items.filter(item =>
item.name.toLowerCase().includes(searchTerm.toLowerCase())
);
}
if (filterType && filterType !== 'all') {
items = items.filter(item => {
if (filterType === 'folders') return item.is_dir;
if (filterType === 'files') return !item.is_dir;
if (filterType === 'starred') return item.starred;
return true;
});
}
return items.sort((a, b) => {
if (a.is_dir && !b.is_dir) return -1;
if (!a.is_dir && b.is_dir) return 1;
return a.name.localeCompare(b.name);
});
}, [path, searchTerm, filterType]);
return (
<ScrollArea className="h-full">
<div className="flex flex-col">
{files.map((item) => (
<button
key={item.id}
className={cn(
"flex items-center gap-3 p-3 text-left text-sm transition-all hover:bg-accent border-b",
selectedFile?.id === item.id && "bg-muted"
)}
onClick={() => setSelectedFile(item)}
>
<div className="flex items-center gap-3 flex-1 min-w-0">
{getFileIcon(item)}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<div className="font-medium truncate">{item.name}</div>
{item.starred && <Star className="w-3 h-3 text-yellow-500 fill-current" />}
{item.shared && <Users className="w-3 h-3 text-blue-500" />}
</div>
<div className="text-xs text-muted-foreground">
{item.is_dir ? 'Folder' : formatFileSize(item.size)}
</div>
</div>
</div>
<div className="text-xs text-muted-foreground">
{formatDistanceToNow(new Date(item.modified), { addSuffix: true })}
</div>
</button>
))}
</div>
</ScrollArea>
);
};
const FileDisplay = ({ file }) => {
if (!file) {
return (
<div className="flex flex-col items-center justify-center h-full text-center text-muted-foreground">
<File className="w-12 h-12 mb-4" />
<div className="text-lg font-medium">No file selected</div>
<div className="text-sm">Select a file to view details</div>
</div>
);
}
return (
<div className="flex flex-col h-full">
<div className="flex items-center p-2">
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon">
<Download className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Download</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon">
<Share className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Share</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon">
<Star className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Star</TooltipContent>
</Tooltip>
</div>
<div className="ml-auto">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Rename</DropdownMenuItem>
<DropdownMenuItem>Make a copy</DropdownMenuItem>
<DropdownMenuItem>Move to trash</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<Separator />
<div className="flex-1 flex-col">
<div className="flex items-start gap-4 p-4">
<div className="p-2 rounded-lg bg-accent">
{getFileIcon(file)}
</div>
<div className="flex-1 min-w-0">
<div className="font-semibold">{file.name}</div>
<div className="text-sm text-muted-foreground">
{file.is_dir ? 'Folder' : `${file.type?.toUpperCase() || 'File'}${formatFileSize(file.size)}`}
</div>
</div>
{file.modified && (
<div className="text-xs text-muted-foreground">
{formatDate(file.modified)}
</div>
)}
</div>
<Separator />
<div className="p-4 space-y-4">
<div>
<div className="text-sm font-medium">Location</div>
<div className="text-sm text-muted-foreground">/{file.path || ''}</div>
</div>
<div>
<div className="text-sm font-medium">Modified</div>
<div className="text-sm text-muted-foreground">
{formatDateTime(file.modified)}
</div>
</div>
{!file.is_dir && (
<div>
<div className="text-sm font-medium">Size</div>
<div className="text-sm text-muted-foreground">{formatFileSize(file.size)}</div>
</div>
)}
</div>
</div>
</div>
);
};
export default function FileManager() {
const [isCollapsed, setIsCollapsed] = useState(false);
const [currentPath, setCurrentPath] = useState('');
const [searchTerm, setSearchTerm] = useState('');
const [filterType, setFilterType] = useState('all');
const [selectedFile, setSelectedFile] = useState(null);
const currentItem = fileSystemData[currentPath];
return (
<TooltipProvider delayDuration={0}>
<ResizablePanelGroup direction="horizontal" className="h-full max-h-[800px]">
{/* Left Sidebar */}
<ResizablePanel
defaultSize={20}
collapsedSize={4}
collapsible={true}
minSize={15}
maxSize={30}
onCollapse={() => setIsCollapsed(true)}
onResize={() => setIsCollapsed(false)}
className={cn(isCollapsed && "min-w-[50px] transition-all duration-300")}
>
<FolderTree
onSelect={setCurrentPath}
selectedPath={currentPath}
isCollapsed={isCollapsed}
/>
</ResizablePanel>
<ResizableHandle withHandle />
{/* Middle File List */}
<ResizablePanel defaultSize={50} minSize={30}>
<Tabs defaultValue="all" className="flex flex-col h-full">
<div className="flex items-center px-4 py-2">
<h1 className="text-xl font-bold">{currentItem?.name || 'My Drive'}</h1>
<TabsList className="ml-auto">
<TabsTrigger value="all">All</TabsTrigger>
<TabsTrigger value="starred">Starred</TabsTrigger>
</TabsList>
</div>
<Separator />
<div className="bg-background/95 p-4 backdrop-blur">
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search files"
className="pl-8"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<Select value={filterType} onValueChange={setFilterType}>
<SelectTrigger className="w-[140px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All items</SelectItem>
<SelectItem value="folders">Folders</SelectItem>
<SelectItem value="files">Files</SelectItem>
<SelectItem value="starred">Starred</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<TabsContent value="all" className="m-0 flex-1">
<FileList
path={currentPath}
searchTerm={searchTerm}
filterType={filterType}
/>
</TabsContent>
<TabsContent value="starred" className="m-0 flex-1">
<FileList
path={currentPath}
searchTerm={searchTerm}
filterType="starred"
/>
</TabsContent>
</Tabs>
</ResizablePanel>
<ResizableHandle withHandle />
{/* Right File Details */}
<ResizablePanel defaultSize={30} minSize={25}>
<FileDisplay file={selectedFile} />
</ResizablePanel>
</ResizablePanelGroup>
</TooltipProvider>
);
}