|
Before Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1005 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
BIN
public/images/tgl-ball.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
@ -3,7 +3,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import axios from "axios";
|
||||
import api from "@/lib/axios";
|
||||
|
||||
type Event = {
|
||||
id?: number;
|
||||
@ -31,38 +31,40 @@ const EventsAdmin = () => {
|
||||
|
||||
const fetchEvents = async () => {
|
||||
try {
|
||||
const res = await axios.get("/api/events?showPrivate=true");
|
||||
if (Array.isArray(res.data)) {
|
||||
setEvents(res.data);
|
||||
} else {
|
||||
console.warn("⚠️ Events-API hat kein Array geliefert:", res.data);
|
||||
setEvents([]);
|
||||
}
|
||||
const res = await api.get("/api/events?showPrivate=true");
|
||||
setEvents(Array.isArray(res.data) ? res.data : []);
|
||||
} catch (error) {
|
||||
console.error("❌ Fehler beim Laden der Events:", error);
|
||||
setEvents([]);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
fetchEvents();
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
if (isEditing && form.id) {
|
||||
await axios.put(`/api/events/${form.id}`, form);
|
||||
await api.put(`/api/events/${form.id}`, form);
|
||||
} else {
|
||||
await axios.post("/api/events", form);
|
||||
await api.post("/api/events", form);
|
||||
}
|
||||
setForm(defaultEvent);
|
||||
setIsEditing(false);
|
||||
fetchEvents();
|
||||
} catch (error) {
|
||||
console.error("❌ Fehler beim Speichern:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
await axios.delete(`/api/events/${id}`);
|
||||
try {
|
||||
await api.delete(`/api/events/${id}`);
|
||||
fetchEvents();
|
||||
} catch (error) {
|
||||
console.error("❌ Fehler beim Löschen:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (event: Event) => {
|
||||
@ -89,14 +91,18 @@ const EventsAdmin = () => {
|
||||
type="number"
|
||||
placeholder="Max. Teilnehmer"
|
||||
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
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder="Gebühr (€)"
|
||||
value={form.fee ?? ""}
|
||||
onChange={(e) => setForm({ ...form, fee: parseFloat(e.target.value) || undefined })}
|
||||
onChange={(e) =>
|
||||
setForm({ ...form, fee: parseFloat(e.target.value) || undefined })
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Adresse"
|
||||
@ -116,7 +122,13 @@ const EventsAdmin = () => {
|
||||
{isEditing ? "Speichern" : "Erstellen"}
|
||||
</Button>
|
||||
{isEditing && (
|
||||
<Button variant="ghost" onClick={() => { setForm(defaultEvent); setIsEditing(false); }}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setForm(defaultEvent);
|
||||
setIsEditing(false);
|
||||
}}
|
||||
>
|
||||
Abbrechen
|
||||
</Button>
|
||||
)}
|
||||
@ -126,17 +138,29 @@ const EventsAdmin = () => {
|
||||
<hr className="my-6" />
|
||||
|
||||
<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">
|
||||
{events.map((ev) => (
|
||||
<li key={ev.id} className="p-3 border rounded-md shadow-sm flex justify-between items-center">
|
||||
<li
|
||||
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="text-sm text-gray-500">{ev.description?.slice(0, 60)}...</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{ev.description?.slice(0, 60)}...
|
||||
</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>
|
||||
<Button variant="outline" onClick={() => handleEdit(ev)}>
|
||||
Bearbeiten
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => handleDelete(ev.id!)}
|
||||
>
|
||||
Löschen
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
@ -144,7 +168,6 @@ const EventsAdmin = () => {
|
||||
) : (
|
||||
<p className="text-gray-500">Noch keine Events vorhanden.</p>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -18,6 +18,7 @@ type NewsItem = {
|
||||
const NewsSection = () => {
|
||||
|
||||
const [news, setNews] = useState<NewsItem[]>([]);
|
||||
const defaultImage = "/images/tgl-ball.png";
|
||||
|
||||
const fetchNews = async () => {
|
||||
try{
|
||||
@ -49,7 +50,7 @@ const NewsSection = () => {
|
||||
src={
|
||||
item.image_url
|
||||
? `${import.meta.env.VITE_API_URL}${item.image_url}`
|
||||
: "/images/default-news.jpg"
|
||||
: defaultImage
|
||||
}
|
||||
alt={item.title}
|
||||
className="w-full h-full object-cover"
|
||||
|
||||
@ -27,6 +27,7 @@ const TeamSection = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const [sliderRef, slider] = useKeenSlider<HTMLDivElement>({
|
||||
loop: true,
|
||||
slides: {
|
||||
perView: 1,
|
||||
spacing: 16,
|
||||
@ -60,6 +61,14 @@ const TeamSection = () => {
|
||||
fetchTeams();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
slider.current?.next();
|
||||
}, 3000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [slider]);
|
||||
|
||||
return (
|
||||
<section id="team" className="py-16">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
|
||||
7
src/lib/axios.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import axios from "axios";
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_URL,
|
||||
});
|
||||
|
||||
export default api;
|
||||
@ -17,6 +17,25 @@ const AlleNeuigkeitenPage = () => {
|
||||
const [news, setNews] = useState<NewsItem[]>([]);
|
||||
const [expandedIds, setExpandedIds] = useState<number[]>([]);
|
||||
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(() => {
|
||||
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
|
||||
const toggleExpand = (id: number) => {
|
||||
@ -50,6 +62,22 @@ const AlleNeuigkeitenPage = () => {
|
||||
<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>
|
||||
|
||||
<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 */}
|
||||
{Object.entries(groupedNews).map(([year, items]) => (
|
||||
<div key={year} className="mb-12">
|
||||
@ -69,7 +97,7 @@ const AlleNeuigkeitenPage = () => {
|
||||
src={
|
||||
item.image_url
|
||||
? `${apiBase}${item.image_url}`
|
||||
: "https://via.placeholder.com/400x300?text=Kein+Bild"
|
||||
: defaultImage
|
||||
}
|
||||
alt={item.title}
|
||||
className="w-full h-full object-cover"
|
||||
|
||||