QoL Updates
This commit is contained in:
parent
d085a26416
commit
8f9cd73e50
@ -5,6 +5,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import Index from "./pages/Index";
|
||||
import NotFound from "./pages/NotFound";
|
||||
import ScrollToTop from "./components/ScrollTop";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
@ -14,6 +15,7 @@ const App = () => (
|
||||
<Toaster />
|
||||
<Sonner />
|
||||
<BrowserRouter>
|
||||
<ScrollToTop />
|
||||
<Routes>
|
||||
<Route path="/" element={<Index />} />
|
||||
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
||||
|
||||
51
src/components/AdresseInput.tsx
Normal file
51
src/components/AdresseInput.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { useState } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
const AdresseInput = () => {
|
||||
const [query, setQuery] = useState("");
|
||||
const [suggestions, setSuggestions] = useState<any[]>([]);
|
||||
|
||||
const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setQuery(value);
|
||||
|
||||
if (value.length >= 3) {
|
||||
const response = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(value)}&addressdetails=1&limit=5`);
|
||||
const data = await response.json();
|
||||
setSuggestions(data);
|
||||
} else {
|
||||
setSuggestions([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelect = (address: string) => {
|
||||
setQuery(address);
|
||||
setSuggestions([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Input
|
||||
placeholder="Straße und Hausnummer"
|
||||
value={query}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
{suggestions.length > 0 && (
|
||||
<div className="absolute z-10 bg-white shadow-md mt-1 rounded-md max-h-60 overflow-y-auto w-full">
|
||||
{suggestions.map((suggestion, index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => handleSelect(suggestion.display_name)}
|
||||
className="px-4 py-2 hover:bg-frog-50 cursor-pointer text-sm"
|
||||
>
|
||||
{suggestion.display_name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdresseInput;
|
||||
59
src/components/IbanInput.tsx
Normal file
59
src/components/IbanInput.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { useState } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
const IbanInput = ({ onValidIban }: { onValidIban?: (iban: string) => void }) => {
|
||||
const [iban, setIban] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const formatIban = (value: string) => {
|
||||
const alphanumeric = value.replace(/[^a-zA-Z0-9]/g, "").toUpperCase();
|
||||
const formatted = alphanumeric.replace(/(.{4})/g, "$1 ").trim();
|
||||
return formatted;
|
||||
};
|
||||
|
||||
const validateIban = (rawIban: string) => {
|
||||
// IBAN Validierung nach Mod-97-10 Regel
|
||||
const rearranged = rawIban.slice(4) + rawIban.slice(0, 4);
|
||||
const converted = rearranged.replace(/[A-Z]/g, (char) => (char.charCodeAt(0) - 55).toString());
|
||||
const mod97 = BigInt(converted) % 97n;
|
||||
return mod97 === 1n;
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const input = e.target.value;
|
||||
if (input.length <= 27) {
|
||||
setIban(formatIban(input));
|
||||
setError(""); // Fehler zurücksetzen während der Eingabe
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
const rawIban = iban.replace(/\s/g, ""); // Leerzeichen entfernen
|
||||
if (rawIban.length >= 15) { // Minimum Länge einer IBAN
|
||||
if (!validateIban(rawIban)) {
|
||||
setError("Ungültige IBAN");
|
||||
} else {
|
||||
setError("");
|
||||
if (onValidIban) {
|
||||
onValidIban(rawIban);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-1">
|
||||
<Input
|
||||
placeholder="IBAN"
|
||||
value={iban}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
maxLength={27}
|
||||
required
|
||||
/>
|
||||
{error && <span className="text-sm text-red-500">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IbanInput;
|
||||
@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Menu, X, Volleyball, ChevronDown } from "lucide-react";
|
||||
import { Menu, X, Volleyball, ChevronDown, Users } from "lucide-react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const Navbar = () => {
|
||||
@ -15,10 +15,10 @@ const Navbar = () => {
|
||||
<nav className="fixed top-0 left-0 w-full z-50 bg-white/30 backdrop-blur-md border-b border-white/20">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between h-16 items-center">
|
||||
<div className="flex items-center">
|
||||
<Link to="/" className="flex items-center">
|
||||
<Volleyball className="h-8 w-8 text-frog-500 mr-2" />
|
||||
<span className="font-bold text-xl text-frog-800">TG Laudenbach</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden md:flex items-center space-x-4">
|
||||
@ -59,6 +59,9 @@ const Navbar = () => {
|
||||
Mitglied werden
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/login" className="ml-4 text-frog-600 hover:text-frog-800">
|
||||
<Users className="h-6 w-6" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu button */}
|
||||
|
||||
14
src/components/ScrollTop.tsx
Normal file
14
src/components/ScrollTop.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import {useEffect} from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
const ScrollToTop = () =>{
|
||||
const {pathname} = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(0,0);
|
||||
}, [pathname]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default ScrollToTop;
|
||||
@ -11,6 +11,7 @@ import TeamDetailPage from "./pages/TeamDetailPage";
|
||||
|
||||
|
||||
import "./index.css"
|
||||
import LoginPage from "./pages/LoginPage";
|
||||
|
||||
|
||||
|
||||
@ -43,6 +44,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
}
|
||||
/>
|
||||
<Route path="/teams/:id" element={<Layout><TeamDetailPage /></Layout>} />
|
||||
<Route path="/login" element={<Layout><LoginPage /></Layout>}/>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
|
||||
@ -1,43 +1,91 @@
|
||||
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: "15. September 2023",
|
||||
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"
|
||||
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: "3. Juli 2023",
|
||||
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"
|
||||
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: "21. Mai 2023",
|
||||
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"
|
||||
image: "https://images.unsplash.com/photo-1547347298-4074fc3086f0?ixlib=rb-4.0.3&auto=format&fit=crop&w=500&q=80",
|
||||
team: "Jugend U16"
|
||||
},
|
||||
// 🔜 später kannst du hier easy mehr News reinladen oder aus ner API holen
|
||||
];
|
||||
|
||||
|
||||
|
||||
const AlleNeuigkeitenPage = () => {
|
||||
const [selectedTeam, setSelectedTeam] = useState("Alle");
|
||||
|
||||
// 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
|
||||
);
|
||||
|
||||
// Gruppieren nach Jahren
|
||||
const groupedNews = filteredNews.reduce((acc: any, item) => {
|
||||
const year = new Date(item.date).getFullYear();
|
||||
if (!acc[year]) acc[year] = [];
|
||||
acc[year].push(item);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<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">
|
||||
{news.map((item) => (
|
||||
{(items as typeof news).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" />
|
||||
</div>
|
||||
<CardHeader>
|
||||
<CardTitle>{item.title}</CardTitle>
|
||||
<CardDescription>{item.date}</CardDescription>
|
||||
<CardDescription>{new Date(item.date).toLocaleDateString("de-DE")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>{item.description}</p>
|
||||
@ -46,6 +94,9 @@ const AlleNeuigkeitenPage = () => {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
26
src/pages/LoginPage.tsx
Normal file
26
src/pages/LoginPage.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
const LoginPage = () => {
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-screen bg-gray-50">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader>
|
||||
<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 />
|
||||
<Button type="submit" className="w-full bg-frog-500 hover:bg-frog-600 text-white">
|
||||
Einloggen
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
@ -1,19 +1,51 @@
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import IbanInput from "@/components/IbanInput";
|
||||
import AdresseInput from "@/components/AdresseInput";
|
||||
|
||||
|
||||
|
||||
const MitgliedWerdenPage = () => {
|
||||
return (
|
||||
<div className="flex justify-center mt-10">
|
||||
<Card className="w-full max-w-md">
|
||||
<Card className="w-full max-w-2xl">
|
||||
<CardContent className="p-6">
|
||||
<h1 className="text-2xl font-bold mb-4 text-center">Mitglied werden</h1>
|
||||
<form className="space-y-4">
|
||||
<Input placeholder="Vorname" />
|
||||
<Input placeholder="Nachname" />
|
||||
<Input placeholder="E-Mail" type="email" />
|
||||
<Input placeholder="Geburtsdatum" type="date" />
|
||||
<Button type="submit" className="w-full bg-frog-500 hover:bg-frog-600 text-white">
|
||||
{/* Mitgliedsdaten */}
|
||||
<h2 className="text-lg font-semibold">Persönliche Daten</h2>
|
||||
<Input placeholder="Vorname" required />
|
||||
<Input placeholder="Nachname" required />
|
||||
{/*<AdresseInput /> -> Für die spätere Verwendung zum AutoComplete*/}
|
||||
<Input placeholder="Straße und Hausnummer" required />
|
||||
<Input placeholder="PLZ und Wohnort" required />
|
||||
<Input placeholder="Telefonnummer" required />
|
||||
<Input placeholder="E-Mail-Adresse" type="email" required />
|
||||
<div className="flex flex-col space-y-1">
|
||||
<label htmlFor="birthdate" className="text-sm font-medium text-gray-700">
|
||||
Geburtsdatum*
|
||||
</label>
|
||||
<Input id="birthdate" type="date" required />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-1">
|
||||
<label htmlFor="entrydate" className="text-sm font-medium text-gray-700">
|
||||
Eintrittsdatum*
|
||||
</label>
|
||||
<Input id="entrydate" type="date" required />
|
||||
</div>
|
||||
|
||||
{/* Bankverbindung */}
|
||||
<h2 className="text-lg font-semibold mt-6">Bankverbindung</h2>
|
||||
<Input placeholder="Kontoinhaber (Name, Vorname)" required />
|
||||
<Input placeholder="Straße und Hausnummer (falls abweichend)" />
|
||||
<Input placeholder="PLZ und Wohnort (falls abweichend)" />
|
||||
<IbanInput />
|
||||
<Input placeholder="BIC" required />
|
||||
|
||||
{/* Submit Button */}
|
||||
<Button type="submit" className="w-full bg-frog-500 hover:bg-frog-600 text-white mt-6">
|
||||
Absenden
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
@ -15,6 +15,7 @@ const teamData = [
|
||||
"/images/damen1-2.jpg",
|
||||
"/images/damen1-3.jpg",
|
||||
],
|
||||
canJoin: false,
|
||||
players: [
|
||||
{ name: "Anna", position: "Außenangriff", nickname: "Speedy", image: "/images/anna.jpg" },
|
||||
{ name: "Lea", position: "Libera", nickname: "Wall-E", image: "/images/lea.jpg" },
|
||||
@ -32,6 +33,7 @@ const teamData = [
|
||||
"/images/players/herren1/carousel/carousel2.jpg",
|
||||
"/images/players/herren1/carousel/carousel2.jpg",
|
||||
],
|
||||
canJoin: true,
|
||||
players: [
|
||||
{ name: "Samuel", position: "Mittelblock", nickname: "BeSamu", image: "/images/players/herren1/samuel.jpg" },
|
||||
{ name: "Sten", position: "Außen/Annahme", nickname: "Stenislav", image: "/images/players/herren1/sten.jpg" },
|
||||
@ -43,6 +45,7 @@ const teamData = [
|
||||
{ name: "Marc", position: "Diagonal", nickname: "Marci", image: "/images/players/herren1/marc.jpg" },
|
||||
{ name: "Phillip", position: "Libero", nickname: "Hifi", image: "/images/players/herren1/phillip.jpg" },
|
||||
{ name: "Kathrin", position: "Trainerin", nickname: "Mutti", image: "/images/players/herren1/phillip.jpg" },
|
||||
{ name: "DU?", position: "Alles", },
|
||||
],
|
||||
},
|
||||
];
|
||||
@ -85,6 +88,14 @@ const TeamDetailPage = () => {
|
||||
<p className="text-gray-600 mt-4">{team.description}</p>
|
||||
<p className="text-sm text-frog-700 font-medium mt-2">Training: {team.trainingTimes}</p>
|
||||
</div>
|
||||
{/*Join Button */}
|
||||
{team.canJoin && (
|
||||
<div className="mt-6 mb-6 flex justify-center">
|
||||
<button className="bg-frog-500 hover:bg-frog-600 text-white font-semibold py-2 px-6 rounded-full shadow-lg transition">
|
||||
Mitmachen
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Spielerübersicht */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user