@xiwebdesign/xicms-core 7.16.1 → 7.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,23 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useMemo, useState } from "react";
4
4
  import { collection, getDocs } from "firebase/firestore";
5
5
  import { getDb } from "../../firebase/client.js";
6
6
  import { Card } from "../ui/primitives.js";
7
+ import { Select } from "../ui/select.js";
8
+ import { AlsTabel, BalkjesLijst, DagStaven, MaandLijn, StatTegel } from "../ui/charts.js";
9
+ import {
10
+ dagenInMaand,
11
+ dagReeks,
12
+ deltaTovVorige,
13
+ huidigMaandId,
14
+ maandLabel,
15
+ topPaden
16
+ } from "./lib.js";
7
17
  function StatsPanel({ siteId }) {
8
18
  const [months, setMonths] = useState([]);
9
19
  const [loading, setLoading] = useState(true);
20
+ const [gekozen, setGekozen] = useState(huidigMaandId());
10
21
  useEffect(() => {
11
22
  const db = getDb();
12
23
  if (!db) {
@@ -16,60 +27,104 @@ function StatsPanel({ siteId }) {
16
27
  let active = true;
17
28
  getDocs(collection(db, "sites", siteId, "stats")).then((snap) => {
18
29
  if (!active) return;
19
- setMonths(snap.docs.map((d) => ({ id: d.id, ...d.data() })));
30
+ const uit = snap.docs.map((d) => ({ id: d.id, ...d.data() })).filter((m) => /^\d{4}-\d{2}$/.test(m.id)).sort((a, b) => a.id.localeCompare(b.id));
31
+ setMonths(uit);
32
+ if (uit.length && !uit.some((m) => m.id === huidigMaandId())) setGekozen(uit[uit.length - 1].id);
20
33
  setLoading(false);
21
34
  }).catch(() => active && setLoading(false));
22
35
  return () => {
23
36
  active = false;
24
37
  };
25
38
  }, [siteId]);
26
- const total = useMemo(() => months.reduce((s, m) => s + (m.total ?? 0), 0), [months]);
27
- const current = useMemo(() => {
28
- const now = /* @__PURE__ */ new Date();
29
- const id = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}`;
30
- return months.find((m) => m.id === id);
31
- }, [months]);
32
- const days = current?.days ?? {};
33
- const maxDay = Math.max(1, ...Object.values(days));
34
- const topPaths = Object.entries(current?.paths ?? {}).sort((a, b) => b[1] - a[1]).slice(0, 8);
39
+ const maand = months.find((m) => m.id === gekozen);
40
+ const vorigeId = useMemo(() => {
41
+ const [j, m] = gekozen.split("-").map(Number);
42
+ const d = new Date(Date.UTC(j, (m ?? 1) - 2, 1));
43
+ return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}`;
44
+ }, [gekozen]);
45
+ const vorige = months.find((m) => m.id === vorigeId);
46
+ const isHuidige = gekozen === huidigMaandId();
47
+ const nDagen = dagenInMaand(gekozen);
48
+ const vandaagDag = (/* @__PURE__ */ new Date()).getUTCDate();
49
+ const dagen = useMemo(() => dagReeks(maand, nDagen), [maand, nDagen]);
50
+ const totaalMaand = maand?.total ?? 0;
51
+ const totaalOoit = useMemo(() => months.reduce((s, m) => s + (m.total ?? 0), 0), [months]);
52
+ const verstreken = isHuidige ? Math.max(1, vandaagDag) : nDagen;
53
+ const delta = deltaTovVorige(maand, vorige, isHuidige ? vandaagDag : nDagen);
54
+ const paden = useMemo(() => topPaden(maand, 8), [maand]);
55
+ const trend = useMemo(
56
+ () => months.slice(-12).map((m) => ({ label: maandLabel(m.id), waarde: m.total ?? 0 })),
57
+ [months]
58
+ );
35
59
  if (loading) return /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Laden\u2026" });
36
60
  if (months.length === 0) {
37
61
  return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-600", children: "Nog geen bezoekersdata. Zodra de live site bezocht wordt, verschijnen hier de cijfers." }) });
38
62
  }
39
63
  return /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
40
- /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4 sm:grid-cols-3", children: [
41
- /* @__PURE__ */ jsxs(Card, { children: [
42
- /* @__PURE__ */ jsx("div", { className: "text-sm text-slate-500", children: "Totaal bezoeken" }),
43
- /* @__PURE__ */ jsx("div", { className: "text-3xl font-bold", children: total.toLocaleString("nl-NL") })
44
- ] }),
45
- /* @__PURE__ */ jsxs(Card, { children: [
46
- /* @__PURE__ */ jsx("div", { className: "text-sm text-slate-500", children: "Deze maand" }),
47
- /* @__PURE__ */ jsx("div", { className: "text-3xl font-bold", children: (current?.total ?? 0).toLocaleString("nl-NL") })
64
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
65
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-slate-500", children: [
66
+ "Bezoek in ",
67
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-slate-700", children: maandLabel(gekozen, true) }),
68
+ isHuidige ? " (loopt nog)" : ""
48
69
  ] }),
49
- /* @__PURE__ */ jsxs(Card, { children: [
50
- /* @__PURE__ */ jsx("div", { className: "text-sm text-slate-500", children: "Maanden gemeten" }),
51
- /* @__PURE__ */ jsx("div", { className: "text-3xl font-bold", children: months.length })
52
- ] })
70
+ /* @__PURE__ */ jsx(
71
+ Select,
72
+ {
73
+ value: gekozen,
74
+ onValueChange: setGekozen,
75
+ placeholder: "Maand",
76
+ className: "w-44",
77
+ options: [...months].reverse().map((m) => ({ value: m.id, label: maandLabel(m.id, true) }))
78
+ }
79
+ )
53
80
  ] }),
