diff --git a/README.md b/README.md index c1afcb4..066cfe3 100644 --- a/README.md +++ b/README.md @@ -45,9 +45,9 @@ SQLite (better-sqlite3, FTS5), Docker Multi-Stage — deploybar als Portainer-St | 👋 **Willkommens-Embed** | Begrüßung neuer Member im Brand-Look (braucht Server-Members-Intent) | | 📋 **Mod-Log** | Gelöschte/bearbeitete Nachrichten in einen privaten Log-Kanal | | 🎮 **Server-Monitor** | DiscordGSM-Stil: eigener Setup-Tab, pro Server ein Live-Embed (🟢/🔴, Spieler-Balken mit %, Map, Spieler-Liste, klickbarer Connect-Link, Ping); **300+ Spiele via gamedig** (Minecraft, Rust, CS2, Valheim, ARK …) + FiveM + HTTP-Check; Down/Up-Alerts (🚨 nach 2 Fehlversuchen, ✅ mit Downtime bei Recovery) in eigenen Alert-Kanal; Spielerzahl in der Presence | -| 👥 **Team-Rechte** | Team-Mitglieder bekommen gezielten Web-Zugriff (Composer, Bewerbungen, Rollen, Server, …) — Brand, System, API-Keys bleiben Owner-only | +| 👥 **Team-Rechte** | Team-Mitglieder bekommen gezielten Web-Zugriff (Composer, Bewerbungen, Rollen, Server, …) — Brand, System, API-Keys bleiben Owner-only; **Audit-Log** im Team-Tab zeigt dem Owner die letzten Aktionen | | 💡 **Feature-Voting** | `/wunsch` → Voting-Post mit 👍; Top-Wünsche öffentlich auf der Roadmap-Seite | -| 🎉 **Giveaways** | `/giveaway` (Admin): Teilnahme-Button, automatische Ziehung nach Ablauf | +| 🎉 **Giveaways** | `/giveaway` (Admin): Teilnahme-Button, automatische Ziehung nach Ablauf; am Gewinner-Post: 🔁 Neu auslosen (Admin) + 👥 Teilnehmerliste | | 📈 **Contribution-Heatmap** | GitHub-Style-Jahreskalender aus dem Commit-Archiv auf der Roadmap-Seite | | 👤 **Member-Bereich** | Discord-Login für alle Member: `/profil` (Rang + XP-Balken, Rollen, Playtester-Badge, eigener Alpha-Key), Rollen-Selfservice im Browser (nutzt die Rollen-Menüs), Wünsche einreichen + upvoten auf der Roadmap; **Member-Gate**: Login nur für Server-Mitglieder (abweisbare bekommen den Invite-Link) | | 🏓 `/ping`, 🗄 `/devlog-backfill` | Lebenszeichen · Kanal-Historie nacharchivieren (Admin) | @@ -63,6 +63,7 @@ SQLite (better-sqlite3, FTS5), Docker Multi-Stage — deploybar als Portainer-St | `/changelog` | öffentlich | Alle Releases mit Notes, Tag- und Pre-Release-Chips | | `/feed.xml` | öffentlich | RSS-Feed der Devlogs | | `/profil` | Member | Eigenes Profil: Rang, XP, Rollen (togglebar), Alpha-Key | +| `/server` | öffentlich | Live-Status aller Game-Server + 24h-Spielerzahl-Verlauf | | `/commits` | nur Admin | Archivierte Commits aller Repos (SHA → Gitea-Link) | | `/settings` | nur Admin | **Setup-Seite** — siehe unten | diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 54bb1b8..7cf76fd 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,5 +1,5 @@ -import { useEffect, useMemo, useState } from 'react'; -import { Routes, Route, NavLink, Navigate } from 'react-router-dom'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import { Routes, Route, NavLink, Navigate, useLocation } from 'react-router-dom'; import { apiGet } from './api.js'; import Devlogs from './pages/Devlogs.jsx'; import Changelog from './pages/Changelog.jsx'; @@ -10,6 +10,33 @@ import Events from './pages/Events.jsx'; import Commits from './pages/Commits.jsx'; import Settings from './pages/Settings.jsx'; import Profil from './pages/Profil.jsx'; +import Server from './pages/Server.jsx'; + +const COMMUNITY_LINKS = [ + ['/galerie', 'Galerie'], + ['/level', 'Level'], + ['/events', 'Events'], + ['/changelog', 'Changelog'], +]; + +/** Dropdown in der Navbar (schließt bei Klick daneben und bei Navigation) */ +function NavDropdown({ label, active, children }) { + const [open, setOpen] = useState(false); + const ref = useRef(null); + useEffect(() => { + const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; + document.addEventListener('mousedown', onDoc); + return () => document.removeEventListener('mousedown', onDoc); + }, []); + return ( +
+ + {open &&
setOpen(false)}>{children}
} +
+ ); +} function DiscordMark() { return ( @@ -51,6 +78,12 @@ export default function App() { // Login-Status einmal beim Laden holen const [me, setMe] = useState({ user: null, admin: false, scopes: [], loading: true }); const [scrolled, setScrolled] = useState(false); + const [drawer, setDrawer] = useState(false); + const location = useLocation(); + const communityActive = COMMUNITY_LINKS.some(([to]) => location.pathname.startsWith(to)); + + // Drawer bei Seitenwechsel schließen + useEffect(() => { setDrawer(false); }, [location.pathname]); // Member-Gate: OAuth war okay, aber User ist nicht auf dem Discord (?gate=1) const [gated, setGated] = useState(() => new URLSearchParams(window.location.search).has('gate')); const [invite, setInvite] = useState(null); @@ -83,30 +116,58 @@ export default function App() {
{me.loading ? null : me.user ? ( -
- {me.user.avatar && } - {me.user.username} - -
+ + {me.user.avatar && } + {me.user.username} + + } + > + Profil + {me.admin && Commits} + Logout + ) : ( Login )}
+ + + {drawer && ( + + )} +
{gated && (
@@ -126,6 +187,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/Lightbox.jsx b/frontend/src/components/Lightbox.jsx new file mode 100644 index 0000000..27210e7 --- /dev/null +++ b/frontend/src/components/Lightbox.jsx @@ -0,0 +1,61 @@ +// Bild-Overlay mit Blättern (Pfeiltasten, Buttons, Wischen) statt neuem Tab. +import { useCallback, useEffect, useState } from 'react'; + +export default function Lightbox({ images, start = 0, onClose }) { + const [index, setIndex] = useState(start); + const [touchX, setTouchX] = useState(null); + + const prev = useCallback(() => setIndex((i) => (i - 1 + images.length) % images.length), [images.length]); + const next = useCallback(() => setIndex((i) => (i + 1) % images.length), [images.length]); + + useEffect(() => { + function onKey(e) { + if (e.key === 'Escape') onClose(); + else if (e.key === 'ArrowLeft') prev(); + else if (e.key === 'ArrowRight') next(); + } + document.addEventListener('keydown', onKey); + document.body.style.overflow = 'hidden'; + return () => { + document.removeEventListener('keydown', onKey); + document.body.style.overflow = ''; + }; + }, [onClose, prev, next]); + + const img = images[index]; + if (!img) return null; + + return ( +
setTouchX(e.touches[0].clientX)} + onTouchEnd={(e) => { + if (touchX === null) return; + const dx = e.changedTouches[0].clientX - touchX; + if (dx > 50) prev(); + else if (dx < -50) next(); + setTouchX(null); + }} + role="dialog" aria-modal="true" + > + + {images.length > 1 && ( + + )} + {img.alt e.stopPropagation()} + /> + {images.length > 1 && ( + + )} +
e.stopPropagation()}> + {img.caption && {img.caption}} + {images.length > 1 && {index + 1} / {images.length}} +
+
+ ); +} diff --git a/frontend/src/components/Skeleton.jsx b/frontend/src/components/Skeleton.jsx new file mode 100644 index 0000000..0b842b1 --- /dev/null +++ b/frontend/src/components/Skeleton.jsx @@ -0,0 +1,38 @@ +// Schimmernde Lade-Platzhalter statt „Lade …"-Text + +/** n Karten-Platzhalter (Devlogs, Roadmap, Changelog, Server …) */ +export function SkeletonCards({ n = 3 }) { + return ( +