Feature-Voting, Giveaways und Contribution-Heatmap
- /wunsch: Voting-Embed mit 👍, Reaction-Tracking (add/remove) in SQLite, Top-20-Rangliste öffentlich auf der Roadmap-Seite - /giveaway (Admin): Preis/Dauer/Gewinnerzahl, 🎉-Teilnahme-Button (toggle), Minuten-Scheduler zieht Gewinner, schließt das Embed ab und pingt sie - Contribution-Heatmap: 52-Wochen-Grid in Brand-Gelb aus dem Commit-Archiv (/api/heatmap) auf der Roadmap-Seite - Setting voting_channel_id auf der Setup-Seite Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -43,12 +43,53 @@ function Milestone({ m }) {
|
||||
);
|
||||
}
|
||||
|
||||
/** 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(
|
||||
<span
|
||||
key={d}
|
||||
className={`hm-cell hm-${level}`}
|
||||
title={`${key}: ${n} Commit${n === 1 ? '' : 's'}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
weeks.push(<span className="hm-week" key={w}>{col}</span>);
|
||||
}
|
||||
const total = days.reduce((sum, d) => sum + d.n, 0);
|
||||
return (
|
||||
<div className="roadmap-group">
|
||||
<h2 className="settings-title">// Aktivität — {total} Commits in 12 Monaten</h2>
|
||||
<div className="hm-wrap">{weeks}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}, []);
|
||||
|
||||
@@ -73,6 +114,24 @@ export default function Roadmap() {
|
||||
<p className="notice">Noch keine Meilensteine angelegt.</p>
|
||||
)}
|
||||
|
||||
{heatmap && heatmap.length > 0 && <Heatmap days={heatmap} />}
|
||||
|
||||
{wishes.length > 0 && (
|
||||
<div className="roadmap-group">
|
||||
<h2 className="settings-title">// Community-Wünsche</h2>
|
||||
{wishes.map((w, i) => (
|
||||
<div className="wish-row" key={`${w.created_at}-${i}`}>
|
||||
<span className="wish-score">👍 {w.score}</span>
|
||||
<span className="wish-idea">{w.idea}</span>
|
||||
<span className="wish-author">{w.author}</span>
|
||||
</div>
|
||||
))}
|
||||
<p className="notice" style={{ padding: '.6rem 0 0' }}>
|
||||
Eigene Idee? Im Discord einfach <code>/wunsch</code> benutzen.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{GROUPS.map(({ id, tag, empty }) => {
|
||||
const items = (data?.milestones ?? []).filter((m) => groupOf(m) === id);
|
||||
if (items.length === 0 && !empty) return null;
|
||||
|
||||
Reference in New Issue
Block a user