Redesign Webinterface: Voxel-Game-Ästhetik

- Selbst gehostete Fonts (Inter Variable, JetBrains Mono) — kein CDN
- Voxel-Würfel-Logo (SVG), Grid-Hintergrund mit grünem Glow, Gradient-Hero
- Devlogs als Timeline mit Voxel-Markern, Datums-Chips und Relativzeit
- Commits als Zeilen-Liste mit SHA-, Repo- und Branch-Chips statt Tabelle
- Discord-Login-Button mit Logo, User-Chip mit Avatar im Header
- Mobile-Layout (Karten, umbrechende Commit-Zeilen)
- Im Browser verifiziert: Desktop + Mobile, Devlogs + Commits (mit Admin-Session)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 00:40:57 +02:00
co-authored by Claude Opus 4.8
parent a70eac08d7
commit d9cc24f4a8
7 changed files with 395 additions and 155 deletions
+25 -37
View File
@@ -2,7 +2,7 @@ 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', hour: '2-digit', minute: '2-digit',
day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit',
});
export default function Commits({ me }) {
@@ -14,6 +14,7 @@ export default function Commits({ me }) {
if (!me.admin) return;
setError(null);
apiGet(`/api/commits?page=${page}`).then(setData).catch((e) => setError(e.status));
window.scrollTo(0, 0);
}, [page, me.admin]);
// Zugriffs-Zustände: nicht eingeloggt / eingeloggt aber kein Admin
@@ -21,12 +22,12 @@ export default function Commits({ me }) {
if (!me.user) {
return (
<div className="notice center">
<p>Der Commit-Feed ist privat.</p>
<a className="btn" href="/auth/login">Login mit Discord</a>
<p>🔒 Der Commit-Feed ist privat.</p>
<a className="btn btn-discord" href="/auth/login">Login mit Discord</a>
</div>
);
}
if (!me.admin) return <p className="notice">Kein Zugriff der Commit-Feed ist privat.</p>;
if (!me.admin) return <p className="notice center">🔒 Kein Zugriff der Commit-Feed ist privat.</p>;
if (error) return <p className="notice">Commits konnten nicht geladen werden ({error}).</p>;
if (!data) return <p className="notice">Lade </p>;
@@ -36,40 +37,27 @@ export default function Commits({ me }) {
return (
<>
<h1>Commits</h1>
<p className="subtitle">{data.total} archivierte Commits</p>
<section className="hero">
<h1>Commits</h1>
<p>{data.total} archivierte Commits über alle Repos</p>
</section>
<div className="table-wrap">
<table className="commits">
<thead>
<tr>
<th>SHA</th>
<th>Message</th>
<th>Repo</th>
<th>Branch</th>
<th>Zeit</th>
</tr>
</thead>
<tbody>
{data.items.map((c) => (
<tr key={c.sha}>
<td>
{c.url ? (
<a href={c.url} target="_blank" rel="noreferrer">
<code>{c.sha.slice(0, 7)}</code>
</a>
) : (
<code>{c.sha.slice(0, 7)}</code>
)}
</td>
<td className="msg">{c.message.split('\n')[0]}</td>
<td>{c.repo}</td>
<td>{c.branch}</td>
<td className="time">{dateFmt.format(new Date(c.committed_at))}</td>
</tr>
))}
</tbody>
</table>
<div className="commit-list">
{data.items.map((c) => (
<div className="commit-row" key={c.sha}>
{c.url ? (
<a className="sha" href={c.url} target="_blank" rel="noreferrer">
{c.sha.slice(0, 7)}
</a>
) : (
<span className="sha">{c.sha.slice(0, 7)}</span>
)}
<span className="commit-msg">{c.message.split('\n')[0]}</span>
<span className="chip chip-repo">{c.repo.split('/').pop()}</span>
<span className="chip chip-branch">{c.branch}</span>
<span className="commit-time">{dateFmt.format(new Date(c.committed_at))}</span>
</div>
))}
</div>
{pages > 1 && (
+40 -15
View File
@@ -6,6 +6,16 @@ const dateFmt = new Intl.DateTimeFormat('de-DE', {
weekday: 'long', day: '2-digit', month: 'long', year: 'numeric',
});
/** Relative Angabe wie „vor 3 Tagen" für die Karten-Ecke */
function relative(iso) {
const days = Math.floor((Date.now() - new Date(iso).getTime()) / 86400000);
if (days <= 0) return 'heute';
if (days === 1) return 'gestern';
if (days < 30) return `vor ${days} Tagen`;
const months = Math.floor(days / 30);
return months === 1 ? 'vor 1 Monat' : `vor ${months} Monaten`;
}
export default function Devlogs() {
const [data, setData] = useState(null);
const [page, setPage] = useState(1);
@@ -14,27 +24,42 @@ export default function Devlogs() {
useEffect(() => {
setError(false);
apiGet(`/api/devlogs?page=${page}`).then(setData).catch(() => setError(true));
window.scrollTo(0, 0);
}, [page]);
if (error) return <p className="notice">Devlogs konnten nicht geladen werden.</p>;
if (!data) return <p className="notice">Lade </p>;
if (data.total === 0) return <p className="notice">Noch keine Devlogs archiviert.</p>;
const pages = Math.ceil(data.total / data.pageSize);
const pages = data ? Math.ceil(data.total / data.pageSize) : 0;
return (
<>
<h1>Devlogs</h1>
<p className="subtitle">{data.total} Einträge aus der Entwicklung von EcoGame</p>
<section className="hero">
<h1>Devlog</h1>
<p>
Die Entwicklung von <strong>EcoGame</strong> einem Voxel-Sandbox-Survival
in Unreal Engine 5. Frisch aus dem Editor, Tag für Tag.
</p>
</section>
{data.items.map((d) => (
<article className="card" key={d.message_id}>
<div className="card-date">{dateFmt.format(new Date(d.posted_at))}</div>
<div className="card-body">
<Markdown text={d.content} />
</div>
</article>
))}
{error && <p className="notice">Devlogs konnten nicht geladen werden.</p>}
{!error && !data && <p className="notice">Lade </p>}
{data?.total === 0 && <p className="notice">Noch keine Devlogs archiviert.</p>}
{data?.total > 0 && (
<div className="timeline">
{data.items.map((d) => (
<article className="card" key={d.message_id}>
<div className="card-meta">
<span className="card-date">
{dateFmt.format(new Date(d.posted_at))}
</span>
<span className="card-rel">{relative(d.posted_at)}</span>
</div>
<div className="card-body">
<Markdown text={d.content} />
</div>
</article>
))}
</div>
)}
{pages > 1 && (
<div className="pager">