import { useState, useEffect } from 'react'; import DOMPurify from 'dompurify'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { X, User, Calendar, FileText, Image, Edit3, Save, ExternalLink, ChevronLeft, ChevronRight, } from 'lucide-react'; import { api } from '../api/client'; import { Button } from './Button'; import { RichTextEditor } from './RichTextEditor'; interface ProjectPageModalProps { archiveId: number; archiveName?: string; onClose: () => void; } export function ProjectPageModal({ archiveId, archiveName, onClose }: ProjectPageModalProps) { const queryClient = useQueryClient(); const [isEditing, setIsEditing] = useState(false); const [selectedImageIndex, setSelectedImageIndex] = useState(null); const [editData, setEditData] = useState<{ title?: string; description?: string; designer?: string; license?: string; profile_title?: string; profile_description?: string; }>({}); const { data: projectPage, isLoading, error } = useQuery({ queryKey: ['archive-project-page', archiveId], queryFn: () => api.getArchiveProjectPage(archiveId), }); const updateMutation = useMutation({ mutationFn: (data: typeof editData) => api.updateArchiveProjectPage(archiveId, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['archive-project-page', archiveId] }); setIsEditing(false); setEditData({}); }, }); // Handle escape key to close modal useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { if (selectedImageIndex !== null) { setSelectedImageIndex(null); } else if (isEditing) { handleCancelEdit(); } else { onClose(); } } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [selectedImageIndex, isEditing, onClose]); // Combine all images for gallery const allImages = [ ...(projectPage?.model_pictures || []), ...(projectPage?.profile_pictures || []), ]; const handleStartEdit = () => { setEditData({ title: projectPage?.title || '', description: projectPage?.description || '', designer: projectPage?.designer || '', license: projectPage?.license || '', profile_title: projectPage?.profile_title || '', profile_description: projectPage?.profile_description || '', }); setIsEditing(true); }; const handleSave = () => { updateMutation.mutate(editData); }; const handleCancelEdit = () => { setIsEditing(false); setEditData({}); }; // Sanitize HTML content using DOMPurify const sanitizeHtml = (html: string) => { return DOMPurify.sanitize(html, { ALLOWED_TAGS: ['p', 'br', 'b', 'strong', 'i', 'em', 'u', 'a', 'ul', 'ol', 'li', 'figure', 'img'], ALLOWED_ATTR: ['href', 'src', 'target', 'rel', 'style'], ADD_ATTR: ['target'], }); }; const hasContent = projectPage && ( projectPage.title || projectPage.description || projectPage.designer || projectPage.profile_title || allImages.length > 0 ); // Handle backdrop click to close modal const handleBackdropClick = (e: React.MouseEvent) => { if (e.target === e.currentTarget) { onClose(); } }; return (
{/* Header */}

Project Page {archiveName && - {archiveName}}

{!isEditing && hasContent && ( )} {isEditing && ( <> )}
{/* Content */}
{isLoading && (
)} {error && (
Failed to load project page data
)} {projectPage && !hasContent && (

No project page data found in this 3MF file.

Project pages are typically included in files downloaded from MakerWorld.

)} {projectPage && hasContent && (
{/* Title & Designer */}
{isEditing ? ( setEditData({ ...editData, title: e.target.value })} placeholder="Title" className="w-full bg-bambu-dark border border-bambu-dark-tertiary rounded-lg px-4 py-2 text-white text-xl font-semibold" /> ) : ( projectPage.title && (

{projectPage.title}

) )}
{isEditing ? (
setEditData({ ...editData, designer: e.target.value })} placeholder="Designer" className="bg-bambu-dark border border-bambu-dark-tertiary rounded px-2 py-1 text-white" />
) : ( projectPage.designer && (
{projectPage.designer} {projectPage.designer_user_id && ( )}
) )} {projectPage.creation_date && (
{projectPage.creation_date}
)} {isEditing ? (
setEditData({ ...editData, license: e.target.value })} placeholder="License" className="bg-bambu-dark border border-bambu-dark-tertiary rounded px-2 py-1 text-white" />
) : ( projectPage.license && (
{projectPage.license}
) )} {projectPage.origin && ( {projectPage.origin} )}
{/* Description */} {(projectPage.description || isEditing) && (

Description

{isEditing ? ( setEditData({ ...editData, description: html })} placeholder="Enter description..." /> ) : (
)}
)} {/* Profile Info */} {(projectPage.profile_title || projectPage.profile_description || isEditing) && (

Print Profile

{isEditing ? (
setEditData({ ...editData, profile_title: e.target.value })} placeholder="Profile Title" className="w-full bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded px-3 py-2 text-white" /> setEditData({ ...editData, profile_description: html })} placeholder="Profile description..." />
) : ( <> {projectPage.profile_title && (

{projectPage.profile_title}

)} {projectPage.profile_description && (
)} {projectPage.profile_user_name && (

by {projectPage.profile_user_name}

)} )}
)} {/* Image Gallery */} {allImages.length > 0 && (

Images ({allImages.length})

{allImages.map((img, index) => ( ))}
)} {/* MakerWorld Link */} {projectPage.design_model_id && ( )}
)}
{/* Image Lightbox */} {selectedImageIndex !== null && allImages[selectedImageIndex] && (
setSelectedImageIndex(null)} > {allImages[selectedImageIndex].name} e.stopPropagation()} />
{selectedImageIndex + 1} / {allImages.length}
)}
); }