2026-01-07 09:31:02 +00:00
|
|
|
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';
|
2026-01-13 18:47:57 +00:00
|
|
|
import {
|
|
|
|
|
MoreVertical,
|
|
|
|
|
Plus,
|
|
|
|
|
LayoutGrid,
|
|
|
|
|
List,
|
|
|
|
|
Loader2,
|
|
|
|
|
AlertCircle,
|
|
|
|
|
} from 'lucide-react';
|
2026-01-07 09:31:02 +00:00
|
|
|
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');
|
2026-01-13 18:47:57 +00:00
|
|
|
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(
|
|
|
|
|
null,
|
|
|
|
|
);
|
2026-01-07 09:31:02 +00:00
|
|
|
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(() => {
|
2026-01-13 18:47:57 +00:00
|
|
|
loadProjects();
|
2026-01-07 09:31:02 +00:00
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const loadProjects = async () => {
|
2026-01-13 18:47:57 +00:00
|
|
|
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);
|
|
|
|
|
}
|
2026-01-07 09:31:02 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// --- Actions ---
|
|
|
|
|
|
|
|
|
|
const handleCreate = async (newProjectData: any) => {
|
2026-01-13 18:47:57 +00:00
|
|
|
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');
|
|
|
|
|
}
|
2026-01-07 09:31:02 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleUpdate = async (updatedProject: any) => {
|
2026-01-13 18:47:57 +00:00
|
|
|
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)),
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-01-07 09:31:02 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDelete = async (id: string) => {
|
2026-01-13 18:47:57 +00:00
|
|
|
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');
|
|
|
|
|
}
|
2026-01-07 09:31:02 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const openProject = (id: string) => {
|
2026-01-13 18:47:57 +00:00
|
|
|
setSelectedProjectId(id);
|
|
|
|
|
setViewState('detail');
|
2026-01-07 09:31:02 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// --- Filtering ---
|
2026-01-13 18:47:57 +00:00
|
|
|
const filteredProjects = projects.filter((p) => {
|
|
|
|
|
const matchesSearch = p.name.toLowerCase().includes(search.toLowerCase());
|
|
|
|
|
const matchesFilter = filter === 'All' || p.daw === filter;
|
|
|
|
|
return matchesSearch && matchesFilter;
|
2026-01-07 09:31:02 +00:00
|
|
|
});
|
|
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
const selectedProject = projects.find((p) => p.id === selectedProjectId);
|
2026-01-07 09:31:02 +00:00
|
|
|
|
|
|
|
|
// --- Render Detail View ---
|
|
|
|
|
if (viewState === 'detail' && selectedProject) {
|
2026-01-13 18:47:57 +00:00
|
|
|
return (
|
|
|
|
|
<ProjectDetailView
|
|
|
|
|
project={selectedProject}
|
|
|
|
|
onBack={() => setViewState('list')}
|
|
|
|
|
onUpdate={handleUpdate}
|
|
|
|
|
onDelete={handleDelete}
|
|
|
|
|
/>
|
|
|
|
|
);
|
2026-01-07 09:31:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Render List View ---
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6 animate-fadeIn pb-20">
|
2026-01-13 18:47:57 +00:00
|
|
|
<div className="flex flex-col md:flex-row justify-between items-end gap-4">
|
|
|
|
|
<div>
|
2026-01-15 22:54:05 +00:00
|
|
|
<h2 className="text-2xl font-display font-bold text-white mb-2">
|
2026-01-13 18:47:57 +00:00
|
|
|
ACTIVE PROJECTS
|
|
|
|
|
</h2>
|
2026-01-16 00:56:34 +00:00
|
|
|
<p className="text-kodo-content-dim font-mono text-sm">
|
2026-01-13 18:47:57 +00:00
|
|
|
Track progress across all your workstations.
|
|
|
|
|
</p>
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
<Button
|
|
|
|
|
variant="primary"
|
|
|
|
|
icon={<Plus className="w-4 h-4" />}
|
|
|
|
|
onClick={() => setShowCreateModal(true)}
|
|
|
|
|
>
|
|
|
|
|
NEW PROJECT
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2026-01-07 09:31:02 +00:00
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
{/* 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)}
|
|
|
|
|
/>
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
<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}
|
2026-01-16 00:56:34 +00:00
|
|
|
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'}`}
|
2026-01-13 18:47:57 +00:00
|
|
|
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>
|
2026-01-16 00:56:34 +00:00
|
|
|
<button className="p-1.5 rounded text-kodo-content-dim hover:text-white">
|
2026-01-13 18:47:57 +00:00
|
|
|
<List className="w-4 h-4" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-01-07 09:31:02 +00:00
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
{/* 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');
|
|
|
|
|
}}
|
2026-01-07 09:31:02 +00:00
|
|
|
>
|
2026-01-16 00:56:34 +00:00
|
|
|
<MoreVertical className="w-4 h-4 text-kodo-content-dim hover:text-white" />
|
2026-01-13 18:47:57 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<h3 className="text-xl font-bold text-white mb-1 group-hover:text-kodo-cyan transition-colors truncate">
|
|
|
|
|
{project.name}
|
|
|
|
|
</h3>
|
2026-01-16 00:56:34 +00:00
|
|
|
<p className="text-xs text-kodo-content-dim mb-4 font-mono">
|
2026-01-13 18:47:57 +00:00
|
|
|
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">
|
2026-01-16 00:56:34 +00:00
|
|
|
<div className="text-[10px] text-kodo-content-dim uppercase font-bold">
|
2026-01-13 18:47:57 +00:00
|
|
|
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">
|
2026-01-16 00:56:34 +00:00
|
|
|
<div className="text-[10px] text-kodo-content-dim uppercase font-bold">
|
2026-01-13 18:47:57 +00:00
|
|
|
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">
|
2026-01-16 00:56:34 +00:00
|
|
|
<span className="text-kodo-content-dim font-bold">
|
2026-01-13 18:47:57 +00:00
|
|
|
{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>
|
|
|
|
|
|
2026-01-16 00:56:34 +00:00
|
|
|
<div className="flex justify-between items-center pt-4 border-t border-kodo-steel">
|
2026-01-13 18:47:57 +00:00
|
|
|
<div className="flex -space-x-2">
|
2026-01-16 00:56:34 +00:00
|
|
|
<div className="w-6 h-6 rounded-full bg-kodo-steel border border-black"></div>
|
2026-01-13 18:47:57 +00:00
|
|
|
{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}
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
)}
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
<Button variant="ghost" size="sm" className="text-xs h-8">
|
|
|
|
|
OPEN
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
))}
|
2026-01-07 09:31:02 +00:00
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
{/* Add New Placeholder */}
|
|
|
|
|
<div
|
2026-01-16 00:56:34 +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-kodo-cyan hover:border-kodo-cyan min-h-[250px]"
|
2026-01-13 18:47:57 +00:00
|
|
|
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" />
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
<span className="font-mono font-bold">START NEW PROJECT</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{filteredProjects.length === 0 && !loading && (
|
2026-01-16 00:56:34 +00:00
|
|
|
<div className="text-center py-20 text-kodo-content-dim">
|
2026-01-13 18:47:57 +00:00
|
|
|
<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}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|