import { useState, useEffect } from 'react'; 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 (basic XSS prevention) const sanitizeHtml = (html: string) => { // Allow basic formatting tags only const allowed = ['p', 'br', 'b', 'strong', 'i', 'em', 'u', 'a', 'ul', 'ol', 'li', 'figure', 'img']; const doc = new DOMParser().parseFromString(html, 'text/html'); const clean = (node: Node): string => { if (node.nodeType === Node.TEXT_NODE) { return node.textContent || ''; } if (node.nodeType === Node.ELEMENT_NODE) { const el = node as Element; const tag = el.tagName.toLowerCase(); if (!allowed.includes(tag)) { // Return children content without the tag return Array.from(el.childNodes).map(clean).join(''); } // Build allowed attributes let attrs = ''; if (tag === 'a' && el.getAttribute('href')) { const href = el.getAttribute('href'); if (href?.toLowerCase().startsWith('http')) { attrs = ` href="${href}" target="_blank" rel="noopener noreferrer"`; } } if (tag === 'img') { const src = el.getAttribute('src'); // Only render img if it has a valid http(s) URL, otherwise skip entirely if (!src?.toLowerCase().startsWith('http')) { return ''; // Skip images without valid URLs } attrs = ` src="${src}" style="max-width: 100%; height: auto;"`; } const children = Array.from(el.childNodes).map(clean).join(''); if (['br', 'img'].includes(tag)) { return `<${tag}${attrs} />`; } return `<${tag}${attrs}>${children}`; } return ''; }; return Array.from(doc.body.childNodes).map(clean).join(''); }; 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}
)}
); }