import React, { useState, useEffect, useRef, useMemo, Suspense, lazy } from 'react'; import { createPortal } from 'react-dom'; import { TransformWrapper, TransformComponent } from 'react-zoom-pan-pinch'; import { DEFAULT_QUICK_ACTIONS } from "@/constants"; import { ImageLightboxEditorConfig } from './ImageLightboxTypes'; const ImageLightboxPrompt = lazy(() => import('./ImageLightboxPrompt').then(m => ({ default: m.ImageLightboxPrompt }))); const PublishDialog = lazy(() => import('@/components/PublishDialog')); interface ImageLightboxProps { isOpen: boolean; onClose: () => void; imageUrl: string; imageTitle: string; originalImageId?: string; onPublish?: (option: 'overwrite' | 'new' | 'version', imageUrl: string, title: string, description?: string, parentId?: string, collectionIds?: string[]) => void; showPrompt?: boolean; generatedImageUrl?: string; // Navigation props currentIndex?: number; totalCount?: number; onNavigate?: (direction: 'prev' | 'next') => void; onPreload?: (direction: 'prev' | 'next') => void; // Editor Config editorConfig?: ImageLightboxEditorConfig; // Wizard navigation onOpenInWizard?: () => void; // Open current image in full wizard inline?: boolean; } import ResponsiveImage from './ResponsiveImage'; export default function ImageLightbox({ isOpen, onClose, imageUrl, imageTitle, originalImageId, onPublish, showPrompt = true, generatedImageUrl, currentIndex, totalCount, onNavigate, editorConfig, onOpenInWizard, inline = false }: ImageLightboxProps) { const [lightboxLoaded, setLightboxLoaded] = useState(false); const [isPanning, setIsPanning] = useState(false); const [scale, setScale] = useState(1); const [showPublishDialog, setShowPublishDialog] = useState(false); const [showPromptField, setShowPromptField] = useState(false); const [showInfoPopup, setShowInfoPopup] = useState(false); const tapTimeoutRef = useRef(null); const swipeStartRef = useRef<{ x: number; y: number; time: number } | null>(null); const isSwipingRef = useRef(false); const isPanningRef = useRef(false); // Detect mobile for disabling zoom const [isMobile, setIsMobile] = useState(() => typeof window !== 'undefined' && window.matchMedia('(max-width: 767px)').matches ); useEffect(() => { const mq = window.matchMedia('(max-width: 767px)'); const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches); mq.addEventListener('change', handler); return () => mq.removeEventListener('change', handler); }, []); // Extract values that ImageLightbox needs directly from editorConfig const { onNavigateHistory, } = editorConfig || {}; // Preload image when lightbox opens useEffect(() => { if (isOpen || inline) { // Ensure prompt field is hidden by default when opening setShowPromptField(false); } }, [isOpen, inline]); // Handle keyboard events useEffect(() => { if (!isOpen && !inline) return; const handleKeyDown = (event: KeyboardEvent) => { // Check if user is typing in the textarea or any input field const target = event.target as HTMLElement; const isTypingInInput = target instanceof HTMLTextAreaElement || target instanceof HTMLInputElement || target?.contentEditable === 'true' || target?.tagName === 'TEXTAREA' || target?.tagName === 'INPUT'; if (event.key === 'Escape') { if (isTypingInInput) { // If typing in input, ESC should hide prompt and clear text (handled in textarea onKeyDown) return; } onClose(); } else if (event.key === 'ArrowUp' && event.ctrlKey && onNavigateHistory) { // Ctrl+ArrowUp for prompt history navigation event.preventDefault(); onNavigateHistory('up'); } else if (event.key === 'ArrowDown' && event.ctrlKey && onNavigateHistory) { // Ctrl+ArrowDown for prompt history navigation event.preventDefault(); onNavigateHistory('down'); } else if (event.key === 'ArrowLeft' && !isTypingInInput && onNavigate) { event.preventDefault(); onNavigate('prev'); } else if (event.key === 'ArrowRight' && !isTypingInInput && onNavigate) { event.preventDefault(); onNavigate('next'); } else if (event.key === ' ' && !isTypingInInput && showPrompt) { // Spacebar to toggle prompt field (only when not typing) event.preventDefault(); setShowPromptField(!showPromptField); } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [isOpen, onClose, onNavigate, currentIndex, totalCount, showPrompt, showPromptField, onNavigateHistory]); // Cleanup timeout on unmount useEffect(() => { return () => { if (tapTimeoutRef.current) { clearTimeout(tapTimeoutRef.current); } }; }, []); // Swipe detection functions const handleSwipeStart = (clientX: number, clientY: number) => { swipeStartRef.current = { x: clientX, y: clientY, time: Date.now() }; isSwipingRef.current = false; }; const handleSwipeEnd = (clientX: number, clientY: number) => { if (!swipeStartRef.current || !onNavigate) { return; } const deltaX = clientX - swipeStartRef.current.x; const deltaY = clientY - swipeStartRef.current.y; const deltaTime = Date.now() - swipeStartRef.current.time; // Swipe thresholds const minSwipeDistance = 50; // Minimum distance for a swipe const maxSwipeTime = 500; // Maximum time for a swipe (ms) const maxVerticalDistance = 100; // Maximum vertical movement for horizontal swipe // Check if this is a valid horizontal swipe if ( Math.abs(deltaX) > minSwipeDistance && Math.abs(deltaY) < maxVerticalDistance && deltaTime < maxSwipeTime && !isPanning // Don't trigger swipe if user was panning/zooming ) { if (deltaX > 0) { // Swipe right = previous image onNavigate('prev'); } else if (deltaX < 0) { // Swipe left = next image onNavigate('next'); } } swipeStartRef.current = null; isSwipingRef.current = false; }; const handlePublishClick = () => { setShowPublishDialog(true); }; const handlePublish = (option: 'overwrite' | 'new' | 'version', title: string, description?: string, parentId?: string, collectionIds?: string[]) => { if (onPublish) { const urlToPublish = generatedImageUrl || imageUrl; onPublish(option, urlToPublish, title, description, parentId, collectionIds); } setShowPublishDialog(false); }; // Provide the publish click handler internally to editorConfig if missing const activeEditorConfig = React.useMemo(() => { if (!editorConfig) return undefined; return { ...editorConfig, onPublishClick: editorConfig.onPublishClick || handlePublishClick, onOpenInWizard: editorConfig.onOpenInWizard || onOpenInWizard, quickActions: editorConfig.quickActions || DEFAULT_QUICK_ACTIONS }; }, [editorConfig, onOpenInWizard]); // Determine if it's a video const isVideo = useMemo(() => { const url = (generatedImageUrl || imageUrl || '').toLowerCase(); const title = (imageTitle || '').toLowerCase(); return url.match(/\.(mp4|webm|ogg|mov)$/) || title.match(/\.(mp4|webm|ogg|mov)$/); }, [generatedImageUrl, imageUrl, imageTitle]); if (!isOpen && !inline) return null; const mediaEl = isVideo ? (