Feature 4: Webinterface — React-Frontend, Discord-OAuth2, REST-API
- Discord-OAuth2-Login (identify-Scope, CSRF-State, signierte Session-Cookies, keine Token-Speicherung) - REST-API: /api/devlogs (öffentlich), /api/commits (nur ADMIN_DISCORD_ID), /api/me - React + Vite Frontend: Devlog-Archiv mit Mini-Markdown-Renderer, Commit-Tabelle, dunkles EcoGame-Theme - Fastify liefert frontend/dist mit SPA-Fallback aus; Vite-Dev-Proxy für lokale Entwicklung - Multi-Stage-Dockerfile (Frontend-Build im Image), neue Env-Vars in Compose + .env.example - README: OAuth2-Setup (Redirect-URLs, Client Secret) und Frontend-Workflow Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { apiGet } from '../api.js';
|
||||
import { Markdown } from '../markdown.jsx';
|
||||
|
||||
const dateFmt = new Intl.DateTimeFormat('de-DE', {
|
||||
weekday: 'long', day: '2-digit', month: 'long', year: 'numeric',
|
||||
});
|
||||
|
||||
export default function Devlogs() {
|
||||
const [data, setData] = useState(null);
|
||||
const [page, setPage] = useState(1);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setError(false);
|
||||
apiGet(`/api/devlogs?page=${page}`).then(setData).catch(() => setError(true));
|
||||
}, [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);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>Devlogs</h1>
|
||||
<p className="subtitle">{data.total} Einträge aus der Entwicklung von EcoGame</p>
|
||||
|
||||
{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>
|
||||
))}
|
||||
|
||||
{pages > 1 && (
|
||||
<div className="pager">
|
||||
<button className="btn" disabled={page <= 1} onClick={() => setPage(page - 1)}>
|
||||
← Neuere
|
||||
</button>
|
||||
<span>Seite {page} / {pages}</span>
|
||||
<button className="btn" disabled={page >= pages} onClick={() => setPage(page + 1)}>
|
||||
Ältere →
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user