Untitled
unknown
plain_text
2 months ago
18 kB
6
Indexable
"use client" import { useState, useEffect } from "react" import { Download, MessageCircle, ZoomIn, ZoomOut } 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" interface Section { title: string; description: string; images: { src: string; alt: string; width: number; height: number; thumbnailSrc?: string; }[]; } interface GalleryProps { sections: Section[]; } interface CommentCounts { [key: string]: number; } interface ImageDialogProps { image: Section['images'][0]; fullSizeImageSrc: string; thumbnailSrc: string; photoId: string; commentCount: number; selectedTab: string; setSelectedTab: (tab: string) => void; isFullImageLoaded: boolean; setIsFullImageLoaded: (loaded: boolean) => void; handleDownload: (src: string) => Promise<void>; isDownloading: boolean; onCommentAdded: (photoId: string) => void; } function ImageDialog({ image, fullSizeImageSrc, thumbnailSrc, photoId, commentCount, selectedTab, setSelectedTab, isFullImageLoaded, setIsFullImageLoaded, handleDownload, isDownloading, onCommentAdded }: ImageDialogProps) { return ( <div className="flex flex-col h-full"> <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"> <TabsTrigger value="preview" className="text-sm px-3"> Vorschau </TabsTrigger> <TabsTrigger value="comments" className="text-sm px-3"> Kommentare {commentCount > 0 && ( <span className="ml-1.5 bg-primary/10 text-primary rounded-full px-1.5 py-0.5 text-xs"> {commentCount} </span> )} </TabsTrigger> </TabsList> <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 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"> <img 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 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 }} onLoad={() => setIsFullImageLoaded(true)} /> </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 photoId={photoId} onCommentAdded={() => onCommentAdded(photoId)} /> </div> </TabsContent> </div> </Tabs> </div> ); } export function Gallery({ sections }: GalleryProps) { const [selectedImage, setSelectedImage] = useState<number | null>(null); 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 [gridSize, setGridSize] = useState<number>(3); // Default: 3 base images per row useEffect(() => { if (!isDialogOpen) { setSelectedTab("preview"); setIsFullImageLoaded(false); } }, [isDialogOpen]); 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(); }, []); 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); } }; // Get column configuration based on slider value const getColumns = () => { // Map gridSize (1-6) to grid templates return { // Format: small screens, medium screens, large screens 1: { xs: 1, sm: 1, md: 2, lg: 2, xl: 2 }, 2: { xs: 1, sm: 2, md: 2, lg: 3, xl: 3 }, 3: { xs: 1, sm: 2, md: 3, lg: 4, xl: 4 }, 4: { xs: 2, sm: 3, md: 4, lg: 5, xl: 6 }, 5: { xs: 2, sm: 3, md: 5, lg: 6, xl: 7 }, 6: { xs: 2, sm: 4, md: 6, lg: 8, xl: 8 } }[gridSize] || { xs: 1, sm: 2, md: 3, lg: 4, xl: 4 }; }; // Calculate image spans based on type and available columns const calculateImageSpans = (image, cols) => { const aspectRatio = image.width / image.height; const isPortrait = aspectRatio < 0.8; const isLandscape = aspectRatio > 1.25; // Default spans let colSpan = 1; let rowSpan = 1; // Landscape images if (isLandscape) { // For larger screens, landscape spans 2 columns if there are enough columns colSpan = cols.md >= 3 ? 2 : 1; // Adjust row span to be shorter rowSpan = 1; } // Portrait images if (isPortrait) { // Portrait typically stays 1 column but is taller colSpan = 1; rowSpan = 2; } return { colSpan, rowSpan, isPortrait, isLandscape }; }; const columns = getColumns(); // Get dynamic grid template rows based on the items const getGridStyles = () => { const baseRowHeight = Math.max(200, 400 - (gridSize * 50)); // Larger rows for smaller grids return { // Extra small screens xs: ` display: grid; grid-template-columns: repeat(${columns.xs}, minmax(0, 1fr)); gap: 1rem; grid-auto-rows: ${baseRowHeight}px; `, // Small screens (640px+) sm: ` @media (min-width: 640px) { grid-template-columns: repeat(${columns.sm}, minmax(0, 1fr)); grid-auto-rows: ${baseRowHeight}px; } `, // Medium screens (768px+) md: ` @media (min-width: 768px) { grid-template-columns: repeat(${columns.md}, minmax(0, 1fr)); grid-auto-rows: ${baseRowHeight}px; } `, // Large screens (1024px+) lg: ` @media (min-width: 1024px) { grid-template-columns: repeat(${columns.lg}, minmax(0, 1fr)); grid-auto-rows: ${baseRowHeight}px; } `, // Extra large screens (1280px+) xl: ` @media (min-width: 1280px) { grid-template-columns: repeat(${columns.xl}, minmax(0, 1fr)); grid-auto-rows: ${baseRowHeight}px; } ` }; }; const gridStyles = getGridStyles(); return ( <div className="space-y-12"> {/* Size control slider */} <div className="relative mb-8 p-4 bg-card rounded-lg border shadow-sm"> <div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4"> <div> <h3 className="font-medium mb-2">Bildgröße anpassen</h3> <p className="text-sm text-muted-foreground">Ziehen Sie den Regler, um die Größe der Vorschaubilder zu ändern</p> </div> <div className="flex items-center gap-4 w-full md:w-1/2 lg:w-1/3"> <ZoomIn className="h-4 w-4 text-muted-foreground" /> <Slider value={[gridSize]} min={1} max={6} step={1} onValueChange={(value) => setGridSize(value[0])} className="flex-1" /> <ZoomOut className="h-4 w-4 text-muted-foreground" /> </div> </div> </div> {sections.map((section, sectionIndex) => ( <div key={sectionIndex} className="space-y-4"> <div className="space-y-2"> <h2 className="text-2xl font-bold">{section.title}</h2> {section.description && ( <p className="text-muted-foreground">{section.description}</p> )} </div> <div className="photo-grid"> <style jsx>{` .photo-grid { ${gridStyles.xs} ${gridStyles.sm} ${gridStyles.md} ${gridStyles.lg} ${gridStyles.xl} } `}</style> {section.images.map((image, imageIndex) => { const photoId = `${section.title}-${imageIndex}`; const commentCount = commentCounts[photoId] || 0; // Calculate spans based on image type and available columns const { colSpan, rowSpan, isPortrait, isLandscape } = calculateImageSpans(image, columns); return ( <div key={imageIndex} className="relative overflow-hidden rounded-lg bg-muted/30 transition-all duration-300" style={{ gridColumn: `span ${colSpan} / span ${colSpan}`, gridRow: `span ${rowSpan} / span ${rowSpan}`, }} > <img src={image.thumbnailSrc} alt={image.alt} className="h-full w-full object-cover" loading="lazy" /> <Dialog onOpenChange={(open) => { setIsDialogOpen(open); if (open) { setSelectedTab("preview"); setIsFullImageLoaded(false); } }}> <DialogTrigger asChild> <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-pointer"> <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> </DialogTrigger> <DialogContent className="w-[90%] h-[90vh] max-w-7xl p-4 sm:p-6"> <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 image={image} fullSizeImageSrc={image.src} thumbnailSrc={image.thumbnailSrc} photoId={photoId} commentCount={commentCount} selectedTab={selectedTab} setSelectedTab={setSelectedTab} isFullImageLoaded={isFullImageLoaded} setIsFullImageLoaded={setIsFullImageLoaded} handleDownload={handleDownload} isDownloading={isDownloading} onCommentAdded={(photoId) => { setCommentCounts(prev => ({ ...prev, [photoId]: (prev[photoId] || 0) + 1 })); }} /> </div> </DialogContent> </Dialog> {commentCount > 0 && ( <Dialog onOpenChange={(open) => { setIsDialogOpen(open); if (open) setSelectedTab("comments"); }}> <DialogTrigger asChild> <div className="absolute bottom-2 right-2 z-10"> <div className="bg-black/70 backdrop-blur-sm rounded-full px-2 py-1 flex items-center gap-1 text-white text-sm cursor-pointer hover:bg-black/80 transition-colors"> <MessageCircle className="h-4 w-4" /> <span>{commentCount}</span> </div> </div> </DialogTrigger> <DialogContent className="w-[90%] h-[90vh] max-w-7xl p-4 sm:p-6"> <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 image={image} fullSizeImageSrc={image.src} thumbnailSrc={image.thumbnailSrc} photoId={photoId} commentCount={commentCount} selectedTab={selectedTab} setSelectedTab={setSelectedTab} isFullImageLoaded={isFullImageLoaded} setIsFullImageLoaded={setIsFullImageLoaded} handleDownload={handleDownload} isDownloading={isDownloading} onCommentAdded={(photoId) => { setCommentCounts(prev => ({ ...prev, [photoId]: (prev[photoId] || 0) + 1 })); }} /> </div> </DialogContent> </Dialog> )} </div> ); })} </div> </div> ))} </div> ); } export default Gallery;
Editor is loading...
Leave a Comment