|
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 { 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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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
@ -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 [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"
|
||||||
|
|||||||