@xiwebdesign/xicms-core 7.13.0 → 7.14.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.
- package/dist/admin/lib/mediaUsage.d.ts +17 -0
- package/dist/admin/lib/mediaUsage.js +32 -1
- package/dist/admin/lib/mediaUsage.js.map +1 -1
- package/dist/admin/media/MediaManager.js +74 -17
- package/dist/admin/media/MediaManager.js.map +1 -1
- package/dist/admin/visual/VisualEditor.js +37 -3
- package/dist/admin/visual/VisualEditor.js.map +1 -1
- package/dist/blocks/BlockErrorBoundary.d.ts +1 -0
- package/dist/blocks/BlockErrorBoundary.js +4 -0
- package/dist/blocks/BlockErrorBoundary.js.map +1 -1
- package/dist/blocks/BlockRenderer.js +1 -1
- package/dist/blocks/BlockRenderer.js.map +1 -1
- package/dist/blocks/reportRenderFout.d.ts +13 -0
- package/dist/blocks/reportRenderFout.js +31 -0
- package/dist/blocks/reportRenderFout.js.map +1 -0
- package/dist/modules/shop/blocks/Cart.js +1 -2
- package/dist/modules/shop/blocks/Cart.js.map +1 -1
- package/dist/modules/shop/blocks/Checkout.js +1 -2
- package/dist/modules/shop/blocks/Checkout.js.map +1 -1
- package/dist/modules/shop/blocks/Donation.js +1 -2
- package/dist/modules/shop/blocks/Donation.js.map +1 -1
- package/dist/modules/shop/blocks/OrderStatus.js +0 -2
- package/dist/modules/shop/blocks/OrderStatus.js.map +1 -1
- package/dist/modules/shop/blocks/ProductDetail.js +1 -7
- package/dist/modules/shop/blocks/ProductDetail.js.map +1 -1
- package/dist/modules/shop/blocks/ProductGrid.js +1 -2
- package/dist/modules/shop/blocks/ProductGrid.js.map +1 -1
- package/dist/modules/shop/blocks/ShopCategories.js +0 -2
- package/dist/modules/shop/blocks/ShopCategories.js.map +1 -1
- package/dist/modules/shop/module.js +1 -0
- package/dist/modules/shop/module.js.map +1 -1
- package/dist/modules/shop/schemas.d.ts +1 -0
- package/dist/modules/shop/schemas.js +32 -0
- package/dist/modules/shop/schemas.js.map +1 -0
- package/dist/modules/types.d.ts +6 -0
- package/dist/schema/shop.d.ts +1 -11
- package/dist/schema/shop.js +1 -20
- package/dist/schema/shop.js.map +1 -1
- package/dist/schema/shopGeld.d.ts +18 -0
- package/dist/schema/shopGeld.js +27 -0
- package/dist/schema/shopGeld.js.map +1 -0
- package/dist/styles.css +17 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -8,3 +8,20 @@ export declare function findMediaUsage(siteId: string, item: {
|
|
|
8
8
|
id: string;
|
|
9
9
|
url?: string;
|
|
10
10
|
}): Promise<string[]>;
|
|
11
|
+
/**
|
|
12
|
+
* Bulk-variant voor het opruim-paneel (7.14): één content-read voor de HELE bibliotheek in
|
|
13
|
+
* plaats van (zoals `findMediaUsage`) alle content per afbeelding opnieuw te lezen — bij
|
|
14
|
+
* 60 foto's scheelt dat ~59 volledige site-reads. Kijkt in meer bronnen dan de losse check:
|
|
15
|
+
* ook symbolen en producten kunnen naar een foto verwijzen.
|
|
16
|
+
*
|
|
17
|
+
* "Ongebruikt" is bewust voorzichtig: een item dat nog verwerkt wordt (status "processing")
|
|
18
|
+
* of jonger is dan een uur telt níét mee — een net geüploade foto die de klant nog in een
|
|
19
|
+
* blok gaat zetten, hoort niet in een opruimlijst.
|
|
20
|
+
*/
|
|
21
|
+
export declare function findUnusedMedia(siteId: string, items: Array<{
|
|
22
|
+
id: string;
|
|
23
|
+
url?: string;
|
|
24
|
+
status?: string;
|
|
25
|
+
createdAt?: unknown;
|
|
26
|
+
size?: number;
|
|
27
|
+
}>): Promise<string[]>;
|
|
@@ -30,7 +30,38 @@ async function findMediaUsage(siteId, item) {
|
|
|
30
30
|
return hits;
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
+
async function findUnusedMedia(siteId, items) {
|
|
34
|
+
const db = getDb();
|
|
35
|
+
if (!db) return [];
|
|
36
|
+
const blobs = [];
|
|
37
|
+
const [pages, drafts, news, symbols, products, settings] = await Promise.all([
|
|
38
|
+
getDocs(collection(db, "sites", siteId, "pages")),
|
|
39
|
+
getDocs(collection(db, "sites", siteId, "drafts")).catch(() => null),
|
|
40
|
+
getDocs(collection(db, "sites", siteId, "news")).catch(() => null),
|
|
41
|
+
getDocs(collection(db, "sites", siteId, "symbols")).catch(() => null),
|
|
42
|
+
getDocs(collection(db, "sites", siteId, "products")).catch(() => null),
|
|
43
|
+
getDoc(doc(db, "sites", siteId, "settings", "public")).catch(() => null)
|
|
44
|
+
]);
|
|
45
|
+
for (const snap of [pages, drafts, news, symbols, products]) {
|
|
46
|
+
for (const d of snap?.docs ?? []) blobs.push(JSON.stringify(d.data()));
|
|
47
|
+
}
|
|
48
|
+
if (settings?.exists()) blobs.push(JSON.stringify(settings.data()));
|
|
49
|
+
const corpus = blobs.join("\n");
|
|
50
|
+
const uurGeleden = Date.now() - 60 * 60 * 1e3;
|
|
51
|
+
const millis = (v) => {
|
|
52
|
+
const t = v;
|
|
53
|
+
return typeof t?.toMillis === "function" ? t.toMillis() : 0;
|
|
54
|
+
};
|
|
55
|
+
return items.filter((it) => {
|
|
56
|
+
if (it.status === "processing") return false;
|
|
57
|
+
if (millis(it.createdAt) > uurGeleden) return false;
|
|
58
|
+
if (it.url && corpus.includes(it.url)) return false;
|
|
59
|
+
if (corpus.includes(it.id)) return false;
|
|
60
|
+
return true;
|
|
61
|
+
}).map((it) => it.id);
|
|
62
|
+
}
|
|
33
63
|
export {
|
|
34
|
-
findMediaUsage
|
|
64
|
+
findMediaUsage,
|
|
65
|
+
findUnusedMedia
|
|
35
66
|
};
|
|
36
67
|
//# sourceMappingURL=mediaUsage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/lib/mediaUsage.ts"],"sourcesContent":["\"use client\";\n\nimport { collection, doc, getDoc, getDocs } from \"firebase/firestore\";\nimport { getDb } from \"../../firebase/client\";\n\n/**\n * Zoekt (goedkoop) of een media-item ergens in gebruik is, zodat we kunnen waarschuwen vóór\n * verwijderen (anders breekt een live pagina met een dode afbeelding). Gescand worden:\n * gepubliceerde pagina's, concepten (drafts), nieuwsartikelen (covers/inline beelden) en de\n * site-instellingen (og-beeld e.d.). Sites zijn klein → een JSON-substring-scan is ruim voldoende.\n */\nexport async function findMediaUsage(\n siteId: string,\n item: { id: string; url?: string },\n): Promise<string[]> {\n const db = getDb();\n if (!db) return [];\n const matches = (blob: string) => (item.url && blob.includes(item.url)) || blob.includes(item.id);\n const hits: string[] = [];\n try {\n const [pages, drafts, news, settings] = await Promise.all([\n getDocs(collection(db, \"sites\", siteId, \"pages\")),\n getDocs(collection(db, \"sites\", siteId, \"drafts\")).catch(() => null),\n getDocs(collection(db, \"sites\", siteId, \"news\")).catch(() => null),\n getDoc(doc(db, \"sites\", siteId, \"settings\", \"public\")).catch(() => null),\n ]);\n for (const d of pages.docs) {\n const data = d.data() as Record<string, unknown>;\n if (matches(JSON.stringify(data.blocks ?? []))) hits.push(String(data.path ?? data.title ?? d.id));\n }\n for (const d of drafts?.docs ?? []) {\n const data = d.data() as Record<string, unknown>;\n if (matches(JSON.stringify(data.blocks ?? []))) hits.push(`${String(data.path ?? d.id)} (concept)`);\n }\n for (const d of news?.docs ?? []) {\n if (matches(JSON.stringify(d.data()))) hits.push(`nieuws: ${String((d.data() as { title?: string }).title ?? d.id)}`);\n }\n if (settings?.exists() && matches(JSON.stringify(settings.data()))) hits.push(\"site-instellingen\");\n return [...new Set(hits)];\n } catch {\n return hits;\n }\n}\n"],"mappings":";AAEA,SAAS,YAAY,KAAK,QAAQ,eAAe;AACjD,SAAS,aAAa;AAQtB,eAAsB,eACpB,QACA,MACmB;AACnB,QAAM,KAAK,MAAM;AACjB,MAAI,CAAC,GAAI,QAAO,CAAC;AACjB,QAAM,UAAU,CAAC,SAAkB,KAAK,OAAO,KAAK,SAAS,KAAK,GAAG,KAAM,KAAK,SAAS,KAAK,EAAE;AAChG,QAAM,OAAiB,CAAC;AACxB,MAAI;AACF,UAAM,CAAC,OAAO,QAAQ,MAAM,QAAQ,IAAI,MAAM,QAAQ,IAAI;AAAA,MACxD,QAAQ,WAAW,IAAI,SAAS,QAAQ,OAAO,CAAC;AAAA,MAChD,QAAQ,WAAW,IAAI,SAAS,QAAQ,QAAQ,CAAC,EAAE,MAAM,MAAM,IAAI;AAAA,MACnE,QAAQ,WAAW,IAAI,SAAS,QAAQ,MAAM,CAAC,EAAE,MAAM,MAAM,IAAI;AAAA,MACjE,OAAO,IAAI,IAAI,SAAS,QAAQ,YAAY,QAAQ,CAAC,EAAE,MAAM,MAAM,IAAI;AAAA,IACzE,CAAC;AACD,eAAW,KAAK,MAAM,MAAM;AAC1B,YAAM,OAAO,EAAE,KAAK;AACpB,UAAI,QAAQ,KAAK,UAAU,KAAK,UAAU,CAAC,CAAC,CAAC,EAAG,MAAK,KAAK,OAAO,KAAK,QAAQ,KAAK,SAAS,EAAE,EAAE,CAAC;AAAA,IACnG;AACA,eAAW,KAAK,QAAQ,QAAQ,CAAC,GAAG;AAClC,YAAM,OAAO,EAAE,KAAK;AACpB,UAAI,QAAQ,KAAK,UAAU,KAAK,UAAU,CAAC,CAAC,CAAC,EAAG,MAAK,KAAK,GAAG,OAAO,KAAK,QAAQ,EAAE,EAAE,CAAC,YAAY;AAAA,IACpG;AACA,eAAW,KAAK,MAAM,QAAQ,CAAC,GAAG;AAChC,UAAI,QAAQ,KAAK,UAAU,EAAE,KAAK,CAAC,CAAC,EAAG,MAAK,KAAK,WAAW,OAAQ,EAAE,KAAK,EAAyB,SAAS,EAAE,EAAE,CAAC,EAAE;AAAA,IACtH;AACA,QAAI,UAAU,OAAO,KAAK,QAAQ,KAAK,UAAU,SAAS,KAAK,CAAC,CAAC,EAAG,MAAK,KAAK,mBAAmB;AACjG,WAAO,CAAC,GAAG,IAAI,IAAI,IAAI,CAAC;AAAA,EAC1B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/lib/mediaUsage.ts"],"sourcesContent":["\"use client\";\n\nimport { collection, doc, getDoc, getDocs } from \"firebase/firestore\";\nimport { getDb } from \"../../firebase/client\";\n\n/**\n * Zoekt (goedkoop) of een media-item ergens in gebruik is, zodat we kunnen waarschuwen vóór\n * verwijderen (anders breekt een live pagina met een dode afbeelding). Gescand worden:\n * gepubliceerde pagina's, concepten (drafts), nieuwsartikelen (covers/inline beelden) en de\n * site-instellingen (og-beeld e.d.). Sites zijn klein → een JSON-substring-scan is ruim voldoende.\n */\nexport async function findMediaUsage(\n siteId: string,\n item: { id: string; url?: string },\n): Promise<string[]> {\n const db = getDb();\n if (!db) return [];\n const matches = (blob: string) => (item.url && blob.includes(item.url)) || blob.includes(item.id);\n const hits: string[] = [];\n try {\n const [pages, drafts, news, settings] = await Promise.all([\n getDocs(collection(db, \"sites\", siteId, \"pages\")),\n getDocs(collection(db, \"sites\", siteId, \"drafts\")).catch(() => null),\n getDocs(collection(db, \"sites\", siteId, \"news\")).catch(() => null),\n getDoc(doc(db, \"sites\", siteId, \"settings\", \"public\")).catch(() => null),\n ]);\n for (const d of pages.docs) {\n const data = d.data() as Record<string, unknown>;\n if (matches(JSON.stringify(data.blocks ?? []))) hits.push(String(data.path ?? data.title ?? d.id));\n }\n for (const d of drafts?.docs ?? []) {\n const data = d.data() as Record<string, unknown>;\n if (matches(JSON.stringify(data.blocks ?? []))) hits.push(`${String(data.path ?? d.id)} (concept)`);\n }\n for (const d of news?.docs ?? []) {\n if (matches(JSON.stringify(d.data()))) hits.push(`nieuws: ${String((d.data() as { title?: string }).title ?? d.id)}`);\n }\n if (settings?.exists() && matches(JSON.stringify(settings.data()))) hits.push(\"site-instellingen\");\n return [...new Set(hits)];\n } catch {\n return hits;\n }\n}\n\n/**\n * Bulk-variant voor het opruim-paneel (7.14): één content-read voor de HELE bibliotheek in\n * plaats van (zoals `findMediaUsage`) alle content per afbeelding opnieuw te lezen — bij\n * 60 foto's scheelt dat ~59 volledige site-reads. Kijkt in meer bronnen dan de losse check:\n * ook symbolen en producten kunnen naar een foto verwijzen.\n *\n * \"Ongebruikt\" is bewust voorzichtig: een item dat nog verwerkt wordt (status \"processing\")\n * of jonger is dan een uur telt níét mee — een net geüploade foto die de klant nog in een\n * blok gaat zetten, hoort niet in een opruimlijst.\n */\nexport async function findUnusedMedia(\n siteId: string,\n items: Array<{ id: string; url?: string; status?: string; createdAt?: unknown; size?: number }>,\n): Promise<string[]> {\n const db = getDb();\n if (!db) return [];\n const blobs: string[] = [];\n const [pages, drafts, news, symbols, products, settings] = await Promise.all([\n getDocs(collection(db, \"sites\", siteId, \"pages\")),\n getDocs(collection(db, \"sites\", siteId, \"drafts\")).catch(() => null),\n getDocs(collection(db, \"sites\", siteId, \"news\")).catch(() => null),\n getDocs(collection(db, \"sites\", siteId, \"symbols\")).catch(() => null),\n getDocs(collection(db, \"sites\", siteId, \"products\")).catch(() => null),\n getDoc(doc(db, \"sites\", siteId, \"settings\", \"public\")).catch(() => null),\n ]);\n for (const snap of [pages, drafts, news, symbols, products]) {\n for (const d of snap?.docs ?? []) blobs.push(JSON.stringify(d.data()));\n }\n if (settings?.exists()) blobs.push(JSON.stringify(settings.data()));\n const corpus = blobs.join(\"\\n\");\n\n const uurGeleden = Date.now() - 60 * 60 * 1000;\n const millis = (v: unknown): number => {\n const t = v as { toMillis?: () => number } | null;\n return typeof t?.toMillis === \"function\" ? t.toMillis() : 0;\n };\n return items\n .filter((it) => {\n if (it.status === \"processing\") return false;\n if (millis(it.createdAt) > uurGeleden) return false;\n if (it.url && corpus.includes(it.url)) return false;\n if (corpus.includes(it.id)) return false;\n return true;\n })\n .map((it) => it.id);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,5 +1,5 @@
|
|
|
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, useRef, useState } from "react";
|
|
4
4
|
import {
|
|
5
5
|
collection,
|
|
@@ -18,13 +18,15 @@ import { Button, Card, Badge, Input } from "../ui/primitives.js";
|
|
|
18
18
|
import { useToast } from "../ui/toast.js";
|
|
19
19
|
import { useConfirm } from "../ui/ConfirmDialog.js";
|
|
20
20
|
import { uploadImage } from "../lib/upload.js";
|
|
21
|
-
import { findMediaUsage } from "../lib/mediaUsage.js";
|
|
21
|
+
import { findMediaUsage, findUnusedMedia } from "../lib/mediaUsage.js";
|
|
22
22
|
const PAGE_SIZE = 48;
|
|
23
23
|
function MediaManager({ siteId }) {
|
|
24
24
|
const { toast } = useToast();
|
|
25
25
|
const confirm = useConfirm();
|
|
26
26
|
const [items, setItems] = useState([]);
|
|
27
27
|
const [busy, setBusy] = useState(false);
|
|
28
|
+
const [ongebruikt, setOngebruikt] = useState(null);
|
|
29
|
+
const [scant, setScant] = useState(false);
|
|
28
30
|
const [pageCount, setPageCount] = useState(1);
|
|
29
31
|
const [hasMore, setHasMore] = useState(false);
|
|
30
32
|
const inputRef = useRef(null);
|
|
@@ -89,28 +91,83 @@ function MediaManager({ siteId }) {
|
|
|
89
91
|
});
|
|
90
92
|
if (!ok) return;
|
|
91
93
|
try {
|
|
92
|
-
await
|
|
93
|
-
if (bucket) {
|
|
94
|
-
const paths = [
|
|
95
|
-
item.path,
|
|
96
|
-
item.variants?.webp?.path,
|
|
97
|
-
...(item.variants?.sizes ?? []).map((v) => v.path)
|
|
98
|
-
].filter((p) => Boolean(p));
|
|
99
|
-
await Promise.all(
|
|
100
|
-
paths.map((p) => deleteObject(storageRef(bucket, p)).catch(
|
|
101
|
-
() => void 0
|
|
102
|
-
/* al weg */
|
|
103
|
-
))
|
|
104
|
-
);
|
|
105
|
-
}
|
|
94
|
+
await verwijderItem(item);
|
|
106
95
|
toast({ title: "Verwijderd" });
|
|
107
96
|
} catch (e) {
|
|
108
97
|
toast({ title: "Verwijderen mislukt", description: e.message, tone: "error" });
|
|
109
98
|
}
|
|
110
99
|
}
|
|
100
|
+
async function verwijderItem(item) {
|
|
101
|
+
const db = getDb();
|
|
102
|
+
const bucket = getBucket();
|
|
103
|
+
if (!db) throw new Error("Geen databaseverbinding.");
|
|
104
|
+
await deleteDoc(doc(db, "sites", siteId, "media", item.id));
|
|
105
|
+
if (bucket) {
|
|
106
|
+
const paths = [
|
|
107
|
+
item.path,
|
|
108
|
+
item.variants?.webp?.path,
|
|
109
|
+
...(item.variants?.sizes ?? []).map((v) => v.path)
|
|
110
|
+
].filter((p) => Boolean(p));
|
|
111
|
+
await Promise.all(
|
|
112
|
+
paths.map((p) => deleteObject(storageRef(bucket, p)).catch(
|
|
113
|
+
() => void 0
|
|
114
|
+
/* al weg */
|
|
115
|
+
))
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function scanOngebruikt() {
|
|
120
|
+
setScant(true);
|
|
121
|
+
try {
|
|
122
|
+
const ids = await findUnusedMedia(siteId, items);
|
|
123
|
+
setOngebruikt(ids);
|
|
124
|
+
if (ids.length === 0) toast({ title: "Niets op te ruimen", description: "Elke afbeelding in de bibliotheek wordt ergens gebruikt.", tone: "success" });
|
|
125
|
+
} catch (e) {
|
|
126
|
+
toast({ title: "Scan mislukt", description: e.message, tone: "error" });
|
|
127
|
+
} finally {
|
|
128
|
+
setScant(false);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function ruimOngebruiktOp() {
|
|
132
|
+
const doelen = items.filter((it) => ongebruikt?.includes(it.id));
|
|
133
|
+
if (doelen.length === 0) return;
|
|
134
|
+
const mb = (doelen.reduce((som, it) => som + (it.size ?? 0), 0) / (1024 * 1024)).toFixed(1);
|
|
135
|
+
const ok = await confirm({
|
|
136
|
+
title: `${doelen.length} ongebruikte afbeelding(en) verwijderen?`,
|
|
137
|
+
description: `Samen \xB1${mb} MB, inclusief alle automatisch gemaakte varianten. Dit kan niet ongedaan worden gemaakt.`,
|
|
138
|
+
confirmLabel: "Opruimen",
|
|
139
|
+
tone: "danger"
|
|
140
|
+
});
|
|
141
|
+
if (!ok) return;
|
|
142
|
+
setBusy(true);
|
|
143
|
+
let gelukt = 0;
|
|
144
|
+
const fouten = [];
|
|
145
|
+
for (const it of doelen) {
|
|
146
|
+
try {
|
|
147
|
+
await verwijderItem(it);
|
|
148
|
+
gelukt++;
|
|
149
|
+
} catch (e) {
|
|
150
|
+
fouten.push(`${it.name}: ${e.message}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
setOngebruikt(null);
|
|
154
|
+
setBusy(false);
|
|
155
|
+
if (gelukt) toast({ title: `${gelukt} afbeelding(en) opgeruimd`, description: `\xB1${mb} MB opslag vrijgemaakt.`, tone: "success" });
|
|
156
|
+
if (fouten.length) toast({ title: "Niet alles kon weg", description: fouten.join(" \xB7 "), tone: "error" });
|
|
157
|
+
}
|
|
111
158
|
return /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
|
|
112
159
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
113
160
|
/* @__PURE__ */ jsx(Button, { disabled: busy, onClick: () => inputRef.current?.click(), children: busy ? "Uploaden\u2026" : "Foto's uploaden" }),
|
|
161
|
+
" ",
|
|
162
|
+
!isDemoActive() && items.length > 0 && (ongebruikt === null ? /* @__PURE__ */ jsx(Button, { variant: "ghost", disabled: scant || busy, onClick: () => void scanOngebruikt(), children: scant ? "Zoeken\u2026" : "\u{1F9F9} Zoek ongebruikte foto's" }) : ongebruikt.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
163
|
+
/* @__PURE__ */ jsxs(Button, { variant: "secondary", disabled: busy, onClick: () => void ruimOngebruiktOp(), children: [
|
|
164
|
+
"\u{1F9F9} ",
|
|
165
|
+
ongebruikt.length,
|
|
166
|
+
" ongebruikte verwijderen"
|
|
167
|
+
] }),
|
|
168
|
+
" ",
|
|
169
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", onClick: () => setOngebruikt(null), children: "Annuleren" })
|
|
170
|
+
] }) : null),
|
|
114
171
|
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-slate-500", children: "Je kunt meerdere foto's tegelijk kiezen. Ze worden automatisch verkleind en geoptimaliseerd (webp) \u2014 dat houdt je site snel en bespaart opslag. Max. 15 MB per bestand." }),
|
|
115
172
|
/* @__PURE__ */ jsx(
|
|
116
173
|
"input",
|
|
@@ -140,7 +197,7 @@ function MediaManager({ siteId }) {
|
|
|
140
197
|
m.height
|
|
141
198
|
] })
|
|
142
199
|
] }),
|
|
143
|
-
m.status === "processing" ? /* @__PURE__ */ jsx(Badge, { tone: "amber", children: "verwerken" }) : /* @__PURE__ */ jsx(Badge, { tone: "green", children: "klaar" })
|
|
200
|
+
m.status === "processing" ? /* @__PURE__ */ jsx(Badge, { tone: "amber", children: "verwerken" }) : ongebruikt?.includes(m.id) ? /* @__PURE__ */ jsx(Badge, { tone: "amber", children: "ongebruikt" }) : /* @__PURE__ */ jsx(Badge, { tone: "green", children: "klaar" })
|
|
144
201
|
] }),
|
|
145
202
|
/* @__PURE__ */ jsx(AltInput, { item: m, onSave: saveAlt }),
|
|
146
203
|
/* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/media/MediaManager.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport {\n collection,\n deleteDoc,\n doc,\n limit,\n onSnapshot,\n orderBy,\n query,\n updateDoc,\n} from \"firebase/firestore\";\nimport { deleteObject, ref as storageRef } from \"firebase/storage\";\nimport { getBucket, getDb } from \"../../firebase/client\";\nimport { DEMO_EVENT, isDemoActive, listDemoMediaAll } from \"../../demo/demoStore\";\nimport type { MediaItem } from \"../../content/types\";\nimport { Button, Card, Badge, Input } from \"../ui/primitives\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { uploadImage } from \"../lib/upload\";\nimport { findMediaUsage } from \"../lib/mediaUsage\";\n\n/** Paginagrootte van de bibliotheek: begrensd laden i.p.v. álles (kosten + geheugen bij 100+ foto's). */\nconst PAGE_SIZE = 48;\n\nexport function MediaManager({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const [items, setItems] = useState<MediaItem[]>([]);\n const [busy, setBusy] = useState(false);\n const [pageCount, setPageCount] = useState(1);\n const [hasMore, setHasMore] = useState(false);\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n // Demo-modus: lokale bibliotheek (geseede foto's + eigen \"uploads\") i.p.v. Firestore.\n if (isDemoActive()) {\n const sync = () => setItems(listDemoMediaAll(siteId));\n sync();\n window.addEventListener(DEMO_EVENT, sync);\n return () => window.removeEventListener(DEMO_EVENT, sync);\n }\n const db = getDb();\n if (!db) return;\n // limit + \"meer laden\" (pageCount groeit): geen volledige collectie-read per open.\n const max = pageCount * PAGE_SIZE;\n const q = query(collection(db, \"sites\", siteId, \"media\"), orderBy(\"createdAt\", \"desc\"), limit(max + 1));\n return onSnapshot(\n q,\n (snap) => {\n setHasMore(snap.docs.length > max);\n setItems(snap.docs.slice(0, max).map((d) => ({ id: d.id, ...(d.data() as Omit<MediaItem, \"id\">) })));\n },\n // Leesfouten (rules/App Check) niet stil slikken — anders lijkt de bibliotheek leeg.\n (err) => toast({ title: \"Kon mediabibliotheek niet laden\", description: err.message, tone: \"error\" }),\n );\n }, [siteId, toast, pageCount]);\n\n async function onFiles(files: FileList) {\n setBusy(true);\n let ok = 0;\n const errors: string[] = [];\n for (const file of Array.from(files)) {\n try {\n await uploadImage(siteId, file);\n ok++;\n } catch (e) {\n errors.push(`${file.name}: ${(e as Error).message}`);\n }\n }\n if (ok > 0) toast({ title: `${ok} foto('s) geüpload`, description: \"Automatisch verkleind; varianten worden op de achtergrond gemaakt.\", tone: \"success\" });\n if (errors.length) toast({ title: \"Sommige uploads mislukten\", description: errors.join(\" · \"), tone: \"error\" });\n setBusy(false);\n }\n\n async function saveAlt(item: MediaItem, alt: string) {\n const db = getDb();\n if (!db) return;\n if ((item.alt ?? \"\") === alt) return; // niets gewijzigd → geen write/toast\n try {\n await updateDoc(doc(db, \"sites\", siteId, \"media\", item.id), { alt });\n toast({ title: \"Alt-tekst opgeslagen\", tone: \"success\" });\n } catch (e) {\n // Niet stil slikken: de klant dacht anders dat de alt-tekst opgeslagen was.\n toast({ title: \"Alt-tekst opslaan mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n async function remove(item: MediaItem) {\n const db = getDb();\n const bucket = getBucket();\n if (!db) return;\n const usedOn = await findMediaUsage(siteId, item);\n const ok = await confirm({\n title: \"Afbeelding verwijderen?\",\n description:\n usedOn.length > 0\n ? `Let op: deze afbeelding wordt gebruikt op ${usedOn.length} gepubliceerde pagina('s) (${usedOn.join(\", \")}). Daar verschijnt dan een gebroken afbeelding.`\n : \"De afbeelding wordt definitief verwijderd.\",\n confirmLabel: \"Verwijderen\",\n tone: \"danger\",\n });\n if (!ok) return;\n try {\n await deleteDoc(doc(db, \"sites\", siteId, \"media\", item.id));\n if (bucket) {\n // Ook de srcset-varianten opruimen die de Cloud Function heeft gemaakt — anders blijven\n // die (publieke) bestanden voor altijd in Storage staan.\n const paths = [\n item.path,\n item.variants?.webp?.path,\n ...(item.variants?.sizes ?? []).map((v) => v.path),\n ].filter((p): p is string => Boolean(p));\n await Promise.all(\n paths.map((p) => deleteObject(storageRef(bucket, p)).catch(() => undefined /* al weg */)),\n );\n }\n toast({ title: \"Verwijderd\" });\n } catch (e) {\n // Rules/offline-fout niet stil slikken — anders lijkt verwijderen \"niets te doen\".\n toast({ title: \"Verwijderen mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n return (\n <div className=\"grid gap-4\">\n <div>\n <Button disabled={busy} onClick={() => inputRef.current?.click()}>\n {busy ? \"Uploaden…\" : \"Foto's uploaden\"}\n </Button>\n <p className=\"mt-1 text-xs text-slate-500\">\n Je kunt meerdere foto's tegelijk kiezen. Ze worden automatisch verkleind en geoptimaliseerd\n (webp) — dat houdt je site snel en bespaart opslag. Max. 15 MB per bestand.\n </p>\n <input\n ref={inputRef}\n type=\"file\"\n accept=\"image/*\"\n multiple\n hidden\n onChange={(e) => {\n if (e.target.files?.length) void onFiles(e.target.files);\n e.target.value = \"\";\n }}\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4\">\n {items.map((m) => (\n <Card key={m.id} className=\"p-3\">\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img src={m.url} alt={m.alt ?? m.name} className=\"mb-2 h-28 w-full rounded-md object-cover\" />\n <div className=\"mb-2 flex items-center justify-between gap-1\">\n <span className=\"truncate text-xs text-slate-500\">\n {m.name}\n {(m as { width?: number; height?: number }).width && (\n <span className=\"text-slate-400\"> · {(m as { width?: number }).width}×{(m as { height?: number }).height}</span>\n )}\n </span>\n {m.status === \"processing\" ? <Badge tone=\"amber\">verwerken</Badge> : <Badge tone=\"green\">klaar</Badge>}\n </div>\n <AltInput item={m} onSave={saveAlt} />\n <div className=\"flex gap-1\">\n <Button variant=\"ghost\" size=\"sm\" className=\"text-xs\" onClick={() => void navigator.clipboard?.writeText(m.url)}>\n Kopieer URL\n </Button>\n <Button variant=\"ghost\" size=\"sm\" className=\"text-xs\" onClick={() => void remove(m)}>\n Verwijder\n </Button>\n </div>\n </Card>\n ))}\n {items.length === 0 && <p className=\"text-sm text-slate-500\">Nog geen media.</p>}\n </div>\n {hasMore && (\n <div>\n <Button variant=\"secondary\" size=\"sm\" onClick={() => setPageCount((c) => c + 1)}>\n Meer laden…\n </Button>\n </div>\n )}\n </div>\n );\n}\n\n/** Controlled alt-veld: volgt externe updates (realtime snapshot) en slaat alleen échte wijzigingen op. */\nfunction AltInput({ item, onSave }: { item: MediaItem; onSave: (item: MediaItem, alt: string) => Promise<void> }) {\n const [value, setValue] = useState(item.alt ?? \"\");\n // Extern gewijzigd (andere tab/verwerking) → veld bijtrekken, behalve terwijl de klant zelf typt.\n const [focused, setFocused] = useState(false);\n useEffect(() => {\n if (!focused) setValue(item.alt ?? \"\");\n }, [item.alt, focused]);\n return (\n <Input\n value={value}\n placeholder=\"Alt-tekst\"\n className=\"mb-2 h-8 text-xs\"\n onChange={(e) => setValue(e.target.value)}\n onFocus={() => setFocused(true)}\n onBlur={() => {\n setFocused(false);\n void onSave(item, value);\n }}\n />\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/media/MediaManager.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport {\n collection,\n deleteDoc,\n doc,\n limit,\n onSnapshot,\n orderBy,\n query,\n updateDoc,\n} from \"firebase/firestore\";\nimport { deleteObject, ref as storageRef } from \"firebase/storage\";\nimport { getBucket, getDb } from \"../../firebase/client\";\nimport { DEMO_EVENT, isDemoActive, listDemoMediaAll } from \"../../demo/demoStore\";\nimport type { MediaItem } from \"../../content/types\";\nimport { Button, Card, Badge, Input } from \"../ui/primitives\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { uploadImage } from \"../lib/upload\";\nimport { findMediaUsage, findUnusedMedia } from \"../lib/mediaUsage\";\n\n/** Paginagrootte van de bibliotheek: begrensd laden i.p.v. álles (kosten + geheugen bij 100+ foto's). */\nconst PAGE_SIZE = 48;\n\nexport function MediaManager({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const [items, setItems] = useState<MediaItem[]>([]);\n const [busy, setBusy] = useState(false);\n /** Ids van aantoonbaar ongebruikte afbeeldingen (na een expliciete scan); null = nog niet gescand. */\n const [ongebruikt, setOngebruikt] = useState<string[] | null>(null);\n const [scant, setScant] = useState(false);\n const [pageCount, setPageCount] = useState(1);\n const [hasMore, setHasMore] = useState(false);\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n // Demo-modus: lokale bibliotheek (geseede foto's + eigen \"uploads\") i.p.v. Firestore.\n if (isDemoActive()) {\n const sync = () => setItems(listDemoMediaAll(siteId));\n sync();\n window.addEventListener(DEMO_EVENT, sync);\n return () => window.removeEventListener(DEMO_EVENT, sync);\n }\n const db = getDb();\n if (!db) return;\n // limit + \"meer laden\" (pageCount groeit): geen volledige collectie-read per open.\n const max = pageCount * PAGE_SIZE;\n const q = query(collection(db, \"sites\", siteId, \"media\"), orderBy(\"createdAt\", \"desc\"), limit(max + 1));\n return onSnapshot(\n q,\n (snap) => {\n setHasMore(snap.docs.length > max);\n setItems(snap.docs.slice(0, max).map((d) => ({ id: d.id, ...(d.data() as Omit<MediaItem, \"id\">) })));\n },\n // Leesfouten (rules/App Check) niet stil slikken — anders lijkt de bibliotheek leeg.\n (err) => toast({ title: \"Kon mediabibliotheek niet laden\", description: err.message, tone: \"error\" }),\n );\n }, [siteId, toast, pageCount]);\n\n async function onFiles(files: FileList) {\n setBusy(true);\n let ok = 0;\n const errors: string[] = [];\n for (const file of Array.from(files)) {\n try {\n await uploadImage(siteId, file);\n ok++;\n } catch (e) {\n errors.push(`${file.name}: ${(e as Error).message}`);\n }\n }\n if (ok > 0) toast({ title: `${ok} foto('s) geüpload`, description: \"Automatisch verkleind; varianten worden op de achtergrond gemaakt.\", tone: \"success\" });\n if (errors.length) toast({ title: \"Sommige uploads mislukten\", description: errors.join(\" · \"), tone: \"error\" });\n setBusy(false);\n }\n\n async function saveAlt(item: MediaItem, alt: string) {\n const db = getDb();\n if (!db) return;\n if ((item.alt ?? \"\") === alt) return; // niets gewijzigd → geen write/toast\n try {\n await updateDoc(doc(db, \"sites\", siteId, \"media\", item.id), { alt });\n toast({ title: \"Alt-tekst opgeslagen\", tone: \"success\" });\n } catch (e) {\n // Niet stil slikken: de klant dacht anders dat de alt-tekst opgeslagen was.\n toast({ title: \"Alt-tekst opslaan mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n async function remove(item: MediaItem) {\n const db = getDb();\n const bucket = getBucket();\n if (!db) return;\n const usedOn = await findMediaUsage(siteId, item);\n const ok = await confirm({\n title: \"Afbeelding verwijderen?\",\n description:\n usedOn.length > 0\n ? `Let op: deze afbeelding wordt gebruikt op ${usedOn.length} gepubliceerde pagina('s) (${usedOn.join(\", \")}). Daar verschijnt dan een gebroken afbeelding.`\n : \"De afbeelding wordt definitief verwijderd.\",\n confirmLabel: \"Verwijderen\",\n tone: \"danger\",\n });\n if (!ok) return;\n try {\n await verwijderItem(item);\n toast({ title: \"Verwijderd\" });\n } catch (e) {\n // Rules/offline-fout niet stil slikken — anders lijkt verwijderen \"niets te doen\".\n toast({ title: \"Verwijderen mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n /** Doc + origineel + alle varianten weg — gedeeld door los verwijderen en de opruimer. */\n async function verwijderItem(item: MediaItem) {\n const db = getDb();\n const bucket = getBucket();\n if (!db) throw new Error(\"Geen databaseverbinding.\");\n await deleteDoc(doc(db, \"sites\", siteId, \"media\", item.id));\n if (bucket) {\n // Ook de srcset-varianten opruimen die de Cloud Function heeft gemaakt — anders blijven\n // die (publieke) bestanden voor altijd in Storage staan.\n const paths = [\n item.path,\n item.variants?.webp?.path,\n ...(item.variants?.sizes ?? []).map((v) => v.path),\n ].filter((p): p is string => Boolean(p));\n await Promise.all(\n paths.map((p) => deleteObject(storageRef(bucket, p)).catch(() => undefined /* al weg */)),\n );\n }\n }\n\n /**\n * Opruimer (7.14): Storage groeide anders onbeperkt — elke ooit geüploade foto (plus zijn\n * webp/srcset-varianten) bleef staan, ook als hij allang uit elk blok was gehaald. De scan is\n * expliciet (één knop, één gecombineerde content-read via findUnusedMedia) en het verwijderen\n * vraagt altijd eerst bevestiging met aantal en omvang.\n */\n async function scanOngebruikt() {\n setScant(true);\n try {\n const ids = await findUnusedMedia(siteId, items);\n setOngebruikt(ids);\n if (ids.length === 0) toast({ title: \"Niets op te ruimen\", description: \"Elke afbeelding in de bibliotheek wordt ergens gebruikt.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Scan mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setScant(false);\n }\n }\n\n async function ruimOngebruiktOp() {\n const doelen = items.filter((it) => ongebruikt?.includes(it.id));\n if (doelen.length === 0) return;\n const mb = (doelen.reduce((som, it) => som + (it.size ?? 0), 0) / (1024 * 1024)).toFixed(1);\n const ok = await confirm({\n title: `${doelen.length} ongebruikte afbeelding(en) verwijderen?`,\n description: `Samen ±${mb} MB, inclusief alle automatisch gemaakte varianten. Dit kan niet ongedaan worden gemaakt.`,\n confirmLabel: \"Opruimen\",\n tone: \"danger\",\n });\n if (!ok) return;\n setBusy(true);\n let gelukt = 0;\n const fouten: string[] = [];\n for (const it of doelen) {\n try { await verwijderItem(it); gelukt++; } catch (e) { fouten.push(`${it.name}: ${(e as Error).message}`); }\n }\n setOngebruikt(null);\n setBusy(false);\n if (gelukt) toast({ title: `${gelukt} afbeelding(en) opgeruimd`, description: `±${mb} MB opslag vrijgemaakt.`, tone: \"success\" });\n if (fouten.length) toast({ title: \"Niet alles kon weg\", description: fouten.join(\" · \"), tone: \"error\" });\n }\n\n return (\n <div className=\"grid gap-4\">\n <div>\n <Button disabled={busy} onClick={() => inputRef.current?.click()}>\n {busy ? \"Uploaden…\" : \"Foto's uploaden\"}\n </Button>{\" \"}\n {!isDemoActive() && items.length > 0 && (\n ongebruikt === null ? (\n <Button variant=\"ghost\" disabled={scant || busy} onClick={() => void scanOngebruikt()}>\n {scant ? \"Zoeken…\" : \"🧹 Zoek ongebruikte foto's\"}\n </Button>\n ) : ongebruikt.length > 0 ? (\n <>\n <Button variant=\"secondary\" disabled={busy} onClick={() => void ruimOngebruiktOp()}>\n 🧹 {ongebruikt.length} ongebruikte verwijderen\n </Button>{\" \"}\n <Button variant=\"ghost\" onClick={() => setOngebruikt(null)}>Annuleren</Button>\n </>\n ) : null\n )}\n <p className=\"mt-1 text-xs text-slate-500\">\n Je kunt meerdere foto's tegelijk kiezen. Ze worden automatisch verkleind en geoptimaliseerd\n (webp) — dat houdt je site snel en bespaart opslag. Max. 15 MB per bestand.\n </p>\n <input\n ref={inputRef}\n type=\"file\"\n accept=\"image/*\"\n multiple\n hidden\n onChange={(e) => {\n if (e.target.files?.length) void onFiles(e.target.files);\n e.target.value = \"\";\n }}\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4\">\n {items.map((m) => (\n <Card key={m.id} className=\"p-3\">\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img src={m.url} alt={m.alt ?? m.name} className=\"mb-2 h-28 w-full rounded-md object-cover\" />\n <div className=\"mb-2 flex items-center justify-between gap-1\">\n <span className=\"truncate text-xs text-slate-500\">\n {m.name}\n {(m as { width?: number; height?: number }).width && (\n <span className=\"text-slate-400\"> · {(m as { width?: number }).width}×{(m as { height?: number }).height}</span>\n )}\n </span>\n {m.status === \"processing\" ? (\n <Badge tone=\"amber\">verwerken</Badge>\n ) : ongebruikt?.includes(m.id) ? (\n <Badge tone=\"amber\">ongebruikt</Badge>\n ) : (\n <Badge tone=\"green\">klaar</Badge>\n )}\n </div>\n <AltInput item={m} onSave={saveAlt} />\n <div className=\"flex gap-1\">\n <Button variant=\"ghost\" size=\"sm\" className=\"text-xs\" onClick={() => void navigator.clipboard?.writeText(m.url)}>\n Kopieer URL\n </Button>\n <Button variant=\"ghost\" size=\"sm\" className=\"text-xs\" onClick={() => void remove(m)}>\n Verwijder\n </Button>\n </div>\n </Card>\n ))}\n {items.length === 0 && <p className=\"text-sm text-slate-500\">Nog geen media.</p>}\n </div>\n {hasMore && (\n <div>\n <Button variant=\"secondary\" size=\"sm\" onClick={() => setPageCount((c) => c + 1)}>\n Meer laden…\n </Button>\n </div>\n )}\n </div>\n );\n}\n\n/** Controlled alt-veld: volgt externe updates (realtime snapshot) en slaat alleen échte wijzigingen op. */\nfunction AltInput({ item, onSave }: { item: MediaItem; onSave: (item: MediaItem, alt: string) => Promise<void> }) {\n const [value, setValue] = useState(item.alt ?? \"\");\n // Extern gewijzigd (andere tab/verwerking) → veld bijtrekken, behalve terwijl de klant zelf typt.\n const [focused, setFocused] = useState(false);\n useEffect(() => {\n if (!focused) setValue(item.alt ?? \"\");\n }, [item.alt, focused]);\n return (\n <Input\n value={value}\n placeholder=\"Alt-tekst\"\n className=\"mb-2 h-8 text-xs\"\n onChange={(e) => setValue(e.target.value)}\n onFocus={() => setFocused(true)}\n onBlur={() => {\n setFocused(false);\n void onSave(item, value);\n }}\n />\n );\n}\n"],"mappings":";AAqLQ,SASI,UATJ,KAUM,YAVN;AAnLR,SAAS,WAAW,QAAQ,gBAAgB;AAC5C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,cAAc,OAAO,kBAAkB;AAChD,SAAS,WAAW,aAAa;AACjC,SAAS,YAAY,cAAc,wBAAwB;AAE3D,SAAS,QAAQ,MAAM,OAAO,aAAa;AAC3C,SAAS,gBAAgB;AACzB,SAAS,kBAAkB;AAC3B,SAAS,mBAAmB;AAC5B,SAAS,gBAAgB,uBAAuB;AAGhD,MAAM,YAAY;AAEX,SAAS,aAAa,EAAE,OAAO,GAAuB;AAC3D,QAAM,EAAE,MAAM,IAAI,SAAS;AAC3B,QAAM,UAAU,WAAW;AAC3B,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAsB,CAAC,CAAC;AAClD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AAEtC,QAAM,CAAC,YAAY,aAAa,IAAI,SAA0B,IAAI;AAClE,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,KAAK;AACxC,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,CAAC;AAC5C,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,WAAW,OAAyB,IAAI;AAE9C,YAAU,MAAM;AAEd,QAAI,aAAa,GAAG;AAClB,YAAM,OAAO,MAAM,SAAS,iBAAiB,MAAM,CAAC;AACpD,WAAK;AACL,aAAO,iBAAiB,YAAY,IAAI;AACxC,aAAO,MAAM,OAAO,oBAAoB,YAAY,IAAI;AAAA,IAC1D;AACA,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AAET,UAAM,MAAM,YAAY;AACxB,UAAM,IAAI,MAAM,WAAW,IAAI,SAAS,QAAQ,OAAO,GAAG,QAAQ,aAAa,MAAM,GAAG,MAAM,MAAM,CAAC,CAAC;AACtG,WAAO;AAAA,MACL;AAAA,MACA,CAAC,SAAS;AACR,mBAAW,KAAK,KAAK,SAAS,GAAG;AACjC,iBAAS,KAAK,KAAK,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,GAAI,EAAE,KAAK,EAA4B,EAAE,CAAC;AAAA,MACrG;AAAA;AAAA,MAEA,CAAC,QAAQ,MAAM,EAAE,OAAO,mCAAmC,aAAa,IAAI,SAAS,MAAM,QAAQ,CAAC;AAAA,IACtG;AAAA,EACF,GAAG,CAAC,QAAQ,OAAO,SAAS,CAAC;AAE7B,iBAAe,QAAQ,OAAiB;AACtC,YAAQ,IAAI;AACZ,QAAI,KAAK;AACT,UAAM,SAAmB,CAAC;AAC1B,eAAW,QAAQ,MAAM,KAAK,KAAK,GAAG;AACpC,UAAI;AACF,cAAM,YAAY,QAAQ,IAAI;AAC9B;AAAA,MACF,SAAS,GAAG;AACV,eAAO,KAAK,GAAG,KAAK,IAAI,KAAM,EAAY,OAAO,EAAE;AAAA,MACrD;AAAA,IACF;AACA,QAAI,KAAK,EAAG,OAAM,EAAE,OAAO,GAAG,EAAE,yBAAsB,aAAa,sEAAsE,MAAM,UAAU,CAAC;AAC1J,QAAI,OAAO,OAAQ,OAAM,EAAE,OAAO,6BAA6B,aAAa,OAAO,KAAK,QAAK,GAAG,MAAM,QAAQ,CAAC;AAC/G,YAAQ,KAAK;AAAA,EACf;AAEA,iBAAe,QAAQ,MAAiB,KAAa;AACnD,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AACT,SAAK,KAAK,OAAO,QAAQ,IAAK;AAC9B,QAAI;AACF,YAAM,UAAU,IAAI,IAAI,SAAS,QAAQ,SAAS,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC;AACnE,YAAM,EAAE,OAAO,wBAAwB,MAAM,UAAU,CAAC;AAAA,IAC1D,SAAS,GAAG;AAEV,YAAM,EAAE,OAAO,6BAA6B,aAAc,EAAY,SAAS,MAAM,QAAQ,CAAC;AAAA,IAChG;AAAA,EACF;AAEA,iBAAe,OAAO,MAAiB;AACrC,UAAM,KAAK,MAAM;AACjB,UAAM,SAAS,UAAU;AACzB,QAAI,CAAC,GAAI;AACT,UAAM,SAAS,MAAM,eAAe,QAAQ,IAAI;AAChD,UAAM,KAAK,MAAM,QAAQ;AAAA,MACvB,OAAO;AAAA,MACP,aACE,OAAO,SAAS,IACZ,6CAA6C,OAAO,MAAM,8BAA8B,OAAO,KAAK,IAAI,CAAC,oDACzG;AAAA,MACN,cAAc;AAAA,MACd,MAAM;AAAA,IACR,CAAC;AACD,QAAI,CAAC,GAAI;AACT,QAAI;AACF,YAAM,cAAc,IAAI;AACxB,YAAM,EAAE,OAAO,aAAa,CAAC;AAAA,IAC/B,SAAS,GAAG;AAEV,YAAM,EAAE,OAAO,uBAAuB,aAAc,EAAY,SAAS,MAAM,QAAQ,CAAC;AAAA,IAC1F;AAAA,EACF;AAGA,iBAAe,cAAc,MAAiB;AAC5C,UAAM,KAAK,MAAM;AACjB,UAAM,SAAS,UAAU;AACzB,QAAI,CAAC,GAAI,OAAM,IAAI,MAAM,0BAA0B;AACnD,UAAM,UAAU,IAAI,IAAI,SAAS,QAAQ,SAAS,KAAK,EAAE,CAAC;AAC1D,QAAI,QAAQ;AAGV,YAAM,QAAQ;AAAA,QACZ,KAAK;AAAA,QACL,KAAK,UAAU,MAAM;AAAA,QACrB,IAAI,KAAK,UAAU,SAAS,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,IAAI;AAAA,MACnD,EAAE,OAAO,CAAC,MAAmB,QAAQ,CAAC,CAAC;AACvC,YAAM,QAAQ;AAAA,QACZ,MAAM,IAAI,CAAC,MAAM,aAAa,WAAW,QAAQ,CAAC,CAAC,EAAE;AAAA,UAAM,MAAM;AAAA;AAAA,QAAsB,CAAC;AAAA,MAC1F;AAAA,IACF;AAAA,EACF;AAQA,iBAAe,iBAAiB;AAC9B,aAAS,IAAI;AACb,QAAI;AACF,YAAM,MAAM,MAAM,gBAAgB,QAAQ,KAAK;AAC/C,oBAAc,GAAG;AACjB,UAAI,IAAI,WAAW,EAAG,OAAM,EAAE,OAAO,sBAAsB,aAAa,4DAA4D,MAAM,UAAU,CAAC;AAAA,IACvJ,SAAS,GAAG;AACV,YAAM,EAAE,OAAO,gBAAgB,aAAc,EAAY,SAAS,MAAM,QAAQ,CAAC;AAAA,IACnF,UAAE;AACA,eAAS,KAAK;AAAA,IAChB;AAAA,EACF;AAEA,iBAAe,mBAAmB;AAChC,UAAM,SAAS,MAAM,OAAO,CAAC,OAAO,YAAY,SAAS,GAAG,EAAE,CAAC;AAC/D,QAAI,OAAO,WAAW,EAAG;AACzB,UAAM,MAAM,OAAO,OAAO,CAAC,KAAK,OAAO,OAAO,GAAG,QAAQ,IAAI,CAAC,KAAK,OAAO,OAAO,QAAQ,CAAC;AAC1F,UAAM,KAAK,MAAM,QAAQ;AAAA,MACvB,OAAO,GAAG,OAAO,MAAM;AAAA,MACvB,aAAa,aAAU,EAAE;AAAA,MACzB,cAAc;AAAA,MACd,MAAM;AAAA,IACR,CAAC;AACD,QAAI,CAAC,GAAI;AACT,YAAQ,IAAI;AACZ,QAAI,SAAS;AACb,UAAM,SAAmB,CAAC;AAC1B,eAAW,MAAM,QAAQ;AACvB,UAAI;AAAE,cAAM,cAAc,EAAE;AAAG;AAAA,MAAU,SAAS,GAAG;AAAE,eAAO,KAAK,GAAG,GAAG,IAAI,KAAM,EAAY,OAAO,EAAE;AAAA,MAAG;AAAA,IAC7G;AACA,kBAAc,IAAI;AAClB,YAAQ,KAAK;AACb,QAAI,OAAQ,OAAM,EAAE,OAAO,GAAG,MAAM,6BAA6B,aAAa,OAAI,EAAE,2BAA2B,MAAM,UAAU,CAAC;AAChI,QAAI,OAAO,OAAQ,OAAM,EAAE,OAAO,sBAAsB,aAAa,OAAO,KAAK,QAAK,GAAG,MAAM,QAAQ,CAAC;AAAA,EAC1G;AAEA,SACE,qBAAC,SAAI,WAAU,cACb;AAAA,yBAAC,SACC;AAAA,0BAAC,UAAO,UAAU,MAAM,SAAS,MAAM,SAAS,SAAS,MAAM,GAC5D,iBAAO,mBAAc,mBACxB;AAAA,MAAU;AAAA,MACT,CAAC,aAAa,KAAK,MAAM,SAAS,MACjC,eAAe,OACb,oBAAC,UAAO,SAAQ,SAAQ,UAAU,SAAS,MAAM,SAAS,MAAM,KAAK,eAAe,GACjF,kBAAQ,iBAAY,qCACvB,IACE,WAAW,SAAS,IACtB,iCACE;AAAA,6BAAC,UAAO,SAAQ,aAAY,UAAU,MAAM,SAAS,MAAM,KAAK,iBAAiB,GAAG;AAAA;AAAA,UAC9E,WAAW;AAAA,UAAO;AAAA,WACxB;AAAA,QAAU;AAAA,QACV,oBAAC,UAAO,SAAQ,SAAQ,SAAS,MAAM,cAAc,IAAI,GAAG,uBAAS;AAAA,SACvE,IACE;AAAA,MAEN,oBAAC,OAAE,WAAU,+BAA8B,0LAG3C;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,KAAK;AAAA,UACL,MAAK;AAAA,UACL,QAAO;AAAA,UACP,UAAQ;AAAA,UACR,QAAM;AAAA,UACN,UAAU,CAAC,MAAM;AACf,gBAAI,EAAE,OAAO,OAAO,OAAQ,MAAK,QAAQ,EAAE,OAAO,KAAK;AACvD,cAAE,OAAO,QAAQ;AAAA,UACnB;AAAA;AAAA,MACF;AAAA,OACF;AAAA,IACA,qBAAC,SAAI,WAAU,wDACZ;AAAA,YAAM,IAAI,CAAC,MACV,qBAAC,QAAgB,WAAU,OAEzB;AAAA,4BAAC,SAAI,KAAK,EAAE,KAAK,KAAK,EAAE,OAAO,EAAE,MAAM,WAAU,4CAA2C;AAAA,QAC5F,qBAAC,SAAI,WAAU,gDACb;AAAA,+BAAC,UAAK,WAAU,mCACb;AAAA,cAAE;AAAA,YACD,EAA0C,SAC1C,qBAAC,UAAK,WAAU,kBAAiB;AAAA;AAAA,cAAK,EAAyB;AAAA,cAAM;AAAA,cAAG,EAA0B;AAAA,eAAO;AAAA,aAE7G;AAAA,UACC,EAAE,WAAW,eACZ,oBAAC,SAAM,MAAK,SAAQ,uBAAS,IAC3B,YAAY,SAAS,EAAE,EAAE,IAC3B,oBAAC,SAAM,MAAK,SAAQ,wBAAU,IAE9B,oBAAC,SAAM,MAAK,SAAQ,mBAAK;AAAA,WAE7B;AAAA,QACA,oBAAC,YAAS,MAAM,GAAG,QAAQ,SAAS;AAAA,QACpC,qBAAC,SAAI,WAAU,cACb;AAAA,8BAAC,UAAO,SAAQ,SAAQ,MAAK,MAAK,WAAU,WAAU,SAAS,MAAM,KAAK,UAAU,WAAW,UAAU,EAAE,GAAG,GAAG,yBAEjH;AAAA,UACA,oBAAC,UAAO,SAAQ,SAAQ,MAAK,MAAK,WAAU,WAAU,SAAS,MAAM,KAAK,OAAO,CAAC,GAAG,uBAErF;AAAA,WACF;AAAA,WA1BS,EAAE,EA2Bb,CACD;AAAA,MACA,MAAM,WAAW,KAAK,oBAAC,OAAE,WAAU,0BAAyB,6BAAe;AAAA,OAC9E;AAAA,IACC,WACC,oBAAC,SACC,8BAAC,UAAO,SAAQ,aAAY,MAAK,MAAK,SAAS,MAAM,aAAa,CAAC,MAAM,IAAI,CAAC,GAAG,8BAEjF,GACF;AAAA,KAEJ;AAEJ;AAGA,SAAS,SAAS,EAAE,MAAM,OAAO,GAAiF;AAChH,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,KAAK,OAAO,EAAE;AAEjD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,YAAU,MAAM;AACd,QAAI,CAAC,QAAS,UAAS,KAAK,OAAO,EAAE;AAAA,EACvC,GAAG,CAAC,KAAK,KAAK,OAAO,CAAC;AACtB,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,aAAY;AAAA,MACZ,WAAU;AAAA,MACV,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK;AAAA,MACxC,SAAS,MAAM,WAAW,IAAI;AAAA,MAC9B,QAAQ,MAAM;AACZ,mBAAW,KAAK;AAChB,aAAK,OAAO,MAAM,KAAK;AAAA,MACzB;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { registerBuiltinSchemas } from "../../blocks/builtinSchemas.js";
|
|
4
|
+
import { useModules } from "../../modules/context.js";
|
|
4
5
|
registerBuiltinSchemas();
|
|
5
6
|
import "@xiwebdesign/xicms-core/admin.css";
|
|
6
7
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
@@ -132,6 +133,27 @@ function VisualEditorInner({ onClose }) {
|
|
|
132
133
|
draftRef.current = draft;
|
|
133
134
|
}, [draft]);
|
|
134
135
|
const historyRef = useRef({ past: [], future: [], lastPush: 0 });
|
|
136
|
+
const undoOpslagSleutel = useCallback((id) => `xicms-undo:${siteId}:${id}`, [siteId]);
|
|
137
|
+
const bewaarHistory = useCallback(() => {
|
|
138
|
+
try {
|
|
139
|
+
const id = draftRef.current?.id;
|
|
140
|
+
if (!id) return;
|
|
141
|
+
sessionStorage.setItem(undoOpslagSleutel(id), JSON.stringify(historyRef.current.past.slice(-25)));
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
}, [undoOpslagSleutel]);
|
|
145
|
+
const herstelHistory = useCallback((id) => {
|
|
146
|
+
try {
|
|
147
|
+
const raw = sessionStorage.getItem(undoOpslagSleutel(id));
|
|
148
|
+
if (!raw || historyRef.current.past.length > 0) return;
|
|
149
|
+
const past = JSON.parse(raw);
|
|
150
|
+
if (Array.isArray(past) && past.length) {
|
|
151
|
+
historyRef.current.past = past;
|
|
152
|
+
setHistVersion((v) => v + 1);
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
}, [undoOpslagSleutel]);
|
|
135
157
|
const [, setHistVersion] = useState(0);
|
|
136
158
|
const pushHistory = useCallback((prev, coalesce) => {
|
|
137
159
|
const h = historyRef.current;
|
|
@@ -226,6 +248,7 @@ function VisualEditorInner({ onClose }) {
|
|
|
226
248
|
if (d) {
|
|
227
249
|
setDraft(d);
|
|
228
250
|
lastSavedRef.current = stableStringify(draftPayload(d));
|
|
251
|
+
herstelHistory(d.id);
|
|
229
252
|
setStatus("idle");
|
|
230
253
|
} else {
|
|
231
254
|
setStatus("none");
|
|
@@ -295,6 +318,13 @@ function VisualEditorInner({ onClose }) {
|
|
|
295
318
|
);
|
|
296
319
|
nietLiveRef.current = wijzigingen.iets;
|
|
297
320
|
const { publishDraft, zetFotoVervanger } = useEditor();
|
|
321
|
+
const actieveModules = useModules();
|
|
322
|
+
useEffect(() => {
|
|
323
|
+
for (const m of actieveModules.defs) {
|
|
324
|
+
if (actieveModules.activeIds.has(m.id)) void m.loadSchemas?.().catch(() => {
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}, [actieveModules]);
|
|
298
328
|
useEffect(() => {
|
|
299
329
|
zetFotoVervanger((field, blockKey) => {
|
|
300
330
|
if (blockKey) setImgPicker({ key: blockKey, field });
|
|
@@ -727,10 +757,14 @@ function VisualEditorInner({ onClose }) {
|
|
|
727
757
|
e.preventDefault();
|
|
728
758
|
e.returnValue = "";
|
|
729
759
|
}
|
|
760
|
+
bewaarHistory();
|
|
730
761
|
};
|
|
731
762
|
const onVisibility = () => {
|
|
732
|
-
if (document.visibilityState === "hidden")
|
|
733
|
-
|
|
763
|
+
if (document.visibilityState === "hidden") {
|
|
764
|
+
void flushSave().catch(() => {
|
|
765
|
+
});
|
|
766
|
+
bewaarHistory();
|
|
767
|
+
}
|
|
734
768
|
};
|
|
735
769
|
const onKeyDown = (e) => {
|
|
736
770
|
if (e.key === "Escape") {
|
|
@@ -765,7 +799,7 @@ function VisualEditorInner({ onClose }) {
|
|
|
765
799
|
);
|
|
766
800
|
}
|
|
767
801
|
};
|
|
768
|
-
}, [siteId, handleClose, flushSave, undo, redo]);
|
|
802
|
+
}, [siteId, handleClose, flushSave, undo, redo, bewaarHistory]);
|
|
769
803
|
function setBlocks(next, opts) {
|
|
770
804
|
mutateDraft((d) => ({ ...d, blocks: next }), opts);
|
|
771
805
|
}
|