Reqorked visibility toggle on samples, should help when dealing with more samples

This commit is contained in:
Jaret Burkett
2025-09-28 14:13:10 -06:00
parent c20240be82
commit c233a80337
2 changed files with 72 additions and 49 deletions

View File

@@ -10,6 +10,10 @@ interface SampleImageCardProps {
className?: string;
onDelete?: () => void;
onClick?: () => void;
/** pass your scroll container element (e.g. containerRef.current) */
observerRoot?: Element | null;
/** optional: tweak pre-load buffer */
rootMargin?: string; // default '200px 0px'
}
const SampleImageCard: React.FC<SampleImageCardProps> = ({
@@ -20,70 +24,87 @@ const SampleImageCard: React.FC<SampleImageCardProps> = ({
children,
className = '',
onClick = () => {},
observerRoot = null,
rootMargin = '200px 0px',
}) => {
const cardRef = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState<boolean>(false);
const [loaded, setLoaded] = useState<boolean>(false);
const videoRef = useRef<HTMLVideoElement | null>(null);
const [isVisible, setIsVisible] = useState(false);
const [loaded, setLoaded] = useState(false);
// Observe both enter and exit
useEffect(() => {
// Create intersection observer to check visibility
const el = cardRef.current;
if (!el) return;
const observer = new IntersectionObserver(
entries => {
if (entries[0].isIntersecting) {
setIsVisible(true);
observer.disconnect();
for (const entry of entries) {
if (entry.target === el) {
setIsVisible(entry.isIntersecting);
}
}
},
{ threshold: 0.1 },
{
root: observerRoot ?? null,
threshold: 0.01,
rootMargin,
},
);
if (cardRef.current) {
observer.observe(cardRef.current);
observer.observe(el);
return () => observer.disconnect();
}, [observerRoot, rootMargin]);
// Pause video when leaving viewport
useEffect(() => {
if (!isVideo(imageUrl)) return;
const v = videoRef.current;
if (!v) return;
if (!isVisible && !v.paused) {
try {
v.pause();
} catch {}
}
}, [isVisible, imageUrl]);
return () => {
observer.disconnect();
};
}, []);
useEffect(() => {
console.log('SampleImageCard isVisible', isVisible, imageUrl);
}, [isVisible, imageUrl]);
const handleLoad = (): void => {
setLoaded(true);
};
const handleLoad = () => setLoaded(true);
return (
<div className={`flex flex-col ${className}`}>
{/* Square image container */}
<div
ref={cardRef}
className="relative w-full cursor-pointer"
style={{ paddingBottom: '100%' }} // Make it square
onClick={onClick}
>
<div ref={cardRef} className="relative w-full cursor-pointer" style={{ paddingBottom: '100%' }} onClick={onClick}>
<div className="absolute inset-0 rounded-t-lg shadow-md">
{isVisible && (
<>
{isVideo(imageUrl) ? (
<video
src={`/api/img/${encodeURIComponent(imageUrl)}`}
className={`w-full h-full object-cover`}
autoPlay={false}
loop
muted
playsInline
/>
) : (
<img
src={`/api/img/${encodeURIComponent(imageUrl)}`}
alt={alt}
onLoad={handleLoad}
className={`w-full h-full object-cover transition-opacity duration-300 ${
loaded ? 'opacity-100' : 'opacity-0'
}`}
/>
)}
</>
)}
{children && <div className="absolute inset-0 flex items-center justify-center">{children}</div>}
{isVisible ? (
isVideo(imageUrl) ? (
<video
ref={videoRef}
src={`/api/img/${encodeURIComponent(imageUrl)}`}
className="w-full h-full object-cover"
preload="none"
playsInline
muted
loop
controls={false}
/>
) : (
<img
src={`/api/img/${encodeURIComponent(imageUrl)}`}
alt={alt}
onLoad={handleLoad}
loading="lazy"
decoding="async"
className={`w-full h-full object-cover transition-opacity duration-300 ${
loaded ? 'opacity-100' : 'opacity-0'
}`}
/>
)
) : null}
{children && isVisible && <div className="absolute inset-0 flex items-center justify-center">{children}</div>}
</div>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import { useMemo, useState } from 'react';
import { useMemo, useState, useRef } from 'react';
import useSampleImages from '@/hooks/useSampleImages';
import SampleImageCard from './SampleImageCard';
import { Job } from '@prisma/client';
@@ -68,6 +68,7 @@ interface SampleImagesProps {
export default function SampleImages({ job }: SampleImagesProps) {
const { sampleImages, status, refreshSampleImages } = useSampleImages(job.id, 5000);
const [selectedSamplePath, setSelectedSamplePath] = useState<string | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const numSamples = useMemo(() => {
if (job?.job_config) {
const jobConfig = JSON.parse(job.job_config) as JobConfig;
@@ -232,7 +233,7 @@ export default function SampleImages({ job }: SampleImagesProps) {
}, [job]);
return (
<div>
<div ref={containerRef} className="absolute top-[80px] left-0 right-0 bottom-0 overflow-y-auto">
<div className="pb-4">
{PageInfoContent}
{sampleImages && (
@@ -245,6 +246,7 @@ export default function SampleImages({ job }: SampleImagesProps) {
sampleImages={sampleImages}
alt="Sample Image"
onClick={() => setSelectedSamplePath(sample)}
observerRoot={containerRef.current}
/>
))}
</div>