Untitled
unknown
plain_text
13 days ago
37 kB
5
Indexable
"use client" import { useState, useEffect, useRef, createRef } from "react" import { Download, MessageCircle, ZoomIn, ZoomOut, ChevronLeft, ChevronRight, X, Heart, Check } from "lucide-react" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog" import { PhotoComments } from "./photo-comments" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Slider } from "@/components/ui/slider" import { toast } from "sonner" import { ScrollToolbar } from "./ScrollToolbar" import { useInView } from "framer-motion" interface Section { title: string; description?: string; images: { src: string; alt: string; width: number; height: number; thumbnailSrc?: string; }[]; } interface GalleryProps { sections: Section[]; clientConfig: any; isSelectionView?: boolean; } interface CommentCounts { [key: string]: number; } interface ImageDialogProps { sections: Section[]; currentSectionIndex: number; currentImageIndex: number; commentCounts: CommentCounts; selectedTab: string; setSelectedTab: (tab: string) => void; isFullImageLoaded: boolean; setIsFullImageLoaded: (loaded: boolean) => void; handleDownload: (src: string) => Promise<void>; isDownloading: boolean; onCommentAdded: (photoId: string) => void; onNavigate: (direction: 'prev' | 'next') => void; onClose: () => void; favorites: {[key: string]: boolean}; toggleFavorite: (photoId: string, e: React.MouseEvent) => void; } function ImageDialog({ sections, currentSectionIndex, currentImageIndex, commentCounts, selectedTab, setSelectedTab, isFullImageLoaded, setIsFullImageLoaded, handleDownload, isDownloading, onCommentAdded, onNavigate, onClose, favorites, toggleFavorite }: ImageDialogProps) { const section = sections[currentSectionIndex]; const image = section.images[currentImageIndex]; const fullSizeImageSrc = image.src; const thumbnailSrc = image.thumbnailSrc; const photoId = `${section.title}-${currentImageIndex}`; const commentCount = commentCounts[photoId] || 0; const isFavorite = favorites[photoId] || false; const hasPrevious = currentImageIndex > 0; const hasNext = currentImageIndex < section.images.length - 1; // Function to handle image click and open in new tab const handleImageClick = () => { if (isFullImageLoaded) { window.open(fullSizeImageSrc, '_blank'); } }; return ( <div className="flex flex-col h-full"> {/* Custom close button */} <button onClick={onClose} className="absolute right-2 top-2 z-50 p-1 rounded-full bg-background/80 backdrop-blur-sm border border-border hover:bg-muted transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-primary" aria-label="Close dialog" > <X className="h-3.5 w-3.5" /> </button> <Tabs value={selectedTab} onValueChange={setSelectedTab} className="flex flex-col h-full"> <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2 mb-4"> <TabsList className="h-8 sm:h-9 p-1 w-auto"> <TabsTrigger value="preview" className="text-xs sm:text-sm px-2 py-1 sm:px-3 h-6 sm:h-7 data-[state=active]:shadow-sm rounded-md mx-0.5" > Vorschau </TabsTrigger> <TabsTrigger value="comments" className="text-xs sm:text-sm px-2 py-1 sm:px-3 h-6 sm:h-7 data-[state=active]:shadow-sm rounded-md mx-0.5" > Kommentare {commentCount > 0 && ( <span className="ml-1 bg-primary/10 text-primary rounded-full px-1 py-0.5 text-xs"> {commentCount} </span> )} </TabsTrigger> </TabsList> <div className="flex items-center gap-2"> <Button variant="ghost" size="sm" className={`h-8 sm:h-9 aspect-square p-0 ${isFavorite ? 'text-pink-500' : 'text-muted-foreground hover:text-white'}`} onClick={(e) => toggleFavorite(photoId, e)} > <Heart className="h-5 w-5 transition-transform duration-200 ease-out" fill={isFavorite ? 'currentColor' : 'none'} style={{ transform: isFavorite ? 'scale(1.1)' : 'scale(1)' }} /> </Button> <Button onClick={() => handleDownload(fullSizeImageSrc)} disabled={isDownloading} size="sm" className="h-8 sm:h-9 text-sm" > <Download className="mr-2 h-4 w-4" /> Bild herunterladen </Button> </div> </div> <div className="flex-1 min-h-0 relative rounded-lg bg-background"> <TabsContent value="preview" className="h-full" > <div className="h-full flex items-center justify-center p-4"> {/* Previous button */} {hasPrevious && ( <button onClick={(e) => { e.stopPropagation(); onNavigate('prev'); }} className="absolute left-4 top-1/2 -translate-y-1/2 z-20 flex items-center justify-center w-10 h-10 bg-black/40 hover:bg-black/60 text-white rounded-full transition-all duration-150" aria-label="Previous image" > <ChevronLeft className="h-6 w-6" /> </button> )} {/* Next button */} {hasNext && ( <button onClick={(e) => { e.stopPropagation(); onNavigate('next'); }} className="absolute right-4 top-1/2 -translate-y-1/2 z-20 flex items-center justify-center w-10 h-10 bg-black/40 hover:bg-black/60 text-white rounded-full transition-all duration-150" aria-label="Next image" > <ChevronRight className="h-6 w-6" /> </button> )} {/* Images */} <img key={`thumb-${photoId}`} src={thumbnailSrc} alt={image.alt} className="max-w-full max-h-full w-auto h-auto object-contain transition-opacity duration-300" style={{ opacity: isFullImageLoaded ? 0 : 1 }} /> <img key={`full-${photoId}`} src={fullSizeImageSrc} alt={image.alt} className="max-w-full max-h-full w-auto h-auto object-contain transition-opacity duration-300 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2" style={{ opacity: isFullImageLoaded ? 1 : 0, cursor: isFullImageLoaded ? "zoom-in" : "default" }} onLoad={() => setIsFullImageLoaded(true)} onClick={handleImageClick} title={isFullImageLoaded ? "Klicken, um in voller Größe zu öffnen" : ""} /> </div> </TabsContent> <TabsContent value="comments" className="absolute inset-0 overflow-y-auto" > <div className="p-4 w-full"> <style jsx global>{` .rounded-lg.border.bg-card { max-width: 100%; overflow: hidden; } .rounded-lg.border.bg-card > div { width: 100%; } .rounded-lg.border.bg-card > div > div { flex-wrap: wrap; gap: 0.5rem; } .rounded-lg.border.bg-card > div > div > span { order: 2; width: 100%; text-align: left; padding-left: 3.25rem; } `}</style> <PhotoComments key={`comments-${photoId}`} photoId={photoId} onCommentAdded={() => onCommentAdded(photoId)} /> {/* Navigation in comments view too */} <div className="flex justify-between items-center mt-4"> {hasPrevious ? ( <Button variant="outline" size="sm" onClick={() => onNavigate('prev')} className="flex items-center gap-1" > <ChevronLeft className="h-4 w-4" /> Vorheriges Bild </Button> ) : <div />} {hasNext && ( <Button variant="outline" size="sm" onClick={() => onNavigate('next')} className="flex items-center gap-1 ml-auto" > Nächstes Bild <ChevronRight className="h-4 w-4" /> </Button> )} </div> </div> </TabsContent> </div> {/* Navigation and counter indicator at the bottom */} <div className="flex items-center justify-center gap-2 mt-4"> <span className="text-sm text-muted-foreground"> {currentImageIndex + 1} / {section.images.length} </span> </div> </Tabs> </div> ); } export function Gallery({ sections, clientConfig, isSelectionView = false }: GalleryProps) { const [currentSectionIndex, setCurrentSectionIndex] = useState<number>(0); const [currentImageIndex, setCurrentImageIndex] = useState<number>(0); const [isDownloading, setIsDownloading] = useState(false); const [isFullImageLoaded, setIsFullImageLoaded] = useState(false); const [selectedTab, setSelectedTab] = useState<string>("preview"); const [isDialogOpen, setIsDialogOpen] = useState(false); const [commentCounts, setCommentCounts] = useState<{[key: string]: number}>({}); const defaultGridSize = clientConfig.gallery?.defaultGridSize || 3; const [gridSize, setGridSize] = useState<number>(defaultGridSize); // Use client config for rounded images or fallback to true const roundedImages = clientConfig.gallery?.roundedImages !== undefined ? clientConfig.gallery.roundedImages : true; const [favorites, setFavorites] = useState<{[key: string]: boolean}>({}); const [showFavoritesOnly, setShowFavoritesOnly] = useState<boolean>(false); const [showCommentsOnly, setShowCommentsOnly] = useState<boolean>(false); // Create a refs object to store references to each image element const imageRefs = useRef<{[key: string]: React.RefObject<HTMLDivElement>}>({}); // For scroll toolbar const footerRef = useRef(null); const inView = useInView(footerRef, { amount: 0.1 }); // Single reference to the currently open Dialog const [activeDialogId, setActiveDialogId] = useState<string | null>(null); // Initialize refs for all images useEffect(() => { // Create refs for all images across all sections sections.forEach((section) => { section.images.forEach((_, imageIndex) => { const photoId = `${section.title}-${imageIndex}`; if (!imageRefs.current[photoId]) { imageRefs.current[photoId] = createRef(); } }); }); }, [sections]); // Function to scroll to the current image in the gallery const scrollToCurrentImage = () => { const photoId = `${sections[currentSectionIndex].title}-${currentImageIndex}`; const ref = imageRefs.current[photoId]; if (ref && ref.current) { // Use a small timeout to ensure DOM has updated setTimeout(() => { ref.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, 100); } }; // Load favorites from localStorage on initial render useEffect(() => { const storedFavorites = localStorage.getItem('favorites'); if (storedFavorites) { try { setFavorites(JSON.parse(storedFavorites)); } catch (e) { console.error('Failed to parse favorites from localStorage:', e); } } }, []); // Add special behavior for selection view to auto-favorite images useEffect(() => { if (isSelectionView && sections.length > 0) { // Create a new favorites object that includes all images in the selection const selectionFavorites = { ...favorites }; // Mark all images in the selection as favorites sections.forEach((section) => { section.images.forEach((_, imageIndex) => { const photoId = `${section.title}-${imageIndex}`; // Only set if not already set (don't override user choices) if (selectionFavorites[photoId] === undefined) { selectionFavorites[photoId] = true; } }); }); // Update favorites state setFavorites(selectionFavorites); // We don't save to localStorage when in selection view // to avoid polluting main gallery favorites } }, [isSelectionView, sections]); // Function to toggle favorites const toggleFavorite = (photoId: string, e: React.MouseEvent) => { e.stopPropagation(); // Prevent opening the image dialog const newFavorites = { ...favorites }; newFavorites[photoId] = !newFavorites[photoId]; setFavorites(newFavorites); localStorage.setItem('favorites', JSON.stringify(newFavorites)); }; // Function to reset all favorites const handleResetAllFavorites = () => { // Clear all favorites setFavorites({}); // Clear from localStorage localStorage.removeItem('favorites'); // Show confirmation toast toast.success('Alle Favoriten wurden zurückgesetzt'); // If currently showing favorites only, turn off this filter if (showFavoritesOnly) { setShowFavoritesOnly(false); } }; // Function to create a shared link with favorites const handleCreateSharedLink = async (password: string, requirePassword: boolean) => { // First, find which selection number to use const clientId = window.location.pathname.split('/')[1]; // Get all favorited photos const selectedFavorites: any = {}; sections.forEach((section) => { section.images.forEach((image, imageIndex) => { const photoId = `${section.title}-${imageIndex}`; if (favorites[photoId]) { if (!selectedFavorites[section.title]) { selectedFavorites[section.title] = []; } selectedFavorites[section.title].push(imageIndex); } }); }); // Create selection data const selectionData = { favorites: selectedFavorites, requirePassword, password: requirePassword ? password : "", createdAt: new Date().toISOString(), parentClientId: clientId }; try { // Determine the next available selection ID let nextSelectionId = 1; try { const response = await fetch(`/api/selections?clientId=${encodeURIComponent(clientId)}`); if (response.ok) { const selections = await response.json(); if (selections && selections.length > 0) { // Extract all numeric selection IDs const numericIds = selections .map(s => parseInt(s.selection_id)) .filter(id => !isNaN(id)); if (numericIds.length > 0) { // Find the highest ID and increment nextSelectionId = Math.max(...numericIds) + 1; } } } } catch (error) { console.error("Error fetching existing selections:", error); // Continue with the default ID 1 if there's an error } // Save the selection to the server const response = await fetch(`/api/selections`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ clientId, selectionId: nextSelectionId.toString(), data: selectionData }), }); if (!response.ok) { throw new Error("Failed to create selection"); } // Return the shareable URL const origin = window.location.origin; const shareableUrl = `${origin}/${clientId}/auswahl/${nextSelectionId}`; // If a password is set, include it in the hash const fullUrl = requirePassword ? `${shareableUrl}#password=${password}` : shareableUrl; return fullUrl; } catch (error) { console.error("Error creating shared link:", error); throw error; } }; // Reset state when dialog closes and scroll to current image useEffect(() => { if (!isDialogOpen) { setSelectedTab("preview"); setIsFullImageLoaded(false); setActiveDialogId(null); // Scroll to the current image when the dialog closes // Small delay to ensure DOM is ready setTimeout(scrollToCurrentImage, 200); } }, [isDialogOpen]); // Fetch comment counts when component mounts useEffect(() => { const fetchCommentCounts = async () => { const clientId = window.location.pathname.split('/')[1]; try { const response = await fetch(`/api/comments?clientId=${encodeURIComponent(clientId)}&index=true`); if (response.ok) { const counts = await response.json(); setCommentCounts(counts); } } catch (error) { console.error('Error fetching comment counts:', error); } }; fetchCommentCounts(); }, []); // Reset full image loading state when image changes useEffect(() => { setIsFullImageLoaded(false); }, [currentImageIndex]); // Handle downloading all favorites const handleDownloadAllFavorites = async () => { const favoritePhotos = []; // Collect all favorited photos sections.forEach((section) => { section.images.forEach((image, imageIndex) => { const photoId = `${section.title}-${imageIndex}`; if (favorites[photoId]) { favoritePhotos.push({ src: image.src, filename: image.src.split('/').pop() || `favorite-${photoId}.jpg` }); } }); }); if (favoritePhotos.length === 0) { toast.error("Keine Favoriten zum Herunterladen gefunden"); return; } // Show initial toast toast.info(`Download von ${favoritePhotos.length} Favoriten wird gestartet...`); let downloadedCount = 0; let failedCount = 0; // Download files one by one with small delay to prevent browser throttling for (const photo of favoritePhotos) { try { await new Promise(resolve => setTimeout(resolve, 300)); // Small delay between downloads const response = await fetch(photo.src, { method: 'GET', headers: { 'Cache-Control': 'no-cache' } }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const blob = await response.blob(); if (blob.size === 0) { throw new Error('Downloaded file is empty'); } const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = photo.filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); downloadedCount++; // Update progress every 3 files or for the last one if (downloadedCount % 3 === 0 || downloadedCount === favoritePhotos.length) { toast.info(`${downloadedCount} von ${favoritePhotos.length} Favoriten heruntergeladen...`); } } catch (error) { console.error(`Failed to download ${photo.filename}:`, error); failedCount++; } } // Show final toast if (failedCount > 0) { toast.warning(`${downloadedCount} von ${favoritePhotos.length} Favoriten heruntergeladen, ${failedCount} fehlgeschlagen.`); } else { toast.success(`Alle ${favoritePhotos.length} Favoriten erfolgreich heruntergeladen!`); } }; const handleDownload = async (imageSrc: string) => { try { setIsDownloading(true); const fileName = imageSrc.split('/').pop() || 'image'; const response = await fetch(imageSrc, { method: 'GET', headers: { 'Cache-Control': 'no-cache' } }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const contentType = response.headers.get('content-type'); if (!contentType?.includes('image')) { throw new Error(`Invalid content type: ${contentType}`); } const blob = await response.blob(); if (blob.size === 0) { throw new Error('Downloaded file is empty'); } const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = fileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); toast.success('Download erfolgreich'); } catch (error) { console.error('Download error:', error); toast.error(`Download fehlgeschlagen: ${error.message}`); } finally { setIsDownloading(false); } }; // Handle image navigation with scrolling to keep the current image visible const handleNavigate = (direction: 'prev' | 'next') => { if (direction === 'prev' && currentImageIndex > 0) { setCurrentImageIndex(currentImageIndex - 1); // Scroll to the new current image after state update setTimeout(scrollToCurrentImage, 100); } else if (direction === 'next' && currentImageIndex < sections[currentSectionIndex].images.length - 1) { setCurrentImageIndex(currentImageIndex + 1); // Scroll to the new current image after state update setTimeout(scrollToCurrentImage, 100); } }; // Handle keyboard navigation useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (!isDialogOpen) return; if (e.key === 'ArrowLeft') { handleNavigate('prev'); } else if (e.key === 'ArrowRight') { handleNavigate('next'); } else if (e.key === 'Escape') { setIsDialogOpen(false); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [isDialogOpen, currentImageIndex, currentSectionIndex]); // Helper function to determine image type const getImageType = (image) => { const aspectRatio = image.width / image.height; if (aspectRatio < 0.8) return 'portrait'; if (aspectRatio > 1.3) return 'landscape'; return 'square'; }; // CSS classes for the grid based on the slider value const getGridClasses = () => { // gridSize ranges from 1 to 6 // Lower values = bigger thumbnails, fewer per row // Higher values = smaller thumbnails, more per row const sizeMap = { 1: 'grid-cols-1 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-2', 2: 'grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3', 3: 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-4', 4: 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6', 5: 'grid-cols-2 sm:grid-cols-3 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-7', 6: 'grid-cols-2 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 xl:grid-cols-8', }; return sizeMap[gridSize] || sizeMap[3]; // Default to 3 if invalid }; return ( <div className="space-y-12"> {/* Main Dialog (single instance for all images) */} <Dialog open={isDialogOpen} onOpenChange={(open) => { setIsDialogOpen(open); }} > <DialogContent className="w-[90%] h-[90vh] max-w-7xl p-4 sm:p-6" closeButtonClassName="sr-only hidden"> <DialogHeader className="mb-4"> <DialogTitle>Bildvorschau</DialogTitle> <DialogDescription> Ansehen und herunterladen des Bildes in voller Auflösung </DialogDescription> </DialogHeader> <div className="flex-1 h-[calc(90vh-8rem)]"> <ImageDialog sections={sections} currentSectionIndex={currentSectionIndex} currentImageIndex={currentImageIndex} commentCounts={commentCounts} selectedTab={selectedTab} setSelectedTab={setSelectedTab} isFullImageLoaded={isFullImageLoaded} setIsFullImageLoaded={setIsFullImageLoaded} handleDownload={handleDownload} isDownloading={isDownloading} onNavigate={handleNavigate} onClose={() => setIsDialogOpen(false)} favorites={favorites} toggleFavorite={toggleFavorite} onCommentAdded={(photoId) => { setCommentCounts(prev => ({ ...prev, [photoId]: (prev[photoId] || 0) + 1 })); }} /> </div> </DialogContent> </Dialog> {sections.map((section, sectionIndex) => { // Count how many favorites are in this section const sectionFavorites = section.images.reduce((count, _, imageIndex) => { const photoId = `${section.title}-${imageIndex}`; return favorites[photoId] ? count + 1 : count; }, 0); // Count how many images with comments are in this section const sectionComments = section.images.reduce((count, _, imageIndex) => { const photoId = `${section.title}-${imageIndex}`; return commentCounts[photoId] ? count + 1 : count; }, 0); // Skip rendering the entire section if filtering and no matching items in this section if ((showFavoritesOnly && sectionFavorites === 0) || (showCommentsOnly && sectionComments === 0)) { return null; } return ( <div key={sectionIndex} id={`section-${section.title}`} className="space-y-4"> <div className="space-y-2"> <h2 className="text-2xl font-bold flex items-center gap-2"> {section.title} {showFavoritesOnly && sectionFavorites > 0 && ( <span className="text-sm text-pink-500 font-normal"> ({sectionFavorites} {sectionFavorites === 1 ? 'Favorit' : 'Favoriten'}) </span> )} {showCommentsOnly && sectionComments > 0 && ( <span className="text-sm text-blue-500 font-normal"> ({sectionComments} {sectionComments === 1 ? 'Kommentar' : 'Kommentare'}) </span> )} </h2> {section.description && ( <p className="text-muted-foreground">{section.description}</p> )} </div> <div className={`grid ${getGridClasses()} gap-4`}> {section.images.map((image, imageIndex) => { const photoId = `${section.title}-${imageIndex}`; const commentCount = commentCounts[photoId] || 0; const imageType = getImageType(image); const isFavorite = favorites[photoId] || false; // Skip non-favorite images when filter is active if ((showFavoritesOnly && !isFavorite) || (showCommentsOnly && commentCount === 0)) { return null; } return ( <div key={imageIndex} ref={imageRefs.current[photoId]} className={` relative overflow-hidden ${roundedImages ? 'rounded-lg' : ''} bg-muted/30 transition-all duration-300 ${imageType === 'landscape' ? 'col-span-2' : ''} ${isFavorite ? 'ring-2 ring-pink-500/50 ring-offset-2 ring-offset-pink-500/30 shadow-lg shadow-pink-500/10' : ''} ${commentCount > 0 && showCommentsOnly ? 'ring-2 ring-blue-500/50 ring-offset-2 ring-offset-blue-500/30 shadow-lg shadow-blue-500/10' : ''} `} style={{ aspectRatio: image.width / image.height }} > <img src={image.thumbnailSrc} alt={image.alt} className={`h-full w-full object-cover transition-all duration-300 ${isFavorite ? 'brightness-105' : ''}`} loading="lazy" /> {/* Main trigger for opening dialog */} <div className="absolute inset-0 flex items-center justify-center gap-2 bg-black/60 opacity-0 hover:opacity-100 transition-opacity duration-200 cursor-zoom-in" onClick={() => { setCurrentSectionIndex(sectionIndex); setCurrentImageIndex(imageIndex); setSelectedTab("preview"); setIsFullImageLoaded(false); setIsDialogOpen(true); }} > <Button variant="secondary" size="icon" className="h-9 w-9 z-10" onClick={(e) => { e.stopPropagation(); handleDownload(image.src); }} > <Download className="h-4 w-4" /> </Button> </div> {/* Favorite heart icon */} <div className="absolute bottom-2 left-2 z-10" onClick={(e) => toggleFavorite(photoId, e)} > <div className={` rounded-full p-1.5 flex items-center justify-center cursor-pointer ${isFavorite ? 'bg-pink-500/20 backdrop-blur-sm' : 'bg-black/30 hover:bg-black/50 backdrop-blur-sm'} transition-all duration-300 `}> <Heart className={` ${isFavorite ? 'text-pink-500 filter drop-shadow-sm' : 'text-white/90'} transition-all duration-300 ${gridSize <= 3 ? 'h-5 w-5' : 'h-4 w-4'} ${isFavorite ? 'scale-110' : 'scale-100'} `} fill={isFavorite ? 'currentColor' : 'none'} style={{ filter: isFavorite ? 'drop-shadow(0 0 2px rgba(236, 72, 153, 0.3))' : 'none', }} /> </div> </div> {/* Comment count indicator that also opens dialog */} {commentCount > 0 && ( <div className="absolute bottom-2 right-2 z-10" onClick={(e) => { e.stopPropagation(); setCurrentSectionIndex(sectionIndex); setCurrentImageIndex(imageIndex); setSelectedTab("comments"); setIsDialogOpen(true); }} > <div className={` rounded-full p-1.5 flex items-center justify-center cursor-pointer bg-black/30 hover:bg-black/50 backdrop-blur-sm transition-all duration-300 `}> <MessageCircle className={` text-white/90 transition-all duration-300 ${gridSize <= 3 ? 'h-5 w-5' : 'h-4 w-4'} `} /> <span className="ml-1 text-white text-xs">{commentCount}</span> </div> </div> )} </div> ); })} </div> </div> ); })} {/* Show message when no favorites are found */} {showFavoritesOnly && !Object.values(favorites).some(v => v) && ( <div className="bg-muted/30 rounded-lg p-8 text-center"> <div className="inline-flex items-center justify-center p-4 rounded-full bg-pink-500/10 mb-4"> <Heart className="h-8 w-8 text-pink-500/50" /> </div> <h3 className="text-xl font-semibold mb-2">Keine Favoriten gefunden</h3> <p className="text-muted-foreground"> Klicken Sie auf das Herz-Symbol bei den Bildern, um sie zu deinen Favoriten hinzuzufügen. </p> </div> )} {/* Show message when no commented images are found */} {showCommentsOnly && !Object.keys(commentCounts).length && ( <div className="bg-muted/30 rounded-lg p-8 text-center"> <div className="inline-flex items-center justify-center p-4 rounded-full bg-blue-500/10 mb-4"> <MessageCircle className="h-8 w-8 text-blue-500/50" /> </div> <h3 className="text-xl font-semibold mb-2">Keine kommentierte Bilder gefunden</h3> <p className="text-muted-foreground"> Schreibe Kommentare zu Bildern, um sie hier anzuzeigen. </p> </div> )} {/* Scroll aware toolbar */} <ScrollToolbar gridSize={gridSize} setGridSize={setGridSize} sections={sections} inView={inView} clientConfig={clientConfig} favorites={favorites} showFavoritesOnly={showFavoritesOnly} setShowFavoritesOnly={setShowFavoritesOnly} commentCounts={commentCounts} showCommentsOnly={showCommentsOnly} setShowCommentsOnly={setShowCommentsOnly} handleDownloadAllFavorites={handleDownloadAllFavorites} handleResetAllFavorites={handleResetAllFavorites} handleCreateSharedLink={handleCreateSharedLink} clientId={window.location.pathname.split('/')[1]} /> {/* Invisible ref element to detect when we're at the bottom */} <div ref={footerRef} className="h-1" /> {/* CSS to fix image layout */} <style jsx global>{` .grid > div { display: flex; overflow: hidden; } .grid > div img { object-fit: cover; width: 100%; height: 100%; max-width: 100%; max-height: 100%; } `}</style> </div> ); }
Editor is loading...
Leave a Comment