gbclient/app/drive/page.tsx
Rodrigo Rodriguez (Pragmatismo) 6e87c5b449
Some checks failed
GBCI / build (push) Failing after 4m39s
feat: Implement file browser and operations components with enhanced UI
- Added FileBrowser component for displaying files and directories.
- Introduced FileOperations component for handling file uploads and folder creation.
- Created FileTree component to visualize the directory structure.
- Developed DriveScreen page to integrate file browsing, operations, and UI controls.
- Enhanced file system data structure for realistic representation.
- Implemented search, filter, and sort functionalities in the file browser.
- Added keyboard shortcuts and improved accessibility features.
2025-06-21 21:40:06 -03:00

585 lines
No EOL
21 KiB
TypeScript

"use client";
import React, { useState, useEffect, useMemo } from 'react';
import { Search, Plus, Upload, Download, Trash2, Copy, Move, Share, Star, Grid, List, Filter, SortAsc, Eye, Edit3, Archive, Clock, Users, Lock, Folder, File, Image, Video, Music, FileText, Code, Database, Package } from 'lucide-react';
// Enhanced file system with realistic data structure
const fileSystemData = {
"": {
name: "Root",
path: "",
is_dir: true,
children: ["projects", "documents", "media", "shared", "archives", "templates"]
},
"projects": {
name: "Projects",
path: "projects",
is_dir: true,
size: null,
modified: "2025-01-15T10:30:00Z",
created: "2024-12-01T09:00:00Z",
starred: true,
shared: false,
tags: ["work", "development"],
children: ["web-apps", "mobile-apps", "data-science", "ai-research", "blockchain"]
},
"projects/web-apps": {
name: "Web Applications",
path: "projects/web-apps",
is_dir: true,
size: null,
modified: "2025-01-14T16:45:00Z",
created: "2024-12-01T09:15:00Z",
starred: false,
shared: true,
tags: ["frontend", "backend", "fullstack"],
children: ["dashboard-pro", "e-commerce-platform", "social-media-app", "file-manager"]
},
"projects/web-apps/dashboard-pro": {
name: "Dashboard Pro",
path: "projects/web-apps/dashboard-pro",
is_dir: true,
size: null,
modified: "2025-01-13T14:20:00Z",
created: "2024-12-10T11:00:00Z",
starred: true,
shared: true,
tags: ["react", "typescript", "tailwind"],
children: ["src", "components", "styles", "tests", "package.json", "README.md", "deployment.yaml"]
},
"projects/web-apps/dashboard-pro/package.json": {
name: "package.json",
path: "projects/web-apps/dashboard-pro/package.json",
is_dir: false,
size: 2048,
type: "json",
modified: "2025-01-13T14:20:00Z",
created: "2024-12-10T11:00:00Z",
starred: false,
shared: false,
tags: ["config", "dependencies"]
},
"projects/web-apps/dashboard-pro/README.md": {
name: "README.md",
path: "projects/web-apps/dashboard-pro/README.md",
is_dir: false,
size: 5120,
type: "markdown",
modified: "2025-01-12T09:30:00Z",
created: "2024-12-10T11:05:00Z",
starred: false,
shared: true,
tags: ["documentation"]
},
"documents": {
name: "Documents",
path: "documents",
is_dir: true,
size: null,
modified: "2025-01-14T12:00:00Z",
created: "2024-11-01T08:00:00Z",
starred: false,
shared: false,
tags: ["personal", "work"],
children: ["proposals", "contracts", "presentations", "reports", "spreadsheets"]
},
"documents/proposals": {
name: "Proposals",
path: "documents/proposals",
is_dir: true,
size: null,
modified: "2025-01-10T15:30:00Z",
created: "2024-11-15T10:00:00Z",
starred: true,
shared: true,
tags: ["business", "client"],
children: ["Q1-2025-Strategy.pdf", "AI-Integration-Proposal.docx", "Budget-Proposal-2025.xlsx"]
},
"documents/proposals/Q1-2025-Strategy.pdf": {
name: "Q1 2025 Strategy.pdf",
path: "documents/proposals/Q1-2025-Strategy.pdf",
is_dir: false,
size: 1048576,
type: "pdf",
modified: "2025-01-10T15:30:00Z",
created: "2025-01-08T14:00:00Z",
starred: true,
shared: true,
tags: ["strategy", "quarterly", "important"]
},
"media": {
name: "Media",
path: "media",
is_dir: true,
size: null,
modified: "2025-01-13T18:45:00Z",
created: "2024-10-01T12:00:00Z",
starred: false,
shared: false,
tags: ["photos", "videos", "audio"],
children: ["photos", "videos", "audio", "graphics"]
},
"media/photos": {
name: "Photos",
path: "media/photos",
is_dir: true,
size: null,
modified: "2025-01-13T18:45:00Z",
created: "2024-10-01T12:15:00Z",
starred: false,
shared: false,
tags: ["photography"],
children: ["vacation-2024.jpg", "team-photo.jpg", "product-shots.jpg", "nature-landscape.jpg"]
},
"media/photos/vacation-2024.jpg": {
name: "vacation-2024.jpg",
path: "media/photos/vacation-2024.jpg",
is_dir: false,
size: 3145728,
type: "image",
modified: "2024-12-25T20:00:00Z",
created: "2024-12-25T20:00:00Z",
starred: true,
shared: false,
tags: ["vacation", "memories"]
},
"shared": {
name: "Shared",
path: "shared",
is_dir: true,
size: null,
modified: "2025-01-12T11:20:00Z",
created: "2024-11-01T08:30:00Z",
starred: false,
shared: true,
tags: ["collaboration", "team"],
children: ["team-resources", "client-deliverables", "public-assets"]
}
};
const getFileIcon = (item) => {
if (item.is_dir) {
return item.starred ? <Star className="w-4 h-4 text-primary" /> : <Folder className="w-4 h-4 text-accent" />;
}
const iconMap = {
pdf: <FileText className="w-4 h-4 text-red-500" />,
docx: <FileText className="w-4 h-4 text-blue-500" />,
xlsx: <Database className="w-4 h-4 text-green-500" />,
json: <Code className="w-4 h-4 text-yellow-500" />,
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" />,
zip: <Archive className="w-4 h-4 text-orange-500" />,
yaml: <Code className="w-4 h-4 text-blue-400" />
};
return iconMap[item.type] || <File className="w-4 h-4 text-muted-foreground" />;
};
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 formatDate = (dateString) => {
if (!dateString) return '';
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
const FileTree = ({ onSelect, selectedPath }) => {
const [expanded, setExpanded] = useState({ "": true, "projects": true });
const toggleExpand = (path) => {
setExpanded(prev => ({ ...prev, [path]: !prev[path] }));
onSelect(path);
};
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={path}>
<div
onClick={() => toggleExpand(path)}
className={`flex items-center cursor-pointer hover:bg-accent hover:text-accent-foreground p-2 rounded transition-colors
${isSelected ? 'bg-primary text-primary-foreground' : 'text-foreground'}`}
style={{ paddingLeft: `${level * 16 + 8}px` }}
>
<div className="flex items-center space-x-2 flex-1">
{getFileIcon(item)}
<span className="text-sm font-medium truncate">{item.name}</span>
{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">
{isExpanded ? '▼' : '▶'}
</div>
</div>
{isExpanded && item.children && (
<div>
{item.children.map(childPath => {
const fullPath = path ? `${path}/${childPath}` : childPath;
return renderTreeItem(fullPath, level + 1);
})}
</div>
)}
</div>
);
};
return (
<div className="w-80 border-r border-border bg-card overflow-y-auto">
<div className="p-4 border-b border-border">
<h2 className="text-lg font-semibold text-card-foreground flex items-center">
<Folder className="w-5 h-5 mr-2 text-primary" />
File Explorer
</h2>
</div>
<div className="p-2">
{renderTreeItem("")}
</div>
</div>
);
};
const FileBrowser = ({ path, viewMode, searchTerm, sortBy, filterType }) => {
const currentItem = fileSystemData[path];
const files = useMemo(() => {
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);
// Apply search filter
if (searchTerm) {
items = items.filter(item =>
item.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
(item.tags && item.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase())))
);
}
// Apply type filter
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;
if (filterType === 'shared') return item.shared;
return true;
});
}
// Apply sorting
items.sort((a, b) => {
switch (sortBy) {
case 'name':
return a.name.localeCompare(b.name);
case 'modified':
return new Date(b.modified || 0) - new Date(a.modified || 0);
case 'size':
return (b.size || 0) - (a.size || 0);
case 'type':
return a.type?.localeCompare(b.type || '') || 0;
default:
return 0;
}
});
return items;
}, [currentItem, searchTerm, sortBy, filterType, path]);
if (viewMode === 'grid') {
return (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4 p-4">
{files.map((item) => (
<div key={item.path} className="group relative">
<div className="bg-card border border-border rounded-lg p-4 hover:shadow-md transition-shadow cursor-pointer hover:bg-accent hover:text-accent-foreground">
<div className="flex flex-col items-center space-y-2">
<div className="text-3xl">
{getFileIcon(item)}
</div>
<div className="text-sm font-medium text-center truncate w-full text-card-foreground group-hover:text-accent-foreground">
{item.name}
</div>
{!item.is_dir && (
<div className="text-xs text-muted-foreground">
{formatFileSize(item.size)}
</div>
)}
<div className="flex space-x-1">
{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>
</div>
</div>
))}
</div>
);
}
return (
<div className="overflow-hidden">
<div className="bg-muted border-b border-border p-2">
<div className="grid grid-cols-12 gap-4 text-sm font-medium text-muted-foreground">
<div className="col-span-5">Name</div>
<div className="col-span-2">Modified</div>
<div className="col-span-1">Size</div>
<div className="col-span-2">Type</div>
<div className="col-span-2">Tags</div>
</div>
</div>
<div className="divide-y divide-border">
{files.map((item) => (
<div key={item.path} className="grid grid-cols-12 gap-4 p-3 hover:bg-accent hover:text-accent-foreground cursor-pointer group">
<div className="col-span-5 flex items-center space-x-3">
{getFileIcon(item)}
<span className="font-medium truncate">{item.name}</span>
<div className="flex space-x-1">
{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>
<div className="col-span-2 text-sm text-muted-foreground flex items-center">
{formatDate(item.modified)}
</div>
<div className="col-span-1 text-sm text-muted-foreground flex items-center">
{formatFileSize(item.size)}
</div>
<div className="col-span-2 text-sm text-muted-foreground flex items-center">
{item.is_dir ? 'Folder' : item.type?.toUpperCase() || '—'}
</div>
<div className="col-span-2 flex items-center">
<div className="flex flex-wrap gap-1">
{item.tags?.slice(0, 2).map(tag => (
<span key={tag} className="text-xs bg-secondary text-secondary-foreground px-2 py-1 rounded-full">
{tag}
</span>
))}
{item.tags?.length > 2 && (
<span className="text-xs text-muted-foreground">+{item.tags.length - 2}</span>
)}
</div>
</div>
</div>
))}
</div>
</div>
);
};
const FileOperations = ({ currentPath, onRefresh }) => {
const [showUploadProgress, setShowUploadProgress] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const handleUpload = async () => {
setShowUploadProgress(true);
// Simulate upload progress
for (let i = 0; i <= 100; i += 10) {
setUploadProgress(i);
await new Promise(resolve => setTimeout(resolve, 100));
}
setTimeout(() => {
setShowUploadProgress(false);
setUploadProgress(0);
onRefresh();
}, 500);
};
const createFolder = () => {
const folderName = prompt('Enter folder name:');
if (folderName) {
onRefresh();
}
};
return (
<div className="border-t border-border bg-card p-4">
<div className="flex flex-wrap gap-2">
<button
onClick={handleUpload}
className="flex items-center space-x-2 bg-primary text-primary-foreground px-4 py-2 rounded-md hover:opacity-90 transition-opacity"
>
<Upload className="w-4 h-4" />
<span>Upload Files</span>
</button>
<button
onClick={createFolder}
className="flex items-center space-x-2 bg-secondary text-secondary-foreground px-4 py-2 rounded-md hover:bg-accent hover:text-accent-foreground transition-colors"
>
<Plus className="w-4 h-4" />
<span>New Folder</span>
</button>
<button className="flex items-center space-x-2 bg-secondary text-secondary-foreground px-4 py-2 rounded-md hover:bg-accent hover:text-accent-foreground transition-colors">
<Download className="w-4 h-4" />
<span>Download</span>
</button>
<button className="flex items-center space-x-2 bg-secondary text-secondary-foreground px-4 py-2 rounded-md hover:bg-accent hover:text-accent-foreground transition-colors">
<Share className="w-4 h-4" />
<span>Share</span>
</button>
<button className="flex items-center space-x-2 bg-destructive text-destructive-foreground px-4 py-2 rounded-md hover:opacity-90 transition-opacity">
<Trash2 className="w-4 h-4" />
<span>Delete</span>
</button>
</div>
{showUploadProgress && (
<div className="mt-4">
<div className="flex justify-between text-sm text-muted-foreground mb-1">
<span>Uploading files...</span>
<span>{uploadProgress}%</span>
</div>
<div className="w-full bg-secondary rounded-full h-2">
<div
className="bg-primary h-2 rounded-full transition-all duration-300"
style={{ width: `${uploadProgress}%` }}
/>
</div>
</div>
)}
</div>
);
};
export default function DriveScreen() {
const [currentPath, setCurrentPath] = useState('');
const [refreshKey, setRefreshKey] = useState(0);
const [viewMode, setViewMode] = useState('list');
const [searchTerm, setSearchTerm] = useState('');
const [sortBy, setSortBy] = useState('name');
const [filterType, setFilterType] = useState('all');
const handleRefresh = () => {
setRefreshKey(prev => prev + 1);
};
const currentItem = fileSystemData[currentPath];
const breadcrumbs = currentPath ? currentPath.split('/') : [];
return (
<div className="flex h-screen bg-background text-foreground">
<FileTree onSelect={setCurrentPath} selectedPath={currentPath} />
<div className="flex-1 flex flex-col overflow-hidden">
{/* Header */}
<div className="border-b border-border bg-card p-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-2">
<h1 className="text-2xl font-bold text-card-foreground">
{currentItem?.name || 'Root'}
</h1>
{currentItem?.starred && <Star className="w-5 h-5 text-yellow-500 fill-current" />}
{currentItem?.shared && <Users className="w-5 h-5 text-blue-500" />}
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => setViewMode(viewMode === 'list' ? 'grid' : 'list')}
className="p-2 hover:bg-accent hover:text-accent-foreground rounded-md transition-colors"
>
{viewMode === 'list' ? <Grid className="w-4 h-4" /> : <List className="w-4 h-4" />}
</button>
</div>
</div>
{/* Breadcrumbs */}
<div className="flex items-center space-x-2 text-sm text-muted-foreground mb-4">
<button
onClick={() => setCurrentPath('')}
className="hover:text-foreground transition-colors"
>
Home
</button>
{breadcrumbs.map((crumb, index) => {
const path = breadcrumbs.slice(0, index + 1).join('/');
return (
<React.Fragment key={path}>
<span>/</span>
<button
onClick={() => setCurrentPath(path)}
className="hover:text-foreground transition-colors"
>
{crumb}
</button>
</React.Fragment>
);
})}
</div>
{/* Search and Filters */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<input
type="text"
placeholder="Search files and folders..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 pr-4 py-2 w-64 bg-input border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-ring text-foreground placeholder:text-muted-foreground"
/>
</div>
<select
value={filterType}
onChange={(e) => setFilterType(e.target.value)}
className="px-3 py-2 bg-input border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-ring text-foreground"
>
<option value="all">All Items</option>
<option value="folders">Folders</option>
<option value="files">Files</option>
<option value="starred">Starred</option>
<option value="shared">Shared</option>
</select>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
className="px-3 py-2 bg-input border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-ring text-foreground"
>
<option value="name">Sort by Name</option>
<option value="modified">Sort by Modified</option>
<option value="size">Sort by Size</option>
<option value="type">Sort by Type</option>
</select>
</div>
</div>
</div>
{/* File Browser */}
<div className="flex-1 overflow-y-auto bg-background">
<FileBrowser
key={`${currentPath}-${refreshKey}`}
path={currentPath}
viewMode={viewMode}
searchTerm={searchTerm}
sortBy={sortBy}
filterType={filterType}
/>
</div>
{/* File Operations */}
<FileOperations currentPath={currentPath} onRefresh={handleRefresh} />
</div>
</div>
);
}