Donut-hover met percentages en werkende update-notificatie

- Donut: naadloze segmenten (donkere spleten weg), hover-tooltip per
  categorie met naam, bedrag en aandeel, middenwaarde schaalt mee
- Update-check via de eigen backend (/api/update-check) i.p.v. direct
  naar de Gitea API, die geen CORS-headers stuurt
- Semver-vergelijking, herhaalde check en release-notes in de melding
- release.ps1 maakt de Gitea-release aan (stap 4)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jordy Hoogeveen
2026-09-04 09:19:23 +02:00
co-authored by Claude Opus 5
parent e45d5b73fb
commit 3c67381de9
24 changed files with 506 additions and 241 deletions
+6 -2
View File
@@ -3,10 +3,14 @@
* Wordt gebruikt door alle componenten om consistent kleuren te krijgen.
*/
export function useTheme(darkMode) {
const bg = darkMode ? "#241A39" : "#eef1f8";
const card = darkMode ? "rgba(22,23,27,0.30)" : "#ffffff";
return {
// Exacte Finex kleuren (dark) / licht alternatief
bg: darkMode ? "#241A39" : "#eef1f8",
card: darkMode ? "rgba(22,23,27,0.30)" : "#ffffff",
bg,
card,
// Ondoorzichtige achtergrond voor popups/modals: kaartkleur over de pagina-bg.
popupBg: `linear-gradient(${card}, ${card}), ${bg}`,
sidebar: darkMode ? "#16171B" : "#f8faff",
border: darkMode ? "rgba(139,92,246,0.15)" : "rgba(0,0,0,0.07)",
muted: darkMode ? "#8b8fa8" : "#94a3b8",
+67
View File
@@ -0,0 +1,67 @@
import { useEffect, useState } from "react";
import { version } from "../../package.json";
// De check loopt via de eigen backend (/api/update-check): de Gitea API stuurt
// geen CORS-headers, dus de browser mag die niet direct bevragen.
const POLL_MS = 30 * 60 * 1000; // periodiek opnieuw kijken
const MIN_GAP_MS = 5 * 60 * 1000; // niet vaker dan dit, ook niet bij focus
/**
* Vergelijkt twee semver-strings. Een pre-release suffix (`-dev`) telt als
* lager dan dezelfde versie zonder suffix.
*/
export function isNieuwer(latest, huidig) {
const parse = (v) => {
const [core, pre] = String(v ?? "").replace(/^v/, "").split("-");
const n = core.split(".").map((x) => parseInt(x, 10) || 0);
return { n: [n[0] ?? 0, n[1] ?? 0, n[2] ?? 0], pre: pre || null };
};
const a = parse(latest), b = parse(huidig);
for (let i = 0; i < 3; i++) {
if (a.n[i] !== b.n[i]) return a.n[i] > b.n[i];
}
return !a.pre && !!b.pre;
}
/**
* Geeft de nieuwste release terug zodra die nieuwer is dan de draaiende versie,
* anders `null`. Vorm: { latest, naam, notes, url, publishedAt }.
*/
export function useUpdateCheck() {
const [update, setUpdate] = useState(null);
useEffect(() => {
// Dev-builds lopen per definitie voor op de laatste release — geen melding.
// Met ?updatecheck in de URL forceer je de check alsnog (voor testen).
const forceer = new URLSearchParams(window.location.search).has("updatecheck");
if (version.includes("-") && !forceer) return;
let gestopt = false;
let laatste = 0;
const check = async () => {
if (gestopt || Date.now() - laatste < MIN_GAP_MS) return;
laatste = Date.now();
try {
const res = await fetch("/api/update-check");
if (!res.ok || gestopt) return;
const rel = await res.json();
if (gestopt) return;
setUpdate(rel?.latest && isNieuwer(rel.latest, version) ? rel : null);
} catch {
// Backend onbereikbaar of offline: gewoon geen melding tonen.
}
};
check();
const iv = setInterval(check, POLL_MS);
window.addEventListener("focus", check);
return () => {
gestopt = true;
clearInterval(iv);
window.removeEventListener("focus", check);
};
}, []);
return update;
}