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 (
{m.title}
{m.due_on && m.state !== 'closed' && (
Ziel: {dueFmt.format(new Date(m.due_on))}
)}
{m.description && (
)}
{pct}%
{total > 0 && {m.closed} / {total} Aufgaben}
);
}
/** GitHub-Style-Heatmap: 52 Wochen × 7 Tage */
function Heatmap({ days }) {
const byDay = new Map(days.map((d) => [d.day, d.n]));
const max = Math.max(1, ...days.map((d) => d.n));
const today = new Date();
// Start: Montag vor ~52 Wochen
const start = new Date(today.getTime() - 364 * 86400000);
start.setDate(start.getDate() - ((start.getDay() + 6) % 7));
const weeks = [];
for (let w = 0; w < 53; w++) {
const col = [];
for (let d = 0; d < 7; d++) {
const date = new Date(start.getTime() + (w * 7 + d) * 86400000);
if (date > today) break;
const key = date.toISOString().slice(0, 10);
const n = byDay.get(key) ?? 0;
const level = n === 0 ? 0 : Math.min(4, Math.ceil((n / max) * 4));
col.push(
);
}
weeks.push({col});
}
const total = days.reduce((sum, d) => sum + d.n, 0);
return (
// Aktivität — {total} Commits in 12 Monaten
{weeks}
);
}
export default function Roadmap() {
const [data, setData] = useState(null);
const [wishes, setWishes] = useState([]);
const [heatmap, setHeatmap] = useState(null);
const [error, setError] = useState(false);
useEffect(() => {
apiGet('/api/roadmap').then(setData).catch(() => setError(true));
apiGet('/api/wishes').then((d) => setWishes(d.wishes)).catch(() => {});
apiGet('/api/heatmap').then((d) => setHeatmap(d.days)).catch(() => {});
window.scrollTo(0, 0);
}, []);
return (
<>
ROADMAP
// Wohin die Reise geht
Roadmap
Meilensteine direkt aus der Entwicklung — live aus dem Issue-Tracker.
{error && Roadmap konnte nicht geladen werden.
}
{!error && !data && Lade …
}
{data?.milestones.length === 0 && (
Noch keine Meilensteine angelegt.
)}
{heatmap && heatmap.length > 0 && }
{wishes.length > 0 && (
// Community-Wünsche
{wishes.map((w, i) => (
👍 {w.score}
{w.idea}
{w.author}
))}
Eigene Idee? Im Discord einfach /wunsch benutzen.
)}
{GROUPS.map(({ id, tag, empty }) => {
const items = (data?.milestones ?? []).filter((m) => groupOf(m) === id);
if (items.length === 0 && !empty) return null;
return (
{tag}
{items.length === 0 &&
{empty}
}
{items.map((m) => (
))}
);
})}
>
);
}