Update photo gallery to display images one by one in a carousel
Introduce a new `SingleImageCarousel` component and refactor the `PhotoGalleryWidget` to utilize it, enabling slideshow functionality for images with auto-play and navigation controls. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 413891e8-d784-4bea-b9f5-91a5a68316b4 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 2c340511-09fa-48b2-b531-1a040ae0b5e1 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/413891e8-d784-4bea-b9f5-91a5a68316b4/RVXhOPb Replit-Helium-Checkpoint-Created: true
This commit is contained in:
parent
d28c36131c
commit
c73e035b3d
@ -1,6 +1,6 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { ChevronLeft, ChevronRight, X, Images } from "lucide-react";
|
||||
import { ChevronLeft, ChevronRight, X, Images, Maximize2 } from "lucide-react";
|
||||
|
||||
interface GalleryImage {
|
||||
folder: string;
|
||||
@ -79,6 +79,92 @@ function Lightbox({
|
||||
);
|
||||
}
|
||||
|
||||
function SingleImageCarousel({
|
||||
images,
|
||||
autoPlay = true,
|
||||
interval = 5000,
|
||||
onExpand,
|
||||
}: {
|
||||
images: GalleryImage[];
|
||||
autoPlay?: boolean;
|
||||
interval?: number;
|
||||
onExpand?: (index: number) => void;
|
||||
}) {
|
||||
const [index, setIndex] = useState(0);
|
||||
const [paused, setPaused] = useState(false);
|
||||
|
||||
const prev = useCallback(() => setIndex((i) => (i - 1 + images.length) % images.length), [images.length]);
|
||||
const next = useCallback(() => setIndex((i) => (i + 1) % images.length), [images.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoPlay || paused || images.length <= 1) return;
|
||||
const timer = setInterval(next, interval);
|
||||
return () => clearInterval(timer);
|
||||
}, [autoPlay, paused, next, interval, images.length]);
|
||||
|
||||
if (images.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full h-full"
|
||||
onMouseEnter={() => setPaused(true)}
|
||||
onMouseLeave={() => setPaused(false)}
|
||||
>
|
||||
<div className="relative w-full aspect-[4/3] overflow-hidden rounded-b-lg">
|
||||
<img
|
||||
src={images[index].large || images[index].thumb}
|
||||
alt={images[index].fileName}
|
||||
className="w-full h-full object-cover transition-opacity duration-500"
|
||||
loading="lazy"
|
||||
data-testid="img-gallery-current"
|
||||
/>
|
||||
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/40 via-transparent to-transparent" />
|
||||
|
||||
<button
|
||||
onClick={prev}
|
||||
className="absolute left-1.5 top-1/2 -translate-y-1/2 w-7 h-7 rounded-full bg-black/50 hover:bg-black/70 flex items-center justify-center text-white/80 hover:text-white transition-all"
|
||||
data-testid="button-gallery-prev"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={next}
|
||||
className="absolute right-1.5 top-1/2 -translate-y-1/2 w-7 h-7 rounded-full bg-black/50 hover:bg-black/70 flex items-center justify-center text-white/80 hover:text-white transition-all"
|
||||
data-testid="button-gallery-next"
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{onExpand && (
|
||||
<button
|
||||
onClick={() => onExpand(index)}
|
||||
className="absolute top-2 right-2 w-7 h-7 rounded-full bg-black/50 hover:bg-black/70 flex items-center justify-center text-white/80 hover:text-white transition-all"
|
||||
data-testid="button-gallery-expand"
|
||||
>
|
||||
<Maximize2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="absolute bottom-2 left-0 right-0 flex justify-center gap-1">
|
||||
{images.slice(0, Math.min(images.length, 20)).map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setIndex(i)}
|
||||
className={`w-1.5 h-1.5 rounded-full transition-all ${i === index ? "bg-white w-3" : "bg-white/40 hover:bg-white/60"}`}
|
||||
data-testid={`button-gallery-dot-${i}`}
|
||||
/>
|
||||
))}
|
||||
{images.length > 20 && (
|
||||
<span className="text-white/50 text-[9px] ml-1">+{images.length - 20}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PhotoGalleryWidget() {
|
||||
const { data: images, isLoading } = useQuery<GalleryImage[]>({
|
||||
queryKey: ["/api/gallery"],
|
||||
@ -93,19 +179,13 @@ export function PhotoGalleryWidget() {
|
||||
<Images className="w-4 h-4 text-primary" />
|
||||
<h3 className="font-bold text-card-foreground text-sm">Fotogalerie</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-0.5 p-0.5">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="aspect-square bg-muted animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
<div className="aspect-[4/3] bg-muted animate-pulse" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!images || images.length === 0) return null;
|
||||
|
||||
const preview = images.slice(0, 6);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-card rounded-lg border border-card-border overflow-hidden h-full" data-testid="widget-gallery">
|
||||
@ -113,23 +193,12 @@ export function PhotoGalleryWidget() {
|
||||
<Images className="w-4 h-4 text-primary" />
|
||||
<h3 className="font-bold text-card-foreground text-sm">Fotogalerie</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-0.5 p-0.5">
|
||||
{preview.map((img, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setLightboxIndex(i)}
|
||||
className="aspect-square overflow-hidden cursor-pointer group"
|
||||
data-testid={`button-gallery-${i}`}
|
||||
>
|
||||
<img
|
||||
src={img.thumb}
|
||||
alt={img.fileName}
|
||||
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"
|
||||
loading="lazy"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<SingleImageCarousel
|
||||
images={images}
|
||||
autoPlay={true}
|
||||
interval={5000}
|
||||
onExpand={(i) => setLightboxIndex(i)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{lightboxIndex !== null && (
|
||||
@ -153,29 +222,24 @@ export default function GalleryPage() {
|
||||
return (
|
||||
<div data-testid="page-gallery">
|
||||
{isLoading ? (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2">
|
||||
{Array.from({ length: 20 }).map((_, i) => (
|
||||
<div key={i} className="aspect-square bg-card rounded-lg animate-pulse" />
|
||||
))}
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="aspect-[4/3] bg-card rounded-lg animate-pulse" />
|
||||
</div>
|
||||
) : images && images.length > 0 ? (
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="bg-card rounded-lg border border-card-border overflow-hidden">
|
||||
<SingleImageCarousel
|
||||
images={images}
|
||||
autoPlay={false}
|
||||
onExpand={(i) => setLightboxIndex(i)}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-center text-muted-foreground text-sm mt-3" data-testid="text-gallery-count">
|
||||
{images.length} Fotos
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2">
|
||||
{(images || []).map((img, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setLightboxIndex(i)}
|
||||
className="aspect-square overflow-hidden rounded-lg cursor-pointer group"
|
||||
data-testid={`button-gallery-full-${i}`}
|
||||
>
|
||||
<img
|
||||
src={img.thumb}
|
||||
alt={img.fileName}
|
||||
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"
|
||||
loading="lazy"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-center text-muted-foreground">Keine Fotos vorhanden</p>
|
||||
)}
|
||||
|
||||
{lightboxIndex !== null && images && (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user