This commit is contained in:
parent
d78ece0dfd
commit
692f9bdd2a
166
package-lock.json
generated
166
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -11,6 +11,8 @@ import "aos/dist/aos.css";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
|
||||
|
||||
const App = () => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TooltipProvider>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
82
src/components/EventsSection.tsx
Normal file
82
src/components/EventsSection.tsx
Normal 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;
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
71
src/pages/EventsPage.tsx
Normal 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;
|
||||
@ -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" />
|
||||
|
||||
@ -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 (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user