Added gallery
Some checks are pending
Deploy Volleyball Dev / deploy (push) Waiting to run

This commit is contained in:
Marc Wieland 2025-04-30 09:05:00 +02:00
parent 55be7e0210
commit 0d4b6b8e1a
5 changed files with 335 additions and 37 deletions

View File

@ -19,7 +19,6 @@ const AdminDashboard = () => {
</div> </div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Jeder Benutzer darf News verwalten */}
<Link to="/admin/news"> <Link to="/admin/news">
<Card className="hover:shadow-lg transition-shadow cursor-pointer"> <Card className="hover:shadow-lg transition-shadow cursor-pointer">
<CardContent className="p-6 text-center"> <CardContent className="p-6 text-center">
@ -29,7 +28,6 @@ const AdminDashboard = () => {
</Card> </Card>
</Link> </Link>
{/* Nur Admins sehen diese Card */}
{isAdmin && ( {isAdmin && (
<Link to="/admin/users"> <Link to="/admin/users">
<Card className="hover:shadow-lg transition-shadow cursor-pointer"> <Card className="hover:shadow-lg transition-shadow cursor-pointer">
@ -58,6 +56,16 @@ const AdminDashboard = () => {
</CardContent> </CardContent>
</Card> </Card>
</Link> </Link>
{/* 🎉 Neue Galerie-Kachel */}
<Link to="/admin/gallery">
<Card className="hover:shadow-lg transition-shadow cursor-pointer">
<CardContent className="p-6 text-center">
<CardTitle className="text-frog-600">Galerie verwalten</CardTitle>
<p className="text-gray-600 mt-2 text-sm">Bilder hochladen & löschen</p>
</CardContent>
</Card>
</Link>
</div> </div>
</div> </div>
); );

View File

@ -0,0 +1,104 @@
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { toast } from "sonner";
const apiBase = import.meta.env.VITE_API_URL;
type GalleryImage = {
id: number;
image_url: string;
};
const GalleryManager = () => {
const [images, setImages] = useState<GalleryImage[]>([]);
const [uploading, setUploading] = useState(false);
const fetchImages = async () => {
try {
const res = await fetch(`${apiBase}/api/gallery`);
const data = await res.json();
setImages(data);
} catch (err) {
console.error("Fehler beim Laden der Galerie:", err);
}
};
useEffect(() => {
fetchImages();
}, []);
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const formData = new FormData();
formData.append("image", file);
setUploading(true);
try {
const res = await fetch(`${apiBase}/api/gallery`, {
method: "POST",
body: formData,
});
if (!res.ok) throw new Error("Fehler beim Hochladen");
toast.success("Bild erfolgreich hochgeladen!");
await fetchImages();
} catch (err) {
toast.error("Fehler beim Hochladen des Bildes");
} finally {
setUploading(false);
}
};
const handleDelete = async (id: number) => {
if (!confirm("Bist du sicher, dass du dieses Bild löschen willst?")) return;
try {
const res = await fetch(`${apiBase}/api/gallery/${id}`, {
method: "DELETE",
});
if (!res.ok) throw new Error("Fehler beim Löschen");
toast.success("Bild gelöscht");
await fetchImages();
} catch (err) {
toast.error("Fehler beim Löschen des Bildes");
}
};
return (
<div className="max-w-6xl mx-auto py-12 px-4">
<h1 className="text-3xl font-bold text-frog-600 mb-6">Galerie verwalten</h1>
<div className="mb-8">
<input type="file" accept="image/*" onChange={handleUpload} />
{uploading && <p className="text-sm text-gray-500 mt-2">Bild wird hochgeladen...</p>}
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-6">
{images.map((img) => (
<Card key={img.id}>
<CardContent className="p-2">
<img
src={`${apiBase}${img.image_url}`}
alt="Galeriebild"
className="w-full h-48 object-cover rounded-md mb-2"
/>
<Button
variant="destructive"
className="w-full"
onClick={() => handleDelete(img.id)}
>
Löschen
</Button>
</CardContent>
</Card>
))}
</div>
</div>
);
};
export default GalleryManager;

View File

@ -1,19 +1,66 @@
import { useEffect, useState } from "react";
import { useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ChevronLeft, ChevronRight, X } from "lucide-react"; import { X } from "lucide-react";
import { Link } from "react-router-dom";
const apiBase = import.meta.env.VITE_API_URL;
type GalleryImage = {
id: number;
image_url: string;
};
type AnimatedImage = GalleryImage & { fading?: boolean };
const GallerySection = () => { const GallerySection = () => {
const [allImages, setAllImages] = useState<GalleryImage[]>([]);
const [displayImages, setDisplayImages] = useState<AnimatedImage[]>([]);
const [selectedImage, setSelectedImage] = useState<string | null>(null); const [selectedImage, setSelectedImage] = useState<string | null>(null);
const images = [ useEffect(() => {
"https://images.unsplash.com/photo-1553005746-5be7d6d48a81?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80", const fetchImages = async () => {
"https://images.unsplash.com/photo-1588286840104-8957b019727f?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80", try {
"https://images.unsplash.com/photo-1599586120429-48281b6f0ece?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80", const res = await fetch(`${apiBase}/api/gallery`);
"https://images.unsplash.com/photo-1592656094267-764a45160876?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80", const data = await res.json();
"https://images.unsplash.com/photo-1547347298-4074fc3086f0?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80", setAllImages(data);
"https://images.unsplash.com/photo-1544213456-e1c259fecc4f?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80",
]; const shuffled = [...data].sort(() => 0.5 - Math.random());
setDisplayImages(shuffled.slice(0, 6));
} catch (err) {
console.error("Fehler beim Laden der Galerie:", err);
}
};
fetchImages();
}, []);
// Bild zufällig austauschen mit Animation
useEffect(() => {
const interval = setInterval(() => {
if (allImages.length <= 6) return;
setDisplayImages((prev) => {
const currentIds = prev.map((img) => img.id);
const remaining = allImages.filter((img) => !currentIds.includes(img.id));
if (remaining.length === 0) return prev;
const newImage = remaining[Math.floor(Math.random() * remaining.length)];
const indexToReplace = Math.floor(Math.random() * prev.length);
const updated = [...prev];
updated[indexToReplace] = { ...updated[indexToReplace], fading: true };
setTimeout(() => {
updated[indexToReplace] = { ...newImage, fading: false };
setDisplayImages([...updated]);
}, 400); // passt zur fade-out duration
return [...updated];
});
}, 5000);
return () => clearInterval(interval);
}, [allImages]);
const openLightbox = (image: string) => { const openLightbox = (image: string) => {
setSelectedImage(image); setSelectedImage(image);
@ -27,6 +74,33 @@ const GallerySection = () => {
return ( return (
<section id="gallery" className="py-16 bg-gray-50"> <section id="gallery" className="py-16 bg-gray-50">
<style>
{`
.fade-img {
transition: opacity 0.4s ease, transform 0.4s ease;
}
.fade-out {
opacity: 0;
transform: scale(0.95);
}
.fade-in {
opacity: 1;
transform: scale(1);
}
.gallery-tile {
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.gallery-tile:hover {
transform: scale(1.03);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
}
`}
</style>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12"> <div className="text-center mb-12">
<h2 className="text-3xl font-bold text-gray-900">Galerie</h2> <h2 className="text-3xl font-bold text-gray-900">Galerie</h2>
@ -34,31 +108,38 @@ const GallerySection = () => {
</div> </div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4"> <div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{images.map((image, index) => ( {displayImages.map((image) => (
<div <div
key={index} key={image.id}
className="aspect-square overflow-hidden rounded-lg cursor-pointer hover:opacity-90 transition-opacity" className="aspect-square overflow-hidden rounded-lg cursor-pointer gallery-tile"
onClick={() => openLightbox(image)} onClick={() => openLightbox(`${apiBase}${image.image_url}`)}
> >
<img <img
src={image} src={`${apiBase}${image.image_url}`}
alt={`Galeriebild ${index + 1}`} alt="Galeriebild"
className="w-full h-full object-cover" className={`w-full h-full object-cover fade-img ${
image.fading ? "fade-out" : "fade-in"
}`}
/> />
</div> </div>
))} ))}
</div> </div>
<div className="text-center mt-12"> <div className="mt-12 flex justify-center">
<Button className="bg-frog-500 hover:bg-frog-600"> <Link to="/gallery">
Mehr Bilder anzeigen <Button className="bg-frog-500 hover:bg-frog-600">
</Button> Mehr Bilder anzeigen
</div> </Button>
</Link>
</div>
</div> </div>
{/* Lightbox */}
{selectedImage && ( {selectedImage && (
<div className="fixed inset-0 bg-black/90 z-50 flex items-center justify-center" onClick={closeLightbox}> <div
className="fixed inset-0 bg-black/90 z-50 flex items-center justify-center"
onClick={closeLightbox}
>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
@ -71,7 +152,10 @@ const GallerySection = () => {
<X className="h-8 w-8" /> <X className="h-8 w-8" />
</Button> </Button>
<div className="relative max-w-4xl max-h-[80vh]" onClick={(e) => e.stopPropagation()}> <div
className="relative max-w-4xl max-h-[80vh]"
onClick={(e) => e.stopPropagation()}
>
<img <img
src={selectedImage} src={selectedImage}
alt="Vergrößertes Bild" alt="Vergrößertes Bild"

View File

@ -25,6 +25,8 @@ import TeamDetail from "./admin/TeamDetail";
import TeamAddPlayer from "./admin/TeamAddPlayer"; import TeamAddPlayer from "./admin/TeamAddPlayer";
import PlayerEdit from "./admin/PlayerEdit"; import PlayerEdit from "./admin/PlayerEdit";
import PlayerManagementPage from "./admin/PlayerManagementPage"; import PlayerManagementPage from "./admin/PlayerManagementPage";
import GalleryManager from "./admin/GalleryManager";
import GalleryPage from "./pages/GalleryPage";
@ -59,6 +61,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
/> />
<Route path="/teams/:id" element={<Layout><TeamDetailPage /></Layout>} /> <Route path="/teams/:id" element={<Layout><TeamDetailPage /></Layout>} />
<Route path="/login" element={<Layout><LoginPage /></Layout>}/> <Route path="/login" element={<Layout><LoginPage /></Layout>}/>
<Route path="/gallery" element={<Layout><GalleryPage /></Layout>}/>
<Route path="/admin/login" element={<LoginPage />} /> <Route path="/admin/login" element={<LoginPage />} />
<Route path="/admin" element={<PrivateRoute><Layout><AdminDashboard /></Layout></PrivateRoute>} /> <Route path="/admin" element={<PrivateRoute><Layout><AdminDashboard /></Layout></PrivateRoute>} />
<Route path="/admin/news" element={<PrivateRoute><Layout><NewsManager /></Layout></PrivateRoute>} /> <Route path="/admin/news" element={<PrivateRoute><Layout><NewsManager /></Layout></PrivateRoute>} />
@ -71,6 +74,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<Route path="/admin/teams/:id/add-player" element={<PrivateRoute><Layout><TeamAddPlayer /></Layout></PrivateRoute>} /> <Route path="/admin/teams/:id/add-player" element={<PrivateRoute><Layout><TeamAddPlayer /></Layout></PrivateRoute>} />
<Route path="/admin/players/:id/edit" element={<PrivateRoute><Layout><PlayerEdit /></Layout></PrivateRoute>} /> <Route path="/admin/players/:id/edit" element={<PrivateRoute><Layout><PlayerEdit /></Layout></PrivateRoute>} />
<Route path="/admin/players" element={<PrivateRoute><Layout><PlayerManagementPage /></Layout></PrivateRoute>} /> <Route path="/admin/players" element={<PrivateRoute><Layout><PlayerManagementPage /></Layout></PrivateRoute>} />
<Route path="/admin/gallery" element={<PrivateRoute><Layout><GalleryManager /></Layout></PrivateRoute>} />
</Routes> </Routes>
</AuthProvider> </AuthProvider>

98
src/pages/GalleryPage.tsx Normal file
View File

@ -0,0 +1,98 @@
// src/pages/GalleryPage.tsx
import { useEffect, useState } from "react";
import { X } from "lucide-react";
import { Button } from "@/components/ui/button";
const apiBase = import.meta.env.VITE_API_URL;
type GalleryImage = {
id: number;
image_url: string;
};
const GalleryPage = () => {
const [images, setImages] = useState<GalleryImage[]>([]);
const [selectedImage, setSelectedImage] = useState<string | null>(null);
useEffect(() => {
const fetchImages = async () => {
try {
const res = await fetch(`${apiBase}/api/gallery`);
const data = await res.json();
setImages(data);
} catch (err) {
console.error("Fehler beim Laden der Galerie:", err);
}
};
fetchImages();
}, []);
const openLightbox = (image: string) => {
setSelectedImage(image);
document.body.style.overflow = "hidden";
};
const closeLightbox = () => {
setSelectedImage(null);
document.body.style.overflow = "auto";
};
return (
<div className="max-w-7xl mx-auto py-12 px-4">
<h1 className="text-4xl font-bold text-center text-frog-600 mb-8">
Bildergalerie 📸
</h1>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
{images.map((image) => (
<div
key={image.id}
onClick={() => openLightbox(`${apiBase}${image.image_url}`)}
className="aspect-square overflow-hidden rounded-md shadow-md cursor-pointer transform hover:scale-105 transition-all duration-300"
>
<img
src={`${apiBase}${image.image_url}`}
alt="Galeriebild"
className="w-full h-full object-cover"
/>
</div>
))}
</div>
{/* Lightbox */}
{selectedImage && (
<div
className="fixed inset-0 bg-black/90 z-50 flex items-center justify-center"
onClick={closeLightbox}
>
<Button
variant="ghost"
size="icon"
className="absolute top-4 right-4 text-white hover:bg-white/10"
onClick={(e) => {
e.stopPropagation();
closeLightbox();
}}
>
<X className="h-8 w-8" />
</Button>
<div
className="relative max-w-4xl max-h-[80vh]"
onClick={(e) => e.stopPropagation()}
>
<img
src={selectedImage}
alt="Vergrößertes Bild"
className="max-w-full max-h-[80vh] object-contain rounded-md shadow-lg"
/>
</div>
</div>
)}
</div>
);
};
export default GalleryPage;