feat: Implement file browser and operations components with enhanced UI
Some checks failed
GBCI / build (push) Failing after 4m39s
Some checks failed
GBCI / build (push) Failing after 4m39s
- 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.
This commit is contained in:
parent
1d458886dd
commit
6e87c5b449
12 changed files with 4235 additions and 38 deletions
|
@ -9,7 +9,7 @@ const examples = [
|
|||
{ name: "Chat", href: "/chat", color: "#25D366" }, // WhatsApp green
|
||||
{ name: "Dashboard", href: "/dashboard", color: "#6366F1" }, // Indigo
|
||||
{ name: "Mail", href: "/mail", color: "#FFD700" }, // Outlook yellow
|
||||
{ name: "Tree", href: "/tree", color: "#10B981" }, // Emerald green
|
||||
{ name: "Drive", href: "/drive", color: "#10B981" }, // Emerald green
|
||||
{ name: "Editor", href: "/editor", color: "#2563EB" }, // Word blue
|
||||
{ name: "Tables", href: "/table", color: "#8B5CF6" }, // Purple
|
||||
{ name: "Meet", href: "/meet", color: "#059669" }, // Google Meet green
|
||||
|
|
585
app/drive/page.tsx
Normal file
585
app/drive/page.tsx
Normal file
|
@ -0,0 +1,585 @@
|
|||
"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>
|
||||
);
|
||||
}
|
|
@ -1,8 +1,917 @@
|
|||
'use client'
|
||||
"use client";
|
||||
|
||||
export default function MainPage() {
|
||||
return (
|
||||
<div className="flex flex-col h-screen bg-gray-50">
|
||||
</div>
|
||||
)
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useEditor, EditorContent, BubbleMenu } from '@tiptap/react';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import TextStyle from '@tiptap/extension-text-style';
|
||||
import FontFamily from '@tiptap/extension-font-family';
|
||||
import Color from '@tiptap/extension-color';
|
||||
import Highlight from '@tiptap/extension-highlight';
|
||||
import TextAlign from '@tiptap/extension-text-align';
|
||||
import Link from '@tiptap/extension-link';
|
||||
import Image from '@tiptap/extension-image';
|
||||
import Underline from '@tiptap/extension-underline';
|
||||
import Table from '@tiptap/extension-table';
|
||||
import TableCell from '@tiptap/extension-table-cell';
|
||||
import TableHeader from '@tiptap/extension-table-header';
|
||||
import TableRow from '@tiptap/extension-table-row';
|
||||
import {
|
||||
Bold, Italic, Underline as UnderlineIcon, AlignLeft, AlignCenter, AlignRight, AlignJustify,
|
||||
Link as LinkIcon, Image as ImageIcon, Save, FileText, Printer, Table as TableIcon,
|
||||
Plus, Minus, Trash2, Type, Palette, Highlighter, FileImage, File, Home,
|
||||
View, ChevronDown, Search,
|
||||
Undo, Redo, Copy, MoreVertical
|
||||
} from 'lucide-react';
|
||||
|
||||
const RibbonTab = ({ label, isActive, onClick, children }) => (
|
||||
<div className="ribbon-tab">
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`ribbon-tab-button ${isActive ? 'active' : ''}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
{isActive && (
|
||||
<div className="ribbon-content">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const RibbonGroup = ({ title, children }) => (
|
||||
<div className="ribbon-group">
|
||||
<div className="ribbon-group-content">
|
||||
{children}
|
||||
</div>
|
||||
<div className="ribbon-group-title">{title}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const RibbonButton = ({ icon: Icon, label, onClick, isActive, size = 'medium', dropdown = false }) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`ribbon-button ${size} ${isActive ? 'active' : ''}`}
|
||||
title={label}
|
||||
>
|
||||
<div className="ribbon-button-content">
|
||||
<Icon size={size === 'large' ? 24 : 16} />
|
||||
{size === 'large' && <span className="ribbon-button-label">{label}</span>}
|
||||
{dropdown && <ChevronDown size={12} className="dropdown-arrow" />}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
export default function RibbonWordClone() {
|
||||
const [fileName, setFileName] = useState('Document 1');
|
||||
const [fontSize, setFontSize] = useState('12');
|
||||
const [fontFamily, setFontFamily] = useState('Calibri');
|
||||
const [textColor, setTextColor] = useState('#000000');
|
||||
const [highlightColor, setHighlightColor] = useState('#ffff00');
|
||||
const [activeTab, setActiveTab] = useState('home');
|
||||
const [zoom, setZoom] = useState(100);
|
||||
const [pages, setPages] = useState([1]);
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
TextStyle,
|
||||
FontFamily,
|
||||
Color,
|
||||
Highlight.configure({ multicolor: true }),
|
||||
TextAlign.configure({ types: ['heading', 'paragraph', 'tableCell'] }),
|
||||
Link.configure({ openOnClick: false }),
|
||||
Image,
|
||||
Underline,
|
||||
Table.configure({
|
||||
resizable: true,
|
||||
HTMLAttributes: {
|
||||
class: 'editor-table',
|
||||
},
|
||||
}),
|
||||
TableRow,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
],
|
||||
content: `
|
||||
<h1 style="text-align: center; font-size: 24px; margin-bottom: 20px;">${fileName}</h1>
|
||||
<p><br></p>
|
||||
<p>Start typing your document here...</p>
|
||||
<p><br></p>
|
||||
`,
|
||||
onUpdate: ({ editor }) => {
|
||||
// Simulate multiple pages based on content height
|
||||
const element = document.querySelector('.ProseMirror');
|
||||
if (element) {
|
||||
const contentHeight = element.scrollHeight;
|
||||
const pageHeight = 1123; // A4 height in pixels at 96 DPI
|
||||
const neededPages = Math.max(1, Math.ceil(contentHeight / pageHeight));
|
||||
if (neededPages !== pages.length) {
|
||||
setPages(Array.from({ length: neededPages }, (_, i) => i + 1));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const addImage = () => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
const handleImageUpload = (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file && editor) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const imageUrl = event.target?.result;
|
||||
editor.chain().focus().setImage({ src: imageUrl }).run();
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const saveDocument = () => {
|
||||
if (editor) {
|
||||
const content = editor.getHTML();
|
||||
const blob = new Blob([content], { type: 'text/html' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${fileName}.html`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
};
|
||||
|
||||
if (!editor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="word-clone">
|
||||
<style jsx global>{`
|
||||
:root {
|
||||
/* 3DBevel Theme */
|
||||
--background: 0 0% 80%;
|
||||
--foreground: 0 0% 10%;
|
||||
--card: 0 0% 75%;
|
||||
--card-foreground: 0 0% 10%;
|
||||
--popover: 0 0% 80%;
|
||||
--popover-foreground: 0 0% 10%;
|
||||
--primary: 210 80% 40%;
|
||||
--primary-foreground: 0 0% 80%;
|
||||
--secondary: 0 0% 70%;
|
||||
--secondary-foreground: 0 0% 10%;
|
||||
--muted: 0 0% 65%;
|
||||
--muted-foreground: 0 0% 30%;
|
||||
--accent: 30 80% 40%;
|
||||
--accent-foreground: 0 0% 80%;
|
||||
--destructive: 0 85% 60%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 70%;
|
||||
--input: 0 0% 70%;
|
||||
--ring: 210 80% 40%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.word-clone {
|
||||
min-height: 100vh;
|
||||
background: hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
/* Title Bar */
|
||||
.title-bar {
|
||||
background: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 2px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.title-bar h1 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.title-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.title-input {
|
||||
background: hsl(var(--input));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--radius);
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
/* Quick Access Toolbar */
|
||||
.quick-access {
|
||||
background: hsl(var(--card));
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
padding: 4px 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.quick-access-btn {
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
color: hsl(var(--foreground));
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.quick-access-btn:hover {
|
||||
background: hsl(var(--muted));
|
||||
border-color: hsl(var(--border));
|
||||
}
|
||||
|
||||
/* Ribbon */
|
||||
.ribbon {
|
||||
background: hsl(var(--card));
|
||||
border-bottom: 2px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.ribbon-tabs {
|
||||
display: flex;
|
||||
background: hsl(var(--muted));
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.ribbon-tab-button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.ribbon-tab-button:hover {
|
||||
background: hsl(var(--secondary));
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.ribbon-tab-button.active {
|
||||
background: hsl(var(--card));
|
||||
color: hsl(var(--foreground));
|
||||
border-bottom-color: hsl(var(--primary));
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ribbon-content {
|
||||
display: flex;
|
||||
padding: 8px;
|
||||
gap: 2px;
|
||||
min-height: 80px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.ribbon-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid hsl(var(--border));
|
||||
padding-right: 8px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.ribbon-group:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.ribbon-group-content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
align-items: flex-start;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.ribbon-group-title {
|
||||
font-size: 10px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
text-align: center;
|
||||
margin-top: 4px;
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.ribbon-button {
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
color: hsl(var(--foreground));
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ribbon-button:hover {
|
||||
background: hsl(var(--muted));
|
||||
border-color: hsl(var(--border));
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.ribbon-button.active {
|
||||
background: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
border-color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.ribbon-button.medium {
|
||||
padding: 6px;
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.ribbon-button.large {
|
||||
padding: 8px;
|
||||
min-width: 48px;
|
||||
min-height: 48px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ribbon-button-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.ribbon-button-label {
|
||||
font-size: 10px;
|
||||
text-align: center;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
right: 2px;
|
||||
}
|
||||
|
||||
/* Format Controls */
|
||||
.format-select {
|
||||
background: hsl(var(--input));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 3px;
|
||||
padding: 4px 6px;
|
||||
font-size: 11px;
|
||||
color: hsl(var(--foreground));
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.color-picker-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.color-picker {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.color-indicator {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 16px;
|
||||
height: 3px;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
/* Editor Area */
|
||||
.editor-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
background: hsl(var(--muted));
|
||||
}
|
||||
|
||||
.editor-sidebar {
|
||||
width: 200px;
|
||||
background: hsl(var(--card));
|
||||
border-right: 1px solid hsl(var(--border));
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.editor-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
max-height: calc(100vh - 200px);
|
||||
}
|
||||
|
||||
.pages-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
transform: scale(${zoom / 100});
|
||||
transform-origin: top center;
|
||||
}
|
||||
|
||||
.page {
|
||||
width: 210mm;
|
||||
min-height: 297mm;
|
||||
background: white;
|
||||
box-shadow:
|
||||
0 0 0 1px hsl(var(--border)),
|
||||
0 4px 8px rgba(0,0,0,0.1),
|
||||
0 8px 16px rgba(0,0,0,0.05);
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-number {
|
||||
position: absolute;
|
||||
top: -30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 12px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
background: hsl(var(--background));
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
padding: 25mm;
|
||||
min-height: 247mm;
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
outline: none;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.ProseMirror img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ProseMirror a {
|
||||
color: hsl(var(--primary));
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Table styles */
|
||||
.editor-table {
|
||||
border-collapse: collapse;
|
||||
margin: 16px 0;
|
||||
width: 100%;
|
||||
border: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.editor-table td,
|
||||
.editor-table th {
|
||||
border: 1px solid hsl(var(--border));
|
||||
padding: 8px 12px;
|
||||
min-width: 50px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.editor-table th {
|
||||
background: hsl(var(--muted));
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Bubble Menu */
|
||||
.bubble-menu {
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
display: flex;
|
||||
padding: 4px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.bubble-menu .ribbon-button {
|
||||
min-width: 28px;
|
||||
min-height: 28px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
/* Status Bar */
|
||||
.status-bar {
|
||||
background: hsl(var(--card));
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
padding: 4px 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 11px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.zoom-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.zoom-slider {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.title-bar,
|
||||
.quick-access,
|
||||
.ribbon,
|
||||
.editor-sidebar,
|
||||
.status-bar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.editor-main {
|
||||
padding: 0;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.pages-container {
|
||||
transform: none;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.page {
|
||||
box-shadow: none;
|
||||
margin: 0;
|
||||
break-after: page;
|
||||
}
|
||||
|
||||
.page-number {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
|
||||
{/* Quick Access Toolbar */}
|
||||
<div className="quick-access">
|
||||
<button className="quick-access-btn" onClick={() => editor.chain().focus().undo().run()}>
|
||||
<Undo size={14} />
|
||||
</button>
|
||||
<button className="quick-access-btn" onClick={() => editor.chain().focus().redo().run()}>
|
||||
<Redo size={14} />
|
||||
</button>
|
||||
<button className="quick-access-btn" onClick={saveDocument}>
|
||||
<Save size={14} />
|
||||
</button>
|
||||
<div className="title-controls">
|
||||
<input
|
||||
type="text"
|
||||
value={fileName}
|
||||
onChange={(e) => setFileName(e.target.value)}
|
||||
className="title-input"
|
||||
placeholder="Document name"
|
||||
/>
|
||||
<button className="quick-access-btn" onClick={saveDocument}>
|
||||
<Save size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ribbon */}
|
||||
<div className="ribbon">
|
||||
<div className="ribbon-tabs">
|
||||
<button
|
||||
className={`ribbon-tab-button ${activeTab === 'home' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('home')}
|
||||
>
|
||||
Home
|
||||
</button>
|
||||
<button
|
||||
className={`ribbon-tab-button ${activeTab === 'insert' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('insert')}
|
||||
>
|
||||
Insert
|
||||
</button>
|
||||
<button
|
||||
className={`ribbon-tab-button ${activeTab === 'layout' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('layout')}
|
||||
>
|
||||
Layout
|
||||
</button>
|
||||
<button
|
||||
className={`ribbon-tab-button ${activeTab === 'view' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('view')}
|
||||
>
|
||||
View
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{activeTab === 'home' && (
|
||||
<div className="ribbon-content">
|
||||
<RibbonGroup title="Clipboard">
|
||||
<RibbonButton
|
||||
icon={Copy}
|
||||
label="Copy"
|
||||
size="large"
|
||||
onClick={() => document.execCommand('copy')} isActive={undefined} />
|
||||
<RibbonButton
|
||||
icon={Copy}
|
||||
label="Paste"
|
||||
size="large"
|
||||
onClick={() => document.execCommand('paste')} isActive={undefined} />
|
||||
<RibbonButton
|
||||
icon={Copy}
|
||||
label="Cut"
|
||||
size="medium"
|
||||
onClick={() => document.execCommand('cut')} isActive={undefined} />
|
||||
</RibbonGroup>
|
||||
|
||||
<RibbonGroup title="Font">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||
<div style={{ display: 'flex', gap: '4px' }}>
|
||||
<select
|
||||
value={fontFamily}
|
||||
onChange={(e) => {
|
||||
setFontFamily(e.target.value);
|
||||
editor.chain().focus().setFontFamily(e.target.value).run();
|
||||
}}
|
||||
className="format-select"
|
||||
style={{ width: '120px' }}
|
||||
>
|
||||
<option value="Calibri">Calibri</option>
|
||||
<option value="Arial">Arial</option>
|
||||
<option value="Times New Roman">Times New Roman</option>
|
||||
<option value="Georgia">Georgia</option>
|
||||
<option value="Verdana">Verdana</option>
|
||||
</select>
|
||||
<select
|
||||
value={fontSize}
|
||||
onChange={(e) => {
|
||||
setFontSize(e.target.value);
|
||||
editor.chain().focus().setFontSize(e.target.value + 'pt').run();
|
||||
}}
|
||||
className="format-select"
|
||||
style={{ width: '60px' }}
|
||||
>
|
||||
<option value="8">8</option>
|
||||
<option value="9">9</option>
|
||||
<option value="10">10</option>
|
||||
<option value="11">11</option>
|
||||
<option value="12">12</option>
|
||||
<option value="14">14</option>
|
||||
<option value="16">16</option>
|
||||
<option value="18">18</option>
|
||||
<option value="20">20</option>
|
||||
<option value="24">24</option>
|
||||
<option value="28">28</option>
|
||||
<option value="36">36</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '2px' }}>
|
||||
<RibbonButton
|
||||
icon={Bold}
|
||||
label="Bold"
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
isActive={editor.isActive('bold')}
|
||||
/>
|
||||
<RibbonButton
|
||||
icon={Italic}
|
||||
label="Italic"
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
isActive={editor.isActive('italic')}
|
||||
/>
|
||||
<RibbonButton
|
||||
icon={UnderlineIcon}
|
||||
label="Underline"
|
||||
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
||||
isActive={editor.isActive('underline')}
|
||||
/>
|
||||
<div className="color-picker-wrapper">
|
||||
<RibbonButton icon={Type} label="Text Color" onClick={undefined} isActive={undefined} />
|
||||
<input
|
||||
type="color"
|
||||
value={textColor}
|
||||
onChange={(e) => {
|
||||
setTextColor(e.target.value);
|
||||
editor.chain().focus().setColor(e.target.value).run();
|
||||
}}
|
||||
className="color-picker"
|
||||
/>
|
||||
<div
|
||||
className="color-indicator"
|
||||
style={{ backgroundColor: textColor }}
|
||||
/>
|
||||
</div>
|
||||
<div className="color-picker-wrapper">
|
||||
<RibbonButton icon={Highlighter} label="Highlight" onClick={undefined} isActive={undefined} />
|
||||
<input
|
||||
type="color"
|
||||
value={highlightColor}
|
||||
onChange={(e) => {
|
||||
setHighlightColor(e.target.value);
|
||||
editor.chain().focus().setHighlight({ color: e.target.value }).run();
|
||||
}}
|
||||
className="color-picker"
|
||||
/>
|
||||
<div
|
||||
className="color-indicator"
|
||||
style={{ backgroundColor: highlightColor }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</RibbonGroup>
|
||||
|
||||
<RibbonGroup title="Paragraph">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||
<div style={{ display: 'flex', gap: '2px' }}>
|
||||
<RibbonButton
|
||||
icon={AlignLeft}
|
||||
label="Align Left"
|
||||
onClick={() => editor.chain().focus().setTextAlign('left').run()}
|
||||
isActive={editor.isActive({ textAlign: 'left' })}
|
||||
/>
|
||||
<RibbonButton
|
||||
icon={AlignCenter}
|
||||
label="Center"
|
||||
onClick={() => editor.chain().focus().setTextAlign('center').run()}
|
||||
isActive={editor.isActive({ textAlign: 'center' })}
|
||||
/>
|
||||
<RibbonButton
|
||||
icon={AlignRight}
|
||||
label="Align Right"
|
||||
onClick={() => editor.chain().focus().setTextAlign('right').run()}
|
||||
isActive={editor.isActive({ textAlign: 'right' })}
|
||||
/>
|
||||
<RibbonButton
|
||||
icon={AlignJustify}
|
||||
label="Justify"
|
||||
onClick={() => editor.chain().focus().setTextAlign('justify').run()}
|
||||
isActive={editor.isActive({ textAlign: 'justify' })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</RibbonGroup>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'insert' && (
|
||||
<div className="ribbon-content">
|
||||
<RibbonGroup title="Illustrations">
|
||||
<RibbonButton
|
||||
icon={ImageIcon}
|
||||
label="Picture"
|
||||
size="large"
|
||||
onClick={addImage} isActive={undefined} />
|
||||
</RibbonGroup>
|
||||
|
||||
<RibbonGroup title="Tables">
|
||||
<RibbonButton
|
||||
icon={TableIcon}
|
||||
label="Table"
|
||||
size="large"
|
||||
onClick={() => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()} isActive={undefined} />
|
||||
</RibbonGroup>
|
||||
|
||||
<RibbonGroup title="Links">
|
||||
<RibbonButton
|
||||
icon={LinkIcon}
|
||||
label="Link"
|
||||
size="large"
|
||||
onClick={() => {
|
||||
const previousUrl = editor.getAttributes('link').href;
|
||||
const url = window.prompt('URL', previousUrl);
|
||||
if (url === null) return;
|
||||
if (url === '') {
|
||||
editor.chain().focus().extendMarkRange('link').unsetLink().run();
|
||||
return;
|
||||
}
|
||||
editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
|
||||
}}
|
||||
isActive={editor.isActive('link')}
|
||||
/>
|
||||
</RibbonGroup>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'view' && (
|
||||
<div className="ribbon-content">
|
||||
<RibbonGroup title="Zoom">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' }}>
|
||||
<div style={{ fontSize: '14px', fontWeight: '600' }}>{zoom}%</div>
|
||||
<input
|
||||
type="range"
|
||||
min="50"
|
||||
max="200"
|
||||
value={zoom}
|
||||
onChange={(e) => setZoom(parseInt(e.target.value))}
|
||||
className="zoom-slider"
|
||||
/>
|
||||
</div>
|
||||
</RibbonGroup>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Editor Area */}
|
||||
<div className="editor-container">
|
||||
<div className="editor-main">
|
||||
<div className="pages-container">
|
||||
{pages.map((pageNum, index) => (
|
||||
<div key={pageNum} className="page">
|
||||
<div className="page-number">Page {pageNum}</div>
|
||||
<div className="page-content">
|
||||
{index === 0 && <EditorContent editor={editor} />}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bubble Menu */}
|
||||
{editor && (
|
||||
<BubbleMenu editor={editor}>
|
||||
<div className="bubble-menu">
|
||||
<RibbonButton
|
||||
icon={Bold}
|
||||
label="Bold"
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
isActive={editor.isActive('bold')}
|
||||
/>
|
||||
<RibbonButton
|
||||
icon={Italic}
|
||||
label="Italic"
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
isActive={editor.isActive('italic')}
|
||||
/>
|
||||
<RibbonButton
|
||||
icon={UnderlineIcon}
|
||||
label="Underline"
|
||||
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
||||
isActive={editor.isActive('underline')}
|
||||
/>
|
||||
<RibbonButton
|
||||
icon={LinkIcon}
|
||||
label="Link"
|
||||
onClick={() => {
|
||||
const previousUrl = editor.getAttributes('link').href;
|
||||
const url = window.prompt('URL', previousUrl);
|
||||
if (url === null) return;
|
||||
if (url === '') {
|
||||
editor.chain().focus().extendMarkRange('link').unsetLink().run();
|
||||
return;
|
||||
}
|
||||
editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
|
||||
}}
|
||||
isActive={editor.isActive('link')}
|
||||
/>
|
||||
</div>
|
||||
</BubbleMenu>
|
||||
)}
|
||||
|
||||
{/* Status Bar */}
|
||||
<div className="status-bar">
|
||||
<div>
|
||||
Page {pages.length} of {pages.length} | Words: {editor.storage.characterCount?.words() || 0}
|
||||
</div>
|
||||
<div className="zoom-controls">
|
||||
<button onClick={() => setZoom(Math.max(50, zoom - 10))}>-</button>
|
||||
<span>{zoom}%</span>
|
||||
<button onClick={() => setZoom(Math.min(200, zoom + 10))}>+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hidden file input */}
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleImageUpload}
|
||||
accept="image/*"
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
"use client";
|
||||
import React, { useState } from 'react';
|
||||
import { UserAuthForm } from '../components/user-auth-form';
|
||||
|
||||
const AuthenticationScreen = () => {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
|
@ -53,7 +52,6 @@ const AuthenticationScreen = () => {
|
|||
<p>Enter your email below to create your account</p>
|
||||
</div>
|
||||
|
||||
<UserAuthForm />
|
||||
|
||||
<p className="auth-terms">
|
||||
By clicking continue, you agree to our Terms of Service and Privacy Policy.
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
|
||||
Lotus 123.
|
||||
https://docs.sheetjs.com/docs/demos/desktop/tauri
|
|
@ -1,26 +0,0 @@
|
|||
"use client";
|
||||
import { useState } from 'react';
|
||||
import { FileTree } from './components/FileTree';
|
||||
import { FileBrowser } from './components/FileBrowser';
|
||||
import { FileOperations } from './components/FileOperations';
|
||||
|
||||
// TODO: XTREE like keyword.
|
||||
|
||||
export default function DriveScreen() {
|
||||
const [currentPath, setCurrentPath] = useState('');
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
|
||||
const handleRefresh = () => {
|
||||
setRefreshKey(prev => prev + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-white">
|
||||
<FileTree onSelect={setCurrentPath} />
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<FileBrowser key={`${currentPath}-${refreshKey}`} path={currentPath} />
|
||||
<FileOperations currentPath={currentPath} onRefresh={handleRefresh} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
17
package.json
17
package.json
|
@ -42,6 +42,22 @@
|
|||
"@tailwindcss/vite": "4.0.17",
|
||||
"@tauri-apps/api": "2.4.0",
|
||||
"@tauri-apps/plugin-opener": "2",
|
||||
"@tiptap/extension-color": "^2.22.3",
|
||||
"@tiptap/extension-font-family": "^2.22.3",
|
||||
"@tiptap/extension-highlight": "^2.22.3",
|
||||
"@tiptap/extension-image": "^2.22.3",
|
||||
"@tiptap/extension-link": "^2.22.3",
|
||||
"@tiptap/extension-table": "^2.22.3",
|
||||
"@tiptap/extension-table-cell": "^2.22.3",
|
||||
"@tiptap/extension-table-header": "^2.22.3",
|
||||
"@tiptap/extension-table-row": "^2.22.3",
|
||||
"@tiptap/extension-text-align": "^2.22.3",
|
||||
"@tiptap/extension-text-style": "^2.22.3",
|
||||
"@tiptap/extension-underline": "^2.22.3",
|
||||
"@tiptap/pm": "^2.22.3",
|
||||
"@tiptap/react": "^2.22.3",
|
||||
"@tiptap/starter-kit": "^2.22.3",
|
||||
"@umoteam/editor": "^7.0.1",
|
||||
"@zitadel/react": "1.0.5",
|
||||
"autoprefixer": "10.4.17",
|
||||
"botframework-directlinejs": "0.15.1",
|
||||
|
@ -64,6 +80,7 @@
|
|||
"react-day-picker": "^9.7.0",
|
||||
"react-dom": "18.3.1",
|
||||
"react-hook-form": "^7.58.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"recharts": "^2.15.4",
|
||||
|
|
2717
pnpm-lock.yaml
generated
2717
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue