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",
|
||||
"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",
|
||||
|
||||
@ -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",
|
||||
|
||||
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 { 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 = () => {
|
||||
<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>
|
||||
|
||||
{/* Gemeinsames Container-Div für Hover */}
|
||||
<div
|
||||
className="relative"
|
||||
onMouseEnter={() => setIsTeamsOpen(true)}
|
||||
@ -40,9 +42,7 @@ const Navbar = () => {
|
||||
|
||||
{/* Dropdown */}
|
||||
{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"
|
||||
>
|
||||
<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>
|
||||
@ -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="#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>
|
||||
|
||||
<Link to="/mitglied-werden" className="w-full">
|
||||
<Button className="w-full bg-frog-500 hover:bg-frog-600 text-white">
|
||||
Mitglied werden
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/login" className="ml-4 text-frog-600 hover:text-frog-800">
|
||||
<Users className="h-6 w-6" />
|
||||
</Link>
|
||||
|
||||
{/* USER ICON + DROPDOWN */}
|
||||
<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>
|
||||
|
||||
{/* 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);
|
||||
65
src/main.tsx
65
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(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<Layout>
|
||||
<Index />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/mitglied-werden"
|
||||
element={
|
||||
<Layout>
|
||||
<MitgliedWerdenPage />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/alle-neuigkeiten"
|
||||
element={
|
||||
<Layout>
|
||||
<AlleNeuigkeitenPage />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route path="/teams/:id" element={<Layout><TeamDetailPage /></Layout>} />
|
||||
<Route path="/login" element={<Layout><LoginPage /></Layout>}/>
|
||||
</Routes>
|
||||
<AuthProvider>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<Layout>
|
||||
<Index />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/mitglied-werden"
|
||||
element={
|
||||
<Layout>
|
||||
<MitgliedWerdenPage />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/alle-neuigkeiten"
|
||||
element={
|
||||
<Layout>
|
||||
<AlleNeuigkeitenPage />
|
||||
</Layout>
|
||||
}
|
||||
/>
|
||||
<Route path="/teams/:id" element={<Layout><TeamDetailPage /></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>
|
||||
</AuthProvider>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
);
|
||||
@ -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<NewsItem[]>([]);
|
||||
|
||||
// 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<string, NewsItem[]>, 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 = () => {
|
||||
<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>
|
||||
|
||||
{/* 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 */}
|
||||
{Object.entries(groupedNews).map(([year, items]) => (
|
||||
<div key={year} className="mb-12">
|
||||
<h2 className="text-2xl font-bold text-frog-500 mb-4">{year}</h2>
|
||||
<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">
|
||||
<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>
|
||||
<CardHeader>
|
||||
<CardTitle>{item.title}</CardTitle>
|
||||
<CardDescription>{new Date(item.date).toLocaleDateString("de-DE")}</CardDescription>
|
||||
<CardDescription>{new Date(item.created_at).toLocaleDateString("de-DE")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>{item.description}</p>
|
||||
@ -95,7 +58,6 @@ const AlleNeuigkeitenPage = () => {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 (
|
||||
<div className="flex justify-center items-center min-h-screen bg-gray-50">
|
||||
<Card className="w-full max-w-md">
|
||||
@ -10,9 +42,22 @@ const LoginPage = () => {
|
||||
<CardTitle className="text-center text-frog-600">Login</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form className="space-y-4">
|
||||
<Input placeholder="E-Mail-Adresse" type="email" required />
|
||||
<Input placeholder="Passwort" type="password" required />
|
||||
<form className="space-y-4" onSubmit={handleLogin}>
|
||||
<Input
|
||||
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">
|
||||
Einloggen
|
||||
</Button>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user