UX-Paket: Navbar aufgeräumt, /server-Seite, Lightbox, Skeletons, Audit-Log, Reroll
- Navbar entschlackt: nur noch Devlog · Roadmap · Server · Community · Setup; Galerie/Level/Events/Changelog im Community-Dropdown, Profil/Commits/Logout im Avatar-Menü; Mobile-Burger mit Drawer - Öffentliche /server-Seite: Live-Status-Karten mit Spieler-Balken, Map, Connect-Button und 24h-Spielerzahl-Chart (SVG); Monitor sampelt jede Abfrage in player_history (7 Tage Retention), GET /api/servers liefert nur öffentliche Felder (keine Query-URLs/Hosts) - Lightbox: Galerie- und Devlog-Bilder öffnen im Overlay mit Pfeiltasten, Buttons, Wisch-Gesten und Zähler statt neuem Tab - Skeleton-Loader: schimmernde Platzhalter auf Devlogs, Roadmap, Galerie, Level, Changelog und Server statt "Lade …" - Team-Audit-Log: audit_log-Tabelle (max 500 Einträge), Logging bei Settings/Composer/Vorlagen/Rollen-Menüs/Gameservern/Team/API-Keys/ Branding, GET /api/auditlog (Owner) + Liste im Team-Tab - Giveaway: 🔁 Reroll-Button (Admin-only) + 👥 Teilnehmerliste am Gewinner-Post Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 (
|
||||
<div
|
||||
className="lightbox"
|
||||
onClick={onClose}
|
||||
onTouchStart={(e) => 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"
|
||||
>
|
||||
<button className="lb-close" onClick={onClose} title="Schließen (Esc)">✕</button>
|
||||
{images.length > 1 && (
|
||||
<button className="lb-nav lb-prev" onClick={(e) => { e.stopPropagation(); prev(); }} title="Zurück (←)">‹</button>
|
||||
)}
|
||||
<img
|
||||
className="lb-img"
|
||||
src={img.src ?? img}
|
||||
alt={img.alt ?? ''}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
{images.length > 1 && (
|
||||
<button className="lb-nav lb-next" onClick={(e) => { e.stopPropagation(); next(); }} title="Weiter (→)">›</button>
|
||||
)}
|
||||
<div className="lb-meta" onClick={(e) => e.stopPropagation()}>
|
||||
{img.caption && <span>{img.caption}</span>}
|
||||
{images.length > 1 && <span className="lb-count">{index + 1} / {images.length}</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Schimmernde Lade-Platzhalter statt „Lade …"-Text
|
||||
|
||||
/** n Karten-Platzhalter (Devlogs, Roadmap, Changelog, Server …) */
|
||||
export function SkeletonCards({ n = 3 }) {
|
||||
return (
|
||||
<div aria-hidden="true">
|
||||
{Array.from({ length: n }, (_, i) => (
|
||||
<div className="card skel-card" key={i} style={{ animationDelay: `${i * 0.08}s` }}>
|
||||
<div className="skel skel-line" style={{ width: '38%' }} />
|
||||
<div className="skel skel-line" style={{ width: '92%' }} />
|
||||
<div className="skel skel-line" style={{ width: '75%' }} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Bild-Raster-Platzhalter (Galerie) */
|
||||
export function SkeletonGrid({ n = 6 }) {
|
||||
return (
|
||||
<div className="gallery-grid" aria-hidden="true">
|
||||
{Array.from({ length: n }, (_, i) => (
|
||||
<div className="skel skel-shot" key={i} style={{ animationDelay: `${i * 0.06}s` }} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Zeilen-Platzhalter (Level-Liste u. Ä.) */
|
||||
export function SkeletonRows({ n = 8 }) {
|
||||
return (
|
||||
<div aria-hidden="true">
|
||||
{Array.from({ length: n }, (_, i) => (
|
||||
<div className="skel skel-row" key={i} style={{ animationDelay: `${i * 0.05}s` }} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user