import React, { useState, useEffect, useRef } from 'react'; import { FiHeart } from 'react-icons/fi'; import { useNavigate } from 'react-router-dom'; // Import custom hooks and components import { useSEO } from '../hooks/useSEO'; import WaterfallGrid from '../components/WaterfallGrid'; import ShareButton from '../components/ShareButton'; import { useInView } from 'react-intersection-observer'; // Import Zustand stores and Firebase services import { useAuthStore, useGalleryStore } from '../stores'; import type { GalleryImage } from '../stores/galleryStore'; import { db } from '../firebase'; import { doc, updateDoc, arrayUnion, arrayRemove } from 'firebase/firestore'; const Gallery: React.FC = () => { const navigate = useNavigate(); const [likedImages, setLikedImages] = useState>(new Set()); const [likeLoading, setLikeLoading] = useState>(new Set()); const [optimisticLikeCounts, setOptimisticLikeCounts] = useState>(new Map()); const { user, isAuthenticated } = useAuthStore(); // Use gallery store const { images, loading, error, hasMore, loadMore, fetchImages } = useGalleryStore(); // SEO optimization for Gallery page useSEO({ title: 'AiFigure Gallery - Explore Amazing AI-Generated Action Figures | Community Showcase', description: 'Browse our community gallery of stunning AI-generated action figures. Discover unique creations from artists worldwide, get inspired, and join the AiFigure community. Free AI art generation platform.', keywords: 'AI action figures gallery, AI generated toys showcase, custom figurines community, AI collectibles display, free AI art community, action figure creator gallery, AI image generation portfolio', ogTitle: 'AiFigure Gallery - Explore Amazing AI-Generated Action Figures | Community Showcase', ogDescription: 'Browse our community gallery of stunning AI-generated action figures. Discover unique creations from artists worldwide, get inspired, and join the AiFigure community.', ogImage: 'https://aifigure.com/gallery-og-image.jpg', ogUrl: window.location.href, twitterTitle: 'AiFigure Gallery - Explore Amazing AI-Generated Action Figures | Community Showcase', twitterDescription: 'Browse our community gallery of stunning AI-generated action figures. Discover unique creations from artists worldwide, get inspired, and join the AiFigure community.', twitterImage: 'https://aifigure.com/gallery-og-image.jpg', canonical: window.location.href, type: 'website', }); // Initialize gallery images if not already loaded useEffect(() => { console.log('🎯 Gallery: Checking if images need to be fetched', { imagesLength: images.length, loading, error: !!error, isAuthenticated }); if (images.length === 0 && !loading && !error) { console.log('🎯 Gallery: Initial fetch - no images in store'); fetchImages(true); } }, [images.length, loading, error, fetchImages, isAuthenticated]); // Include dependencies // Use react-intersection-observer for more reliable detection const { ref: loadMoreRef, inView: isIntersecting } = useInView({ threshold: 0.3, // More sensitive - trigger when 30% visible rootMargin: '50px', // Smaller margin for earlier detection triggerOnce: false, // Allow multiple triggers delay: 100, // Small delay to prevent false positives }); // Prevent multiple simultaneous loads const lastLoadTime = useRef(0); const loadAttempts = useRef(0); // Debug state for development const [debugMode] = useState(() => { return new URLSearchParams(window.location.search).get('debug') === 'infinite-scroll'; }); // Store element reference for scroll fallback const elementRef = React.useRef(null); // Fallback scroll detection for when intersection observer fails React.useEffect(() => { let scrollTimeout: NodeJS.Timeout; const handleScroll = () => { // Clear existing timeout if (scrollTimeout) clearTimeout(scrollTimeout); // Set new timeout to check scroll position scrollTimeout = setTimeout(() => { if (!elementRef.current || !hasMore || loading) return; const element = elementRef.current; const rect = element.getBoundingClientRect(); const windowHeight = window.innerHeight; // Check if element is near viewport (within 200px of bottom) const isNearBottom = rect.top <= windowHeight + 200 && rect.bottom >= 0; console.log('📜 Scroll fallback check:', { isNearBottom, elementTop: rect.top, windowHeight, hasMore, loading }); if (isNearBottom && hasMore && !loading) { const now = Date.now(); const timeSinceLastLoad = now - lastLoadTime.current; if (timeSinceLastLoad > 300) { console.log('🔄 Fallback scroll triggered: loading more images'); loadAttempts.current += 1; lastLoadTime.current = now; setTimeout(() => loadMore(), 100); } } }, 100); // Debounce scroll events }; // Add fallback scroll detection as backup (always enabled for reliability) console.log('📜 Scroll fallback enabled as backup mechanism'); window.addEventListener('scroll', handleScroll, { passive: true }); return () => { if (scrollTimeout) clearTimeout(scrollTimeout); window.removeEventListener('scroll', handleScroll); }; }, [hasMore, loading, loadMore]); // Load more when intersection observer triggers React.useEffect(() => { const now = Date.now(); const timeSinceLastLoad = now - lastLoadTime.current; console.log('👀 Intersection state: isIntersecting =', isIntersecting, ', hasMore =', hasMore, ', loading =', loading, ', timeSinceLastLoad =', timeSinceLastLoad); // Debug element position if (debugMode && elementRef.current) { const rect = elementRef.current.getBoundingClientRect(); console.log('📍 Trigger element position:', { top: rect.top, bottom: rect.bottom, height: rect.height, viewportHeight: window.innerHeight }); } // More aggressive loading: trigger when element is in view and conditions are met if (isIntersecting && hasMore && !loading && timeSinceLastLoad > 300) { console.log('🔄 Infinite scroll triggered: loading more images'); loadAttempts.current += 1; lastLoadTime.current = now; // Add a small delay to ensure smooth scrolling setTimeout(() => { loadMore(); }, 100); } else if (isIntersecting && !hasMore) { console.log('🎉 No more images to load'); } else if (isIntersecting && loading) { console.log('⏳ Already loading, skipping...'); } else if (isIntersecting && timeSinceLastLoad <= 300) { console.log('⚡ Throttled: too soon since last load'); } }, [isIntersecting, hasMore, loading, loadMore, debugMode]); // Initialize liked images when images or user changes useEffect(() => { if (user && images.length > 0) { const userLikedImages = new Set(); images.forEach(image => { if (image.likes?.includes(user.uid)) { userLikedImages.add(image.id); } }); setLikedImages(userLikedImages); } else { // For anonymous users, show no liked images (they can still like but it won't persist) setLikedImages(new Set()); } // Reset optimistic counts when images change setOptimisticLikeCounts(new Map()); }, [images, user]); // Restore scroll position when returning from image page useEffect(() => { const restoreScrollPosition = sessionStorage.getItem('restoreScrollPosition'); if (restoreScrollPosition) { const scrollPos = parseInt(restoreScrollPosition, 10); if (scrollPos > 0) { // Use setTimeout to ensure the page has rendered setTimeout(() => { window.scrollTo({ top: scrollPos, behavior: 'auto' }); }, 100); } sessionStorage.removeItem('restoreScrollPosition'); } }, []); // Handle like/unlike functionality with optimistic updates const handleLike = async (imageId: string, currentLikes: string[] = [], currentCount: number = 0) => { if (likeLoading.has(imageId)) return; // For anonymous users, only show visual feedback without persistence if (!user) { alert('Please sign in to save your likes permanently'); return; } const imageRef = doc(db, 'publicImages', imageId); const wasLiked = likedImages.has(imageId); // Optimistic UI update - update immediately setLikedImages(prev => { const newSet = new Set(prev); if (wasLiked) { newSet.delete(imageId); } else { newSet.add(imageId); } return newSet; }); // Update optimistic like count setOptimisticLikeCounts(prev => { const newMap = new Map(prev); newMap.set(imageId, wasLiked ? currentCount - 1 : currentCount + 1); return newMap; }); setLikeLoading(prev => new Set(prev).add(imageId)); try { if (wasLiked) { // Unlike: remove user from likes array await updateDoc(imageRef, { likes: arrayRemove(user.uid), likesCount: Math.max(0, currentLikes.length - 1) }); } else { // Like: add user to likes array await updateDoc(imageRef, { likes: arrayUnion(user.uid), likesCount: currentLikes.length + 1 }); } } catch (error) { console.error('Error updating like:', error); // Revert optimistic updates on error setLikedImages(prev => { const newSet = new Set(prev); if (wasLiked) { newSet.add(imageId); // Re-add if it was liked before } else { newSet.delete(imageId); // Remove if it wasn't liked before } return newSet; }); // Revert optimistic like count setOptimisticLikeCounts(prev => { const newMap = new Map(prev); newMap.set(imageId, currentCount); return newMap; }); alert('Failed to update like. Please try again.'); } finally { setLikeLoading(prev => { const newSet = new Set(prev); newSet.delete(imageId); return newSet; }); } }; // Handle image click to navigate to image page const handleImageClick = (image: GalleryImage) => { // Store current scroll position const scrollPosition = window.scrollY; navigate(`/image/${image.id}`, { state: { scrollPosition }, replace: false }); }; return (
{/* Debug Panel */} {debugMode && (
🔧 Infinite Scroll Debug
Images: {images.length}
Loading: {loading ? '✅' : '❌'}
Has More: {hasMore ? '✅' : '❌'}
Intersecting: {isIntersecting ? '✅' : '❌'}
Load Attempts: {loadAttempts.current}
Time Since Load: {Date.now() - lastLoadTime.current}ms
Trigger Visible: {isIntersecting ? '✅' : '❌'}
IO Supported: {typeof IntersectionObserver !== 'undefined' ? '✅' : '❌'}
)} {/* Header - constrained width */}

