Login integriert
This commit is contained in:
parent
8f9cd73e50
commit
8a92201b74
10
package-lock.json
generated
10
package-lock.json
generated
@ -43,6 +43,7 @@
|
|||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"embla-carousel-react": "^8.3.0",
|
"embla-carousel-react": "^8.3.0",
|
||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
"keen-slider": "^6.8.6",
|
"keen-slider": "^6.8.6",
|
||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
@ -4950,6 +4951,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/keen-slider": {
|
||||||
"version": "6.8.6",
|
"version": "6.8.6",
|
||||||
"resolved": "https://registry.npmjs.org/keen-slider/-/keen-slider-6.8.6.tgz",
|
"resolved": "https://registry.npmjs.org/keen-slider/-/keen-slider-6.8.6.tgz",
|
||||||
|
|||||||
@ -46,6 +46,7 @@
|
|||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"embla-carousel-react": "^8.3.0",
|
"embla-carousel-react": "^8.3.0",
|
||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
"keen-slider": "^6.8.6",
|
"keen-slider": "^6.8.6",
|
||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
|
|||||||
53
src/admin/AdminDashboard.tsx
Normal file
53
src/admin/AdminDashboard.tsx
Normal file
@ -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 (
|
||||||
|
<div className="max-w-6xl mx-auto py-12 px-4">
|
||||||
|
<div className="flex justify-between items-center mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-frog-600">Willkommen, {username}!</h1>
|
||||||
|
<Button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="bg-frog-500 hover:bg-frog-600 text-white flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<LogOut className="w-4 h-4" />
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
<Link to="/admin/news">
|
||||||
|
<Card className="hover:shadow-lg transition-shadow cursor-pointer">
|
||||||
|
<CardContent className="p-6 text-center">
|
||||||
|
<CardTitle className="text-frog-600">News verwalten</CardTitle>
|
||||||
|
<p className="text-gray-600 mt-2 text-sm">News erstellen, bearbeiten und löschen</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Hier später weitere Admin-Bereiche */}
|
||||||
|
<Link to="/admin/users">
|
||||||
|
<Card className="hover:shadow-lg transition-shadow cursor-pointer">
|
||||||
|
<CardContent className="p-6 text-center">
|
||||||
|
<CardTitle className="text-frog-600">Benutzer verwalten</CardTitle>
|
||||||
|
<p className="text-gray-600 mt-2 text-sm">Admins erstellen, bearbeiten und löschen</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminDashboard;
|
||||||
208
src/admin/NewsManager.tsx
Normal file
208
src/admin/NewsManager.tsx
Normal file
@ -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<NewsItem[]>([]);
|
||||||
|
const [showForm, setShowForm] = useState(false);
|
||||||
|
const [editMode, setEditMode] = useState(false);
|
||||||
|
const [currentId, setCurrentId] = useState<number | null>(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<number | null>(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 (
|
||||||
|
<div className="max-w-5xl mx-auto py-12 px-4">
|
||||||
|
<div className="flex justify-between items-center mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-frog-600">News verwalten</h1>
|
||||||
|
<Button
|
||||||
|
className="bg-frog-500 hover:bg-frog-600 text-white"
|
||||||
|
onClick={() => {
|
||||||
|
resetForm();
|
||||||
|
setEditMode(false);
|
||||||
|
setShowForm(!showForm);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showForm ? "Abbrechen" : "+ Neue News"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showForm && (
|
||||||
|
<div className="mb-8 border p-4 rounded-md bg-gray-50">
|
||||||
|
<h2 className="text-xl font-bold text-frog-600 mb-4">
|
||||||
|
{editMode ? "News bearbeiten" : "Neue News anlegen"}
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Input placeholder="Titel" value={newTitle} onChange={(e) => setNewTitle(e.target.value)} />
|
||||||
|
<Textarea placeholder="Beschreibung" value={newDescription} onChange={(e) => setNewDescription(e.target.value)} />
|
||||||
|
<Input placeholder="Bild-URL" value={newImageUrl} onChange={(e) => setNewImageUrl(e.target.value)} />
|
||||||
|
<Input placeholder="Team" value={newTeam} onChange={(e) => setNewTeam(e.target.value)} />
|
||||||
|
<Button onClick={handleCreateOrUpdateNews} className="bg-frog-500 hover:bg-frog-600 text-white w-full">
|
||||||
|
{editMode ? "Änderungen speichern" : "Speichern"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showDeleteModal && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow-lg max-w-sm w-full">
|
||||||
|
<h2 className="text-xl font-bold text-frog-600 mb-4 text-center">News wirklich löschen?</h2>
|
||||||
|
<div className="flex justify-center gap-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="border-gray-300 text-gray-600 hover:bg-gray-100"
|
||||||
|
onClick={() => setShowDeleteModal(false)}
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handleDelete}
|
||||||
|
>
|
||||||
|
Ja, löschen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
{news.map((item) => (
|
||||||
|
<Card key={item.id} className="overflow-hidden">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{item.title}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-gray-600 mb-2">{item.description}</p>
|
||||||
|
<p className="text-xs text-gray-400">Erstellt am {new Date(item.created_at).toLocaleDateString("de-DE")}</p>
|
||||||
|
<div className="flex gap-2 mt-4">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="border-frog-500 text-frog-600 hover:bg-frog-50"
|
||||||
|
onClick={() => handleEdit(item)}
|
||||||
|
>
|
||||||
|
Bearbeiten
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => confirmDelete(item.id)}
|
||||||
|
>
|
||||||
|
Löschen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewsManager;
|
||||||
@ -1,11 +1,14 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Menu, X, Volleyball, ChevronDown, Users } from "lucide-react";
|
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 Navbar = () => {
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
const [isTeamsOpen, setIsTeamsOpen] = useState(false);
|
const [isTeamsOpen, setIsTeamsOpen] = useState(false);
|
||||||
|
const { isAuthenticated, username, logout } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const toggleMenu = () => {
|
const toggleMenu = () => {
|
||||||
setIsMenuOpen(!isMenuOpen);
|
setIsMenuOpen(!isMenuOpen);
|
||||||
@ -25,7 +28,6 @@ const Navbar = () => {
|
|||||||
<a href="#" className="text-gray-700 hover:text-frog-600 px-3 py-2 rounded-md font-medium">Startseite</a>
|
<a href="#" className="text-gray-700 hover:text-frog-600 px-3 py-2 rounded-md font-medium">Startseite</a>
|
||||||
<a href="#news" className="text-gray-700 hover:text-frog-600 px-3 py-2 rounded-md font-medium">Aktuelles</a>
|
<a href="#news" className="text-gray-700 hover:text-frog-600 px-3 py-2 rounded-md font-medium">Aktuelles</a>
|
||||||
|
|
||||||
{/* Gemeinsames Container-Div für Hover */}
|
|
||||||
<div
|
<div
|
||||||
className="relative"
|
className="relative"
|
||||||
onMouseEnter={() => setIsTeamsOpen(true)}
|
onMouseEnter={() => setIsTeamsOpen(true)}
|
||||||
@ -40,9 +42,7 @@ const Navbar = () => {
|
|||||||
|
|
||||||
{/* Dropdown */}
|
{/* Dropdown */}
|
||||||
{isTeamsOpen && (
|
{isTeamsOpen && (
|
||||||
<div
|
<div className="absolute left-0 mt-2 w-40 bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 z-10">
|
||||||
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/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/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/herren1" className="block px-4 py-2 text-gray-700 hover:bg-frog-50 hover:text-frog-600">Herren 1</Link>
|
||||||
@ -54,14 +54,52 @@ const Navbar = () => {
|
|||||||
<a href="#gallery" className="text-gray-700 hover:text-frog-600 px-3 py-2 rounded-md font-medium">Galerie</a>
|
<a href="#gallery" className="text-gray-700 hover:text-frog-600 px-3 py-2 rounded-md font-medium">Galerie</a>
|
||||||
<a href="#about" className="text-gray-700 hover:text-frog-600 px-3 py-2 rounded-md font-medium">Über uns</a>
|
<a href="#about" className="text-gray-700 hover:text-frog-600 px-3 py-2 rounded-md font-medium">Über uns</a>
|
||||||
<a href="#contact" className="text-gray-700 hover:text-frog-600 px-3 py-2 rounded-md font-medium">Kontakt</a>
|
<a href="#contact" className="text-gray-700 hover:text-frog-600 px-3 py-2 rounded-md font-medium">Kontakt</a>
|
||||||
|
|
||||||
<Link to="/mitglied-werden" className="w-full">
|
<Link to="/mitglied-werden" className="w-full">
|
||||||
<Button className="w-full bg-frog-500 hover:bg-frog-600 text-white">
|
<Button className="w-full bg-frog-500 hover:bg-frog-600 text-white">
|
||||||
Mitglied werden
|
Mitglied werden
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/login" className="ml-4 text-frog-600 hover:text-frog-800">
|
|
||||||
<Users className="h-6 w-6" />
|
{/* USER ICON + DROPDOWN */}
|
||||||
</Link>
|
<div className="relative group ml-4">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => {
|
||||||
|
if (isAuthenticated) {
|
||||||
|
navigate("/admin");
|
||||||
|
} else {
|
||||||
|
navigate("/admin/login");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Users className="h-6 w-6 text-frog-600 hover:text-frog-800" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{isAuthenticated && (
|
||||||
|
<>
|
||||||
|
<span className="text-sm text-frog-600 ml-2 hidden md:inline">
|
||||||
|
{username}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Dropdown */}
|
||||||
|
<div className="absolute right-0 mt-2 hidden group-hover:block bg-white rounded-md shadow-lg z-20 p-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="w-full text-left text-gray-700 hover:bg-frog-50 hover:text-frog-600"
|
||||||
|
onClick={() => {
|
||||||
|
logout();
|
||||||
|
navigate("/admin/login");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile menu button */}
|
{/* Mobile menu button */}
|
||||||
|
|||||||
14
src/components/PrivateRoute.tsx
Normal file
14
src/components/PrivateRoute.tsx
Normal file
@ -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 <Navigate to="/admin/login" replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PrivateRoute;
|
||||||
58
src/context/AuthContext.tsx
Normal file
58
src/context/AuthContext.tsx
Normal file
@ -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<AuthContextType>({
|
||||||
|
token: null,
|
||||||
|
login: () => {},
|
||||||
|
logout: () => {},
|
||||||
|
isAuthenticated: false,
|
||||||
|
username: null
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const [token, setToken] = useState<string | null>(null);
|
||||||
|
const [username, setUsername] = useState<string | null>(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 (
|
||||||
|
<AuthContext.Provider value={{ token, username, login, logout, isAuthenticated }}>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAuth = () => useContext(AuthContext);
|
||||||
@ -12,12 +12,17 @@ import TeamDetailPage from "./pages/TeamDetailPage";
|
|||||||
|
|
||||||
import "./index.css"
|
import "./index.css"
|
||||||
import LoginPage from "./pages/LoginPage";
|
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(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
<AuthProvider>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
path="/"
|
path="/"
|
||||||
@ -45,7 +50,11 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
|||||||
/>
|
/>
|
||||||
<Route path="/teams/:id" element={<Layout><TeamDetailPage /></Layout>} />
|
<Route path="/teams/:id" element={<Layout><TeamDetailPage /></Layout>} />
|
||||||
<Route path="/login" element={<Layout><LoginPage /></Layout>}/>
|
<Route path="/login" element={<Layout><LoginPage /></Layout>}/>
|
||||||
|
<Route path="/admin/login" element={<LoginPage />} />
|
||||||
|
<Route path="/admin" element={<PrivateRoute><Layout><AdminDashboard /></Layout></PrivateRoute>} />
|
||||||
|
<Route path="/admin/news" element={<PrivateRoute><Layout><NewsManager /></Layout></PrivateRoute>} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
</AuthProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
@ -1,54 +1,28 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
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 AlleNeuigkeitenPage = () => {
|
||||||
const [selectedTeam, setSelectedTeam] = useState("Alle");
|
const [news, setNews] = useState<NewsItem[]>([]);
|
||||||
|
|
||||||
// Teams dynamisch auslesen
|
useEffect(() => {
|
||||||
const teams = ["Alle", ...Array.from(new Set(news.map((item) => item.team)))];
|
fetch("http://192.168.50.65:3000/api/news")
|
||||||
|
.then((res) => res.json())
|
||||||
// Nach Datum absteigend sortieren
|
.then((data) => setNews(data))
|
||||||
const sortedNews = [...news].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
.catch((err) => console.error("Fehler beim Laden der News:", err));
|
||||||
|
}, []);
|
||||||
// Nach Team filtern
|
|
||||||
const filteredNews = sortedNews.filter((item) =>
|
|
||||||
selectedTeam === "Alle" ? true : item.team === selectedTeam
|
|
||||||
);
|
|
||||||
|
|
||||||
// Gruppieren nach Jahren
|
// Gruppieren nach Jahren
|
||||||
const groupedNews = filteredNews.reduce((acc: any, item) => {
|
const groupedNews = news.reduce((acc: Record<string, NewsItem[]>, item) => {
|
||||||
const year = new Date(item.date).getFullYear();
|
const year = new Date(item.created_at).getFullYear().toString();
|
||||||
if (!acc[year]) acc[year] = [];
|
if (!acc[year]) acc[year] = [];
|
||||||
acc[year].push(item);
|
acc[year].push(item);
|
||||||
return acc;
|
return acc;
|
||||||
@ -58,34 +32,23 @@ const AlleNeuigkeitenPage = () => {
|
|||||||
<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>
|
||||||
|
|
||||||
{/* Team-Filter */}
|
|
||||||
<div className="flex justify-center mb-8">
|
|
||||||
<select
|
|
||||||
value={selectedTeam}
|
|
||||||
onChange={(e) => setSelectedTeam(e.target.value)}
|
|
||||||
className="border border-gray-300 rounded-md p-2"
|
|
||||||
>
|
|
||||||
{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]) => (
|
||||||
<div key={year} className="mb-12">
|
<div key={year} className="mb-12">
|
||||||
<h2 className="text-2xl font-bold text-frog-500 mb-4">{year}</h2>
|
<h2 className="text-2xl font-bold text-frog-500 mb-4">{year}</h2>
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
{(items as typeof news).map((item) => (
|
{items.map((item) => (
|
||||||
<Card key={item.id} className="overflow-hidden">
|
<Card key={item.id} className="overflow-hidden">
|
||||||
<div className="h-48 overflow-hidden">
|
<div className="h-48 overflow-hidden">
|
||||||
<img src={item.image} alt={item.title} className="w-full h-full object-cover" />
|
<img
|
||||||
|
src={item.image_url || "https://via.placeholder.com/400x300?text=Kein+Bild"}
|
||||||
|
alt={item.title}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{item.title}</CardTitle>
|
<CardTitle>{item.title}</CardTitle>
|
||||||
<CardDescription>{new Date(item.date).toLocaleDateString("de-DE")}</CardDescription>
|
<CardDescription>{new Date(item.created_at).toLocaleDateString("de-DE")}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<p>{item.description}</p>
|
<p>{item.description}</p>
|
||||||
@ -95,7 +58,6 @@ const AlleNeuigkeitenPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,8 +1,40 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
|
||||||
const LoginPage = () => {
|
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 (
|
return (
|
||||||
<div className="flex justify-center items-center min-h-screen bg-gray-50">
|
<div className="flex justify-center items-center min-h-screen bg-gray-50">
|
||||||
<Card className="w-full max-w-md">
|
<Card className="w-full max-w-md">
|
||||||
@ -10,9 +42,22 @@ const LoginPage = () => {
|
|||||||
<CardTitle className="text-center text-frog-600">Login</CardTitle>
|
<CardTitle className="text-center text-frog-600">Login</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form className="space-y-4">
|
<form className="space-y-4" onSubmit={handleLogin}>
|
||||||
<Input placeholder="E-Mail-Adresse" type="email" required />
|
<Input
|
||||||
<Input placeholder="Passwort" type="password" required />
|
placeholder="Benutzername"
|
||||||
|
type="text"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Passwort"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{error && <p className="text-red-500 text-sm">{error}</p>}
|
||||||
<Button type="submit" className="w-full bg-frog-500 hover:bg-frog-600 text-white">
|
<Button type="submit" className="w-full bg-frog-500 hover:bg-frog-600 text-white">
|
||||||
Einloggen
|
Einloggen
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user