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