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

This commit is contained in:
Marc Wieland 2025-06-01 16:19:05 +02:00
parent d78ece0dfd
commit 692f9bdd2a
14 changed files with 454 additions and 82 deletions

166
package-lock.json generated
View File

@ -54,6 +54,7 @@
"next-themes": "^0.3.0",
"pdfjs-dist": "^5.2.133",
"react": "^18.3.1",
"react-big-calendar": "^1.19.2",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-google-recaptcha": "^3.1.0",
@ -1116,6 +1117,16 @@
"node": ">=14"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@radix-ui/number": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz",
@ -2695,6 +2706,18 @@
"node": ">=14.0.0"
}
},
"node_modules/@restart/hooks": {
"version": "0.4.16",
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz",
"integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==",
"license": "MIT",
"dependencies": {
"dequal": "^2.0.3"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz",
@ -3404,6 +3427,12 @@
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"license": "MIT"
},
"node_modules/@types/warning": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz",
"integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==",
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.11.0.tgz",
@ -4858,6 +4887,12 @@
"node": ">=12"
}
},
"node_modules/date-arithmetic": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-arithmetic/-/date-arithmetic-4.1.0.tgz",
"integrity": "sha512-QWxYLR5P/6GStZcdem+V1xoto6DMadYWpMXU82ES3/RfR3Wdwr3D0+be7mgOJ+Ov0G9D5Dmb9T17sNLQYj9XOg==",
"license": "MIT"
},
"node_modules/date-fns": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
@ -4868,6 +4903,12 @@
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"license": "MIT"
},
"node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
@ -5847,6 +5888,11 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/globalize": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/globalize/-/globalize-0.1.1.tgz",
"integrity": "sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA=="
},
"node_modules/globals": {
"version": "15.11.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-15.11.0.tgz",
@ -6492,6 +6538,12 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
},
"node_modules/lodash.castarray": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
@ -7009,6 +7061,15 @@
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
}
},
"node_modules/luxon": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz",
"integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==",
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/magic-string": {
"version": "0.30.12",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz",
@ -7205,6 +7266,12 @@
"integrity": "sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==",
"license": "MIT"
},
"node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"node_modules/merge-refs": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-1.3.0.tgz",
@ -7759,6 +7826,27 @@
"license": "MIT",
"optional": true
},
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/moment-timezone": {
"version": "0.5.48",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
"integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
"license": "MIT",
"dependencies": {
"moment": "^2.29.4"
},
"engines": {
"node": "*"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -8485,6 +8573,43 @@
"react": ">=16.4.1"
}
},
"node_modules/react-big-calendar": {
"version": "1.19.2",
"resolved": "https://registry.npmjs.org/react-big-calendar/-/react-big-calendar-1.19.2.tgz",
"integrity": "sha512-2orH+TOXPJBlQGwSl9ZnTK2WZR9OfVf0r1s8mnbpjvtENZfmWHP6nXqxmten1vkvzOMqefVGjh5GurM27HHOZw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.20.7",
"clsx": "^1.2.1",
"date-arithmetic": "^4.1.0",
"dayjs": "^1.11.7",
"dom-helpers": "^5.2.1",
"globalize": "^0.1.1",
"invariant": "^2.2.4",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"luxon": "^3.2.1",
"memoize-one": "^6.0.0",
"moment": "^2.29.4",
"moment-timezone": "^0.5.40",
"prop-types": "^15.8.1",
"react-overlays": "^5.2.1",
"uncontrollable": "^7.2.1"
},
"peerDependencies": {
"react": "^16.14.0 || ^17 || ^18 || ^19",
"react-dom": "^16.14.0 || ^17 || ^18 || ^19"
}
},
"node_modules/react-big-calendar/node_modules/clsx": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/react-day-picker": {
"version": "8.10.1",
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz",
@ -8559,6 +8684,12 @@
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"license": "MIT"
},
"node_modules/react-lifecycles-compat": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
"license": "MIT"
},
"node_modules/react-loading-skeleton": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.5.0.tgz",
@ -8595,6 +8726,26 @@
"react": ">=18"
}
},
"node_modules/react-overlays": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.2.1.tgz",
"integrity": "sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.13.8",
"@popperjs/core": "^2.11.6",
"@restart/hooks": "^0.4.7",
"@types/warning": "^3.0.0",
"dom-helpers": "^5.2.0",
"prop-types": "^15.7.2",
"uncontrollable": "^7.2.1",
"warning": "^4.0.3"
},
"peerDependencies": {
"react": ">=16.3.0",
"react-dom": ">=16.3.0"
}
},
"node_modules/react-pdf": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-9.2.1.tgz",
@ -9686,6 +9837,21 @@
}
}
},
"node_modules/uncontrollable": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz",
"integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.6.3",
"@types/react": ">=16.9.11",
"invariant": "^2.2.4",
"react-lifecycles-compat": "^3.0.4"
},
"peerDependencies": {
"react": ">=15.0.0"
}
},
"node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",

