import React, { useRef, useEffect, useState, ReactNode, KeyboardEvent } from 'react'; import { FaTrashAlt, FaEye, FaEyeSlash } from 'react-icons/fa'; import { openConfirm } from './ConfirmModal'; import classNames from 'classnames'; import { apiClient } from '@/utils/api'; import { isVideo } from '@/utils/basic'; interface DatasetImageCardProps { imageUrl: string; alt: string; children?: ReactNode; className?: string; onDelete?: () => void; } const DatasetImageCard: React.FC = ({ imageUrl, alt, children, className = '', onDelete = () => {}, }) => { const cardRef = useRef(null); const [isVisible, setIsVisible] = useState(false); const [inViewport, setInViewport] = useState(false); const [loaded, setLoaded] = useState(false); const [isCaptionLoaded, setIsCaptionLoaded] = useState(false); const [caption, setCaption] = useState(''); const [savedCaption, setSavedCaption] = useState(''); const isGettingCaption = useRef(false); const fetchCaption = async () => { if (isGettingCaption.current || isCaptionLoaded) return; isGettingCaption.current = true; apiClient .get(`/api/caption/${encodeURIComponent(imageUrl)}`) .then(res => res.data) .then(data => { console.log('Caption fetched:', data); setCaption(data || ''); setSavedCaption(data || ''); setIsCaptionLoaded(true); }) .catch(error => { console.error('Error fetching caption:', error); }) .finally(() => { isGettingCaption.current = false; }); }; const saveCaption = () => { const trimmedCaption = caption.trim(); if (trimmedCaption === savedCaption) return; apiClient .post('/api/img/caption', { imgPath: imageUrl, caption: trimmedCaption }) .then(res => res.data) .then(data => { console.log('Caption saved:', data); setSavedCaption(trimmedCaption); }) .catch(error => { console.error('Error saving caption:', error); }); }; // Only fetch caption when the component is both in viewport and visible useEffect(() => { if (inViewport && isVisible) { fetchCaption(); } }, [inViewport, isVisible]); useEffect(() => { // Create intersection observer to check viewport visibility const observer = new IntersectionObserver( entries => { if (entries[0].isIntersecting) { setInViewport(true); // Initialize isVisible to true when first coming into view if (!isVisible) { setIsVisible(true); } } else { setInViewport(false); } }, { threshold: 0.1 }, ); if (cardRef.current) { observer.observe(cardRef.current); } return () => { observer.disconnect(); }; }, []); const toggleVisibility = (): void => { setIsVisible(prev => !prev); if (!isVisible && !isCaptionLoaded) { fetchCaption(); } }; const handleLoad = (): void => { setLoaded(true); }; const handleKeyDown = (e: KeyboardEvent): void => { // If Enter is pressed without Shift, prevent default behavior and save if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); saveCaption(); } }; const isCaptionCurrent = caption.trim() === savedCaption; const isItAVideo = isVideo(imageUrl); return (
{/* Square image container */}
{inViewport && isVisible && ( <> {isItAVideo ? (
{/* Text area below the image */}
{inViewport && isVisible && isCaptionLoaded && (
{ e.preventDefault(); saveCaption(); }} onBlur={saveCaption} >