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 '../../context/ToastContext';
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-3 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 className="p-1.5 rounded bg-kodo-slate text-white">
<LayoutGrid className="w-4 h-4" />
</button>
<button className="p-1.5 rounded text-kodo-content-dim hover:text-white">
<List className="w-4 h-4" />
</button>
</div>
</div>
</div>
{/* Projects Grid */}
{loading ? (
<div className="flex justify-center py-20">
<Loader2 className="w-8 h-8 text-kodo-cyan animate-spin" />
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredProjects.map((project) => (
<Card
key={project.id}
variant="gaming"
className="group cursor-pointer hover:border-kodo-cyan/50 transition-all hover:-translate-y-1"
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-kodo-cyan 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>
<span className="text-kodo-cyan">{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
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-kodo-cyan hover:border-kodo-cyan min-h-[250px]"
onClick={() => setShowCreateModal(true)}
>
<div className="w-16 h-16 bg-kodo-ink rounded-full flex items-center justify-center mb-4 group-hover:scale-110 transition-transform">
<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-20 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>
);
};