diff --git a/package-lock.json b/package-lock.json index 642f0a5c8..6cd1129bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,9 @@ "@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.4", "@react-pdf/renderer": "^4.3.0", + "@tailwindcss/aspect-ratio": "^0.4.2", "@tanstack/react-query": "^5.56.2", + "aos": "^2.3.4", "axios": "^1.9.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -3152,6 +3154,15 @@ "@swc/counter": "^0.1.3" } }, + "node_modules/@tailwindcss/aspect-ratio": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/aspect-ratio/-/aspect-ratio-0.4.2.tgz", + "integrity": "sha512-8QPrypskfBa7QIMuKHg2TA7BqES6vhBrDLOv8Unb6FcFyd3TjKbc6lcmb9UPQHxfl24sXoJ41ux/H7qQQvfaSQ==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1" + } + }, "node_modules/@tailwindcss/line-clamp": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.4.tgz", @@ -3733,6 +3744,17 @@ "node": ">= 8" } }, + "node_modules/aos": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/aos/-/aos-2.3.4.tgz", + "integrity": "sha512-zh/ahtR2yME4I51z8IttIt4lC1Nw0ktsFtmeDzID1m9naJnWXhCoARaCgNOGXb5CLy3zm+wqmRAEgMYB5E2HUw==", + "license": "MIT", + "dependencies": { + "classlist-polyfill": "^1.0.3", + "lodash.debounce": "^4.0.6", + "lodash.throttle": "^4.0.1" + } + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -4204,6 +4226,12 @@ "url": "https://polar.sh/cva" } }, + "node_modules/classlist-polyfill": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/classlist-polyfill/-/classlist-polyfill-1.2.0.tgz", + "integrity": "sha512-GzIjNdcEtH4ieA2S8NmrSxv7DfEV5fmixQeyTmqmRmRJPGpRBaSnA2a0VrCjyT8iW8JjEdMbKzDotAJf+ajgaQ==", + "license": "Unlicense" + }, "node_modules/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", @@ -6470,6 +6498,12 @@ "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", "dev": true }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", @@ -6483,6 +6517,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "license": "MIT" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", diff --git a/package.json b/package.json index ac2caf7b2..fbb28a022 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,9 @@ "@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.4", "@react-pdf/renderer": "^4.3.0", + "@tailwindcss/aspect-ratio": "^0.4.2", "@tanstack/react-query": "^5.56.2", + "aos": "^2.3.4", "axios": "^1.9.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/public/images/players/herren1/carousel/carousel1.jpg b/public/images/players/herren1/carousel/carousel1.jpg deleted file mode 100644 index 6f6c4832e..000000000 Binary files a/public/images/players/herren1/carousel/carousel1.jpg and /dev/null differ diff --git a/public/images/players/herren1/carousel/carousel2.jpg b/public/images/players/herren1/carousel/carousel2.jpg deleted file mode 100644 index 17084c5c0..000000000 Binary files a/public/images/players/herren1/carousel/carousel2.jpg and /dev/null differ diff --git a/public/images/players/herren1/carousel/carousel3.jpg b/public/images/players/herren1/carousel/carousel3.jpg deleted file mode 100644 index d234715e0..000000000 Binary files a/public/images/players/herren1/carousel/carousel3.jpg and /dev/null differ diff --git a/public/images/players/herren1/carousel/carousel4.jpg b/public/images/players/herren1/carousel/carousel4.jpg deleted file mode 100644 index 0ab70eb34..000000000 Binary files a/public/images/players/herren1/carousel/carousel4.jpg and /dev/null differ diff --git a/public/images/players/herren1/carousel/carousel5.jpg b/public/images/players/herren1/carousel/carousel5.jpg deleted file mode 100644 index 44bed3b51..000000000 Binary files a/public/images/players/herren1/carousel/carousel5.jpg and /dev/null differ diff --git a/public/images/players/herren1/david.jpg b/public/images/players/herren1/david.jpg deleted file mode 100644 index 2a7e63760..000000000 Binary files a/public/images/players/herren1/david.jpg and /dev/null differ diff --git a/public/images/players/herren1/erik.jpg b/public/images/players/herren1/erik.jpg deleted file mode 100644 index 2de081b87..000000000 Binary files a/public/images/players/herren1/erik.jpg and /dev/null differ diff --git a/public/images/players/herren1/lasse.jpg b/public/images/players/herren1/lasse.jpg deleted file mode 100644 index 0edbc0adb..000000000 Binary files a/public/images/players/herren1/lasse.jpg and /dev/null differ diff --git a/public/images/players/herren1/marc.jpg b/public/images/players/herren1/marc.jpg deleted file mode 100644 index d1e8abccd..000000000 Binary files a/public/images/players/herren1/marc.jpg and /dev/null differ diff --git a/public/images/players/herren1/peter.jpg b/public/images/players/herren1/peter.jpg deleted file mode 100644 index 86b4c5047..000000000 Binary files a/public/images/players/herren1/peter.jpg and /dev/null differ diff --git a/public/images/players/herren1/phillip.jpg b/public/images/players/herren1/phillip.jpg deleted file mode 100644 index ea33f7247..000000000 Binary files a/public/images/players/herren1/phillip.jpg and /dev/null differ diff --git a/public/images/players/herren1/samuel.jpg b/public/images/players/herren1/samuel.jpg deleted file mode 100644 index eabd00ccf..000000000 Binary files a/public/images/players/herren1/samuel.jpg and /dev/null differ diff --git a/public/images/players/herren1/sten.jpg b/public/images/players/herren1/sten.jpg deleted file mode 100644 index d05727fe3..000000000 Binary files a/public/images/players/herren1/sten.jpg and /dev/null differ diff --git a/public/images/players/herren1/tony.jpg b/public/images/players/herren1/tony.jpg deleted file mode 100644 index e8273799f..000000000 Binary files a/public/images/players/herren1/tony.jpg and /dev/null differ diff --git a/public/images/tgl-ball.png b/public/images/tgl-ball.png new file mode 100644 index 000000000..2df913b3f Binary files /dev/null and b/public/images/tgl-ball.png differ diff --git a/src/App.tsx b/src/App.tsx index 3a8e4f4dc..7b9e53323 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,8 @@ import { BrowserRouter, Routes, Route } from "react-router-dom"; import Index from "./pages/Index"; import NotFound from "./pages/NotFound"; import ScrollToTop from "./components/ScrollTop"; +import AOS from "aos"; +import "aos/dist/aos.css"; const queryClient = new QueryClient(); diff --git a/src/admin/EventsAdmin.tsx b/src/admin/EventsAdmin.tsx index f3e692ed8..38b57bfcd 100644 --- a/src/admin/EventsAdmin.tsx +++ b/src/admin/EventsAdmin.tsx @@ -3,7 +3,7 @@ import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; -import axios from "axios"; +import api from "@/lib/axios"; type Event = { id?: number; @@ -31,38 +31,40 @@ const EventsAdmin = () => { const fetchEvents = async () => { try { - const res = await axios.get("/api/events?showPrivate=true"); - if (Array.isArray(res.data)) { - setEvents(res.data); - } else { - console.warn("⚠️ Events-API hat kein Array geliefert:", res.data); - setEvents([]); - } + const res = await api.get("/api/events?showPrivate=true"); + setEvents(Array.isArray(res.data) ? res.data : []); } catch (error) { console.error("❌ Fehler beim Laden der Events:", error); setEvents([]); } }; - useEffect(() => { fetchEvents(); }, []); const handleSubmit = async () => { - if (isEditing && form.id) { - await axios.put(`/api/events/${form.id}`, form); - } else { - await axios.post("/api/events", form); + try { + if (isEditing && form.id) { + await api.put(`/api/events/${form.id}`, form); + } else { + await api.post("/api/events", form); + } + setForm(defaultEvent); + setIsEditing(false); + fetchEvents(); + } catch (error) { + console.error("❌ Fehler beim Speichern:", error); } - setForm(defaultEvent); - setIsEditing(false); - fetchEvents(); }; const handleDelete = async (id: number) => { - await axios.delete(`/api/events/${id}`); - fetchEvents(); + try { + await api.delete(`/api/events/${id}`); + fetchEvents(); + } catch (error) { + console.error("❌ Fehler beim Löschen:", error); + } }; const handleEdit = (event: Event) => { @@ -89,14 +91,18 @@ const EventsAdmin = () => { type="number" placeholder="Max. Teilnehmer" value={form.max_participants ?? ""} - onChange={(e) => setForm({ ...form, max_participants: parseInt(e.target.value) || undefined })} + onChange={(e) => + setForm({ ...form, max_participants: parseInt(e.target.value) || undefined }) + } /> setForm({ ...form, fee: parseFloat(e.target.value) || undefined })} + onChange={(e) => + setForm({ ...form, fee: parseFloat(e.target.value) || undefined }) + } /> { {isEditing ? "Speichern" : "Erstellen"} {isEditing && ( - )} @@ -126,25 +138,36 @@ const EventsAdmin = () => {

Bestehende Events

- {Array.isArray(events) && events.length > 0 ? ( + {events.length > 0 ? ( - ) : ( + ) : (

Noch keine Events vorhanden.

- )} - + )} ); }; diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx index f4ed2eeae..4df5e0a38 100644 --- a/src/components/Hero.tsx +++ b/src/components/Hero.tsx @@ -7,13 +7,13 @@ const Hero = () => { className="relative min-h-[600px] md:min-h-[700px] lg:min-h-[800px] bg-cover bg-center bg-no-repeat pt-28 pb-16" style={{ backgroundImage: "url('/images/abteilung-bg.jpg')" }} > -
+
-

- Willkommen bei der TG Laudenbach - Abteilung Volleyball +

+ Willkommen bei der TG Laudenbach - Abteilung Volleyball

Volleyball mit Leidenschaft und Teamgeist – werde Teil unserer Gemeinschaft! diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 888cb0f38..4cf0a88a4 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -3,7 +3,7 @@ import { Button } from "@/components/ui/button"; import { Menu, X, Volleyball, ChevronDown, Users } from "lucide-react"; import { Link, useNavigate } from "react-router-dom"; import { useAuth } from '@/context/AuthContext'; -import axios from 'axios'; +import ThemeToggle from './ThemeToggle'; type Team = { @@ -94,6 +94,10 @@ const Navbar = () => { + + + {/* Account Menu */} +

{ diff --git a/src/components/NewsSection.tsx b/src/components/NewsSection.tsx index a4485b8f3..39952c1c9 100644 --- a/src/components/NewsSection.tsx +++ b/src/components/NewsSection.tsx @@ -18,6 +18,7 @@ type NewsItem = { const NewsSection = () => { const [news, setNews] = useState([]); + const defaultImage = "/images/tgl-ball.png"; const fetchNews = async () => { try{ @@ -49,7 +50,7 @@ const NewsSection = () => { src={ item.image_url ? `${import.meta.env.VITE_API_URL}${item.image_url}` - : "/images/default-news.jpg" + : defaultImage } alt={item.title} className="w-full h-full object-cover" diff --git a/src/components/TeamSection.tsx b/src/components/TeamSection.tsx index 8a5c2f155..7454d39a2 100644 --- a/src/components/TeamSection.tsx +++ b/src/components/TeamSection.tsx @@ -27,6 +27,7 @@ const TeamSection = () => { const [loading, setLoading] = useState(true); const [sliderRef, slider] = useKeenSlider({ + loop: true, slides: { perView: 1, spacing: 16, @@ -60,6 +61,14 @@ const TeamSection = () => { fetchTeams(); }, []); + useEffect(() => { + const interval = setInterval(() => { + slider.current?.next(); + }, 3000); + + return () => clearInterval(interval); + }, [slider]); + return (
diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx new file mode 100644 index 000000000..14d694c6e --- /dev/null +++ b/src/components/ThemeToggle.tsx @@ -0,0 +1,22 @@ +import { useTheme } from "@/context/ThemeContext"; +import { Sun, Moon } from "lucide-react"; + +const ThemeToggle = () => { + const { theme, toggleTheme } = useTheme(); + + return ( + + ); +}; + +export default ThemeToggle; diff --git a/src/context/ThemeContext.tsx b/src/context/ThemeContext.tsx new file mode 100644 index 000000000..680e837ce --- /dev/null +++ b/src/context/ThemeContext.tsx @@ -0,0 +1,40 @@ +import { createContext, useContext, useEffect, useState } from "react"; + +type Theme = "light" | "dark"; + +interface ThemeContextType { + theme: Theme; + toggleTheme: () => void; +} + +const ThemeContext = createContext({ + theme: "light", + toggleTheme: () => {}, +}); + +export const ThemeProvider = ({ children }: { children: React.ReactNode }) => { + const [theme, setTheme] = useState("light"); + + useEffect(() => { + const stored = localStorage.getItem("theme") as Theme | null; + const system = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; + const initial = stored || system; + setTheme(initial); + document.documentElement.classList.toggle("dark", initial === "dark"); + }, []); + + const toggleTheme = () => { + const newTheme = theme === "dark" ? "light" : "dark"; + setTheme(newTheme); + localStorage.setItem("theme", newTheme); + document.documentElement.classList.toggle("dark", newTheme === "dark"); + }; + + return ( + + {children} + + ); +}; + +export const useTheme = () => useContext(ThemeContext); diff --git a/src/layout/Layout.tsx b/src/layout/Layout.tsx index 6b008c4ce..31e2a74de 100644 --- a/src/layout/Layout.tsx +++ b/src/layout/Layout.tsx @@ -1,19 +1,30 @@ +import { ReactNode, useEffect } from "react"; import Navbar from "@/components/Navbar"; import Footer from "@/components/Footer"; -import { ReactNode } from "react"; +import AOS from "aos"; +import "aos/dist/aos.css"; type LayoutProps = { - children: ReactNode; - }; - - const Layout = ({ children }: LayoutProps) => { - return ( -
- -
{children}
-
-
- ); - }; - - export default Layout; \ No newline at end of file + children: ReactNode; +}; + +const Layout = ({ children }: LayoutProps) => { + useEffect(() => { + AOS.init({ + duration: 800, // Dauer der Animationen + once: true, // nur einmal pro Element + offset: 100, // wie weit gescrollt werden muss + easing: "ease-out-cubic", + }); + }, []); + + return ( +
+ +
{children}
+
+
+ ); +}; + +export default Layout; diff --git a/src/lib/axios.ts b/src/lib/axios.ts new file mode 100644 index 000000000..ea02df908 --- /dev/null +++ b/src/lib/axios.ts @@ -0,0 +1,7 @@ +import axios from "axios"; + +const api = axios.create({ + baseURL: import.meta.env.VITE_API_URL, +}); + +export default api; \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index c7fe1b841..2edf3db93 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -33,10 +33,14 @@ import Impressum from "./pages/Impressum"; import Satzung from "./pages/Satzung"; import Beitraege from "./pages/Beitraege"; +import { ThemeProvider } from "@/context/ThemeContext"; +import VideosPage from "./pages/VideosPage"; + ReactDOM.createRoot(document.getElementById("root")!).render( + @@ -72,6 +76,9 @@ ReactDOM.createRoot(document.getElementById("root")!).render( } /> } /> } /> + } /> + + } /> } /> } /> @@ -86,8 +93,10 @@ 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 a64ee3ace..f8f01e0a1 100644 --- a/src/pages/AlleNeuigkeiten.tsx +++ b/src/pages/AlleNeuigkeiten.tsx @@ -17,6 +17,25 @@ const AlleNeuigkeitenPage = () => { const [news, setNews] = useState([]); const [expandedIds, setExpandedIds] = useState([]); const [activeCardId, setActiveCardId] = useState(null); + const [selectedTeam, setSelectedTeam] = useState("Alle Teams"); + + const defaultImage = "/images/tgl-ball.png"; + + //Filtern nach Teams + const teams = Array.from(new Set(news.map((n) => n.team).filter(Boolean))); + + const filteredNews = selectedTeam === "Alle Teams" ? news : news.filter((n) => n.team === selectedTeam); + + const groupedNews = filteredNews.reduce((acc: Record, item) => { + const year = new Date(item.created_at).getFullYear().toString(); + if (!acc[year]) acc[year] = []; + acc[year].push(item); + return acc; + }, {}); + + + + useEffect(() => { fetch(`${apiBase}/api/news`) @@ -31,13 +50,6 @@ const AlleNeuigkeitenPage = () => { }; - // Gruppieren nach Jahren - 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; - }, {}); //Toggle der Cards const toggleExpand = (id: number) => { @@ -47,8 +59,24 @@ const AlleNeuigkeitenPage = () => { } return ( -
-

Alle Neuigkeiten

+
+

Alle Neuigkeiten

+ +
+ +
+ {/* News nach Jahren gruppiert */} {Object.entries(groupedNews).map(([year, items]) => ( @@ -69,7 +97,7 @@ const AlleNeuigkeitenPage = () => { src={ item.image_url ? `${apiBase}${item.image_url}` - : "https://via.placeholder.com/400x300?text=Kein+Bild" + : defaultImage } alt={item.title} className="w-full h-full object-cover" diff --git a/src/pages/VideosPage.tsx b/src/pages/VideosPage.tsx new file mode 100644 index 000000000..e2d2857f1 --- /dev/null +++ b/src/pages/VideosPage.tsx @@ -0,0 +1,73 @@ +import { useRef } from "react"; +import { Rewind, FastForward, Pause, Play } from "lucide-react"; + +const VideosPage = () => { + const videoRef = useRef(null); + + const skip = (seconds: number) => { + if (videoRef.current) { + videoRef.current.currentTime += seconds; + } + }; + + const togglePlayPause = () => { + const video = videoRef.current; + if (!video) return; + if (video.paused) { + video.play(); + } else { + video.pause(); + } + }; + + return ( +
+

+ Trainingsvideo: Satz 1 +

+ +
+ + + {/* Custom Steuerung */} +
+ + + +
+
+
+ ); +}; + +export default VideosPage; diff --git a/tailwind.config.ts b/tailwind.config.ts index dc69d7da5..a6568cdac 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -114,5 +114,6 @@ export default { plugins: [ require("tailwindcss-animate"), require("@tailwindcss/typography"), + require('@tailwindcss/aspect-ratio') ], } satisfies Config;