import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
FolderKanban,
Loader2,
Plus,
Trash2,
Edit3,
Archive,
ListTodo,
Package,
Clock,
CheckCircle2,
AlertTriangle,
ChevronRight,
MoreVertical,
} from 'lucide-react';
import { api } from '../api/client';
import type { ProjectListItem, ProjectCreate, ProjectUpdate } from '../api/client';
import { Button } from '../components/Button';
import { ConfirmModal } from '../components/ConfirmModal';
import { useToast } from '../contexts/ToastContext';
const PROJECT_COLORS = [
'#ef4444', // red
'#f97316', // orange
'#eab308', // yellow
'#22c55e', // green
'#06b6d4', // cyan
'#3b82f6', // blue
'#8b5cf6', // violet
'#ec4899', // pink
'#6b7280', // gray
];
interface ProjectModalProps {
project?: ProjectListItem;
onClose: () => void;
onSave: (data: ProjectCreate | ProjectUpdate) => void;
isLoading: boolean;
}
export function ProjectModal({ project, onClose, onSave, isLoading }: ProjectModalProps) {
const [name, setName] = useState(project?.name || '');
const [description, setDescription] = useState(project?.description || '');
const [color, setColor] = useState(project?.color || PROJECT_COLORS[0]);
const [targetCount, setTargetCount] = useState(project?.target_count?.toString() || '');
const [status, setStatus] = useState(project?.status || 'active');
const [tags, setTags] = useState((project as ProjectListItem & { tags?: string })?.tags || '');
const [dueDate, setDueDate] = useState((project as ProjectListItem & { due_date?: string })?.due_date?.split('T')[0] || '');
const [priority, setPriority] = useState((project as ProjectListItem & { priority?: string })?.priority || 'normal');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSave({
name: name.trim(),
description: description.trim() || undefined,
color,
target_count: targetCount ? parseInt(targetCount, 10) : undefined,
tags: tags.trim() || undefined,
due_date: dueDate || undefined,
priority,
...(project && { status }),
});
};
return (
{project ? 'Edit Project' : 'New Project'}
);
}
interface ProjectCardProps {
project: ProjectListItem;
onClick: () => void;
onEdit: () => void;
onDelete: () => void;
}
function ProjectCard({ project, onClick, onEdit, onDelete }: ProjectCardProps) {
const progressPercent = project.progress_percent ?? 0;
const isCompleted = project.status === 'completed';
const isArchived = project.status === 'archived';
const [showActions, setShowActions] = useState(false);
// Status icon and color
const getStatusConfig = () => {
if (isCompleted) return { icon: CheckCircle2, color: 'text-bambu-green', bg: 'bg-bambu-green/10' };
if (isArchived) return { icon: Archive, color: 'text-bambu-gray', bg: 'bg-bambu-gray/10' };
if (project.queue_count > 0) return { icon: Clock, color: 'text-blue-400', bg: 'bg-blue-400/10' };
return { icon: FolderKanban, color: 'text-bambu-gray', bg: 'bg-bambu-gray/10' };
};
const statusConfig = getStatusConfig();
return (
{/* Color accent bar with glow */}
{/* Header */}
{project.name}
{project.target_count ? (
= 100
? 'bg-bambu-green/20 text-bambu-green'
: 'bg-bambu-dark text-bambu-gray'
}`}>
{project.archive_count}/{project.target_count} parts
) : project.archive_count > 0 ? (
{project.archive_count} print{project.archive_count !== 1 ? 's' : ''}
) : null}
{isCompleted && (
Done
)}
{isArchived && (
Archived
)}
{project.description && (
{project.description}
)}
{/* Filament materials/colors */}
{project.archives && project.archives.length > 0 && (() => {
const materials = [...new Set(project.archives.map(a => a.filament_type).filter(Boolean))];
const colors = [...new Set(project.archives.map(a => a.filament_color).filter(Boolean))] as string[];
if (materials.length === 0 && colors.length === 0) return null;
return (
{/* Material types as text badges */}
{materials.slice(0, 3).map((mat) => (
{mat}
))}
{/* Colors as swatches */}
{colors.length > 0 && (
{colors.slice(0, 5).map((col) => (
))}
{colors.length > 5 && (
+{colors.length - 5}
)}
)}
);
})()}
{/* Actions menu */}
e.stopPropagation()}>
setShowActions(!showActions)}
>
{showActions && (
<>
setShowActions(false)} />
{ onEdit(); setShowActions(false); }}
>
Edit
{ onDelete(); setShowActions(false); }}
>
Delete
>
)}
{/* Progress section - show for all projects */}
{project.target_count ? (
<>
Progress
= 100 ? 'text-bambu-green font-medium' : 'text-white'}>
{project.archive_count} / {project.target_count}
= 100
? 'linear-gradient(90deg, #22c55e, #4ade80)'
: `linear-gradient(90deg, ${project.color || '#6b7280'}, ${project.color || '#6b7280'}cc)`,
boxShadow: `0 0 8px ${progressPercent >= 100 ? '#22c55e' : project.color || '#6b7280'}60`
}}
/>
{progressPercent.toFixed(0)}% complete
>
) : project.archive_count > 0 ? (
{project.archive_count} print{project.archive_count !== 1 ? 's' : ''} completed
{project.queue_count > 0 && (
{project.queue_count} in queue
)}
) : (
No prints yet
)}
{/* Archive thumbnails - compact 4-column grid */}
{project.archives && project.archives.length > 0 && (
{project.archives.slice(0, 4).map((archive) => (
{archive.thumbnail_path ? (
) : (
)}
{archive.status === 'failed' && (
)}
))}
{project.archive_count > 4 && (
+{project.archive_count - 4} more
)}
)}
{/* Stats footer */}
{project.queue_count > 0 && (
{project.queue_count}
)}
);
}
export function ProjectsPage() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const { showToast } = useToast();
const [showModal, setShowModal] = useState(false);
const [editingProject, setEditingProject] = useState
();
const [statusFilter, setStatusFilter] = useState('active');
const [deleteConfirm, setDeleteConfirm] = useState(null);
const { data: projects, isLoading } = useQuery({
queryKey: ['projects', statusFilter === 'all' ? undefined : statusFilter],
queryFn: () => api.getProjects(statusFilter === 'all' ? undefined : statusFilter),
});
const createMutation = useMutation({
mutationFn: (data: ProjectCreate) => api.createProject(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['projects'] });
setShowModal(false);
showToast('Project created', 'success');
},
onError: (error: Error) => {
showToast(error.message, 'error');
},
});
const updateMutation = useMutation({
mutationFn: ({ id, data }: { id: number; data: ProjectUpdate }) =>
api.updateProject(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['projects'] });
setShowModal(false);
setEditingProject(undefined);
showToast('Project updated', 'success');
},
onError: (error: Error) => {
showToast(error.message, 'error');
},
});
const deleteMutation = useMutation({
mutationFn: (id: number) => api.deleteProject(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['projects'] });
setDeleteConfirm(null);
showToast('Project deleted', 'success');
},
onError: (error: Error) => {
showToast(error.message, 'error');
},
});
const handleSave = (data: ProjectCreate | ProjectUpdate) => {
if (editingProject) {
updateMutation.mutate({ id: editingProject.id, data });
} else {
createMutation.mutate(data as ProjectCreate);
}
};
const handleEdit = (project: ProjectListItem) => {
setEditingProject(project);
setShowModal(true);
};
const handleClick = (project: ProjectListItem) => {
// Navigate to project detail page
navigate(`/projects/${project.id}`);
};
const handleDeleteClick = (id: number) => {
setDeleteConfirm(id);
};
const handleDeleteConfirm = () => {
if (deleteConfirm !== null) {
deleteMutation.mutate(deleteConfirm);
}
};
// Count projects by status for filter badges
const projectCounts = projects?.reduce((acc, p) => {
acc[p.status] = (acc[p.status] || 0) + 1;
acc.all = (acc.all || 0) + 1;
return acc;
}, {} as Record) || {};
return (
{/* Header */}
Projects
Organize and track your 3D printing projects
setShowModal(true)} className="sm:w-auto w-full">
New Project
{/* Filter tabs */}
{[
{ key: 'active', label: 'Active', icon: Clock },
{ key: 'completed', label: 'Completed', icon: CheckCircle2 },
{ key: 'archived', label: 'Archived', icon: Archive },
{ key: 'all', label: 'All', icon: FolderKanban },
].map(({ key, label, icon: Icon }) => (
setStatusFilter(key)}
className={`flex items-center gap-2 px-4 py-2 text-sm rounded-lg transition-all ${
statusFilter === key
? 'bg-bambu-card text-white shadow-sm'
: 'text-bambu-gray hover:text-white'
}`}
>
{label}
{projectCounts[key] > 0 && (
{projectCounts[key]}
)}
))}
{/* Content */}
{isLoading ? (
) : projects?.length === 0 ? (
{statusFilter === 'all' ? 'No projects yet' : `No ${statusFilter} projects`}
{statusFilter === 'all'
? 'Create your first project to start organizing related prints, tracking progress, and managing your builds.'
: `You don't have any ${statusFilter} projects. Projects will appear here when their status changes.`
}
{statusFilter === 'all' && (
setShowModal(true)}>
Create Your First Project
)}
) : (
{projects?.map((project) => (
handleClick(project)}
onEdit={() => handleEdit(project)}
onDelete={() => handleDeleteClick(project.id)}
/>
))}
)}
{/* Delete Confirmation Modal */}
{deleteConfirm !== null && (
setDeleteConfirm(null)}
/>
)}
{/* Modal */}
{showModal && (
{
setShowModal(false);
setEditingProject(undefined);
}}
onSave={handleSave}
isLoading={createMutation.isPending || updateMutation.isPending}
/>
)}
);
}