54
- /* @__PURE__ */ jsxs(Card, { children: [
55
- /* @__PURE__ */ jsx("h3", { className: "mb-3 text-base font-semibold", children: "Bezoeken per dag (deze maand)" }),
56
- Object.keys(days).length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Nog geen data deze maand." }) : /* @__PURE__ */ jsx("div", { className: "flex items-end gap-1", style: { height: 140 }, children: Array.from({ length: 31 }, (_, i) => String(i + 1).padStart(2, "0")).map((d) => /* @__PURE__ */ jsx("div", { className: "flex flex-1 flex-col items-center justify-end", title: `Dag ${d}: ${days[d] ?? 0}`, children: /* @__PURE__ */ jsx(
57
- "div",
81
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4 lg:grid-cols-4", children: [
82
+ /* @__PURE__ */ jsx(
83
+ StatTegel,
58
84
  {
59
- className: "w-full rounded-t bg-brand-500",
60
- style: { height: `${(days[d] ?? 0) / maxDay * 120}px` }
85
+ label: "Weergaven deze maand",
86
+ waarde: totaalMaand,
87
+ delta,
88
+ deltaLabel: `t.o.v. ${maandLabel(vorigeId)}${isHuidige ? " (t/m vandaag)" : ""}`
61
89
  }
62
- ) }, d)) })
90
+ ),
91
+ isHuidige ? /* @__PURE__ */ jsx(StatTegel, { label: "Vandaag", waarde: dagen[vandaagDag - 1] ?? 0 }) : /* @__PURE__ */ jsx(StatTegel, { label: "Drukste dag", waarde: Math.max(0, ...dagen) }),
92
+ /* @__PURE__ */ jsx(StatTegel, { label: "Gemiddeld per dag", waarde: Math.round(totaalMaand / verstreken) }),
93
+ /* @__PURE__ */ jsx(StatTegel, { label: "Totaal gemeten", waarde: totaalOoit })
63
94
  ] }),
64
95
  /* @__PURE__ */ jsxs(Card, { children: [
65
- /* @__PURE__ */ jsx("h3", { className: "mb-3 text-base font-semibold", children: "Best bezochte pagina's (deze maand)" }),
66
- topPaths.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Nog geen data." }) : /* @__PURE__ */ jsx("ul", { className: "text-sm", children: topPaths.map(([p, n]) => /* @__PURE__ */ jsxs("li", { className: "flex justify-between border-b border-slate-100 py-1 last:border-0", children: [
67
- /* @__PURE__ */ jsxs("span", { className: "truncate text-slate-700", children: [
68
- "/",
69
- p === "home" ? "" : p.replace(/_/g, "/")
70
- ] }),
71
- /* @__PURE__ */ jsx("span", { className: "font-medium", children: n.toLocaleString("nl-NL") })
72
- ] }, p)) })
96
+ /* @__PURE__ */ jsx("h3", { className: "mb-4 text-sm font-semibold text-slate-800", children: "Per dag" }),
97
+ totaalMaand === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Nog geen data in deze maand." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
98
+ /* @__PURE__ */ jsx(
99
+ DagStaven,
100
+ {
101
+ waarden: dagen,
102
+ accentIndex: isHuidige ? vandaagDag - 1 : void 0,
103
+ hoogte: 170,
104
+ titel: `Weergaven per dag in ${maandLabel(gekozen, true)}`
105
+ }
106
+ ),
107
+ /* @__PURE__ */ jsx(
108
+ AlsTabel,
109
+ {
110
+ kop: ["Dag", "Weergaven"],
111
+ rijen: dagen.map((v, i) => [`${i + 1} ${maandLabel(gekozen)}`, v]).filter(([, v]) => v > 0)
112
+ }
113
+ )
114
+ ] })
115
+ ] }),
116
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-4 lg:grid-cols-2", children: [
117
+ /* @__PURE__ */ jsxs(Card, { children: [
118
+ /* @__PURE__ */ jsx("h3", { className: "mb-4 text-sm font-semibold text-slate-800", children: "Maand op maand" }),
119
+ trend.length < 2 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Nog te weinig maanden voor een trend \u2014 die groeit hier vanzelf." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
120
+ /* @__PURE__ */ jsx(MaandLijn, { punten: trend, hoogte: 170, titel: "Weergaven per maand, laatste twaalf maanden" }),
121
+ /* @__PURE__ */ jsx(AlsTabel, { kop: ["Maand", "Weergaven"], rijen: trend.map((p) => [p.label, p.waarde]) })
122
+ ] })
123
+ ] }),
124
+ /* @__PURE__ */ jsxs(Card, { children: [
125
+ /* @__PURE__ */ jsx("h3", { className: "mb-4 text-sm font-semibold text-slate-800", children: "Best bezochte pagina's" }),
126
+ paden.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Nog geen data in deze maand." }) : /* @__PURE__ */ jsx(BalkjesLijst, { items: paden })
127
+ ] })
73
128
  ] }),
74
129
  /* @__PURE__ */ jsx("p", { className: "text-xs text-slate-400", children: "Privacy-vriendelijk geteld (geen cookies, geen persoonsgegevens) \u2014 \xE9\xE9n telling per bezoeker-sessie per pagina." })
