ramimu's picture
Upload 586 files
1c72248 verified
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<DatasetImageCardProps> = ({
imageUrl,
alt,
children,
className = '',
onDelete = () => {},
}) => {
const cardRef = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState<boolean>(false);
const [inViewport, setInViewport] = useState<boolean>(false);
const [loaded, setLoaded] = useState<boolean>(false);
const [isCaptionLoaded, setIsCaptionLoaded] = useState<boolean>(false);
const [caption, setCaption] = useState<string>('');
const [savedCaption, setSavedCaption] = useState<string>('');
const isGettingCaption = useRef<boolean>(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<HTMLTextAreaElement>): 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 (
<div className={`flex flex-col ${className}`}>
{/* Square image container */}
<div
ref={cardRef}
className="relative w-full"
style={{ paddingBottom: '100%' }} // Make it square
>
<div className="absolute inset-0 rounded-t-lg shadow-md">
{inViewport && isVisible && (
<>
{isItAVideo ? (
<video
src={`/api/img/${encodeURIComponent(imageUrl)}`}
className={`w-full h-full object-contain`}
autoPlay={false}
loop
muted
controls
/>
) : (
<img
src={`/api/img/${encodeURIComponent(imageUrl)}`}
alt={alt}
onLoad={handleLoad}
className={`w-full h-full object-contain transition-opacity duration-300 ${
loaded ? 'opacity-100' : 'opacity-0'
}`}
/>
)}
</>
)}
{!isVisible && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-800 bg-opacity-75 rounded-t-lg">
<span className="text-white text-lg"></span>
</div>
)}
{children && <div className="absolute inset-0 flex items-center justify-center">{children}</div>}
<div className="absolute top-1 right-1 flex space-x-2">
<button
className="bg-gray-800 rounded-full p-2"
onClick={() => {
openConfirm({
title: `Delete ${isItAVideo ? 'video' : 'image'}`,
message: `Are you sure you want to delete this ${isItAVideo ? 'video' : 'image'}? This action cannot be undone.`,
type: 'warning',
confirmText: 'Delete',
onConfirm: () => {
apiClient
.post('/api/img/delete', { imgPath: imageUrl })
.then(() => {
console.log('Image deleted:', imageUrl);
onDelete();
})
.catch(error => {
console.error('Error deleting image:', error);
});
},
});
}}
>
<FaTrashAlt />
</button>
</div>
</div>
</div>
{/* Text area below the image */}
<div
className={classNames('w-full p-2 bg-gray-800 text-white text-sm rounded-b-lg h-[75px]', {
'border-blue-500 border-2': !isCaptionCurrent,
'border-transparent border-2': isCaptionCurrent,
})}
>
{inViewport && isVisible && isCaptionLoaded && (
<form
onSubmit={e => {
e.preventDefault();
saveCaption();
}}
onBlur={saveCaption}
>
<textarea
className="w-full bg-transparent resize-none outline-none focus:ring-0 focus:outline-none"
value={caption}
rows={3}
onChange={e => setCaption(e.target.value)}
onKeyDown={handleKeyDown}
/>
</form>
)}
{(!inViewport || !isVisible) && isCaptionLoaded && (
<div className="w-full h-full flex items-center justify-center text-gray-400">
{isVisible ? "Scroll into view to edit caption" : "Show content to edit caption"}
</div>
)}
{!isCaptionLoaded && (
<div className="w-full h-full flex items-center justify-center text-gray-400">
Loading caption...
</div>
)}
</div>
</div>
);
};
export default DatasetImageCard;