Untitled

 avatar
unknown
plain_text
2 months ago
20 kB
6
Indexable
"use client"

import { useState, useEffect } from "react"
import { Download, MessageCircle, ZoomIn, ZoomOut, ChevronLeft, ChevronRight } 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 {
  section: Section;
  currentImageIndex: number;
  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;
  onNavigate: (direction: 'prev' | 'next') => void;
}

function ImageDialog({
  section,
  currentImageIndex,
  photoId,
  commentCount,
  selectedTab,
  setSelectedTab,
  isFullImageLoaded,
  setIsFullImageLoaded,
  handleDownload,
  isDownloading,
  onCommentAdded,
  onNavigate
}: ImageDialogProps) {
  const image = section.images[currentImageIndex];
  const fullSizeImageSrc = image.src;
  const thumbnailSrc = image.thumbnailSrc;

  const hasPrevious = currentImageIndex > 0;
  const hasNext = currentImageIndex < section.images.length - 1;

  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">
              {/* Previous button */}
              {hasPrevious && (
                <button 
                  onClick={(e) => {
                    e.stopPropagation();
                    onNavigate('prev');
                  }}
                  className="absolute left-2 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"
                  aria-label="Previous image"
                >
                  <ChevronLeft className="h-6 w-6" />
                </button>
              )}

              {/* Next button */}
              {hasNext && (
                <button 
                  onClick={(e) => {
                    e.stopPropagation();
                    onNavigate('next');
                  }}
                  className="absolute right-2 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"
                  aria-label="Next image"
                >
                  <ChevronRight className="h-6 w-6" />
                </button>
              )}

              {/* Images */}
              <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>

        {/* 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 }: GalleryProps) {
  const [selectedSectionIndex, setSelectedSectionIndex] = useState<number>(0);
  const [selectedImageIndex, setSelectedImageIndex] = 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 [gridSize, setGridSize] = useState<number>(3); // Default: 3 images per row on desktop

  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();
  }, []);

  // Reset full image loading state when image changes
  useEffect(() => {
    setIsFullImageLoaded(false);
  }, [selectedImageIndex]);

  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
  const handleNavigate = (direction: 'prev' | 'next') => {
    const currentSection = sections[selectedSectionIndex];
    
    if (direction === 'prev' && selectedImageIndex > 0) {
      setSelectedImageIndex(selectedImageIndex - 1);
    } else if (direction === 'next' && selectedImageIndex < currentSection.images.length - 1) {
      setSelectedImageIndex(selectedImageIndex + 1);
    }
  };

  // Handle keyboard navigation
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if (!isDialogOpen) return;
      
      if (e.key === 'ArrowLeft') {
        handleNavigate('prev');
      } else if (e.key === 'ArrowRight') {
        handleNavigate('next');
      }
    };
    
    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [isDialogOpen, selectedImageIndex, selectedSectionIndex]);

  // 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">
      {/* 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">Fotogröße anpassen</h3>
            <p className="text-sm text-muted-foreground">Ziehen Sie den Regler, um die Anzahl der Bilder pro Reihe 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={`grid ${getGridClasses()} gap-4`}>
            {section.images.map((image, imageIndex) => {
              const photoId = `${section.title}-${imageIndex}`;
              const commentCount = commentCounts[photoId] || 0;
              const imageType = getImageType(image);
              
              return (
                <div
                  key={imageIndex}
                  className={`
                    relative overflow-hidden rounded-lg bg-muted/30 transition-opacity duration-300
                    ${imageType === 'landscape' ? 'col-span-2' : ''} 
                  `}
                  style={{
                    aspectRatio: imageType === 'portrait' 
                      ? '2/3' 
                      : imageType === 'landscape' 
                        ? '16/9' 
                        : '1/1',
                    height: imageType === 'portrait' ? '100%' : 'auto'
                  }}
                >
                  <img
                    src={image.thumbnailSrc}
                    alt={image.alt}
                    className="h-full w-full object-cover"
                    loading="lazy"
                  />

                  <Dialog 
                    open={isDialogOpen && selectedSectionIndex === sectionIndex && selectedImageIndex === imageIndex}
                    onOpenChange={(open) => {
                      setIsDialogOpen(open);
                      if (open) {
                        setSelectedSectionIndex(sectionIndex);
                        setSelectedImageIndex(imageIndex);
                        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 
                          section={section}
                          currentImageIndex={selectedImageIndex}
                          photoId={`${section.title}-${selectedImageIndex}`}
                          commentCount={commentCounts[`${section.title}-${selectedImageIndex}`] || 0}
                          selectedTab={selectedTab}
                          setSelectedTab={setSelectedTab}
                          isFullImageLoaded={isFullImageLoaded}
                          setIsFullImageLoaded={setIsFullImageLoaded}
                          handleDownload={handleDownload}
                          isDownloading={isDownloading}
                          onNavigate={handleNavigate}
                          onCommentAdded={(photoId) => {
                            setCommentCounts(prev => ({
                              ...prev,
                              [photoId]: (prev[photoId] || 0) + 1
                            }));
                          }}
                        />
                      </div>
                    </DialogContent>
                  </Dialog>

                  {commentCount > 0 && (
                    <Dialog 
                      open={isDialogOpen && selectedSectionIndex === sectionIndex && selectedImageIndex === imageIndex}
                      onOpenChange={(open) => {
                        setIsDialogOpen(open);
                        if (open) {
                          setSelectedSectionIndex(sectionIndex);
                          setSelectedImageIndex(imageIndex);
                          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 
                            section={section}
                            currentImageIndex={selectedImageIndex}
                            photoId={`${section.title}-${selectedImageIndex}`}
                            commentCount={commentCounts[`${section.title}-${selectedImageIndex}`] || 0}
                            selectedTab={selectedTab}
                            setSelectedTab={setSelectedTab}
                            isFullImageLoaded={isFullImageLoaded}
                            setIsFullImageLoaded={setIsFullImageLoaded}
                            handleDownload={handleDownload}
                            isDownloading={isDownloading}
                            onNavigate={handleNavigate}
                            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