75
130
  ] });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/stats/StatsPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport { collection, getDocs } from \"firebase/firestore\";\nimport { getDb } from \"../../firebase/client\";\nimport { Card } from \"../ui/primitives\";\n\ninterface MonthStat {\n id: string;\n total?: number;\n days?: Record<string, number>;\n paths?: Record<string, number>;\n}\n\n/** Eenvoudige, privacy-vriendelijke bezoekersstatistieken (eigen teller). */\nexport function StatsPanel({ siteId }: { siteId: string }) {\n const [months, setMonths] = useState<MonthStat[]>([]);\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n const db = getDb();\n if (!db) {\n setLoading(false);\n return;\n }\n let active = true;\n getDocs(collection(db, \"sites\", siteId, \"stats\"))\n .then((snap) => {\n if (!active) return;\n setMonths(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<MonthStat, \"id\">) })));\n setLoading(false);\n })\n .catch(() => active && setLoading(false));\n return () => {\n active = false;\n };\n }, [siteId]);\n\n const total = useMemo(() => months.reduce((s, m) => s + (m.total ?? 0), 0), [months]);\n const current = useMemo(() => {\n const now = new Date();\n const id = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, \"0\")}`;\n return months.find((m) => m.id === id);\n }, [months]);\n\n const days = current?.days ?? {};\n const maxDay = Math.max(1, ...Object.values(days));\n const topPaths = Object.entries(current?.paths ?? {})\n .sort((a, b) => b[1] - a[1])\n .slice(0, 8);\n\n if (loading) return <p className=\"text-sm text-slate-500\">Laden…</p>;\n\n if (months.length === 0) {\n return (\n <Card>\n <p className=\"text-sm text-slate-600\">\n Nog geen bezoekersdata. Zodra de live site bezocht wordt, verschijnen hier de cijfers.\n </p>\n </Card>\n );\n }\n\n return (\n <div className=\"grid gap-4\">\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-3\">\n <Card>\n <div className=\"text-sm text-slate-500\">Totaal bezoeken</div>\n <div className=\"text-3xl font-bold\">{total.toLocaleString(\"nl-NL\")}</div>\n </Card>\n <Card>\n <div className=\"text-sm text-slate-500\">Deze maand</div>\n <div className=\"text-3xl font-bold\">{(current?.total ?? 0).toLocaleString(\"nl-NL\")}</div>\n </Card>\n <Card>\n <div className=\"text-sm text-slate-500\">Maanden gemeten</div>\n <div className=\"text-3xl font-bold\">{months.length}</div>\n </Card>\n </div>\n\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Bezoeken per dag (deze maand)</h3>\n {Object.keys(days).length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen data deze maand.</p>\n ) : (\n <div className=\"flex items-end gap-1\" style={{ height: 140 }}>\n {Array.from({ length: 31 }, (_, i) => String(i + 1).padStart(2, \"0\")).map((d) => (\n <div key={d} className=\"flex flex-1 flex-col items-center justify-end\" title={`Dag ${d}: ${days[d] ?? 0}`}>\n <div\n className=\"w-full rounded-t bg-brand-500\"\n style={{ height: `${((days[d] ?? 0) / maxDay) * 120}px` }}\n />\n </div>\n ))}\n </div>\n )}\n </Card>\n\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Best bezochte pagina's (deze maand)</h3>\n {topPaths.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen data.</p>\n ) : (\n <ul className=\"text-sm\">\n {topPaths.map(([p, n]) => (\n <li key={p} className=\"flex justify-between border-b border-slate-100 py-1 last:border-0\">\n <span className=\"truncate text-slate-700\">/{p === \"home\" ? \"\" : p.replace(/_/g, \"/\")}</span>\n <span className=\"font-medium\">{n.toLocaleString(\"nl-NL\")}</span>\n </li>\n ))}\n </ul>\n )}\n </Card>\n\n <p className=\"text-xs text-slate-400\">\n Privacy-vriendelijk geteld (geen cookies, geen persoonsgegevens) — één telling per\n bezoeker-sessie per pagina.\n </p>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/stats/StatsPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport { collection, getDocs } from \"firebase/firestore\";\nimport { getDb } from \"../../firebase/client\";\nimport { Card } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { AlsTabel, BalkjesLijst, DagStaven, MaandLijn, StatTegel } from \"../ui/charts\";\nimport {\n dagenInMaand,\n dagReeks,\n deltaTovVorige,\n huidigMaandId,\n maandLabel,\n topPaden,\n type MaandStat,\n} from \"./lib\";\n\n/**\n * Bezoekersstatistieken (7.17) — van drie kale tellers naar een leesbaar verhaal: tegels met\n * context (delta t.o.v. vorige maand, t/m dezelfde dag), een dag-grafiek met hover, de\n * maandtrend als lijn, en de toppagina's als balkjes. Zelfde privacy-vriendelijke bron\n * (`sites/{siteId}/stats`, eigen teller, geen cookies) — er is alleen beter naar gekeken.\n */\nexport function StatsPanel({ siteId }: { siteId: string }) {\n const [months, setMonths] = useState<MaandStat[]>([]);\n const [loading, setLoading] = useState(true);\n const [gekozen, setGekozen] = useState<string>(huidigMaandId());\n\n useEffect(() => {\n const db = getDb();\n if (!db) {\n setLoading(false);\n return;\n }\n let active = true;\n getDocs(collection(db, \"sites\", siteId, \"stats\"))\n .then((snap) => {\n if (!active) return;\n const uit = snap.docs\n .map((d) => ({ id: d.id, ...(d.data() as Omit<MaandStat, \"id\">) }))\n .filter((m) => /^\\d{4}-\\d{2}$/.test(m.id))\n .sort((a, b) => a.id.localeCompare(b.id));\n setMonths(uit);\n // Standaard de nieuwste maand mét data (meestal de huidige).\n if (uit.length && !uit.some((m) => m.id === huidigMaandId())) setGekozen(uit[uit.length - 1].id);\n setLoading(false);\n })\n .catch(() => active && setLoading(false));\n return () => {\n active = false;\n };\n }, [siteId]);\n\n const maand = months.find((m) => m.id === gekozen);\n const vorigeId = useMemo(() => {\n const [j, m] = gekozen.split(\"-\").map(Number);\n const d = new Date(Date.UTC(j, (m ?? 1) - 2, 1));\n return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, \"0\")}`;\n }, [gekozen]);\n const vorige = months.find((m) => m.id === vorigeId);\n\n const isHuidige = gekozen === huidigMaandId();\n const nDagen = dagenInMaand(gekozen);\n const vandaagDag = new Date().getUTCDate();\n const dagen = useMemo(() => dagReeks(maand, nDagen), [maand, nDagen]);\n const totaalMaand = maand?.total ?? 0;\n const totaalOoit = useMemo(() => months.reduce((s, m) => s + (m.total ?? 0), 0), [months]);\n // Gemiddelde over de dagen die er al ZIJN: een lopende maand door 31 delen praat het cijfer omlaag.\n const verstreken = isHuidige ? Math.max(1, vandaagDag) : nDagen;\n const delta = deltaTovVorige(maand, vorige, isHuidige ? vandaagDag : nDagen);\n const paden = useMemo(() => topPaden(maand, 8), [maand]);\n const trend = useMemo(\n () => months.slice(-12).map((m) => ({ label: maandLabel(m.id), waarde: m.total ?? 0 })),\n [months],\n );\n\n if (loading) return <p className=\"text-sm text-slate-500\">Laden…</p>;\n\n if (months.length === 0) {\n return (\n <Card>\n <p className=\"text-sm text-slate-600\">\n Nog geen bezoekersdata. Zodra de live site bezocht wordt, verschijnen hier de cijfers.\n </p>\n </Card>\n );\n }\n\n return (\n <div className=\"grid gap-4\">\n {/* Eén filterregel boven de inhoud: de maand waar alles hieronder over gaat. */}\n <div className=\"flex items-center justify-between gap-3\">\n <p className=\"text-sm text-slate-500\">\n Bezoek in <span className=\"font-medium text-slate-700\">{maandLabel(gekozen, true)}</span>\n {isHuidige ? \" (loopt nog)\" : \"\"}\n </p>\n <Select\n value={gekozen}\n onValueChange={setGekozen}\n placeholder=\"Maand\"\n className=\"w-44\"\n options={[...months].reverse().map((m) => ({ value: m.id, label: maandLabel(m.id, true) }))}\n />\n </div>\n\n <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n <StatTegel\n label=\"Weergaven deze maand\"\n waarde={totaalMaand}\n delta={delta}\n deltaLabel={`t.o.v. ${maandLabel(vorigeId)}${isHuidige ? \" (t/m vandaag)\" : \"\"}`}\n />\n {isHuidige ? (\n <StatTegel label=\"Vandaag\" waarde={dagen[vandaagDag - 1] ?? 0} />\n ) : (\n <StatTegel label=\"Drukste dag\" waarde={Math.max(0, ...dagen)} />\n )}\n <StatTegel label=\"Gemiddeld per dag\" waarde={Math.round(totaalMaand / verstreken)} />\n <StatTegel label=\"Totaal gemeten\" waarde={totaalOoit} />\n </div>\n\n <Card>\n <h3 className=\"mb-4 text-sm font-semibold text-slate-800\">Per dag</h3>\n {totaalMaand === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen data in deze maand.</p>\n ) : (\n <>\n <DagStaven\n waarden={dagen}\n accentIndex={isHuidige ? vandaagDag - 1 : undefined}\n hoogte={170}\n titel={`Weergaven per dag in ${maandLabel(gekozen, true)}`}\n />\n <AlsTabel\n kop={[\"Dag\", \"Weergaven\"]}\n rijen={dagen.map((v, i) => [`${i + 1} ${maandLabel(gekozen)}`, v] as [string, number]).filter(([, v]) => v > 0)}\n />\n </>\n )}\n </Card>\n\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <Card>\n <h3 className=\"mb-4 text-sm font-semibold text-slate-800\">Maand op maand</h3>\n {trend.length < 2 ? (\n <p className=\"text-sm text-slate-500\">Nog te weinig maanden voor een trend — die groeit hier vanzelf.</p>\n ) : (\n <>\n <MaandLijn punten={trend} hoogte={170} titel=\"Weergaven per maand, laatste twaalf maanden\" />\n <AlsTabel kop={[\"Maand\", \"Weergaven\"]} rijen={trend.map((p) => [p.label, p.waarde] as [string, number])} />\n </>\n )}\n </Card>\n\n <Card>\n <h3 className=\"mb-4 text-sm font-semibold text-slate-800\">Best bezochte pagina&apos;s</h3>\n {paden.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen data in deze maand.</p>\n ) : (\n <BalkjesLijst items={paden} />\n )}\n </Card>\n </div>\n\n <p className=\"text-xs text-slate-400\">\n Privacy-vriendelijk geteld (geen cookies, geen persoonsgegevens) — één telling per\n bezoeker-sessie per pagina.\n </p>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Gedeelde stats-helpers — gebruikt door het Statistieken-paneel én de Start-widget, zodat
