veza/apps/web/src/components/studio/ProjectsManager.tsx

323 lines
11 KiB
TypeScript
Raw Normal View History

import React, { useState, useEffect } from 'react';
import { Card } from '../ui/card';
import { Button } from '../ui/button';
import { Badge } from '../ui/badge';
import { SearchInput } from '../ui/input';
import {
MoreVertical,
Plus,
LayoutGrid,
List,
Loader2,
AlertCircle,
} from 'lucide-react';
import { useToast } from '../../components/feedback/ToastProvider';
import { CreateProjectModal } from './projects/CreateProjectModal';
import { ProjectDetailView } from './projects/ProjectDetailView';
import { projectService, Project } from '../../services/projectService';
import { logger } from '@/utils/logger';
export const ProjectsManager: React.FC = () => {
const { addToast } = useToast();
const [viewState, setViewState] = useState<'list' | 'detail'>('list');
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(
null,
);
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState('All');
const [search, setSearch] = useState('');
const [showCreateModal, setShowCreateModal] = useState(false);
useEffect(() => {
loadProjects();
}, []);
const loadProjects = async () => {
try {
setLoading(true);
const response = await projectService.list();
setProjects(response.projects || []);
} catch (error) {
logger.error('Failed to load projects', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
// Fallback for demo if API fails
setProjects([
{
id: 'p1',
name: 'Neon Genesis',
daw: 'Ableton',
bpm: 128,
key: 'C Min',
status: 'In Progress',
collaborators: 2,
modified: '2h ago',
progress: 65,
},
{
id: 'p2',
name: 'Night City Drift',
daw: 'FL Studio',
bpm: 140,
key: 'F# Min',
status: 'Mixing',
collaborators: 0,
modified: '1d ago',
progress: 80,
},
{
id: 'p3',
name: 'Mainframe Breach',
daw: 'Logic Pro',
bpm: 174,
key: 'D Maj',
status: 'Mastering',
collaborators: 1,
modified: '3d ago',
progress: 95,
},
]);
} finally {
setLoading(false);
}
};
// --- Actions ---
const handleCreate = async (newProjectData: any) => {
try {
const newProject = await projectService.create(newProjectData);
setProjects([newProject, ...projects]);
addToast(`Project "${newProject.name}" created`, 'success');
} catch (e) {
// Fallback
const mockProject = { id: `p-${Date.now()}`, ...newProjectData };
setProjects([mockProject, ...projects]);
addToast('Project created (Offline Mode)', 'success');
}
};
const handleUpdate = async (updatedProject: any) => {
try {
await projectService.update(updatedProject.id, updatedProject);
setProjects((prev) =>
prev.map((p) => (p.id === updatedProject.id ? updatedProject : p)),
);
} catch (e) {
setProjects((prev) =>
prev.map((p) => (p.id === updatedProject.id ? updatedProject : p)),
);
}
};
const handleDelete = async (id: string) => {
try {
await projectService.delete(id);
setProjects((prev) => prev.filter((p) => p.id !== id));
setViewState('list');
setSelectedProjectId(null);
addToast('Project deleted', 'info');
} catch (e) {
addToast('Failed to delete project', 'error');
}
};
const openProject = (id: string) => {
setSelectedProjectId(id);
setViewState('detail');
};
// --- Filtering ---
const filteredProjects = projects.filter((p) => {
const matchesSearch = p.name.toLowerCase().includes(search.toLowerCase());
const matchesFilter = filter === 'All' || p.daw === filter;
return matchesSearch && matchesFilter;
});
const selectedProject = projects.find((p) => p.id === selectedProjectId);
// --- Render Detail View ---
if (viewState === 'detail' && selectedProject) {
return (
<ProjectDetailView
project={selectedProject}
onBack={() => setViewState('list')}
onUpdate={handleUpdate}
onDelete={handleDelete}
/>
);
}
// --- Render List View ---
return (
<div className="space-y-6 animate-fadeIn pb-20">
<div className="flex flex-col md:flex-row justify-between items-end gap-4">
<div>
<h2 className="text-2xl font-display font-bold text-white mb-2">
ACTIVE PROJECTS
</h2>
<p className="text-kodo-content-dim font-mono text-sm">
Track progress across all your workstations.
</p>
</div>
<Button
variant="primary"
icon={<Plus className="w-4 h-4" />}
onClick={() => setShowCreateModal(true)}
>
NEW PROJECT
</Button>
</div>
{/* Filter Bar */}
<div className="flex flex-col md:flex-row gap-4 items-center bg-kodo-ink/50 p-4 rounded-xl border border-kodo-steel/50">
<div className="w-full md:w-72">
<SearchInput
placeholder="Search projects..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
<div className="flex gap-2 overflow-x-auto w-full md:w-auto pb-2 md:pb-0">
{['All', 'Ableton', 'FL Studio', 'Logic Pro'].map((daw) => (
<button
key={daw}
className={`px-4 py-1.5 rounded-lg text-xs font-bold uppercase tracking-wider transition-colors border ${filter === daw ? 'bg-kodo-cyan text-black border-kodo-cyan' : 'bg-kodo-slate text-kodo-content-dim border-transparent hover:border-kodo-steel'}`}
onClick={() => setFilter(daw)}
>
{daw}
</button>
))}
</div>
<div className="ml-auto flex gap-2">
<div className="bg-kodo-void p-1 rounded-lg border border-kodo-steel flex">
<Button variant="secondary" size="icon" className="p-1.5">
<LayoutGrid className="w-4 h-4" />
</Button>
<Button variant="ghost" size="icon" className="p-1.5">
<List className="w-4 h-4" />
</Button>
</div>
</div>
</div>
{/* Projects Grid */}
{loading ? (
<div className="flex justify-center py-24">
aesthetic-improvements: reduce decorative cyan across multiple component categories (80/20 rule, batch 11) - Social: FeedView, ConnectionsView, GroupsView, ExploreView, GroupDetailView loading spinners and decorative text, CreatePostModal decorative select text and hashtag links, PostCard decorative tag links and waveform bars and view comments link, CreateGroupModal decorative icon (9 instances) - Settings: DataExportModal decorative icon, LoginHistory decorative IP text, AppearanceSettingsView decorative icon and selected theme checkmark, BackupsView decorative icon, CloudIntegrationView decorative icon, AccessibilitySettingsView decorative icon, SecuritySettings decorative icon, PasskeyModal decorative icon and loading spinner (8 instances) - Studio: ProjectsManager loading spinner and progress percentage text, CloudFileBrowser decorative music icons, AIToolsView decorative music icon, ConnectivityView decorative icon, CreateProjectModal decorative icon, CloudSettingsView decorative icon (6 instances) - Admin: AdminDashboardView loading spinner and decorative chart bars and icon, AdminSettingsView decorative icon, AdminModerationView loading spinner, AdminUsersView loading spinner (5 instances) - Inventory: InventoryView loading spinner, EquipmentCard decorative price icon, EquipmentDetailView loading spinner and decorative icons and price text, AddEquipmentView decorative icon (5 instances) - Seller: CreateProductView decorative icon, SellerDashboardView loading spinner and decorative icon and sales text (3 instances) - Live: LiveStreamDetailView decorative streamer name text (1 instance) - Developer: DeveloperDashboardView loading spinner, WebhooksView decorative icon (2 instances) - Upload: BulkUploadModal decorative icon, FilePreviewCard decorative audio file icon, MetadataForm decorative button text, CoverArtUploadModal decorative icon, LyricsEditorModal decorative icon (5 instances) - Notifications: NotificationItem decorative follow icon and mark as read button, NotificationBell decorative mark all read link (3 instances) - Total: ~46 files, ~46 instances replaced - Preserved: Active/selected states (CloudFileBrowser selected files checkmarks, CreatePostModal post type active state, GroupCard/GroupDetailView public/private badges - semantic indicators, DataExportModal checkbox accents - focus/interaction, AppearanceSettingsView selected theme - active state, PasskeyModal checkbox accent - focus/interaction, LyricsEditorModal checkbox accent - focus/interaction, FileUploadZone drag active state - active state, EquipmentDetailView support link - functional link, FlashSaleModal link - functional link, EquipmentDetailView image indicator dots - active state), primary actions, design system variants - Action 11.3.1.3 in progress (eleventh batch: social, settings, studio, admin, inventory, seller, live, developer, upload, notifications components)
2026-01-16 10:26:33 +00:00
<Loader2 className="w-8 h-8 text-kodo-steel animate-spin" />
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{filteredProjects.map((project) => (
<Card
key={project.id}
variant="glass"
className="group cursor-pointer hover:border-kodo-steel/50 transition-colors"
onClick={() => openProject(project.id)}
>
<div className="flex justify-between items-start mb-4">
<Badge
label={project.daw}
variant={
project.daw === 'Ableton'
? 'cyan'
: project.daw === 'FL Studio'
? 'gold'
: 'magenta'
}
/>
<div
onClick={(e) => {
e.stopPropagation();
addToast('Options menu');
}}
>
<MoreVertical className="w-4 h-4 text-kodo-content-dim hover:text-white" />
</div>
</div>
<h3 className="text-xl font-bold text-white mb-1 group-hover:text-white transition-colors truncate">
{project.name}
</h3>
<p className="text-xs text-kodo-content-dim mb-4 font-mono">
Last edited {project.modified}
</p>
<div className="grid grid-cols-2 gap-2 mb-4">
<div className="bg-white/5 p-2 rounded text-center border border-white/5">
<div className="text-[10px] text-kodo-content-dim uppercase font-bold">
BPM
</div>
<div className="font-bold text-white">{project.bpm}</div>
</div>
<div className="bg-white/5 p-2 rounded text-center border border-white/5">
<div className="text-[10px] text-kodo-content-dim uppercase font-bold">
Key
</div>
<div className="font-bold text-white">{project.key}</div>
</div>
</div>
<div className="mb-4">
<div className="flex justify-between text-xs mb-1">
<span className="text-kodo-content-dim font-bold">
{project.status}
</span>
aesthetic-improvements: reduce decorative cyan across multiple component categories (80/20 rule, batch 11) - Social: FeedView, ConnectionsView, GroupsView, ExploreView, GroupDetailView loading spinners and decorative text, CreatePostModal decorative select text and hashtag links, PostCard decorative tag links and waveform bars and view comments link, CreateGroupModal decorative icon (9 instances) - Settings: DataExportModal decorative icon, LoginHistory decorative IP text, AppearanceSettingsView decorative icon and selected theme checkmark, BackupsView decorative icon, CloudIntegrationView decorative icon, AccessibilitySettingsView decorative icon, SecuritySettings decorative icon, PasskeyModal decorative icon and loading spinner (8 instances) - Studio: ProjectsManager loading spinner and progress percentage text, CloudFileBrowser decorative music icons, AIToolsView decorative music icon, ConnectivityView decorative icon, CreateProjectModal decorative icon, CloudSettingsView decorative icon (6 instances) - Admin: AdminDashboardView loading spinner and decorative chart bars and icon, AdminSettingsView decorative icon, AdminModerationView loading spinner, AdminUsersView loading spinner (5 instances) - Inventory: InventoryView loading spinner, EquipmentCard decorative price icon, EquipmentDetailView loading spinner and decorative icons and price text, AddEquipmentView decorative icon (5 instances) - Seller: CreateProductView decorative icon, SellerDashboardView loading spinner and decorative icon and sales text (3 instances) - Live: LiveStreamDetailView decorative streamer name text (1 instance) - Developer: DeveloperDashboardView loading spinner, WebhooksView decorative icon (2 instances) - Upload: BulkUploadModal decorative icon, FilePreviewCard decorative audio file icon, MetadataForm decorative button text, CoverArtUploadModal decorative icon, LyricsEditorModal decorative icon (5 instances) - Notifications: NotificationItem decorative follow icon and mark as read button, NotificationBell decorative mark all read link (3 instances) - Total: ~46 files, ~46 instances replaced - Preserved: Active/selected states (CloudFileBrowser selected files checkmarks, CreatePostModal post type active state, GroupCard/GroupDetailView public/private badges - semantic indicators, DataExportModal checkbox accents - focus/interaction, AppearanceSettingsView selected theme - active state, PasskeyModal checkbox accent - focus/interaction, LyricsEditorModal checkbox accent - focus/interaction, FileUploadZone drag active state - active state, EquipmentDetailView support link - functional link, FlashSaleModal link - functional link, EquipmentDetailView image indicator dots - active state), primary actions, design system variants - Action 11.3.1.3 in progress (eleventh batch: social, settings, studio, admin, inventory, seller, live, developer, upload, notifications components)
2026-01-16 10:26:33 +00:00
<span className="text-kodo-steel">{project.progress}%</span>
</div>
<div className="h-1.5 bg-kodo-steel rounded-full overflow-hidden">
<div
className="h-full bg-kodo-cyan"
style={{ width: `${project.progress}%` }}
></div>
</div>
</div>
<div className="flex justify-between items-center pt-4 border-t border-kodo-steel">
<div className="flex -space-x-2">
<div className="w-6 h-6 rounded-full bg-kodo-steel border border-black"></div>
{project.collaborators > 0 && (
<div className="w-6 h-6 rounded-full bg-kodo-ink border border-black flex items-center justify-center text-[10px] text-white">
+{project.collaborators}
</div>
)}
</div>
<Button variant="ghost" size="sm" className="text-xs h-8">
OPEN
</Button>
</div>
</Card>
))}
{/* Add New Placeholder */}
<div
aesthetic-improvements: replace secondary cyan hover states with steel - Button outline variant: hover:border-kodo-cyan/50 → hover:border-kodo-steel/50 - Header secondary nav: hover:text-kodo-cyan → hover:text-white, hover:bg-kodo-cyan/5 → hover:bg-white/5 - FileManagerView: hover:border-kodo-cyan/50 → hover:border-kodo-steel/50 (kept selected state cyan) - ProjectsManager: hover:border-kodo-cyan/50 → hover:border-kodo-steel/50, hover:text-kodo-cyan → hover:text-white - GroupDetailView: hover:border-kodo-cyan/30 → hover:border-kodo-steel/50 - AIToolsView: hover:border-kodo-cyan/50 → hover:border-kodo-steel/50 - CloudFileBrowser: hover:border-kodo-cyan/50 → hover:border-kodo-steel/50 (kept selected state cyan) - ProfileView: hover:border-kodo-cyan/50 → hover:border-kodo-steel/50 - CourseCard: hover:border-kodo-cyan/50 → hover:border-kodo-steel/50 - TwoFactorSetup: hover:border-kodo-cyan → hover:border-kodo-steel/50 - GearView: hover:text-kodo-cyan → hover:text-white, hover:border-kodo-cyan → hover:border-kodo-steel/50 - ChatInput: hover:text-kodo-cyan → hover:text-white (3 instances) - ChatMessage: hover:text-kodo-cyan → hover:text-white (2 instances) - ChatRoom: hover:text-kodo-cyan → hover:text-white - AddToPlaylistModal: hover:border-kodo-cyan → hover:border-kodo-steel/50, hover:text-kodo-cyan → hover:text-white - Preserved focus rings (cyan) and active/selected states (cyan) as per audit - Action 11.3.1.2 in progress (first batch of ~15 files)
2026-01-16 09:51:30 +00:00
className="border-2 border-dashed border-kodo-steel rounded-xl flex flex-col items-center justify-center p-8 hover:bg-kodo-slate/20 transition-colors cursor-pointer text-kodo-content-dim hover:text-white hover:border-kodo-steel/50 min-h-[250px]"
onClick={() => setShowCreateModal(true)}
>
<div className="w-16 h-16 bg-kodo-ink rounded-full flex items-center justify-center mb-4 transition-opacity group-hover:opacity-80">
<Plus className="w-8 h-8 opacity-50" />
</div>
<span className="font-mono font-bold">START NEW PROJECT</span>
</div>
</div>
)}
{filteredProjects.length === 0 && !loading && (
<div className="text-center py-24 text-kodo-content-dim">
<AlertCircle className="w-12 h-12 mx-auto mb-2 opacity-30" />
<p>No projects found matching your filters.</p>
</div>
)}
{showCreateModal && (
<CreateProjectModal
onClose={() => setShowCreateModal(false)}
onCreate={handleCreate}
/>
)}
</div>
);
};