Carl-Bot-Paket: Moderation, Auto-/Sticky-Roles, Level-System, Tags, geplante Posts
- Moderation: /warn (DM + Mod-Log), /warns (Historie + löschen), /timeout (max 28d), /purge (bulk, 14-Tage-Limit sauber gemeldet); Mod-Log erweitert um Join/Leave und Nick-/Rollen-Änderungen - Auto-Rolle bei Join + Sticky-Roles (Rollen bei Leave gesichert, bei Rejoin wiederhergestellt; managed/@everyone ausgenommen) — Rollen-Tab - Level-System (opt-in): 15-25 XP/Nachricht mit 60s-Cooldown, MEE6-Formel, Level-Up-Announce, Rollen-Belohnungen (Level=RolleID), /rank mit Fortschritt, öffentliche Bestenliste /level; Community-Tab - Tags: /tag mit Autocomplete, Verwaltung im Composer-Tab (/api/tags) - Geplante Posts: einmalig/täglich/wöchentlich, Minuten-Scheduler, DST-sicher über Neuberechnung aus Zeitfeldern; Composer-Tab (/api/scheduled) - Autocomplete-Dispatch im InteractionCreate; alles smoke-getestet Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { apiGet } from '../api.js';
|
||||
|
||||
/** Muss der Formel in src/bot/levels.js entsprechen */
|
||||
function xpForLevel(level) {
|
||||
let total = 0;
|
||||
for (let n = 0; n < level; n++) total += 5 * n * n + 50 * n + 100;
|
||||
return total;
|
||||
}
|
||||
|
||||
const MEDALS = ['🥇', '🥈', '🥉'];
|
||||
|
||||
export default function Level() {
|
||||
const [rows, setRows] = useState(null);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
apiGet('/api/levels').then((d) => setRows(d.levels)).catch(() => setError(true));
|
||||
window.scrollTo(0, 0);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="page-header">
|
||||
<div className="page-bg-text">LEVEL</div>
|
||||
<div className="page-header-grid" aria-hidden="true" />
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">// Bestenliste</div>
|
||||
<h1 className="page-title">Level</h1>
|
||||
<p className="page-subtitle">
|
||||
XP gibt's fürs Mitreden im Discord — hier stehen die Aktivsten. <code>/rank</code> zeigt deinen Platz.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="content">
|
||||
{error && <p className="notice">Bestenliste konnte nicht geladen werden.</p>}
|
||||
{!error && !rows && <p className="notice">Lade …</p>}
|
||||
{rows?.length === 0 && <p className="notice">Noch keine XP vergeben — schreibt was im Discord! ✍️</p>}
|
||||
|
||||
{rows?.length > 0 && (
|
||||
<div className="lvl-list">
|
||||
{rows.map((r, i) => {
|
||||
const base = xpForLevel(r.level);
|
||||
const next = xpForLevel(r.level + 1);
|
||||
const pct = Math.floor(((r.xp - base) / (next - base)) * 100);
|
||||
return (
|
||||
<div className="lvl-row" key={r.user_id}>
|
||||
<span className="lvl-rank">{MEDALS[i] ?? `#${i + 1}`}</span>
|
||||
<span className="lvl-name">{r.username ?? r.user_id}</span>
|
||||
<div className="lvl-progress">
|
||||
<div className="progress"><div className="progress-bar" style={{ width: `${pct}%` }} /></div>
|
||||
</div>
|
||||
<span className="lvl-level">Lv. {r.level}</span>
|
||||
<span className="lvl-xp">{r.xp.toLocaleString('de-DE')} XP</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user