Aktuellste VErsion
Some checks are pending
Deploy Volleyball Dev / deploy (push) Waiting to run

This commit is contained in:
Marc Wieland 2025-05-16 18:38:48 +02:00
parent 269dd3c7a5
commit cc0c8c95f5
20 changed files with 113 additions and 45 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1005 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

BIN
public/images/tgl-ball.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@ -3,7 +3,7 @@ import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import axios from "axios"; import api from "@/lib/axios";
type Event = { type Event = {
id?: number; id?: number;
@ -31,38 +31,40 @@ const EventsAdmin = () => {
const fetchEvents = async () => { const fetchEvents = async () => {
try { try {
const res = await axios.get("/api/events?showPrivate=true"); const res = await api.get("/api/events?showPrivate=true");
if (Array.isArray(res.data)) { setEvents(Array.isArray(res.data) ? res.data : []);
setEvents(res.data);
} else {
console.warn("⚠️ Events-API hat kein Array geliefert:", res.data);
setEvents([]);
}
} catch (error) { } catch (error) {
console.error("❌ Fehler beim Laden der Events:", error); console.error("❌ Fehler beim Laden der Events:", error);
setEvents([]); setEvents([]);
} }
}; };
useEffect(() => { useEffect(() => {
fetchEvents(); fetchEvents();
}, []); }, []);
const handleSubmit = async () => { const handleSubmit = async () => {
if (isEditing && form.id) { try {
await axios.put(`/api/events/${form.id}`, form); if (isEditing && form.id) {
} else { await api.put(`/api/events/${form.id}`, form);
await axios.post("/api/events", form); } else {
await api.post("/api/events", form);
}
setForm(defaultEvent);
setIsEditing(false);
fetchEvents();
} catch (error) {
console.error("❌ Fehler beim Speichern:", error);
} }
setForm(defaultEvent);
setIsEditing(false);
fetchEvents();
}; };
const handleDelete = async (id: number) => { const handleDelete = async (id: number) => {
await axios.delete(`/api/events/${id}`); try {
fetchEvents(); await api.delete(`/api/events/${id}`);
fetchEvents();
} catch (error) {
console.error("❌ Fehler beim Löschen:", error);
}
}; };
const handleEdit = (event: Event) => { const handleEdit = (event: Event) => {
@ -89,14 +91,18 @@ const EventsAdmin = () => {
type="number" type="number"
placeholder="Max. Teilnehmer" placeholder="Max. Teilnehmer"
value={form.max_participants ?? ""} value={form.max_participants ?? ""}
onChange={(e) => setForm({ ...form, max_participants: parseInt(e.target.value) || undefined })} onChange={(e) =>
setForm({ ...form, max_participants: parseInt(e.target.value) || undefined })
}
/> />
<Input <Input
type="number" type="number"
step="0.01" step="0.01"
placeholder="Gebühr (€)" placeholder="Gebühr (€)"
value={form.fee ?? ""} value={form.fee ?? ""}
onChange={(e) => setForm({ ...form, fee: parseFloat(e.target.value) || undefined })} onChange={(e) =>
setForm({ ...form, fee: parseFloat(e.target.value) || undefined })
}
/> />
<Input <Input
placeholder="Adresse" placeholder="Adresse"
@ -116,7 +122,13 @@ const EventsAdmin = () => {
{isEditing ? "Speichern" : "Erstellen"} {isEditing ? "Speichern" : "Erstellen"}
</Button> </Button>
{isEditing && ( {isEditing && (
<Button variant="ghost" onClick={() => { setForm(defaultEvent); setIsEditing(false); }}> <Button
variant="ghost"
onClick={() => {
setForm(defaultEvent);
setIsEditing(false);
}}
>
Abbrechen Abbrechen
</Button> </Button>
)} )}
@ -126,25 +138,36 @@ const EventsAdmin = () => {
<hr className="my-6" /> <hr className="my-6" />
<h2 className="text-xl font-semibold mb-2">Bestehende Events</h2> <h2 className="text-xl font-semibold mb-2">Bestehende Events</h2>
{Array.isArray(events) && events.length > 0 ? ( {events.length > 0 ? (
<ul className="space-y-2"> <ul className="space-y-2">
{events.map((ev) => ( {events.map((ev) => (
<li key={ev.id} className="p-3 border rounded-md shadow-sm flex justify-between items-center"> <li
<div> key={ev.id}
className="p-3 border rounded-md shadow-sm flex justify-between items-center"
>
<div>
<div className="font-medium">{ev.title}</div> <div className="font-medium">{ev.title}</div>
<div className="text-sm text-gray-500">{ev.description?.slice(0, 60)}...</div> <div className="text-sm text-gray-500">
</div> {ev.description?.slice(0, 60)}...
<div className="space-x-2">
<Button variant="outline" onClick={() => handleEdit(ev)}>Bearbeiten</Button>
<Button variant="destructive" onClick={() => handleDelete(ev.id!)}>Löschen</Button>
</div> </div>
</div>
<div className="space-x-2">
<Button variant="outline" onClick={() => handleEdit(ev)}>
Bearbeiten
</Button>
<Button
variant="destructive"
onClick={() => handleDelete(ev.id!)}
>
Löschen
</Button>
</div>
</li> </li>
))} ))}
</ul> </ul>
) : ( ) : (
<p className="text-gray-500">Noch keine Events vorhanden.</p> <p className="text-gray-500">Noch keine Events vorhanden.</p>
)} )}
</div> </div>
); );
}; };

View File

@ -18,6 +18,7 @@ type NewsItem = {
const NewsSection = () => { const NewsSection = () => {
const [news, setNews] = useState<NewsItem[]>([]); const [news, setNews] = useState<NewsItem[]>([]);
const defaultImage = "/images/tgl-ball.png";
const fetchNews = async () => { const fetchNews = async () => {
try{ try{
@ -49,7 +50,7 @@ const NewsSection = () => {
src={ src={
item.image_url item.image_url
? `${import.meta.env.VITE_API_URL}${item.image_url}` ? `${import.meta.env.VITE_API_URL}${item.image_url}`
: "/images/default-news.jpg" : defaultImage
} }
alt={item.title} alt={item.title}
className="w-full h-full object-cover" className="w-full h-full object-cover"

View File

@ -27,6 +27,7 @@ const TeamSection = () => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [sliderRef, slider] = useKeenSlider<HTMLDivElement>({ const [sliderRef, slider] = useKeenSlider<HTMLDivElement>({
loop: true,
slides: { slides: {
perView: 1, perView: 1,
spacing: 16, spacing: 16,
@ -60,6 +61,14 @@ const TeamSection = () => {
fetchTeams(); fetchTeams();
}, []); }, []);
useEffect(() => {
const interval = setInterval(() => {
slider.current?.next();
}, 3000);
return () => clearInterval(interval);
}, [slider]);
return ( return (
<section id="team" className="py-16"> <section id="team" className="py-16">
<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">

7
src/lib/axios.ts Normal file
View File

@ -0,0 +1,7 @@
import axios from "axios";
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL,
});
export default api;

View File

@ -17,6 +17,25 @@ const AlleNeuigkeitenPage = () => {
const [news, setNews] = useState<NewsItem[]>([]); const [news, setNews] = useState<NewsItem[]>([]);
const [expandedIds, setExpandedIds] = useState<number[]>([]); const [expandedIds, setExpandedIds] = useState<number[]>([]);
const [activeCardId, setActiveCardId] = useState<number | null>(null); const [activeCardId, setActiveCardId] = useState<number | null>(null);
const [selectedTeam, setSelectedTeam] = useState<string>("Alle Teams");
const defaultImage = "/images/tgl-ball.png";
//Filtern nach Teams
const teams = Array.from(new Set(news.map((n) => n.team).filter(Boolean)));
const filteredNews = selectedTeam === "Alle Teams" ? news : news.filter((n) => n.team === selectedTeam);
const groupedNews = filteredNews.reduce((acc: Record<string, NewsItem[]>, item) => {
const year = new Date(item.created_at).getFullYear().toString();
if (!acc[year]) acc[year] = [];
acc[year].push(item);
return acc;
}, {});
useEffect(() => { useEffect(() => {
fetch(`${apiBase}/api/news`) fetch(`${apiBase}/api/news`)
@ -31,13 +50,6 @@ const AlleNeuigkeitenPage = () => {
}; };
// Gruppieren nach Jahren
const groupedNews = news.reduce((acc: Record<string, NewsItem[]>, item) => {
const year = new Date(item.created_at).getFullYear().toString();
if (!acc[year]) acc[year] = [];
acc[year].push(item);
return acc;
}, {});
//Toggle der Cards //Toggle der Cards
const toggleExpand = (id: number) => { const toggleExpand = (id: number) => {
@ -47,8 +59,24 @@ const AlleNeuigkeitenPage = () => {
} }
return ( return (
<div className="max-w-5xl mx-auto px-4 py-12"> <div className="max-w-5xl mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-center mb-8 text-frog-600">Alle Neuigkeiten</h1> <h1 className="text-3xl font-bold text-center mb-8 text-frog-600">Alle Neuigkeiten</h1>
<div className="mb-8 flex justify-center">
<select
value={selectedTeam}
onChange={(e) => setSelectedTeam(e.target.value)}
className="border border-gray-300 rounded-md px-4 py-2 text-gray-700"
>
<option value="Alle Teams">Alle Teams</option>
{teams.map((team) => (
<option key={team} value={team}>
{team}
</option>
))}
</select>
</div>
{/* News nach Jahren gruppiert */} {/* News nach Jahren gruppiert */}
{Object.entries(groupedNews).map(([year, items]) => ( {Object.entries(groupedNews).map(([year, items]) => (
@ -69,7 +97,7 @@ const AlleNeuigkeitenPage = () => {
src={ src={
item.image_url item.image_url
? `${apiBase}${item.image_url}` ? `${apiBase}${item.image_url}`
: "https://via.placeholder.com/400x300?text=Kein+Bild" : defaultImage
} }
alt={item.title} alt={item.title}
className="w-full h-full object-cover" className="w-full h-full object-cover"