Untitled
unknown
plain_text
8 months ago
37 kB
6
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