feat: ATM10-Statistikportal
Statistikportal für den All-the-Mods-10-Server (Minecraft 1.21.1, NeoForge 21.1): - mod/: serverseitige NeoForge-Mod atmstats – Ereignisse, Live-Zähler, Servergesundheit, FTB-Dateien; Outbox auf der Festplatte, signierter gzip-Versand per HTTPS mit Backoff - backend/: Fastify + PostgreSQL – idempotenter Ingest, Projektionen, Parser für SNBT, FTB Quests/Teams, world/stats, world/advancements und latest.log; Ranglisten, Snapshots, Retention, öffentliche API, SSE-Live-Updates, Admin mit argon2/CSRF, SFTP-Worker - web/: Next.js-Dashboard – Übersicht, Ranglisten, Spielerprofile, ATM10, Server, Datenquellen, Verwaltung - shared/: Protokoll v1 und API-Typen; tools/: Simulator, Log-Anonymisierer, RCON-Client - deploy/: Docker Compose hinter Traefik, Backup; docs/: Architektur, Datenquellen, Betrieb Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
56d2078e6a
commit
e66f89f49e
214 files changed
+27077
No files matched your search
@@ -0,0 +1,74 @@
|
||||
import type { LeaderboardDto, Period } from '@atm/shared/api';
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { PeriodTabs } from '@/components/PeriodTabs';
|
||||
import { PlayerHead } from '@/components/PlayerHead';
|
||||
import { ApiDown, EmptyState, PageTitle } from '@/components/States';
|
||||
import { apiFind } from '@/lib/api';
|
||||
import { formatUnit } from '@/lib/format';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
type Props = { params: Promise<{ board: string }>; searchParams: Promise<{ period?: string }> };
|
||||
|
||||
const PERIOD_TEXT: Record<Period, string> = {
|
||||
all: 'über die gesamte Spielzeit',
|
||||
'30d': 'Zuwachs der letzten 30 Tage',
|
||||
'7d': 'Zuwachs der letzten 7 Tage',
|
||||
};
|
||||
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { board } = await params;
|
||||
const data = await apiFind<LeaderboardDto>(`/api/leaderboards/${encodeURIComponent(board)}`).catch(() => null);
|
||||
return { title: data?.board.title ?? 'Rangliste' };
|
||||
}
|
||||
|
||||
export default async function BoardPage({ params, searchParams }: Props) {
|
||||
const { board } = await params;
|
||||
const raw = (await searchParams).period;
|
||||
const period: Period = raw === '7d' || raw === '30d' ? raw : 'all';
|
||||
let data: LeaderboardDto | null;
|
||||
try {
|
||||
data = await apiFind<LeaderboardDto>(`/api/leaderboards/${encodeURIComponent(board)}?period=${period}`);
|
||||
} catch {
|
||||
return <ApiDown />;
|
||||
}
|
||||
if (!data) notFound();
|
||||
const max = Math.max(...data.entries.map((e) => e.value), 1);
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="text-sm mb-2">
|
||||
<Link href="/leaderboards" className="text-stone-3 hover:text-stone">Alle Ranglisten</Link>
|
||||
</p>
|
||||
<PageTitle title={data.board.title}>
|
||||
{data.board.description}, {PERIOD_TEXT[period]}.
|
||||
</PageTitle>
|
||||
<div className="mb-6">
|
||||
<PeriodTabs base={`/leaderboards/${data.board.id}`} active={period} />
|
||||
</div>
|
||||
{data.entries.length === 0 ? (
|
||||
<EmptyState title="Noch keine Einträge">
|
||||
{period === 'all' ? 'Sobald Spieler Werte in dieser Kategorie haben, erscheinen sie hier.' : 'In diesem Zeitraum hat sich niemand verbessert.'}
|
||||
</EmptyState>
|
||||
) : (
|
||||
<ol className="rounded-lg bg-slate-1 border border-slate-2 divide-y divide-slate-2">
|
||||
{data.entries.map((e) => (
|
||||
<li key={e.uuid} className="flex items-center gap-3 px-4 py-2.5">
|
||||
<span className={`w-8 text-right font-semibold tabular-nums ${e.rank <= 3 ? 'text-gold' : 'text-stone-3'}`}>{e.rank}</span>
|
||||
<PlayerHead uuid={e.uuid} name={e.name} size={28} />
|
||||
<Link href={`/players/${e.name ?? e.uuid}`} className="w-40 truncate hover:text-gold">
|
||||
{e.name ?? 'Unbekannt'}
|
||||
</Link>
|
||||
<span className="hidden sm:block flex-1 h-1.5 rounded-full bg-slate-2 overflow-hidden" aria-hidden>
|
||||
<span className="block h-full rounded-full bg-gold-dim" style={{ width: `${(e.value / max) * 100}%` }} />
|
||||
</span>
|
||||
<span className="ml-auto sm:ml-0 sm:w-28 text-right tabular-nums">{formatUnit(e.value, data.board.unit)}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user