3
+ * paden en maanden op beide plekken hetzelfde heten.
4
+ */
5
+ export interface MaandStat {
6
+ id: string;
7
+ total?: number;
8
+ days?: Record<string, number>;
9
+ paths?: Record<string, number>;
10
+ }
11
+ /** trackView slaat paden veilig op: "home" voor "/" en "_" voor "/". Terug naar leesbaar. */
12
+ export declare function padLabel(sleutel: string): string;
13
+ /** "2026-09" → "sep 2026" (kort, voor as-labels en de maandkiezer). */
14
+ export declare function maandLabel(id: string, lang?: boolean): string;
15
+ /** Het maand-id van nu (UTC — trackView telt in UTC). */
16
+ export declare function huidigMaandId(): string;
17
+ /** Aantal dagen in een "YYYY-MM"-maand. */
18
+ export declare function dagenInMaand(id: string): number;
19
+ /** days-record → array per dag (index 0 = dag 1), met nullen voor ontbrekende dagen. */
20
+ export declare function dagReeks(stat: MaandStat | undefined, nDagen: number): number[];
21
+ /** Top-N paden van een maand, genormaliseerd en gesorteerd. */
22
+ export declare function topPaden(stat: MaandStat | undefined, n: number): {
23
+ label: string;
24
+ waarde: number;
25
+ }[];
26
+ /**
27
+ * Eerlijke maand-op-maand-delta: vergelijk t/m dezelfde dag. Halverwege de maand het hele
28
+ * vorige maandtotaal ernaast leggen leest als "−50%" terwijl het gewoon de 15e is.
29
+ */
30
+ export declare function deltaTovVorige(huidig: MaandStat | undefined, vorige: MaandStat | undefined, totDag: number): number | null;
@@ -0,0 +1,40 @@
1
+ function padLabel(sleutel) {
2
+ return sleutel === "home" ? "/" : `/${sleutel.replace(/_/g, "/")}`;
3
+ }
4
+ function maandLabel(id, lang = false) {
5
+ const [j, m] = id.split("-").map(Number);
6
+ const d = new Date(Date.UTC(j, (m ?? 1) - 1, 1));
7
+ return d.toLocaleDateString("nl-NL", { month: lang ? "long" : "short", year: "numeric", timeZone: "UTC" });
8
+ }
9
+ function huidigMaandId() {
10
+ const nu = /* @__PURE__ */ new Date();
11
+ return `${nu.getUTCFullYear()}-${String(nu.getUTCMonth() + 1).padStart(2, "0")}`;
12
+ }
13
+ function dagenInMaand(id) {
14
+ const [j, m] = id.split("-").map(Number);
15
+ return new Date(Date.UTC(j, m ?? 1, 0)).getUTCDate();
16
+ }
17
+ function dagReeks(stat, nDagen) {
18
+ return Array.from({ length: nDagen }, (_, i) => stat?.days?.[String(i + 1).padStart(2, "0")] ?? 0);
19
+ }
20
+ function topPaden(stat, n) {
21
+ return Object.entries(stat?.paths ?? {}).sort((a, b) => b[1] - a[1]).slice(0, n).map(([p, w]) => ({ label: padLabel(p), waarde: w }));
22
+ }
23
+ function deltaTovVorige(huidig, vorige, totDag) {
24
+ if (!huidig || !vorige) return null;
25
+ const som = (s) => Object.entries(s.days ?? {}).reduce((acc, [d, n]) => Number(d) <= totDag ? acc + n : acc, 0);
26
+ const nu = som(huidig);
27
+ const toen = som(vorige);
28
+ if (toen === 0) return null;
29
+ return (nu - toen) / toen * 100;
30
+ }
31
+ export {
32
+ dagReeks,
33
+ dagenInMaand,
34
+ deltaTovVorige,
35
+ huidigMaandId,
36
+ maandLabel,
37
+ padLabel,
38
+ topPaden
39
+ };
40
+ //# sourceMappingURL=lib.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/admin/stats/lib.ts"],"sourcesContent":["/**\n * Gedeelde stats-helpers — gebruikt door het Statistieken-paneel én de Start-widget, zodat\n * paden en maanden op beide plekken hetzelfde heten.\n */\n\nexport interface MaandStat {\n id: string; // \"YYYY-MM\"\n total?: number;\n days?: Record<string, number>;\n paths?: Record<string, number>;\n}\n\n/** trackView slaat paden veilig op: \"home\" voor \"/\" en \"_\" voor \"/\". Terug naar leesbaar. */\nexport function padLabel(sleutel: string): string {\n return sleutel === \"home\" ? \"/\" : `/${sleutel.replace(/_/g, \"/\")}`;\n}\n\n/** \"2026-09\" → \"sep 2026\" (kort, voor as-labels en de maandkiezer). */\nexport function maandLabel(id: string, lang = false): string {\n const [j, m] = id.split(\"-\").map(Number);\n const d = new Date(Date.UTC(j, (m ?? 1) - 1, 1));\n return d.toLocaleDateString(\"nl-NL\", { month: lang ? \"long\" : \"short\", year: \"numeric\", timeZone: \"UTC\" });\n}\n\n/** Het maand-id van nu (UTC — trackView telt in UTC). */\nexport function huidigMaandId(): string {\n const nu = new Date();\n return `${nu.getUTCFullYear()}-${String(nu.getUTCMonth() + 1).padStart(2, \"0\")}`;\n}\n\n/** Aantal dagen in een \"YYYY-MM\"-maand. */\nexport function dagenInMaand(id: string): number {\n const [j, m] = id.split(\"-\").map(Number);\n return new Date(Date.UTC(j, m ?? 1, 0)).getUTCDate();\n}\n\n/** days-record → array per dag (index 0 = dag 1), met nullen voor ontbrekende dagen. */\nexport function dagReeks(stat: MaandStat | undefined, nDagen: number): number[] {\n return Array.from({ length: nDagen }, (_, i) => stat?.days?.[String(i + 1).padStart(2, \"0\")] ?? 0);\n}\n\n/** Top-N paden van een maand, genormaliseerd en gesorteerd. */\nexport function topPaden(stat: MaandStat | undefined, n: number): { label: string; waarde: number }[] {\n return Object.entries(stat?.paths ?? {})\n .sort((a, b) => b[1] - a[1])\n .slice(0, n)\n .map(([p, w]) => ({ label: padLabel(p), waarde: w }));\n}\n\n/**\n * Eerlijke maand-op-maand-delta: vergelijk t/m dezelfde dag. Halverwege de maand het hele\n * vorige maandtotaal ernaast leggen leest als \"−50%\" terwijl het gewoon de 15e is.\n */\nexport function deltaTovVorige(huidig: MaandStat | undefined, vorige: MaandStat | undefined, totDag: number): number | null {\n if (!huidig || !vorige) return null;\n const som = (s: MaandStat) =>\n Object.entries(s.days ?? {}).reduce((acc, [d, n]) => (Number(d) <= totDag ? acc + n : acc), 0);\n const nu = som(huidig);\n const toen = som(vorige);\n if (toen === 0) return null;\n return ((nu - toen) / toen) * 100;\n}\n"],"mappings":"AAaO,SAAS,SAAS,SAAyB;AAChD,SAAO,YAAY,SAAS,MAAM,IAAI,QAAQ,QAAQ,MAAM,GAAG,CAAC;AAClE;AAGO,SAAS,WAAW,IAAY,OAAO,OAAe;AAC3D,QAAM,CAAC,GAAG,CAAC,IAAI,GAAG,MAAM,GAAG,EAAE,IAAI,MAAM;AACvC,QAAM,IAAI,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,GAAG,CAAC,CAAC;AAC/C,SAAO,EAAE,mBAAmB,SAAS,EAAE,OAAO,OAAO,SAAS,SAAS,MAAM,WAAW,UAAU,MAAM,CAAC;AAC3G;AAGO,SAAS,gBAAwB;AACtC,QAAM,KAAK,oBAAI,KAAK;AACpB,SAAO,GAAG,GAAG,eAAe,CAAC,IAAI,OAAO,GAAG,YAAY,IAAI,CAAC,EAAE,SAAS,GAAG,GAAG,CAAC;AAChF;AAGO,SAAS,aAAa,IAAoB;AAC/C,QAAM,CAAC,GAAG,CAAC,IAAI,GAAG,MAAM,GAAG,EAAE,IAAI,MAAM;AACvC,SAAO,IAAI,KAAK,KAAK,IAAI,GAAG,KAAK,GAAG,CAAC,CAAC,EAAE,WAAW;AACrD;AAGO,SAAS,SAAS,MAA6B,QAA0B;AAC9E,SAAO,MAAM,KAAK,EAAE,QAAQ,OAAO,GAAG,CAAC,GAAG,MAAM,MAAM,OAAO,OAAO,IAAI,CAAC,EAAE,SAAS,GAAG,GAAG,CAAC,KAAK,CAAC;AACnG;AAGO,SAAS,SAAS,MAA6B,GAAgD;AACpG,SAAO,OAAO,QAAQ,MAAM,SAAS,CAAC,CAAC,EACpC,KAAK,CAAC,GAAG,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,EAC1B,MAAM,GAAG,CAAC,EACV,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,SAAS,CAAC,GAAG,QAAQ,EAAE,EAAE;AACxD;AAMO,SAAS,eAAe,QAA+B,QAA+B,QAA+B;AAC1H,MAAI,CAAC,UAAU,CAAC,OAAQ,QAAO;AAC/B,QAAM,MAAM,CAAC,MACX,OAAO,QAAQ,EAAE,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,MAAO,OAAO,CAAC,KAAK,SAAS,MAAM,IAAI,KAAM,CAAC;AAC/F,QAAM,KAAK,IAAI,MAAM;AACrB,QAAM,OAAO,IAAI,MAAM;AACvB,MAAI,SAAS,EAAG,QAAO;AACvB,UAAS,KAAK,QAAQ,OAAQ;AAChC;","names":[]}
@@ -0,0 +1,51 @@
1
+ export declare const CHART: {
2
+ readonly serie: "#3987e5";
3
+ readonly accent: "#184f95";
4
+ readonly wash: "rgba(57, 135, 229, 0.10)";
5
+ readonly track: "#e9f1fc";
6
+ readonly grid: "#e2e8f0";
7
+ readonly as: "#cbd5e1";
8
+ };
9
+ /**
10
+ * Kolomgrafiek voor één maand: dag 1..N. `accentIndex` (bv. vandaag) kleurt één kolom in de
11
+ * donkere stap van dezelfde ramp — zelfde reeks, gemarkeerd, geen tweede kleur.
12
+ */
13
+ export declare function DagStaven({ waarden, accentIndex, hoogte, titel, labelVan, }: {
14
+ /** Waarde per dag, index 0 = dag 1. */
15
+ waarden: number[];
16
+ accentIndex?: number;
17
+ hoogte?: number;
18
+ titel: string;
19
+ /** Tooltiplabel per index (standaard "dag N"). */
20
+ labelVan?: (i: number) => string;
21
+ }): import("react").JSX.Element;
22
+ /** Lijngrafiek over maanden: 2px lijn, wash-vulling, crosshair-tooltip, eindpunt gemarkeerd. */
23
+ export declare function MaandLijn({ punten, hoogte, titel, }: {
24
+ punten: {
25
+ label: string;
26
+ waarde: number;
27
+ }[];
28
+ hoogte?: number;
29
+ titel: string;
30
+ }): import("react").JSX.Element | null;
31
+ /** Horizontale balkjeslijst (toppagina's): label · waarde · balk. De track is een lichtere stap
32
+ * van dezelfde ramp, zodat einden vergelijkbaar zijn zonder raster. */
33
+ export declare function BalkjesLijst({ items }: {
34
+ items: {
35
+ label: string;
36
+ waarde: number;
37
+ }[];
38
+ }): import("react").JSX.Element;
39
+ /** Stat-tegel: label · grote waarde (proportionele cijfers) · optionele delta t.o.v. een genoemde periode. */
40
+ export declare function StatTegel({ label, waarde, delta, deltaLabel, }: {
41
+ label: string;
42
+ waarde: number | string;
43
+ /** Relatief verschil in procenten (bv. +12) — kleur volgt de richting (meer bezoek = goed). */
44
+ delta?: number | null;
45
+ deltaLabel?: string;
46
+ }): import("react").JSX.Element;
47
+ /** Toegankelijke tabelweergave achter een uitklapper — alles wat een grafiek toont, zonder muis. */
48
+ export declare function AlsTabel({ kop, rijen }: {
49
+ kop: [string, string];
50
+ rijen: [string, number][];
51
+ }): import("react").JSX.Element;
@@ -0,0 +1,240 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useId, useRef, useState } from "react";
4
+ const CHART = {
5
+ serie: "#3987e5",
6
+ accent: "#184f95",
7
+ wash: "rgba(57, 135, 229, 0.10)",
8
+ track: "#e9f1fc",
9
+ grid: "#e2e8f0",
10
+ as: "#cbd5e1"
11
+ };
12
+ const nf = new Intl.NumberFormat("nl-NL");
13
+ function netteMax(max) {
14
+ if (max <= 4) return 4;
15
+ const macht = 10 ** Math.floor(Math.log10(max));
16
+ for (const f of [1, 2, 5, 10]) if (max <= f * macht) return f * macht;
17
+ return 10 * macht;
18
+ }
19
+ function Tooltip({ t }) {
20
+ if (!t) return null;
21
+ return /* @__PURE__ */ jsxs(
22
+ "div",
23
+ {
24
+ className: "pointer-events-none absolute z-10 rounded-lg border border-slate-200 bg-white px-2.5 py-1.5 whitespace-nowrap shadow-md",
25
+ style: { left: t.x, top: Math.max(26, t.y), transform: "translate(-50%, calc(-100% - 8px))" },
26
+ role: "status",
27
+ children: [
28
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold text-slate-900", children: nf.format(t.waarde) }),
29
+ /* @__PURE__ */ jsx("span", { className: "ml-1.5 text-xs text-slate-500", children: t.titel })
30
+ ]
31
+ }
32
+ );
33
+ }
34
+ function Raster({ plotH }) {
35
+ return /* @__PURE__ */ jsxs("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-x-0 top-0", style: { height: plotH }, children: [
36
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-x-0", style: { top: 0, borderTop: `1px solid ${CHART.grid}` } }),
37
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-x-0", style: { top: plotH / 2, borderTop: `1px solid ${CHART.grid}` } }),
38
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-x-0", style: { top: plotH, borderTop: `1px solid ${CHART.as}` } })
39
+ ] });
40
+ }
41
+ function DagStaven({
42
+ waarden,
43
+ accentIndex,
44
+ hoogte = 150,
45
+ titel,
46
+ labelVan
47
+ }) {
48
+ const ref = useRef(null);
49
+ const [tip, setTip] = useState(null);
50
+ const [hover, setHover] = useState(null);
51
+ const uid = useId();
52
+ const n = Math.max(1, waarden.length);
53
+ const max = netteMax(Math.max(1, ...waarden));
54
+ const plotH = hoogte - 18;
55
+ const maxWaarde = Math.max(...waarden);
56
+ const maxIdx = waarden.indexOf(maxWaarde);
57
+ const label = labelVan ?? ((i) => `dag ${i + 1}`);
58
+ const move = (i) => {
59
+ const host = ref.current;
60
+ if (!host) return;
61
+ const r = host.getBoundingClientRect();
62
+ setHover(i);
63
+ setTip({ x: (i + 0.5) / n * r.width, y: plotH * (1 - waarden[i] / max), titel: label(i), waarde: waarden[i] });
64
+ };
65
+ const leave = () => {
66
+ setHover(null);
67
+ setTip(null);
68
+ };
69
+ return /* @__PURE__ */ jsxs("div", { ref, className: "relative", role: "img", "aria-labelledby": `${uid}-t`, style: { height: hoogte }, children: [
70
+ /* @__PURE__ */ jsx("span", { id: `${uid}-t`, className: "sr-only", children: titel }),
71
+ /* @__PURE__ */ jsx(Tooltip, { t: tip }),
72
+ /* @__PURE__ */ jsx(Raster, { plotH }),
73
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-x-0 top-0 flex items-end", style: { height: plotH, gap: 2 }, children: waarden.map((v, i) => /* @__PURE__ */ jsx(
74
+ "button",
75
+ {
76
+ type: "button",
77
+ tabIndex: v > 0 ? 0 : -1,
78
+ "aria-label": `${label(i)}: ${nf.format(v)} weergaven`,
79
+ className: "flex h-full min-w-0 flex-1 cursor-default items-end justify-center focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600",
80
+ onPointerMove: () => move(i),
81
+ onFocus: () => move(i),
82
+ onPointerLeave: leave,
83
+ onBlur: leave,
84
+ children: v > 0 && /* @__PURE__ */ jsx(
85
+ "span",
86
+ {
87
+ "aria-hidden": "true",
88
+ className: "block w-full",
89
+ style: {
90
+ maxWidth: 24,
91
+ height: Math.max(3, v / max * (plotH - 6)),
92
+ borderRadius: "4px 4px 0 0",
93
+ background: i === accentIndex ? CHART.accent : CHART.serie,
94
+ opacity: hover === null || hover === i ? 1 : 0.45,
95
+ transition: "opacity 0.1s"
96
+ }
97
+ }
98
+ )
99
+ },
100
+ i
101
+ )) }),
102
+ /* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-x-0", style: { top: plotH + 3 }, children: [0, 9, 19, n - 1].filter((v, i, a) => v >= 0 && v < n && a.indexOf(v) === i).map((i) => /* @__PURE__ */ jsx("span", { className: "absolute -translate-x-1/2 text-[10px] text-slate-400", style: { left: `${(i + 0.5) / n * 100}%` }, children: i + 1 }, i)) }),
103
+ maxWaarde > 0 && hover === null && /* @__PURE__ */ jsx(
104
+ "span",
105
+ {
106
+ "aria-hidden": "true",
107
+ className: "pointer-events-none absolute -translate-x-1/2 text-[10px] font-medium text-slate-500",
108
+ style: { left: `${(maxIdx + 0.5) / n * 100}%`, top: Math.max(0, plotH * (1 - maxWaarde / max) - 16) },
109
+ children: nf.format(maxWaarde)
110
+ }
111
+ ),
112
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "pointer-events-none absolute -top-0.5 right-0 bg-white pl-1 text-[10px] text-slate-400", style: { fontVariantNumeric: "tabular-nums" }, children: nf.format(max) })
113
+ ] });
114
+ }
115
+ function MaandLijn({
116
+ punten,
117
+ hoogte = 150,
118
+ titel
119
+ }) {
120
+ const ref = useRef(null);
121
+ const [tip, setTip] = useState(null);
122
+ const [kruis, setKruis] = useState(null);
123
+ const uid = useId();
124
+ const n = punten.length;
125
+ const max = netteMax(Math.max(1, ...punten.map((p) => p.waarde)));
126
+ const plotH = hoogte - 18;
127
+ const px = (i) => n === 1 ? 50 : 2 + i / (n - 1) * 96;
128
+ const py = (v) => 6 + (plotH - 12) * (1 - v / max);
129
+ const pad = punten.map((p, i) => `${i === 0 ? "M" : "L"} ${px(i)} ${py(p.waarde)}`).join(" ");
130
+ const move = (e) => {
131
+ const host = ref.current;
132
+ if (!host || n === 0) return;
133
+ const r = host.getBoundingClientRect();
134
+ const fx = (e.clientX - r.left) / r.width * 100;
135
+ const i = Math.max(0, Math.min(n - 1, Math.round((fx - 2) / 96 * (n - 1))));
136
+ setKruis(i);
137
+ setTip({ x: px(i) / 100 * r.width, y: py(punten[i].waarde), titel: punten[i].label, waarde: punten[i].waarde });
138
+ };
139
+ const leave = () => {
140
+ setKruis(null);
141
+ setTip(null);
142
+ };
143
+ if (n === 0) return null;
144
+ const focus = kruis ?? n - 1;
145
+ return /* @__PURE__ */ jsxs("div", { ref, className: "relative", role: "img", "aria-labelledby": `${uid}-t`, style: { height: hoogte }, onPointerMove: move, onPointerLeave: leave, children: [
146
+ /* @__PURE__ */ jsx("span", { id: `${uid}-t`, className: "sr-only", children: titel }),
147
+ /* @__PURE__ */ jsx(Tooltip, { t: tip }),
148
+ /* @__PURE__ */ jsx(Raster, { plotH }),
149
+ /* @__PURE__ */ jsxs("svg", { viewBox: `0 0 100 ${plotH}`, preserveAspectRatio: "none", className: "absolute inset-x-0 top-0 block w-full", style: { height: plotH }, "aria-hidden": "true", children: [
150
+ /* @__PURE__ */ jsx("path", { d: `${pad} L ${px(n - 1)} ${plotH} L ${px(0)} ${plotH} Z`, fill: CHART.wash }),
151
+ /* @__PURE__ */ jsx("path", { d: pad, fill: "none", stroke: CHART.serie, strokeWidth: "2", strokeLinejoin: "round", strokeLinecap: "round", vectorEffect: "non-scaling-stroke" }),
152
+ kruis !== null && /* @__PURE__ */ jsx("line", { x1: px(kruis), x2: px(kruis), y1: "0", y2: plotH, stroke: CHART.as, strokeWidth: "1", vectorEffect: "non-scaling-stroke" })
153
+ ] }),
154
+ /* @__PURE__ */ jsx(
155
+ "span",
156
+ {
157
+ "aria-hidden": "true",
158
+ className: "pointer-events-none absolute h-2 w-2 -translate-x-1/2 -translate-y-1/2 rounded-full",
159
+ style: {
160
+ left: `${px(focus)}%`,
161
+ top: py(punten[focus].waarde),
162
+ background: focus === n - 1 ? CHART.accent : CHART.serie,
163
+ boxShadow: "0 0 0 2px #ffffff"
164
+ }
165
+ }
166
+ ),
167
+ /* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: "pointer-events-none absolute inset-x-0", style: { top: plotH + 3 }, children: [0, Math.floor((n - 1) / 2), n - 1].filter((v, i, a) => a.indexOf(v) === i).map((i) => /* @__PURE__ */ jsx(
168
+ "span",
169
+ {
170
+ className: "absolute text-[10px] text-slate-400",
171
+ style: { left: `${px(i)}%`, transform: i === 0 ? "none" : i === n - 1 ? "translateX(-100%)" : "translateX(-50%)" },
172
+ children: punten[i].label
173
+ },
174
+ i
175
+ )) }),
176
+ kruis === null && /* @__PURE__ */ jsx(
177
+ "span",
178
+ {
179
+ "aria-hidden": "true",
180
+ className: "pointer-events-none absolute text-[11px] font-semibold text-slate-700",
181
+ style: { left: `${px(n - 1)}%`, top: Math.max(0, py(punten[n - 1].waarde) - 20), transform: "translateX(-100%)" },
182
+ children: nf.format(punten[n - 1].waarde)
183
+ }
184
+ ),
185
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "pointer-events-none absolute -top-0.5 right-0 bg-white pl-1 text-[10px] text-slate-400", style: { fontVariantNumeric: "tabular-nums" }, children: nf.format(max) })
186
+ ] });
187
+ }
188
+ function BalkjesLijst({ items }) {
189
+ const max = Math.max(1, ...items.map((i) => i.waarde));
190
+ return /* @__PURE__ */ jsx("ul", { className: "grid gap-2.5", children: items.map((it) => /* @__PURE__ */ jsxs("li", { className: "grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 gap-y-1", children: [
191
+ /* @__PURE__ */ jsx("span", { className: "truncate text-sm text-slate-700", title: it.label, children: it.label }),
192
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-slate-900", style: { fontVariantNumeric: "tabular-nums" }, children: nf.format(it.waarde) }),
193
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "col-span-2 block h-1.5 w-full overflow-hidden rounded-full", style: { background: CHART.track }, children: /* @__PURE__ */ jsx("span", { className: "block h-full rounded-full", style: { width: `${it.waarde / max * 100}%`, background: CHART.serie } }) })
194
+ ] }, it.label)) });
195
+ }
196
+ function StatTegel({
197
+ label,
198
+ waarde,
199
+ delta,
200
+ deltaLabel
201
+ }) {
202
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-slate-200 bg-white p-4", children: [
203
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: label }),
204
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-3xl font-semibold text-slate-900", children: typeof waarde === "number" ? nf.format(waarde) : waarde }),
205
+ delta != null && Number.isFinite(delta) && /* @__PURE__ */ jsxs("p", { className: `mt-1 text-xs font-medium ${delta >= 0 ? "text-emerald-700" : "text-red-700"}`, children: [
206
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: delta >= 0 ? "\u25B2" : "\u25BC" }),
207
+ " ",
208
+ Math.abs(Math.round(delta)),
209
+ "%",
210
+ deltaLabel ? /* @__PURE__ */ jsxs("span", { className: "font-normal text-slate-500", children: [
211
+ " ",
212
+ deltaLabel
213
+ ] }) : null
214
+ ] })
215
+ ] });
216
+ }
217
+ function AlsTabel({ kop, rijen }) {
218
+ return /* @__PURE__ */ jsxs("details", { className: "mt-2", children: [
219
+ /* @__PURE__ */ jsx("summary", { className: "cursor-pointer text-xs text-slate-400 hover:text-slate-600", children: "Bekijk als tabel" }),
220
+ /* @__PURE__ */ jsxs("table", { className: "mt-2 w-full text-sm", children: [
221
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "text-left text-xs text-slate-500", children: [
222
+ /* @__PURE__ */ jsx("th", { className: "py-1 font-medium", children: kop[0] }),
223
+ /* @__PURE__ */ jsx("th", { className: "py-1 text-right font-medium", children: kop[1] })
224
+ ] }) }),
225
+ /* @__PURE__ */ jsx("tbody", { children: rijen.map(([l, w]) => /* @__PURE__ */ jsxs("tr", { className: "border-t border-slate-100", children: [
226
+ /* @__PURE__ */ jsx("td", { className: "py-1 text-slate-700", children: l }),
227
+ /* @__PURE__ */ jsx("td", { className: "py-1 text-right text-slate-900", style: { fontVariantNumeric: "tabular-nums" }, children: nf.format(w) })
228
+ ] }, l)) })
229
+ ] })
230
+ ] });
231
+ }
232
+ export {
233
+ AlsTabel,
234
+ BalkjesLijst,
235
+ CHART,
236
+ DagStaven,
237
+ MaandLijn,
238
+ StatTegel
239
+ };
240
+ //# sourceMappingURL=charts.js.map