View File

@ -57,6 +57,7 @@
"next-themes": "^0.3.0",
"pdfjs-dist": "^5.2.133",
"react": "^18.3.1",
"react-big-calendar": "^1.19.2",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-google-recaptcha": "^3.1.0",

View File

@ -11,6 +11,8 @@ import "aos/dist/aos.css";
const queryClient = new QueryClient();
const App = () => (
<QueryClientProvider client={queryClient}>
<TooltipProvider>

View File

@ -6,6 +6,8 @@ import { useAuth } from "@/context/AuthContext";
const AdminDashboard = () => {
const { isAuthenticated, username, isAdmin, logout } = useAuth();
const defaultImage = "/images/tgl-ball.png";
return (
<div className="max-w-6xl mx-auto py-12 px-4">
<div className="flex justify-between items-center mb-8">

View File

@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Textarea } from "@/components/ui/textarea";
import { toast } from "sonner";
import { set } from "date-fns";
const apiBase = import.meta.env.VITE_API_URL;
@ -115,6 +116,38 @@ const TeamDetail = () => {
}
};
const handleCarouselUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if(!files) return;
const uploadPaths: string[] = JSON.parse(carouselImages || "[]");
for (const file of files){
const formData = new FormData();
formData.append("image", file);
const res = await fetch(`${apiBase}/api/teams/${id}/carousel-upload`, {
method: "POST",
body: formData,
});
if(res.ok){
const {path} = await res.json();
uploadPaths.push(path);
}else{
toast.error("Fehler beim Hochladen des Bildes.");
}
}
setCarouselImages(JSON.stringify(uploadPaths));
};
const removeCarouselImage = (index: number) => {
const paths: string[] = JSON.parse(carouselImages || "[]");
paths.splice(index, 1);
setCarouselImages(JSON.stringify(paths));
};
if (loading) return <p className="text-center py-12">Lade Team...</p>;
return (
@ -144,11 +177,31 @@ const TeamDetail = () => {
<label className="text-gray-700">Neue Spieler gesucht?</label>
</div>
<Textarea
value={carouselImages}
onChange={(e) => setCarouselImages(e.target.value)}
placeholder="Karussell-Bilder (JSON-Array z.B. [\"//uploads/x.jpg\"])"
<div>
<label className="block mb-1 font-medium text-gray-700">Karussell-Bilder</label>
<input
type="file"
accept="image/*"
multiple
onChange={handleCarouselUpload}
className="block"
/>
<div className="grid grid-cols-3 gap-4 mt-4">
{JSON.parse(carouselImages || "[]").map((img: string, idx: number) => (
<div key={idx} className="relative group">
<img src={`${apiBase}${img}`} className="rounded shadow" />
<button
type="button"
onClick={() => removeCarouselImage(idx)}
className="absolute top-1 right-1 bg-red-600 text-white rounded-full px-2 py-1 text-xs opacity-80 group-hover:opacity-100"
>
</button>
</div>
))}
</div>
</div>
<Button onClick={handleUpdateTeam} className="bg-frog-500 hover:bg-frog-600 text-white">
Team speichern

View File

@ -1,14 +1,11 @@
import { Button } from "@/components/ui/button";
const AboutSection = () => {
return (
<section id="about" className="py-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid md:grid-cols-2 gap-12 items-center">
<div>
<h2 className="text-3xl font-bold text-gray-900 mb-6">Über TG Laudenbach</h2>
<div className="space-y-4 text-lg">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="space-y-6 text-lg">
<h2 className="text-3xl font-bold text-gray-900">Über TG Laudenbach</h2>
<p>
Der Volleyballverein TG Laudenbach wurde 1974 gegründet und blickt auf eine erfolgreiche Geschichte zurück.
Wir sind mehr als nur ein Sportverein wir sind eine Gemeinschaft aus begeisterten Volleyballern jeden Alters und Spielniveaus.
@ -21,38 +18,6 @@ const AboutSection = () => {
Neben dem sportlichen Erfolg ist uns auch das Miteinander wichtig. Regelmäßige Vereinsfeste, gemeinsame Ausflüge und
unser jährliches Beachvolleyball-Turnier sorgen für ein aktives Vereinsleben.
</p>
<div className="pt-4">
<Button className="bg-frog-500 hover:bg-frog-600">
Mehr über uns erfahren
</Button>
</div>
</div>
</div>
<div className="space-y-6">
<div className="rounded-xl overflow-hidden shadow-md">
<img
src="https://images.unsplash.com/photo-1574271143515-5cddf8da19be?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80"
alt="Teamfoto TG Laudenbach"
className="w-full h-auto"
/>
</div>
<div className="grid grid-cols-2 gap-6">
<div className="rounded-xl overflow-hidden shadow-md">
<img
src="https://images.unsplash.com/photo-1529676468461-bd9955e9e4f8?ixlib=rb-4.0.3&auto=format&fit=crop&w=400&q=80"
alt="Volleyball Training"
className="w-full h-auto"
/>
</div>
<div className="rounded-xl overflow-hidden shadow-md">
<img
src="https://images.unsplash.com/photo-1612214070475-1e73f478188c?ixlib=rb-4.0.3&auto=format&fit=crop&w=400&q=80"
alt="Volleyballhalle"
className="w-full h-auto"
/>
</div>
</div>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,82 @@
import { CalendarDays, Clock } from "lucide-react";
import { Link } from "react-router-dom";
const mockEvents = [
{
title: "Heimspiel Herren 1 vs. TSG Nußloch 5",
date: "2025-06-08",
location: "Bergstraßenhalle Laudenbach",
},
{
title: "Sommerfest der Abteilung",
date: "2025-06-15",
location: "Vereinsheim",
},
{
title: "Training Jugend U16",
date: "2025-06-17",
location: "Halle 2",
},
];
const EventsSection = () => {
const getDaysUntil = (dateStr: string) => {
const today = new Date();
const target = new Date(dateStr);
const diffTime = target.getTime() - today.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return diffDays;
};
return (
<section className="bg-gray-50 py-12" id="events">
<div className="section-container text-center">
<h2 className="text-2xl md:text-3xl font-bold mb-4 flex justify-center items-center gap-2">
Kommende Events
</h2>
<p className="text-gray-600 mb-6 text-sm md:text-base">
Spieltage & Events im Überblick
</p>
<div className="flex flex-wrap justify-center gap-4">
{mockEvents.map((event, i) => {
const daysLeft = getDaysUntil(event.date);
return (
<div
key={i}
className="bg-white shadow-sm rounded-xl p-4 text-left border border-gray-200 text-sm w-[280px]"
>
<p className="text-gray-500 text-xs mb-1">
📅 {new Date(event.date).toLocaleDateString("de-DE")}
</p>
<h3 className="font-semibold text-base">{event.title}</h3>
<p className="text-gray-600 text-sm mb-1">{event.location}</p>
<div className="flex items-center gap-1 text-primary text-xs mt-2">
<Clock className="w-4 h-4" />
{daysLeft > 0
? `Noch ${daysLeft} Tag${daysLeft > 1 ? "e" : ""}`
: daysLeft === 0
? "Heute!"
: "Bereits vorbei"}
</div>
</div>
);
})}
</div>
<div className="mt-8">
<Link
to="/events"
className="inline-block bg-primary text-white text-sm py-2 px-4 rounded-lg hover:bg-primary/90 transition"
>
Zum Eventkalender
</Link>
</div>
</div>
</section>
);
};
export default EventsSection;

View File

@ -74,17 +74,18 @@ const Navbar = () => {
{isTeamsOpen && (
<div className="absolute left-0 mt-2 w-40 bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 z-10">
<Link to="/teams/damen1" className="block px-4 py-2 text-gray-700 hover:bg-frog-50 hover:text-frog-600">Damen 1</Link>
<Link to="/teams/damen2" className="block px-4 py-2 text-gray-700 hover:bg-frog-50 hover:text-frog-600">Damen 2</Link>
<Link to="/teams/herren1" className="block px-4 py-2 text-gray-700 hover:bg-frog-50 hover:text-frog-600">Herren 1</Link>
<Link to="/teams/herren2" className="block px-4 py-2 text-gray-700 hover:bg-frog-50 hover:text-frog-600">Herren 2</Link>
<Link to="/teams/4" className="block px-3 py-1 text-gray-700 hover:text-frog-600">Damen 1</Link>
<Link to="/teams/5" className="block px-3 py-1 text-gray-700 hover:text-frog-600">Damen 2</Link>
<Link to="/teams/1" className="block px-3 py-1 text-gray-700 hover:text-frog-600">Herren 1</Link>
<Link to="/teams/2" className="block px-3 py-1 text-gray-700 hover:text-frog-600">Herren 2</Link>
<Link to="/teams/9" className="block px-3 py-1 text-gray-700 hover:text-frog-600">Mixed</Link>
</div>
)}
</div>
<button onClick={() => navigateAndScroll("gallery")} className="text-gray-700 hover:text-frog-600 px-3 py-2 rounded-md font-medium">Galerie</button>
<button onClick={() => navigateAndScroll("about")} className="text-gray-700 hover:text-frog-600 px-3 py-2 rounded-md font-medium">Über uns</button>
<button onClick={() => navigateAndScroll("about")} className="text-gray-700 hover:text-frog-600 px-3 py-2 rounded-md font-medium whitespace-nowrap">Über uns</button>
<button onClick={() => navigateAndScroll("contact")} className="text-gray-700 hover:text-frog-600 px-3 py-2 rounded-md font-medium">Kontakt</button>
<Link to="/mitglied-werden" className="w-full">
@ -171,10 +172,11 @@ const Navbar = () => {
<div>
<div className="block px-3 py-2 rounded-md text-base font-medium text-gray-700">Teams</div>
<div className="pl-6">
<Link to="/teams/damen1" className="block px-3 py-1 text-gray-700 hover:text-frog-600">Damen 1</Link>
<Link to="/teams/damen2" className="block px-3 py-1 text-gray-700 hover:text-frog-600">Damen 2</Link>
<Link to="/teams/herren1" className="block px-3 py-1 text-gray-700 hover:text-frog-600">Herren 1</Link>
<Link to="/teams/herren2" className="block px-3 py-1 text-gray-700 hover:text-frog-600">Herren 2</Link>
<Link to="/teams/4" className="block px-3 py-1 text-gray-700 hover:text-frog-600">Damen 1</Link>
<Link to="/teams/5" className="block px-3 py-1 text-gray-700 hover:text-frog-600">Damen 2</Link>
<Link to="/teams/1" className="block px-3 py-1 text-gray-700 hover:text-frog-600">Herren 1</Link>
<Link to="/teams/2" className="block px-3 py-1 text-gray-700 hover:text-frog-600">Herren 2</Link>
<Link to="/teams/9" className="block px-3 py-1 text-gray-700 hover:text-frog-600">Mixed</Link>
</div>
</div>

View File

@ -1,8 +1,12 @@
import { Navigate } from "react-router-dom";
import { useAuth } from "@/context/AuthContext";
const PrivateRoute = ({ children }: { children: JSX.Element }) => {
const { isAuthenticated } = useAuth();
const ProtectedRoute = ({ children }: { children: JSX.Element }) => {
const { isAuthenticated, isAuthLoading } = useAuth();
if (isAuthLoading) {
return <div className="text-center py-12">🔒 Authentifizierung wird geprüft...</div>;
}
if (!isAuthenticated) {
return <Navigate to="/admin/login" replace />;
@ -11,4 +15,4 @@ const PrivateRoute = ({ children }: { children: JSX.Element }) => {
return children;
};
export default PrivateRoute;
export default ProtectedRoute;

View File

@ -9,6 +9,7 @@ interface AuthContextType {
login: (token: string) => void;
logout: () => void;
isAuthenticated: boolean;
isAuthLoading: boolean;
}
const AuthContext = createContext<AuthContextType>({
@ -19,36 +20,47 @@ const AuthContext = createContext<AuthContextType>({
login: () => {},
logout: () => {},
isAuthenticated: false,
isAuthLoading: true
});
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [token, setToken] = useState<string | null>(null);
const [username, setUsername] = useState<string | null>(null);
const [role, setRole] = useState<string | null>(null);
const [isAuthLoading, setIsAuthLoading] = useState(true); // NEU
const isAdmin = role === "admin";
const isAuthenticated = !!token;
useEffect(() => {
const storedToken = localStorage.getItem("token");
if (storedToken) {
setToken(storedToken);
try {
const decoded: any = jwtDecode(storedToken);
setToken(storedToken);
setUsername(decoded.username);
setRole(decoded.role);
} catch (error) {
console.error("Token konnte nicht gelesen werden");
}
}
setIsAuthLoading(false); // Wichtig, auch wenn kein Token
}, []);
const login = (newToken: string) => {
localStorage.setItem("token", newToken);
setToken(newToken);
try {
const decoded: any = jwtDecode(newToken);
setUsername(decoded.username);
setRole(decoded.role);
} catch (err) {
console.error("Token konnte nicht gelesen werden");
}
};
const logout = () => {
localStorage.removeItem("token");
setToken(null);
@ -57,10 +69,15 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
};
return (
<AuthContext.Provider value={{ token, username, role, isAdmin, login, logout, isAuthenticated }}>
<AuthContext.Provider value={{
token, username, role, isAdmin,
login, logout, isAuthenticated,
isAuthLoading
}}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => useContext(AuthContext);

View File

@ -35,6 +35,7 @@ import Beitraege from "./pages/Beitraege";
import { ThemeProvider } from "@/context/ThemeContext";
import VideosPage from "./pages/VideosPage";
import EventsPage from "./pages/EventsPage";
@ -77,6 +78,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<Route path="/impressum" element={<Layout><Impressum /></Layout>} />
<Route path="/beitraege" element={<Layout><Beitraege /></Layout>} />
<Route path="/videos" element={<Layout><VideosPage/></Layout>} />
<Route path="/events" element={<Layout><EventsPage /></Layout>} />
<Route path="/admin" element={<PrivateRoute><Layout><AdminDashboard /></Layout></PrivateRoute>} />

71
src/pages/EventsPage.tsx Normal file
View File

@ -0,0 +1,71 @@
import { Calendar, dateFnsLocalizer } from "react-big-calendar";
import "react-big-calendar/lib/css/react-big-calendar.css";
import { parse, startOfWeek, format, getDay } from "date-fns";
import { de } from "d:/Projects/web/tg/volleyball-dev-frontend/node_modules/date-fns/locale/de";
import { useMemo } from "react";
const locales = {
de: de,
};
const localizer = dateFnsLocalizer({
format,
parse,
startOfWeek: () => startOfWeek(new Date(), { locale: de }),
getDay,
locales,
});
const events = [
{
title: "Heimspiel Herren 1",
start: new Date("2025-06-08T15:00:00"),
end: new Date("2025-06-08T17:00:00"),
allDay: false,
},
{
title: "Sommerfest",
start: new Date("2025-06-15"),
end: new Date("2025-06-15"),
allDay: true,
},
{
title: "Training Jugend U16",
start: new Date("2025-06-17T18:00:00"),
end: new Date("2025-06-17T19:30:00"),
allDay: false,
},
];
const EventsPage = () => {
return (
<div className="min-h-screen bg-white py-12 px-4 md:px-12">
<h1 className="text-3xl font-bold mb-6 text-center">Eventkalender</h1>
<div className="bg-white rounded-xl shadow-md p-4">
<Calendar
localizer={localizer}
events={events}
startAccessor="start"
endAccessor="end"
style={{ height: 600 }}
views={["month", "week", "agenda"]}
messages={{
today: "Heute",
previous: "Zurück",
next: "Weiter",
month: "Monat",
week: "Woche",
day: "Tag",
agenda: "Liste",
date: "Datum",
time: "Uhrzeit",
event: "Event",
noEventsInRange: "Keine Events im gewählten Zeitraum",
}}
/>
</div>
</div>
);
};
export default EventsPage;

View File

@ -7,6 +7,7 @@ import TeamSection from "@/components/TeamSection";
import GallerySection from "@/components/GallerySection";
import AboutSection from "@/components/AboutSection";
import ContactSection from "@/components/ContactSection";
import EventsSection from "../components/EventsSection";
const Index = () => {
const location = useLocation();
@ -27,6 +28,8 @@ const Index = () => {
<Hero />
<div id="news" />
<NewsSection />
<div id="events" />
<EventsSection/>
<div id="team" />
<TeamSection />
<div id="gallery" />

View File

@ -3,26 +3,29 @@ import { useNavigate } from "react-router-dom";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useAuth } from "@/context/AuthContext";
const apiBase = import.meta.env.VITE_API_URL;
const LoginPage = () => {
const [email, setEmail] = useState(""); // Wird eigentlich Username sein!
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const navigate = useNavigate();
const [loginAttempts, setLoginAttempts] = useState(0);
const [captcha, setCaptcha] = useState("");
const [showCaptcha, setShowCaptcha] = useState(false);
const navigate = useNavigate();
const { login } = useAuth(); // <-- Context-Funktion holen
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
try {
if(showCaptcha && captcha.toLowerCase() !== "laudenbach") {
// Captcha check
if (showCaptcha && captcha.toLowerCase() !== "laudenbach") {
setError("Captcha falsch. Bitte versuche es erneut.");
return;
}
@ -42,6 +45,7 @@ const LoginPage = () => {
const data = await res.json();
localStorage.setItem("token", data.token);
login(data.token);
navigate("/admin");
} catch (err: any) {
console.error(err);
@ -51,13 +55,11 @@ const LoginPage = () => {
setError("Login fehlgeschlagen. Bitte prüfe Benutzername und Passwort.");
const newAttempts = loginAttempts + 1;
setLoginAttempts(newAttempts);
if(newAttempts >= 3) {
if (newAttempts >= 5) {
setShowCaptcha(true);
}
}
}
};
return (