import { useMemo, useState } from 'react'; import { uploadMedia } from '../../services/api'; import type { FullProfile, Project } from '../../types'; interface ProjectsFormProps { profile: FullProfile; onChange: (profile: FullProfile) => void; requestConfirm: (title: string, message: string, onConfirm: () => void) => void; } const emptyProject: Project = { title: '', short_description: '', category: '', tech_stack: [], github_link: '', hf_link: '', live_demo_link: '', image_path: '', image_paths: [], video_path: '', featured: false, public_display: true, chatbot_visible: true, internal_chatbot_notes: '', safe_notes: '', }; function textToList(value: string): string[] { return value .split('\n') .map((item) => item.trim()) .filter(Boolean); } function ProjectsForm({ profile, onChange, requestConfirm }: ProjectsFormProps) { const projects = profile.projects ?? []; const [selectedIndex, setSelectedIndex] = useState(0); const [uploadMessage, setUploadMessage] = useState(''); const [uploadError, setUploadError] = useState(''); const [isUploading, setIsUploading] = useState(false); const safeSelectedIndex = projects.length === 0 ? -1 : Math.min(selectedIndex, projects.length - 1); const selectedProject = safeSelectedIndex >= 0 ? projects[safeSelectedIndex] : null; const selectedImages = useMemo(() => { if (!selectedProject) { return []; } const imagePaths = selectedProject.image_paths ?? []; if (imagePaths.length > 0) { return imagePaths.slice(0, 3); } return selectedProject.image_path ? [selectedProject.image_path] : []; }, [selectedProject]); const updateProjects = (nextProjects: Project[]) => { onChange({ ...profile, projects: nextProjects, }); }; const updateSelectedProject = (nextProject: Project) => { if (safeSelectedIndex < 0) { return; } const nextProjects = [...projects]; nextProjects[safeSelectedIndex] = nextProject; updateProjects(nextProjects); }; const addProject = () => { const nextProjects = [...projects, { ...emptyProject }]; updateProjects(nextProjects); setSelectedIndex(nextProjects.length - 1); setUploadMessage(''); setUploadError(''); }; const moveSelectedProject = (direction: 'up' | 'down') => { if (safeSelectedIndex < 0) { return; } const targetIndex = direction === 'up' ? safeSelectedIndex - 1 : safeSelectedIndex + 1; if (targetIndex < 0 || targetIndex >= projects.length) { return; } const nextProjects = [...projects]; [nextProjects[safeSelectedIndex], nextProjects[targetIndex]] = [ nextProjects[targetIndex], nextProjects[safeSelectedIndex], ]; updateProjects(nextProjects); setSelectedIndex(targetIndex); setUploadMessage(''); setUploadError(''); }; const deleteSelectedProject = () => { if (!selectedProject || safeSelectedIndex < 0) { return; } const projectTitle = selectedProject.title || `Project ${safeSelectedIndex + 1}`; requestConfirm( 'Delete project?', `Are you sure you want to delete "${projectTitle}"? This action cannot be undone after saving.`, () => { const nextProjects = projects.filter((_, index) => index !== safeSelectedIndex); updateProjects(nextProjects); setSelectedIndex(Math.max(0, safeSelectedIndex - 1)); setUploadMessage(''); setUploadError(''); }, ); }; const handleImageUpload = async (event: React.ChangeEvent) => { if (!selectedProject) { return; } const files = Array.from(event.target.files ?? []); event.target.value = ''; if (files.length === 0) { return; } const remainingSlots = 3 - selectedImages.length; if (remainingSlots <= 0) { setUploadError('Maximum 3 images are allowed for one project.'); return; } if (files.length > remainingSlots) { setUploadError(`You can upload only ${remainingSlots} more image(s) for this project.`); return; } setIsUploading(true); setUploadMessage(''); setUploadError(''); try { const uploadedPaths: string[] = []; for (const file of files) { const paths = await uploadMedia(file); uploadedPaths.push(...paths); } const imageOnlyPaths = uploadedPaths.filter((path) => /\.(png|jpg|jpeg|webp|gif)$/i.test(path), ); if (imageOnlyPaths.length === 0) { setUploadError('No image path was returned from the upload.'); return; } const nextImages = [...selectedImages, ...imageOnlyPaths].slice(0, 3); updateSelectedProject({ ...selectedProject, image_paths: nextImages, image_path: nextImages[0] ?? '', }); setUploadMessage(`Uploaded ${imageOnlyPaths.length} image(s).`); } catch (error) { setUploadError(error instanceof Error ? error.message : 'Failed to upload image.'); } finally { setIsUploading(false); } }; const removeImage = (imageIndex: number) => { if (!selectedProject) { return; } requestConfirm( 'Remove image?', 'Are you sure you want to remove this image path from the selected project?', () => { const nextImages = selectedImages.filter((_, index) => index !== imageIndex); updateSelectedProject({ ...selectedProject, image_paths: nextImages, image_path: nextImages[0] ?? '', }); }, ); }; return (

Project Editor

Select one project, update it, reorder it, add a new project, or delete the selected project.

{projects.length === 0 && (

No projects yet. Click Add Project to create one.

)} {projects.length > 0 && (
{selectedProject && ( <>