diff --git a/package-lock.json b/package-lock.json
index 5b6042341..936f034ae 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -43,6 +43,7 @@
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.3.0",
"input-otp": "^1.2.4",
+ "jwt-decode": "^4.0.0",
"keen-slider": "^6.8.6",
"lucide-react": "^0.462.0",
"next-themes": "^0.3.0",
@@ -4950,6 +4951,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/jwt-decode": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
+ "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/keen-slider": {
"version": "6.8.6",
"resolved": "https://registry.npmjs.org/keen-slider/-/keen-slider-6.8.6.tgz",
diff --git a/package.json b/package.json
index 0bd6bfe97..6f8ae0923 100644
--- a/package.json
+++ b/package.json
@@ -46,6 +46,7 @@
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.3.0",
"input-otp": "^1.2.4",
+ "jwt-decode": "^4.0.0",
"keen-slider": "^6.8.6",
"lucide-react": "^0.462.0",
"next-themes": "^0.3.0",
diff --git a/src/admin/AdminDashboard.tsx b/src/admin/AdminDashboard.tsx
new file mode 100644
index 000000000..2659bb46d
--- /dev/null
+++ b/src/admin/AdminDashboard.tsx
@@ -0,0 +1,53 @@
+import { Link, useNavigate } from "react-router-dom";
+import { Card, CardContent, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { useAuth } from "@/context/AuthContext";
+import { LogOut } from "lucide-react";
+
+const AdminDashboard = () => {
+ const { logout, username } = useAuth();
+ const navigate = useNavigate();
+
+ const handleLogout = () => {
+ logout();
+ navigate("/");
+ };
+
+ return (
+
+
+
Willkommen, {username}!
+
+
+
+
+
+
+
+ News verwalten
+ News erstellen, bearbeiten und löschen
+
+
+
+
+ {/* Hier später weitere Admin-Bereiche */}
+
+
+
+ Benutzer verwalten
+ Admins erstellen, bearbeiten und löschen
+
+
+
+
+
+ );
+};
+
+export default AdminDashboard;
diff --git a/src/admin/NewsManager.tsx b/src/admin/NewsManager.tsx
new file mode 100644
index 000000000..a658c613d
--- /dev/null
+++ b/src/admin/NewsManager.tsx
@@ -0,0 +1,208 @@
+import { useEffect, useState } from "react";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
+
+type NewsItem = {
+ id: number;
+ title: string;
+ description: string;
+ image_url: string;
+ team: string;
+ created_at: string;
+};
+
+const NewsManager = () => {
+ const [news, setNews] = useState([]);
+ const [showForm, setShowForm] = useState(false);
+ const [editMode, setEditMode] = useState(false);
+ const [currentId, setCurrentId] = useState(null);
+ const [newTitle, setNewTitle] = useState("");
+ const [newDescription, setNewDescription] = useState("");
+ const [newImageUrl, setNewImageUrl] = useState("");
+ const [newTeam, setNewTeam] = useState("");
+ const [showDeleteModal, setShowDeleteModal] = useState(false);
+ const [deleteId, setDeleteId] = useState(null);
+
+ useEffect(() => {
+ loadNews();
+ }, []);
+
+ const loadNews = async () => {
+ try {
+ const res = await fetch("http://192.168.50.65:3000/api/news");
+ const data = await res.json();
+ setNews(data);
+ } catch (err) {
+ console.error("Fehler beim Laden der News:", err);
+ }
+ };
+
+ const handleCreateOrUpdateNews = async () => {
+ const method = editMode ? "PUT" : "POST";
+ const url = editMode
+ ? `http://192.168.50.65:3000/api/news/${currentId}`
+ : `http://192.168.50.65:3000/api/news`;
+
+ try {
+ const res = await fetch(url, {
+ method,
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ title: newTitle,
+ description: newDescription,
+ image_url: newImageUrl,
+ team: newTeam,
+ }),
+ });
+
+ if (res.ok) {
+ await loadNews();
+ setShowForm(false);
+ setEditMode(false);
+ resetForm();
+ } else {
+ console.error("Fehler beim Speichern der News");
+ }
+ } catch (err) {
+ console.error("Fehler beim Speichern der News:", err);
+ }
+ };
+
+ const handleEdit = (item: NewsItem) => {
+ setNewTitle(item.title);
+ setNewDescription(item.description);
+ setNewImageUrl(item.image_url);
+ setNewTeam(item.team);
+ setCurrentId(item.id);
+ setEditMode(true);
+ setShowForm(true);
+ };
+
+ const confirmDelete = (id: number) => {
+ setDeleteId(id);
+ setShowDeleteModal(true);
+ };
+
+ const handleDelete = async () => {
+ if (!deleteId) return;
+
+ try {
+ const res = await fetch(`http://192.168.50.65:3000/api/news/${deleteId}`, {
+ method: "DELETE",
+ });
+
+ if (res.ok) {
+ await loadNews();
+ } else {
+ console.error("Fehler beim Löschen der News");
+ }
+ } catch (err) {
+ console.error("Fehler beim Löschen der News:", err);
+ } finally {
+ setShowDeleteModal(false);
+ setDeleteId(null);
+ }
+ };
+
+
+ const resetForm = () => {
+ setNewTitle("");
+ setNewDescription("");
+ setNewImageUrl("");
+ setNewTeam("");
+ setCurrentId(null);
+ };
+
+ return (
+
+
+
News verwalten
+
+
+
+ {showForm && (
+
+
+ {editMode ? "News bearbeiten" : "Neue News anlegen"}
+
+
+ setNewTitle(e.target.value)} />
+
+
+ )}
+
+ {showDeleteModal && (
+
+
+
News wirklich löschen?
+
+
+
+
+
+
+ )}
+
+
+ {news.map((item) => (
+
+
+ {item.title}
+
+
+ {item.description}
+ Erstellt am {new Date(item.created_at).toLocaleDateString("de-DE")}
+
+
+
+
+
+
+ ))}
+
+
+ );
+};
+
+export default NewsManager;
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index 49cf067ea..0aa7e8246 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -1,11 +1,14 @@
import { useState } from 'react';
import { Button } from "@/components/ui/button";
import { Menu, X, Volleyball, ChevronDown, Users } from "lucide-react";
-import { Link } from "react-router-dom";
+import { Link, useNavigate } from "react-router-dom";
+import { useAuth } from '@/context/AuthContext';
const Navbar = () => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isTeamsOpen, setIsTeamsOpen] = useState(false);
+ const { isAuthenticated, username, logout } = useAuth();
+ const navigate = useNavigate();
const toggleMenu = () => {
setIsMenuOpen(!isMenuOpen);
@@ -25,7 +28,6 @@ const Navbar = () => {
Startseite
Aktuelles
- {/* Gemeinsames Container-Div für Hover */}
setIsTeamsOpen(true)}
@@ -40,9 +42,7 @@ const Navbar = () => {
{/* Dropdown */}
{isTeamsOpen && (
-
+
Damen 1
Damen 2
Herren 1
@@ -54,14 +54,52 @@ const Navbar = () => {
Galerie
Über uns
Kontakt
+
-
-
-
+
+ {/* USER ICON + DROPDOWN */}
+
+
+
+ {isAuthenticated && (
+ <>
+
+ {username}
+
+
+ {/* Dropdown */}
+
+
+
+ >
+ )}
+
+
{/* Mobile menu button */}
diff --git a/src/components/PrivateRoute.tsx b/src/components/PrivateRoute.tsx
new file mode 100644
index 000000000..4a4b4eab1
--- /dev/null
+++ b/src/components/PrivateRoute.tsx
@@ -0,0 +1,14 @@
+import { Navigate } from "react-router-dom";
+import { useAuth } from "@/context/AuthContext";
+
+const PrivateRoute = ({ children }: { children: JSX.Element }) => {
+ const { isAuthenticated } = useAuth();
+
+ if (!isAuthenticated) {
+ return
;
+ }
+
+ return children;
+};
+
+export default PrivateRoute;
diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx
new file mode 100644
index 000000000..449d4c4de
--- /dev/null
+++ b/src/context/AuthContext.tsx
@@ -0,0 +1,58 @@
+import { createContext, useContext, useState, useEffect, ReactNode } from "react";
+import {jwtDecode} from "jwt-decode";
+
+
+
+interface AuthContextType {
+ token: string | null;
+ username: string | null;
+ login: (token: string) => void;
+ logout: () => void;
+ isAuthenticated: boolean;
+}
+
+const AuthContext = createContext
({
+ token: null,
+ login: () => {},
+ logout: () => {},
+ isAuthenticated: false,
+ username: null
+});
+
+export const AuthProvider = ({ children }: { children: ReactNode }) => {
+ const [token, setToken] = useState(null);
+ const [username, setUsername] = useState(null);
+
+ useEffect(() => {
+ const storedToken = localStorage.getItem("token");
+ if (storedToken) {
+ setToken(storedToken);
+ try {
+ const decoded: any = jwtDecode(storedToken);
+ setUsername(decoded.username); // <-- Username speichern
+ } catch (error) {
+ console.error("Token konnte nicht gelesen werden");
+ }
+ }
+ }, []);
+
+ const login = (newToken: string) => {
+ localStorage.setItem("token", newToken);
+ setToken(newToken);
+ };
+
+ const logout = () => {
+ localStorage.removeItem("token");
+ setToken(null);
+ };
+
+ const isAuthenticated = !!token;
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useAuth = () => useContext(AuthContext);
diff --git a/src/main.tsx b/src/main.tsx
index 5618c5f8b..930cff42c 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -12,40 +12,49 @@ import TeamDetailPage from "./pages/TeamDetailPage";
import "./index.css"
import LoginPage from "./pages/LoginPage";
+import AdminDashboard from "./admin/AdminDashboard";
+import NewsManager from "./admin/NewsManager";
+import { AuthProvider } from "./context/AuthContext";
+import PrivateRoute from "./components/PrivateRoute";
ReactDOM.createRoot(document.getElementById("root")!).render(
-
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
- } />
- }/>
-
+
+
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+ } />
+ }/>
+ } />
+ } />
+ } />
+
+
);
\ No newline at end of file
diff --git a/src/pages/AlleNeuigkeiten.tsx b/src/pages/AlleNeuigkeiten.tsx
index 0f2630ddc..798339b5c 100644
--- a/src/pages/AlleNeuigkeiten.tsx
+++ b/src/pages/AlleNeuigkeiten.tsx
@@ -1,54 +1,28 @@
+import { useEffect, useState } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
-import { useState } from "react";
-import { Button } from "@/components/ui/button";
-import { Select, SelectItem } from "@/components/ui/select"; // Falls du ein Select UI-Element hast
-
-const news = [
- {
- id: 1,
- title: "Saisonstart 2023/24",
- date: "2023-09-15",
- description: "Die neue Saison beginnt mit einem Heimspiel gegen VfB Mosbach. Kommt vorbei und unterstützt unser Team!",
- image: "https://images.unsplash.com/photo-1612872087720-bb876e2e67d1?ixlib=rb-4.0.3&auto=format&fit=crop&w=500&q=80",
- team: "Herren I"
- },
- {
- id: 2,
- title: "Beachvolleyball-Turnier",
- date: "2023-07-03",
- description: "Unser jährliches Beachvolleyball-Turnier war ein voller Erfolg! 24 Teams kämpften um den Sieg.",
- image: "https://images.unsplash.com/photo-1583514555852-6f77e7c9e081?ixlib=rb-4.0.3&auto=format&fit=crop&w=500&q=80",
- team: "Mixed-Team"
- },
- {
- id: 3,
- title: "Neuer Trainer für die Jugendmannschaft",
- date: "2023-05-21",
- description: "Wir freuen uns, Marc Schneider als neuen Trainer für unsere U16-Mannschaft begrüßen zu dürfen.",
- image: "https://images.unsplash.com/photo-1547347298-4074fc3086f0?ixlib=rb-4.0.3&auto=format&fit=crop&w=500&q=80",
- team: "Jugend U16"
- },
-];
-
+type NewsItem = {
+ id: number;
+ title: string;
+ description: string;
+ image_url: string;
+ team: string;
+ created_at: string;
+};
const AlleNeuigkeitenPage = () => {
- const [selectedTeam, setSelectedTeam] = useState("Alle");
+ const [news, setNews] = useState([]);
- // Teams dynamisch auslesen
- const teams = ["Alle", ...Array.from(new Set(news.map((item) => item.team)))];
-
- // Nach Datum absteigend sortieren
- const sortedNews = [...news].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
-
- // Nach Team filtern
- const filteredNews = sortedNews.filter((item) =>
- selectedTeam === "Alle" ? true : item.team === selectedTeam
- );
+ useEffect(() => {
+ fetch("http://192.168.50.65:3000/api/news")
+ .then((res) => res.json())
+ .then((data) => setNews(data))
+ .catch((err) => console.error("Fehler beim Laden der News:", err));
+ }, []);
// Gruppieren nach Jahren
- const groupedNews = filteredNews.reduce((acc: any, item) => {
- const year = new Date(item.date).getFullYear();
+ const groupedNews = news.reduce((acc: Record, item) => {
+ const year = new Date(item.created_at).getFullYear().toString();
if (!acc[year]) acc[year] = [];
acc[year].push(item);
return acc;
@@ -58,34 +32,23 @@ const AlleNeuigkeitenPage = () => {
Alle Neuigkeiten
- {/* Team-Filter */}
-
-
-
-
{/* News nach Jahren gruppiert */}
{Object.entries(groupedNews).map(([year, items]) => (
{year}
- {(items as typeof news).map((item) => (
+ {items.map((item) => (
-

+
{item.title}
- {new Date(item.date).toLocaleDateString("de-DE")}
+ {new Date(item.created_at).toLocaleDateString("de-DE")}
{item.description}
@@ -95,7 +58,6 @@ const AlleNeuigkeitenPage = () => {
))}
-
);
};
diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx
index 205d237ec..1a7d0b5fd 100644
--- a/src/pages/LoginPage.tsx
+++ b/src/pages/LoginPage.tsx
@@ -1,8 +1,40 @@
+import { useState } from "react";
+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";
const LoginPage = () => {
+ const [email, setEmail] = useState(""); // Wird eigentlich Username sein!
+ const [password, setPassword] = useState("");
+ const [error, setError] = useState("");
+ const navigate = useNavigate();
+
+ const handleLogin = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError("");
+
+ try {
+ const res = await fetch("http://192.168.50.65:3000/api/login", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ username: email, password }),
+ });
+
+ if (!res.ok) {
+ throw new Error("Login fehlgeschlagen");
+ }
+
+ const data = await res.json();
+ localStorage.setItem("token", data.token);
+
+ navigate("/admin");
+ } catch (err) {
+ console.error(err);
+ setError("Login fehlgeschlagen. Bitte prüfe Benutzername und Passwort.");
+ }
+ };
+
return (
@@ -10,9 +42,22 @@ const LoginPage = () => {
Login
-