Community-Paket: Playtester-Programm, Starboard, Screenshot-Galerie
- Playtester: /playtester-setup postet Bewerbungs-Embed mit 🧪-Button; Toggle vergibt Rolle + DB-Eintrag; Liste + Rollen-Auswahl auf der Setup-Seite - Starboard: ab konfigurierbarem ⭐-Schwellwert Repost als Embed in den Best-of-Kanal (Dedupe, kein Selbst-Boarding); neue Intents/Partials für Reaktionen - Galerie: Bilder aus dem Screenshot-Kanal lokal archiviert (live + /galerie-backfill, Delete-Sync, Bot reagiert mit ⭐) → öffentliche /galerie-Seite mit Hover-Grid - Setup-Seite: Sektion // Community (Rolle, Starboard-Kanal + Schwellwert, Screenshot-Kanal) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { apiGet } from '../api.js';
|
||||
|
||||
const dateFmt = new Intl.DateTimeFormat('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
|
||||
export default function Galerie() {
|
||||
const [data, setData] = useState(null);
|
||||
const [page, setPage] = useState(1);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setError(false);
|
||||
apiGet(`/api/gallery?page=${page}`).then(setData).catch(() => setError(true));
|
||||
window.scrollTo(0, 0);
|
||||
}, [page]);
|
||||
|
||||
const pages = data ? Math.ceil(data.total / data.pageSize) : 0;
|
||||
const shots = (data?.items ?? []).flatMap((item) =>
|
||||
item.images.map((src) => ({ src, author: item.author_name, date: item.posted_at, key: src }))
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="page-header">
|
||||
<div className="page-bg-text">GALERIE</div>
|
||||
<div className="page-header-grid" aria-hidden="true" />
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">// Community</div>
|
||||
<h1 className="page-title">Galerie</h1>
|
||||
<p className="page-subtitle">Screenshots aus der Community — direkt aus dem Discord.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="content">
|
||||
{error && <p className="notice">Galerie konnte nicht geladen werden.</p>}
|
||||
{!error && !data && <p className="notice">Lade …</p>}
|
||||
{data?.total === 0 && <p className="notice">Noch keine Screenshots — poste welche im Discord!</p>}
|
||||
|
||||
<div className="gallery-grid">
|
||||
{shots.map((s) => (
|
||||
<a className="shot" href={s.src} target="_blank" rel="noreferrer" key={s.key}>
|
||||
<img src={s.src} alt={`Screenshot von ${s.author}`} loading="lazy" />
|
||||
<span className="shot-meta">
|
||||
<span className="shot-author">{s.author}</span>
|
||||
<span className="shot-date">{dateFmt.format(new Date(s.date))}</span>
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{pages > 1 && (
|
||||
<div className="pager">
|
||||
<button className="btn" disabled={page <= 1} onClick={() => setPage(page - 1)}>
|
||||
← Neuere
|
||||
</button>
|
||||
<span className="pager-info">
|
||||
{String(page).padStart(2, '0')} / {String(pages).padStart(2, '0')}
|
||||
</span>
|
||||
<button className="btn" disabled={page >= pages} onClick={() => setPage(page + 1)}>
|
||||
Ältere →
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -26,6 +26,8 @@ export default function Settings({ me }) {
|
||||
const [form, setForm] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [feedback, setFeedback] = useState(null);
|
||||
// Community
|
||||
const [playtesters, setPlaytesters] = useState([]);
|
||||
// API-Keys
|
||||
const [keys, setKeys] = useState([]);
|
||||
const [newKeyName, setNewKeyName] = useState('');
|
||||
@@ -41,6 +43,7 @@ export default function Settings({ me }) {
|
||||
})
|
||||
.catch((e) => setError(e.status));
|
||||
apiGet('/api/apikeys').then((d) => setKeys(d.keys)).catch(() => {});
|
||||
apiGet('/api/playtesters').then((d) => setPlaytesters(d.playtesters)).catch(() => {});
|
||||
}, [me.admin]);
|
||||
|
||||
async function createKey() {
|
||||
@@ -295,6 +298,65 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Community */}
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Community</h2>
|
||||
|
||||
<div className="field">
|
||||
<label>Playtester-Rolle <span className="field-hint">Bewerbungs-Post via /playtester-setup; leer = aus</span></label>
|
||||
<select
|
||||
value={form.playtester_role_id ?? ''}
|
||||
onChange={(e) => setForm({ ...form, playtester_role_id: e.target.value })}
|
||||
>
|
||||
<option value="">— deaktiviert —</option>
|
||||
{(data.roles ?? []).map((r) => (
|
||||
<option key={r.id} value={r.id}>@{r.name} — {r.guild}</option>
|
||||
))}
|
||||
</select>
|
||||
{playtesters.length > 0 && (
|
||||
<div className="key-list" style={{ marginTop: '.8rem' }}>
|
||||
{playtesters.map((p) => (
|
||||
<div className="key-row" key={p.user_id}>
|
||||
<span className="key-name">{p.username}</span>
|
||||
<span className="key-used">seit {p.applied_at.slice(0, 10)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label>Starboard-Kanal <span className="field-hint">Nachrichten mit genug ⭐ landen hier; leer = aus</span></label>
|
||||
<div className="field-row">
|
||||
<select
|
||||
value={form.starboard_channel_id ?? ''}
|
||||
onChange={(e) => setForm({ ...form, starboard_channel_id: e.target.value })}
|
||||
>
|
||||
<option value="">— deaktiviert —</option>
|
||||
{channelOptions}
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
style={{ width: '7rem', flex: 'none' }}
|
||||
title="Schwellwert (Anzahl ⭐)"
|
||||
value={form.starboard_threshold}
|
||||
onChange={(e) => setForm({ ...form, starboard_threshold: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label>Screenshot-Kanal <span className="field-hint">Bilder landen in der öffentlichen Galerie; Historie: /galerie-backfill</span></label>
|
||||
<select
|
||||
value={form.screenshot_channel_id ?? ''}
|
||||
onChange={(e) => setForm({ ...form, screenshot_channel_id: e.target.value })}
|
||||
>
|
||||
<option value="">— deaktiviert —</option>
|
||||
{channelOptions}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Backups */}
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Backups</h2>
|
||||
|
||||
Reference in New Issue
Block a user