{!isAuthenticated ? 'AI Action Figure Generator' : 'Community Gallery'}

{!isAuthenticated ? 'Create your amazing action figures with one click!' : 'Explore amazing AI-generated action figures created by our community'}

{!isAuthenticated && <>

Sign up to create your own figures for FREE!

}
{/* Gallery Grid - full width */}
{/* Error State */} {error && (
!

Error Loading Gallery

{error}

)} {/* Gallery Grid */} {!error && ( <> {images.length === 0 && !loading ? (

No images yet

Be the first to share your AI-generated action figures!

) : (
({ id: img.id, imageUrl: img.imageUrl, }))} baseColumnWidth={280} gap={16} onItemClick={(item) => { const fullImage = images.find(img => img.id === item.id); if (fullImage) handleImageClick(fullImage); }} > {(item, style) => { const fullImage = images.find(img => img.id === item.id); if (!fullImage) return null; return (
AI generated action figure
{/*

{fullImage.userEmail?.split('@')[0] || 'Anonymous'}

{fullImage.createdAt.toLocaleDateString()}

*/}
{/* Like Button */}
{ e.stopPropagation(); const currentCount = fullImage.likesCount || fullImage.likes?.length || 0; handleLike(fullImage.id, fullImage.likes || [], currentCount); }} > {likeLoading.has(fullImage.id) ? (
) : ( optimisticLikeCounts.get(fullImage.id) ?? fullImage.likesCount ?? fullImage.likes?.length ?? 0 )}
{/* Share Button */}
); }}
{/* Loading skeletons for initial load */} {/* {loading && images.length === 0 && (
{Array.from({ length: 12 }).map((_, index) => ( ))}
)} */} {loading && images.length === 0 && (
Loading gallery...
)} {/* Infinite scroll trigger - made taller and more visible */}
{ loadMoreRef(node); // Set ref for useInView elementRef.current = node; // Set ref for scroll fallback }} className=" h-32 flex flex-col items-center justify-center mt-8 mb-4" data-testid="infinite-scroll-trigger" > {loading && images.length > 0 && (
)} {images.length === 0 && !loading && (
No images found
)}
)} )}
navigate('/create')} > + Create your own now
); }; export default Gallery;