Öffentliche Roadmap-Seite aus Gitea-Milestones
- GET /api/roadmap (public): Milestones des Setting roadmap_repo, 5-min-Cache, letzter guter Stand als Fallback bei Gitea-Ausfall - /roadmap: Gruppen In Arbeit / Geplant / Fertig, Fortschrittsbalken im Brand-Look, Ziel-Monat, Markdown-Beschreibungen; Nav-Link öffentlich - Setup-Seite: Roadmap-Repo konfigurierbar Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { apiGet } from '../api.js';
|
||||
import { Markdown } from '../markdown.jsx';
|
||||
|
||||
const dueFmt = new Intl.DateTimeFormat('de-DE', { month: 'long', year: 'numeric' });
|
||||
|
||||
/** Milestone-Gruppe bestimmen: Fertig / In Arbeit / Geplant */
|
||||
function groupOf(m) {
|
||||
if (m.state === 'closed') return 'done';
|
||||
return m.closed > 0 ? 'active' : 'planned';
|
||||
}
|
||||
|
||||
const GROUPS = [
|
||||
{ id: 'active', tag: '// In Arbeit', empty: 'Gerade nichts in Arbeit.' },
|
||||
{ id: 'planned', tag: '// Geplant', empty: null },
|
||||
{ id: 'done', tag: '// Fertig', empty: null },
|
||||
];
|
||||
|
||||
function Milestone({ m }) {
|
||||
const total = m.open + m.closed;
|
||||
const pct = total > 0 ? Math.round((m.closed / total) * 100) : m.state === 'closed' ? 100 : 0;
|
||||
return (
|
||||
<article className="card milestone">
|
||||
<div className="card-meta">
|
||||
<span className="milestone-title">{m.title}</span>
|
||||
{m.due_on && m.state !== 'closed' && (
|
||||
<span className="card-rel">Ziel: {dueFmt.format(new Date(m.due_on))}</span>
|
||||
)}
|
||||
</div>
|
||||
{m.description && (
|
||||
<div className="card-body">
|
||||
<Markdown text={m.description} />
|
||||
</div>
|
||||
)}
|
||||
<div className="progress">
|
||||
<div className="progress-bar" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
<div className="progress-info">
|
||||
<span>{pct}%</span>
|
||||
{total > 0 && <span>{m.closed} / {total} Aufgaben</span>}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Roadmap() {
|
||||
const [data, setData] = useState(null);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
apiGet('/api/roadmap').then(setData).catch(() => setError(true));
|
||||
window.scrollTo(0, 0);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="page-header">
|
||||
<div className="page-bg-text">ROADMAP</div>
|
||||
<div className="page-header-grid" aria-hidden="true" />
|
||||
<div className="page-header-content">
|
||||
<div className="page-tag">// Wohin die Reise geht</div>
|
||||
<h1 className="page-title">Roadmap</h1>
|
||||
<p className="page-subtitle">
|
||||
Meilensteine direkt aus der Entwicklung — live aus dem Issue-Tracker.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="content">
|
||||
{error && <p className="notice">Roadmap konnte nicht geladen werden.</p>}
|
||||
{!error && !data && <p className="notice">Lade …</p>}
|
||||
{data?.milestones.length === 0 && (
|
||||
<p className="notice">Noch keine Meilensteine angelegt.</p>
|
||||
)}
|
||||
|
||||
{GROUPS.map(({ id, tag, empty }) => {
|
||||
const items = (data?.milestones ?? []).filter((m) => groupOf(m) === id);
|
||||
if (items.length === 0 && !empty) return null;
|
||||
return (
|
||||
<div className="roadmap-group" key={id}>
|
||||
<h2 className="settings-title">{tag}</h2>
|
||||
{items.length === 0 && <p className="notice">{empty}</p>}
|
||||
{items.map((m) => (
|
||||
<Milestone m={m} key={m.title} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -270,6 +270,15 @@ export default function Settings({ me }) {
|
||||
onChange={(e) => setForm({ ...form, bug_report_repo: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Roadmap-Repo <span className="field-hint">Milestones dieses Repos speisen die öffentliche /roadmap-Seite</span></label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="D4rkst3r/EcoGame"
|
||||
value={form.roadmap_repo}
|
||||
onChange={(e) => setForm({ ...form, roadmap_repo: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Watchdog */}
|
||||
|
||||
Reference in New Issue
Block a user