@xiwebdesign/xicms-core 6.0.0 → 6.1.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/settings/MailSettings.d.ts +10 -0
- package/dist/admin/settings/MailSettings.js +166 -0
- package/dist/admin/settings/MailSettings.js.map +1 -0
- package/dist/admin/settings/SettingsPanel.js +8 -2
- package/dist/admin/settings/SettingsPanel.js.map +1 -1
- package/dist/admin/shop/ConnectionsTab.d.ts +7 -3
- package/dist/admin/shop/ConnectionsTab.js +8 -116
- package/dist/admin/shop/ConnectionsTab.js.map +1 -1
- package/dist/admin/shop/ShopPanel.d.ts +2 -1
- package/dist/admin/shop/ShopPanel.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site-brede mailprovider-configuratie (Resend / Postmark / eigen SMTP) + afzender-identiteit.
|
|
3
|
+
* Deze instelling wordt door MEERDERE modules gebruikt (formulier-notificaties én de shop), dus
|
|
4
|
+
* hoort site-breed thuis in de algemene instellingen — niet in een module-paneel. Geheimen gaan
|
|
5
|
+
* uitsluitend via callables naar `sites/{siteId}/private/mail` (deny-all rules); dit paneel toont
|
|
6
|
+
* alleen gemaskeerde status. Alleen een siteadmin kan opslaan.
|
|
7
|
+
*/
|
|
8
|
+
export declare function MailSettings({ siteId }: {
|
|
9
|
+
siteId: string;
|
|
10
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
import { getFunctions, httpsCallable } from "firebase/functions";
|
|
5
|
+
import { getFirebaseApp } from "../../firebase/client";
|
|
6
|
+
import { functionsRegion } from "../../firebase/region";
|
|
7
|
+
import { Badge, Button, Card, Field, Input } from "../ui/primitives";
|
|
8
|
+
import { Switch } from "../ui/switch";
|
|
9
|
+
import { useToast } from "../ui/toast";
|
|
10
|
+
function callFunction(name) {
|
|
11
|
+
return async (data) => {
|
|
12
|
+
const app = getFirebaseApp();
|
|
13
|
+
if (!app) throw new Error("Firebase niet beschikbaar.");
|
|
14
|
+
const fn = httpsCallable(getFunctions(app, functionsRegion()), name);
|
|
15
|
+
const res = await fn(data);
|
|
16
|
+
return res.data;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function Segmented({
|
|
20
|
+
value,
|
|
21
|
+
options,
|
|
22
|
+
onChange
|
|
23
|
+
}) {
|
|
24
|
+
return /* @__PURE__ */ jsx("div", { className: "inline-flex rounded-lg border border-slate-200 bg-slate-50 p-0.5", children: options.map((o) => /* @__PURE__ */ jsx(
|
|
25
|
+
"button",
|
|
26
|
+
{
|
|
27
|
+
type: "button",
|
|
28
|
+
onClick: () => onChange(o.value),
|
|
29
|
+
className: `rounded-md px-3 py-1.5 text-sm font-medium transition ${value === o.value ? "bg-white text-brand-700 shadow-sm" : "text-slate-500 hover:text-slate-700"}`,
|
|
30
|
+
children: o.label
|
|
31
|
+
},
|
|
32
|
+
o.value
|
|
33
|
+
)) });
|
|
34
|
+
}
|
|
35
|
+
function mailProviderLabel(p) {
|
|
36
|
+
return p === "smtp" ? "Eigen SMTP" : p === "resend" ? "Resend" : "Postmark";
|
|
37
|
+
}
|
|
38
|
+
function MailSettings({ siteId }) {
|
|
39
|
+
const { toast } = useToast();
|
|
40
|
+
const [status, setStatus] = useState(null);
|
|
41
|
+
const [busy, setBusy] = useState(false);
|
|
42
|
+
const [mailProvider, setMailProvider] = useState("smtp");
|
|
43
|
+
const [resendKey, setResendKey] = useState("");
|
|
44
|
+
const [postmarkToken, setPostmarkToken] = useState("");
|
|
45
|
+
const [smtp, setSmtp] = useState({ host: "", port: "465", secure: true, user: "", pass: "" });
|
|
46
|
+
const [fromName, setFromName] = useState("");
|
|
47
|
+
const [fromEmail, setFromEmail] = useState("");
|
|
48
|
+
const [notifyTo, setNotifyTo] = useState("");
|
|
49
|
+
const refresh = useCallback(async () => {
|
|
50
|
+
try {
|
|
51
|
+
const m = await callFunction("getMailStatus")({ siteId });
|
|
52
|
+
setStatus(m);
|
|
53
|
+
setMailProvider(m.provider ?? (m.host ? "smtp" : m.hasPostmarkToken ? "postmark" : m.hasResendKey ? "resend" : "smtp"));
|
|
54
|
+
setSmtp((p) => ({ ...p, host: m.host ?? p.host, port: String(m.port ?? 465), secure: m.secure !== false, user: m.user ?? p.user }));
|
|
55
|
+
setFromName(m.fromName ?? "");
|
|
56
|
+
setFromEmail(m.fromEmail ?? "");
|
|
57
|
+
setNotifyTo(m.notifyTo ?? "");
|
|
58
|
+
} catch {
|
|
59
|
+
setStatus(null);
|
|
60
|
+
}
|
|
61
|
+
}, [siteId]);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
void refresh();
|
|
64
|
+
}, [refresh]);
|
|
65
|
+
async function saveMail() {
|
|
66
|
+
setBusy(true);
|
|
67
|
+
try {
|
|
68
|
+
const mail = { provider: mailProvider, fromName, fromEmail, notifyTo };
|
|
69
|
+
if (mailProvider === "resend") mail.resendApiKey = resendKey.trim() || void 0;
|
|
70
|
+
if (mailProvider === "postmark") mail.postmarkToken = postmarkToken.trim() || void 0;
|
|
71
|
+
if (mailProvider === "smtp") {
|
|
72
|
+
mail.host = smtp.host.trim();
|
|
73
|
+
mail.port = Number(smtp.port) || 465;
|
|
74
|
+
mail.secure = smtp.secure;
|
|
75
|
+
mail.user = smtp.user.trim();
|
|
76
|
+
if (smtp.pass) mail.pass = smtp.pass;
|
|
77
|
+
}
|
|
78
|
+
await callFunction("saveMailSettings")({ siteId, mail });
|
|
79
|
+
toast({ title: "E-mailinstellingen opgeslagen", tone: "success" });
|
|
80
|
+
setResendKey("");
|
|
81
|
+
setPostmarkToken("");
|
|
82
|
+
setSmtp((p) => ({ ...p, pass: "" }));
|
|
83
|
+
await refresh();
|
|
84
|
+
} catch (e) {
|
|
85
|
+
toast({ title: "Opslaan mislukt", description: e instanceof Error ? e.message : void 0, tone: "error" });
|
|
86
|
+
} finally {
|
|
87
|
+
setBusy(false);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function sendTest() {
|
|
91
|
+
setBusy(true);
|
|
92
|
+
try {
|
|
93
|
+
const res = await callFunction("sendShopTestMail")({ siteId });
|
|
94
|
+
toast({ title: "Testmail verstuurd", description: `Naar ${res.to} \u2014 check de inbox (en spam).`, tone: "success" });
|
|
95
|
+
} catch (e) {
|
|
96
|
+
toast({ title: "Testmail mislukt", description: e instanceof Error ? e.message : void 0, tone: "error" });
|
|
97
|
+
} finally {
|
|
98
|
+
setBusy(false);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
|
|
102
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [
|
|
103
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
104
|
+
/* @__PURE__ */ jsx("h3", { className: "text-base font-semibold", children: "E-mail (formulieren & shop)" }),
|
|
105
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-slate-500", children: "De provider waarmee deze site e-mails verstuurt \u2014 formulier-notificaties \xE9n bestelbevestigingen. Een provider verstuurt alleen vanaf een geverifieerd domein." })
|
|
106
|
+
] }),
|
|
107
|
+
status && /* @__PURE__ */ jsx(Badge, { tone: status.configured ? "green" : "amber", children: status.configured ? `${mailProviderLabel(mailProvider)} actief` : "Nog niet ingesteld" })
|
|
108
|
+
] }),
|
|
109
|
+
/* @__PURE__ */ jsx(
|
|
110
|
+
Segmented,
|
|
111
|
+
{
|
|
112
|
+
value: mailProvider,
|
|
113
|
+
onChange: setMailProvider,
|
|
114
|
+
options: [
|
|
115
|
+
{ value: "smtp", label: "Eigen SMTP" },
|
|
116
|
+
{ value: "resend", label: "Resend" },
|
|
117
|
+
{ value: "postmark", label: "Postmark" }
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
),
|
|
121
|
+
mailProvider === "smtp" && /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
|
|
122
|
+
/* @__PURE__ */ jsxs("p", { className: "text-sm text-slate-600", children: [
|
|
123
|
+
"Via je eigen mailbox: bv. het adres bij je hostingpartij, of Gmail (smtp.gmail.com, poort 465, met een",
|
|
124
|
+
" ",
|
|
125
|
+
/* @__PURE__ */ jsx("a", { className: "underline", href: "https://myaccount.google.com/apppasswords", target: "_blank", rel: "noreferrer", children: "app-wachtwoord" }),
|
|
126
|
+
")."
|
|
127
|
+
] }),
|
|
128
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-3 md:grid-cols-[1fr_110px]", children: [
|
|
129
|
+
/* @__PURE__ */ jsx(Field, { label: "SMTP-server", children: /* @__PURE__ */ jsx(Input, { value: smtp.host, onChange: (e) => setSmtp((p) => ({ ...p, host: e.target.value })), placeholder: "smtp.gmail.com" }) }),
|
|
130
|
+
/* @__PURE__ */ jsx(Field, { label: "Poort", children: /* @__PURE__ */ jsx(Input, { value: smtp.port, onChange: (e) => setSmtp((p) => ({ ...p, port: e.target.value })), placeholder: "465" }) })
|
|
131
|
+
] }),
|
|
132
|
+
/* @__PURE__ */ jsx(Switch, { checked: smtp.secure, onCheckedChange: (v) => setSmtp((p) => ({ ...p, secure: v })), label: "SSL (poort 465). Uit = STARTTLS (poort 587)" }),
|
|
133
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-3 md:grid-cols-2", children: [
|
|
134
|
+
/* @__PURE__ */ jsx(Field, { label: "Gebruikersnaam", children: /* @__PURE__ */ jsx(Input, { value: smtp.user, onChange: (e) => setSmtp((p) => ({ ...p, user: e.target.value })), placeholder: "info@jouwdomein.nl" }) }),
|
|
135
|
+
/* @__PURE__ */ jsx(Field, { label: "Wachtwoord", hint: status?.hasSmtpPass ? "Leeg laten = ongewijzigd" : void 0, children: /* @__PURE__ */ jsx(Input, { type: "password", value: smtp.pass, onChange: (e) => setSmtp((p) => ({ ...p, pass: e.target.value })) }) })
|
|
136
|
+
] })
|
|
137
|
+
] }),
|
|
138
|
+
mailProvider === "resend" && /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
|
|
139
|
+
/* @__PURE__ */ jsxs("p", { className: "text-sm text-slate-600", children: [
|
|
140
|
+
/* @__PURE__ */ jsx("a", { className: "underline", href: "https://resend.com", target: "_blank", rel: "noreferrer", children: "Resend" }),
|
|
141
|
+
" verstuurt vanaf een geverifieerd domein. Maak een API-sleutel (re_\u2026) en verifieer je afzenderdomein in het Resend-dashboard."
|
|
142
|
+
] }),
|
|
143
|
+
/* @__PURE__ */ jsx(Field, { label: "Resend API-sleutel (re_\u2026)", hint: status?.hasResendKey ? "Leeg laten = ongewijzigd" : void 0, children: /* @__PURE__ */ jsx(Input, { type: "password", value: resendKey, onChange: (e) => setResendKey(e.target.value), placeholder: "re_\u2026" }) })
|
|
144
|
+
] }),
|
|
145
|
+
mailProvider === "postmark" && /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
|
|
146
|
+
/* @__PURE__ */ jsxs("p", { className: "text-sm text-slate-600", children: [
|
|
147
|
+
/* @__PURE__ */ jsx("a", { className: "underline", href: "https://postmarkapp.com", target: "_blank", rel: "noreferrer", children: "Postmark" }),
|
|
148
|
+
" verstuurt vanaf een geverifieerde afzender/domein (Sender Signature). Kopieer het Server-token."
|
|
149
|
+
] }),
|
|
150
|
+
/* @__PURE__ */ jsx(Field, { label: "Postmark server-token", hint: status?.hasPostmarkToken ? "Leeg laten = ongewijzigd" : void 0, children: /* @__PURE__ */ jsx(Input, { type: "password", value: postmarkToken, onChange: (e) => setPostmarkToken(e.target.value), placeholder: "xxxxxxxx-xxxx-\u2026" }) })
|
|
151
|
+
] }),
|
|
152
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-3 md:grid-cols-2", children: [
|
|
153
|
+
/* @__PURE__ */ jsx(Field, { label: "Afzendernaam", children: /* @__PURE__ */ jsx(Input, { value: fromName, onChange: (e) => setFromName(e.target.value), placeholder: "Mijn Site" }) }),
|
|
154
|
+
/* @__PURE__ */ jsx(Field, { label: "Afzenderadres", children: /* @__PURE__ */ jsx(Input, { value: fromEmail, onChange: (e) => setFromEmail(e.target.value), placeholder: "info@jouwdomein.nl" }) })
|
|
155
|
+
] }),
|
|
156
|
+
/* @__PURE__ */ jsx(Field, { label: "Notificatie-adres", hint: "Hier ontvang je zelf een mail bij elk formulier/elke bestelling; ook het doel van de testmail", children: /* @__PURE__ */ jsx(Input, { value: notifyTo, onChange: (e) => setNotifyTo(e.target.value), placeholder: "notificaties@jouwdomein.nl" }) }),
|
|
157
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
158
|
+
/* @__PURE__ */ jsx(Button, { onClick: saveMail, disabled: busy, children: "Opslaan" }),
|
|
159
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: sendTest, disabled: busy || !status?.configured, children: "Testmail versturen" })
|
|
160
|
+
] })
|
|
161
|
+
] }) });
|
|
162
|
+
}
|
|
163
|
+
export {
|
|
164
|
+
MailSettings
|
|
165
|
+
};
|
|
166
|
+
//# sourceMappingURL=MailSettings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/settings/MailSettings.tsx"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useState } from \"react\";\nimport { getFunctions, httpsCallable } from \"firebase/functions\";\nimport { getFirebaseApp } from \"../../firebase/client\";\nimport { functionsRegion } from \"../../firebase/region\";\nimport { Badge, Button, Card, Field, Input } from \"../ui/primitives\";\nimport { Switch } from \"../ui/switch\";\nimport { useToast } from \"../ui/toast\";\n\ntype MailProvider = \"resend\" | \"postmark\" | \"smtp\";\n\ninterface MailStatus {\n provider?: MailProvider;\n configured: boolean;\n host?: string;\n port?: number;\n secure?: boolean;\n user?: string;\n fromName?: string;\n fromEmail?: string;\n notifyTo?: string;\n hasResendKey?: boolean;\n hasPostmarkToken?: boolean;\n hasSmtpPass?: boolean;\n}\n\n/**\n * Generieke callable-helper (niet shop-gebonden): roept een functie op naam aan via de\n * client-Firebase. Bewust hier lokaal zodat de algemene instellingen niet van de opt-in\n * shop-module afhangen — mail is een site-brede instelling (zie docs/SHARED-SETTINGS.md).\n */\nfunction callFunction<TIn, TOut>(name: string) {\n return async (data: TIn): Promise<TOut> => {\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase niet beschikbaar.\");\n const fn = httpsCallable(getFunctions(app, functionsRegion()), name);\n const res = await fn(data as Record<string, unknown>);\n return res.data as TOut;\n };\n}\n\n/** Segmented control voor providerkeuze. */\nfunction Segmented<T extends string>({\n value,\n options,\n onChange,\n}: {\n value: T;\n options: { value: T; label: string }[];\n onChange: (v: T) => void;\n}) {\n return (\n <div className=\"inline-flex rounded-lg border border-slate-200 bg-slate-50 p-0.5\">\n {options.map((o) => (\n <button\n key={o.value}\n type=\"button\"\n onClick={() => onChange(o.value)}\n className={`rounded-md px-3 py-1.5 text-sm font-medium transition ${\n value === o.value ? \"bg-white text-brand-700 shadow-sm\" : \"text-slate-500 hover:text-slate-700\"\n }`}\n >\n {o.label}\n </button>\n ))}\n </div>\n );\n}\n\nfunction mailProviderLabel(p: MailProvider): string {\n return p === \"smtp\" ? \"Eigen SMTP\" : p === \"resend\" ? \"Resend\" : \"Postmark\";\n}\n\n/**\n * Site-brede mailprovider-configuratie (Resend / Postmark / eigen SMTP) + afzender-identiteit.\n * Deze instelling wordt door MEERDERE modules gebruikt (formulier-notificaties én de shop), dus\n * hoort site-breed thuis in de algemene instellingen — niet in een module-paneel. Geheimen gaan\n * uitsluitend via callables naar `sites/{siteId}/private/mail` (deny-all rules); dit paneel toont\n * alleen gemaskeerde status. Alleen een siteadmin kan opslaan.\n */\nexport function MailSettings({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const [status, setStatus] = useState<MailStatus | null>(null);\n const [busy, setBusy] = useState(false);\n\n const [mailProvider, setMailProvider] = useState<MailProvider>(\"smtp\");\n const [resendKey, setResendKey] = useState(\"\");\n const [postmarkToken, setPostmarkToken] = useState(\"\");\n const [smtp, setSmtp] = useState({ host: \"\", port: \"465\", secure: true, user: \"\", pass: \"\" });\n const [fromName, setFromName] = useState(\"\");\n const [fromEmail, setFromEmail] = useState(\"\");\n const [notifyTo, setNotifyTo] = useState(\"\");\n\n const refresh = useCallback(async () => {\n try {\n const m = await callFunction<{ siteId: string }, MailStatus>(\"getMailStatus\")({ siteId });\n setStatus(m);\n setMailProvider(m.provider ?? (m.host ? \"smtp\" : m.hasPostmarkToken ? \"postmark\" : m.hasResendKey ? \"resend\" : \"smtp\"));\n setSmtp((p) => ({ ...p, host: m.host ?? p.host, port: String(m.port ?? 465), secure: m.secure !== false, user: m.user ?? p.user }));\n setFromName(m.fromName ?? \"\");\n setFromEmail(m.fromEmail ?? \"\");\n setNotifyTo(m.notifyTo ?? \"\");\n } catch {\n setStatus(null);\n }\n }, [siteId]);\n\n useEffect(() => {\n void refresh();\n }, [refresh]);\n\n async function saveMail() {\n setBusy(true);\n try {\n const mail: Record<string, unknown> = { provider: mailProvider, fromName, fromEmail, notifyTo };\n if (mailProvider === \"resend\") mail.resendApiKey = resendKey.trim() || undefined;\n if (mailProvider === \"postmark\") mail.postmarkToken = postmarkToken.trim() || undefined;\n if (mailProvider === \"smtp\") {\n mail.host = smtp.host.trim();\n mail.port = Number(smtp.port) || 465;\n mail.secure = smtp.secure;\n mail.user = smtp.user.trim();\n if (smtp.pass) mail.pass = smtp.pass;\n }\n await callFunction<{ siteId: string; mail: Record<string, unknown> }, { ok: boolean }>(\"saveMailSettings\")({ siteId, mail });\n toast({ title: \"E-mailinstellingen opgeslagen\", tone: \"success\" });\n setResendKey(\"\");\n setPostmarkToken(\"\");\n setSmtp((p) => ({ ...p, pass: \"\" }));\n await refresh();\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n async function sendTest() {\n setBusy(true);\n try {\n // `sendShopTestMail` is een generieke testverzending (naam ongewijzigd voor compat).\n const res = await callFunction<{ siteId: string }, { to: string }>(\"sendShopTestMail\")({ siteId });\n toast({ title: \"Testmail verstuurd\", description: `Naar ${res.to} — check de inbox (en spam).`, tone: \"success\" });\n } catch (e) {\n toast({ title: \"Testmail mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n return (\n <Card>\n <div className=\"grid gap-4\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <div>\n <h3 className=\"text-base font-semibold\">E-mail (formulieren & shop)</h3>\n <p className=\"text-xs text-slate-500\">\n De provider waarmee deze site e-mails verstuurt — formulier-notificaties én\n bestelbevestigingen. Een provider verstuurt alleen vanaf een geverifieerd domein.\n </p>\n </div>\n {status && (\n <Badge tone={status.configured ? \"green\" : \"amber\"}>\n {status.configured ? `${mailProviderLabel(mailProvider)} actief` : \"Nog niet ingesteld\"}\n </Badge>\n )}\n </div>\n\n <Segmented\n value={mailProvider}\n onChange={setMailProvider}\n options={[\n { value: \"smtp\", label: \"Eigen SMTP\" },\n { value: \"resend\", label: \"Resend\" },\n { value: \"postmark\", label: \"Postmark\" },\n ]}\n />\n\n {mailProvider === \"smtp\" && (\n <div className=\"grid gap-3\">\n <p className=\"text-sm text-slate-600\">\n Via je eigen mailbox: bv. het adres bij je hostingpartij, of Gmail (smtp.gmail.com, poort 465, met een{\" \"}\n <a className=\"underline\" href=\"https://myaccount.google.com/apppasswords\" target=\"_blank\" rel=\"noreferrer\">app-wachtwoord</a>).\n </p>\n <div className=\"grid gap-3 md:grid-cols-[1fr_110px]\">\n <Field label=\"SMTP-server\"><Input value={smtp.host} onChange={(e) => setSmtp((p) => ({ ...p, host: e.target.value }))} placeholder=\"smtp.gmail.com\" /></Field>\n <Field label=\"Poort\"><Input value={smtp.port} onChange={(e) => setSmtp((p) => ({ ...p, port: e.target.value }))} placeholder=\"465\" /></Field>\n </div>\n <Switch checked={smtp.secure} onCheckedChange={(v) => setSmtp((p) => ({ ...p, secure: v }))} label=\"SSL (poort 465). Uit = STARTTLS (poort 587)\" />\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Gebruikersnaam\"><Input value={smtp.user} onChange={(e) => setSmtp((p) => ({ ...p, user: e.target.value }))} placeholder=\"info@jouwdomein.nl\" /></Field>\n <Field label=\"Wachtwoord\" hint={status?.hasSmtpPass ? \"Leeg laten = ongewijzigd\" : undefined}>\n <Input type=\"password\" value={smtp.pass} onChange={(e) => setSmtp((p) => ({ ...p, pass: e.target.value }))} />\n </Field>\n </div>\n </div>\n )}\n\n {mailProvider === \"resend\" && (\n <div className=\"grid gap-3\">\n <p className=\"text-sm text-slate-600\">\n <a className=\"underline\" href=\"https://resend.com\" target=\"_blank\" rel=\"noreferrer\">Resend</a> verstuurt vanaf een geverifieerd domein.\n Maak een API-sleutel (re_…) en verifieer je afzenderdomein in het Resend-dashboard.\n </p>\n <Field label=\"Resend API-sleutel (re_…)\" hint={status?.hasResendKey ? \"Leeg laten = ongewijzigd\" : undefined}>\n <Input type=\"password\" value={resendKey} onChange={(e) => setResendKey(e.target.value)} placeholder=\"re_…\" />\n </Field>\n </div>\n )}\n\n {mailProvider === \"postmark\" && (\n <div className=\"grid gap-3\">\n <p className=\"text-sm text-slate-600\">\n <a className=\"underline\" href=\"https://postmarkapp.com\" target=\"_blank\" rel=\"noreferrer\">Postmark</a> verstuurt vanaf een geverifieerde afzender/domein (Sender Signature).\n Kopieer het Server-token.\n </p>\n <Field label=\"Postmark server-token\" hint={status?.hasPostmarkToken ? \"Leeg laten = ongewijzigd\" : undefined}>\n <Input type=\"password\" value={postmarkToken} onChange={(e) => setPostmarkToken(e.target.value)} placeholder=\"xxxxxxxx-xxxx-…\" />\n </Field>\n </div>\n )}\n\n {/* Gedeelde afzender-velden */}\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Afzendernaam\"><Input value={fromName} onChange={(e) => setFromName(e.target.value)} placeholder=\"Mijn Site\" /></Field>\n <Field label=\"Afzenderadres\"><Input value={fromEmail} onChange={(e) => setFromEmail(e.target.value)} placeholder=\"info@jouwdomein.nl\" /></Field>\n </div>\n <Field label=\"Notificatie-adres\" hint=\"Hier ontvang je zelf een mail bij elk formulier/elke bestelling; ook het doel van de testmail\">\n <Input value={notifyTo} onChange={(e) => setNotifyTo(e.target.value)} placeholder=\"notificaties@jouwdomein.nl\" />\n </Field>\n\n <div className=\"flex items-center gap-2\">\n <Button onClick={saveMail} disabled={busy}>Opslaan</Button>\n <Button variant=\"secondary\" onClick={sendTest} disabled={busy || !status?.configured}>Testmail versturen</Button>\n </div>\n </div>\n </Card>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -16,6 +16,7 @@ import { useToast } from "../ui/toast";
|
|
|
16
16
|
import { useConfirm } from "../ui/ConfirmDialog";
|
|
17
17
|
import { ModuleSettingsForm } from "./moduleSettingsForm";
|
|
18
18
|
import { SnapshotsCard } from "./SnapshotsCard";
|
|
19
|
+
import { MailSettings } from "./MailSettings";
|
|
19
20
|
function SettingsPanel({ siteId }) {
|
|
20
21
|
const { toast } = useToast();
|
|
21
22
|
const confirm = useConfirm();
|
|
@@ -256,9 +257,14 @@ function SettingsPanel({ siteId }) {
|
|
|
256
257
|
] }, d.id);
|
|
257
258
|
}) })
|
|
258
259
|
] }),
|
|
260
|
+
/* @__PURE__ */ jsx(MailSettings, { siteId }),
|
|
259
261
|
/* @__PURE__ */ jsxs(Card, { children: [
|
|
260
|
-
/* @__PURE__ */ jsx("h3", { className: "mb-1 text-base font-semibold", children: "Formulier-
|
|
261
|
-
/* @__PURE__ */
|
|
262
|
+
/* @__PURE__ */ jsx("h3", { className: "mb-1 text-base font-semibold", children: "Formulier-afzender" }),
|
|
263
|
+
/* @__PURE__ */ jsxs("p", { className: "mb-3 text-xs text-slate-500", children: [
|
|
264
|
+
"De afzender-identiteit die formulier-notificaties gebruiken. Kies de XiCMS-afzender (werkt direct) of het eigen domein van de klant (vereist domeinverificatie). De provider waarmee daadwerkelijk verstuurd wordt, stel je hierboven bij ",
|
|
265
|
+
/* @__PURE__ */ jsx("strong", { children: "E-mail" }),
|
|
266
|
+
" in."
|
|
267
|
+
] }),
|
|
262
268
|
/* @__PURE__ */ jsxs("div", { className: "grid gap-3 md:grid-cols-2", children: [
|
|
263
269
|
/* @__PURE__ */ jsx(Field, { label: "Afzender-modus", children: /* @__PURE__ */ jsx(
|
|
264
270
|
Select,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/settings/SettingsPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { doc, onSnapshot, writeBatch } from \"firebase/firestore\";\nimport { getFunctions, httpsCallable } from \"firebase/functions\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport { functionsRegion } from \"../../firebase/region\";\nimport { SiteSettingsSchema, toPublicSettings, type SiteSettings } from \"../../schema/settings\";\nimport { useModules } from \"../../modules/context\";\nimport type { ModulesSiteConfig } from \"../../modules/types\";\nimport { builtinThemePresets } from \"../../theme/presets\";\nimport { Button, Card, Field, Input } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { Switch } from \"../ui/switch\";\nimport { ArrayEditor } from \"../ui/ArrayEditor\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { ModuleSettingsForm } from \"./moduleSettingsForm\";\nimport { SnapshotsCard } from \"./SnapshotsCard\";\n\nexport function SettingsPanel({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const { defs } = useModules();\n const [s, setS] = useState<SiteSettings>({});\n const [loaded, setLoaded] = useState(false);\n const [exporting, setExporting] = useState(false);\n const [restoring, setRestoring] = useState(false);\n const restoreInput = useRef<HTMLInputElement>(null);\n\n async function downloadBackup(silent = false): Promise<boolean> {\n const app = getFirebaseApp();\n if (!app) return false;\n setExporting(true);\n try {\n const fn = httpsCallable<{ siteId: string }, Record<string, unknown>>(\n getFunctions(app, functionsRegion()),\n \"exportSite\",\n );\n const res = await fn({ siteId });\n const blob = new Blob([JSON.stringify(res.data, null, 2)], { type: \"application/json\" });\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = `xicms-backup-${siteId}-${new Date().toISOString().slice(0, 10)}.json`;\n a.click();\n URL.revokeObjectURL(url);\n if (!silent) toast({ title: \"Back-up gedownload\", tone: \"success\" });\n return true;\n } catch (e) {\n toast({ title: \"Back-up mislukt\", description: (e as Error).message, tone: \"error\" });\n return false;\n } finally {\n setExporting(false);\n }\n }\n\n async function restoreFromFile(file: File) {\n let backup: Record<string, unknown>;\n try {\n backup = JSON.parse(await file.text()) as Record<string, unknown>;\n } catch {\n toast({ title: \"Ongeldig bestand\", description: \"Dit is geen geldig back-upbestand.\", tone: \"error\" });\n return;\n }\n if (backup._format !== \"xicms-export\") {\n toast({ title: \"Ongeldig bestand\", description: \"Dit lijkt geen XiCMS-back-up.\", tone: \"error\" });\n return;\n }\n if (backup.siteId && backup.siteId !== siteId) {\n const ok = await confirm({\n title: \"Back-up van een andere site\",\n description: `Deze back-up is van “${String(backup.siteId)}”, niet van deze site (${siteId}). Toch terugzetten?`,\n confirmLabel: \"Toch terugzetten\",\n tone: \"danger\",\n });\n if (!ok) return;\n }\n const ok = await confirm({\n title: \"Back-up terugzetten?\",\n description: `Dit VERVANGT alle huidige pagina's, nieuws, media en instellingen door de inhoud van de back-up${backup.exportedAt ? ` van ${new Date(String(backup.exportedAt)).toLocaleString(\"nl-NL\")}` : \"\"}. We maken eerst automatisch een back-up van de huidige stand.`,\n confirmLabel: \"Terugzetten\",\n tone: \"danger\",\n });\n if (!ok) return;\n setRestoring(true);\n try {\n // Veiligheid: eerst de huidige stand wegschrijven (als download) vóór we overschrijven.\n // Lukt die back-up niet, dan NIET doorgaan met de destructieve restore (anders ben je de\n // huidige stand kwijt zonder vangnet). De gebruiker kan het daarna opnieuw proberen.\n if (!(await downloadBackup(true))) {\n toast({ title: \"Terugzetten afgebroken\", description: \"De veiligheidsback-up van de huidige stand mislukte; er is niets overschreven.\", tone: \"error\" });\n return;\n }\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase niet geconfigureerd.\");\n const fn = httpsCallable(getFunctions(app, functionsRegion()), \"restoreSite\");\n await fn({ siteId, backup });\n toast({ title: \"Back-up teruggezet\", description: \"Herlaad de pagina om alles bij te werken.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Terugzetten mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setRestoring(false);\n }\n }\n\n useEffect(() => {\n const db = getDb();\n if (!db) {\n setLoaded(true);\n return;\n }\n return onSnapshot(doc(db, \"sites\", siteId, \"settings\", \"site\"), (snap) => {\n if (snap.exists()) setS(snap.data() as SiteSettings);\n setLoaded(true);\n });\n }, [siteId]);\n\n async function save() {\n const db = getDb();\n if (!db) return;\n const parsed = SiteSettingsSchema.safeParse(s);\n if (!parsed.success) {\n toast({ title: \"Ongeldige instellingen\", description: parsed.error.issues[0]?.message, tone: \"error\" });\n return;\n }\n // Volledige (rol-only) settings + de publiek-veilige subset die de live site leest —\n // atomair in één batch, zodat `site` en `public` nooit uit sync kunnen raken.\n try {\n const batch = writeBatch(db);\n batch.set(doc(db, \"sites\", siteId, \"settings\", \"site\"), parsed.data, { merge: true });\n batch.set(doc(db, \"sites\", siteId, \"settings\", \"public\"), toPublicSettings(parsed.data), { merge: true });\n await batch.commit();\n toast({ title: \"Opgeslagen\", description: \"Wijzigingen zijn direct live.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n if (!loaded) return <p className=\"text-sm text-slate-500\">Laden…</p>;\n\n const forms = s.forms ?? { mode: \"xicms\" as const };\n const theme = s.theme ?? { primary: \"#4f46e5\" };\n const footer = s.footer ?? {};\n\n // Hulpfuncties voor de module-instellingen.\n const moduleState = (id: string, defaultEnabled: boolean) => {\n const raw = (s.modules as ModulesSiteConfig | undefined)?.[id];\n if (typeof raw === \"boolean\") return { enabled: raw, config: {} as Record<string, unknown> };\n return { enabled: raw?.enabled ?? defaultEnabled, config: raw?.config ?? {} };\n };\n const setModule = (id: string, next: { enabled: boolean; config: Record<string, unknown> }) =>\n setS({ ...s, modules: { ...(s.modules as ModulesSiteConfig | undefined), [id]: next } });\n\n return (\n <div className=\"grid gap-4\">\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Algemeen</h3>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Sitenaam\"><Input value={s.name ?? \"\"} onChange={(e) => setS({ ...s, name: e.target.value })} /></Field>\n <Field label=\"Domein\"><Input value={s.domain ?? \"\"} onChange={(e) => setS({ ...s, domain: e.target.value })} /></Field>\n <Field label=\"Contact-e-mail\"><Input value={s.contactEmail ?? \"\"} onChange={(e) => setS({ ...s, contactEmail: e.target.value })} /></Field>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Huisstijl</h3>\n <div className=\"grid gap-3\">\n <Field label=\"Thema-preset\" hint=\"Vult kleuren, lettertype en afronding in één keer in.\">\n <Select\n value=\"\"\n placeholder=\"Kies een preset…\"\n onValueChange={(id) => {\n const preset = builtinThemePresets.find((p) => p.id === id);\n if (preset) setS({ ...s, theme: { ...theme, ...preset.theme } });\n }}\n options={builtinThemePresets.map((p) => ({ value: p.id, label: p.label }))}\n />\n </Field>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Primaire kleur\"><Input value={theme.primary} onChange={(e) => setS({ ...s, theme: { ...theme, primary: e.target.value } })} /></Field>\n <Field label=\"Accentkleur\"><Input value={theme.accent ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, accent: e.target.value } })} /></Field>\n <Field label=\"Achtergrondkleur\"><Input value={theme.background ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, background: e.target.value } })} placeholder=\"#ffffff\" /></Field>\n <Field label=\"Tekstkleur\"><Input value={theme.foreground ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, foreground: e.target.value } })} placeholder=\"#0f172a\" /></Field>\n </div>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">SEO (standaard)</h3>\n <div className=\"grid gap-3\">\n <Field label=\"Standaard-omschrijving\">\n <Input value={s.seoDefaults?.description ?? \"\"} onChange={(e) => setS({ ...s, seoDefaults: { ...s.seoDefaults, description: e.target.value } })} />\n </Field>\n <Field label=\"OG-afbeelding (URL)\">\n <Input value={s.seoDefaults?.ogImage ?? \"\"} onChange={(e) => setS({ ...s, seoDefaults: { ...s.seoDefaults, ogImage: e.target.value } })} />\n </Field>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Footer</h3>\n <p className=\"mb-3 text-xs text-slate-500\">Kolommen met links, social-links en copyright. Wijzigingen zijn direct live.</p>\n <div className=\"grid gap-4\">\n <div>\n <span className=\"text-sm font-medium\">Kolommen</span>\n <ArrayEditor\n items={footer.columns ?? []}\n onChange={(columns) => setS({ ...s, footer: { ...footer, columns } })}\n newItem={() => ({ heading: \"\", links: [] })}\n addLabel=\"+ Kolom\"\n emptyLabel=\"Nog geen kolommen.\"\n renderRow={(col, update) => (\n <div className=\"grid gap-2\">\n <Field label=\"Kop\"><Input value={col.heading ?? \"\"} onChange={(e) => update({ heading: e.target.value })} /></Field>\n <ArrayEditor\n items={col.links ?? []}\n onChange={(links) => update({ links })}\n newItem={() => ({ label: \"\", href: \"\" })}\n addLabel=\"+ Link\"\n renderRow={(link, updL) => (\n <div className=\"grid grid-cols-2 gap-2\">\n <Input value={link.label} placeholder=\"Label\" onChange={(e) => updL({ label: e.target.value })} />\n <Input value={link.href} placeholder=\"/pad of https://…\" onChange={(e) => updL({ href: e.target.value })} />\n </div>\n )}\n />\n </div>\n )}\n />\n </div>\n <div>\n <span className=\"text-sm font-medium\">Social-links</span>\n <ArrayEditor\n items={footer.social ?? []}\n onChange={(social) => setS({ ...s, footer: { ...footer, social } })}\n newItem={() => ({ platform: \"\", href: \"\" })}\n addLabel=\"+ Social-link\"\n renderRow={(link, update) => (\n <div className=\"grid grid-cols-2 gap-2\">\n <Input value={link.platform} placeholder=\"bv. Instagram\" onChange={(e) => update({ platform: e.target.value })} />\n <Input value={link.href} placeholder=\"https://…\" onChange={(e) => update({ href: e.target.value })} />\n </div>\n )}\n />\n </div>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Copyright (leeg = automatisch)\">\n <Input value={footer.copyright ?? \"\"} onChange={(e) => setS({ ...s, footer: { ...footer, copyright: e.target.value } })} />\n </Field>\n <label className=\"flex items-center gap-2 self-end text-sm\">\n <Switch checked={footer.showAdminLink !== false} onCheckedChange={(v) => setS({ ...s, footer: { ...footer, showAdminLink: v } })} />\n “Beheer”-link tonen\n </label>\n </div>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Modules</h3>\n <p className=\"mb-3 text-xs text-slate-500\">Zet functies aan/uit en stel ze in. Uitgezette modules verbergen hun blokken en beheer-paneel.</p>\n <div className=\"grid gap-3\">\n {defs\n .filter((d) => !d.core)\n .map((d) => {\n const st = moduleState(d.id, d.defaultEnabled ?? true);\n return (\n <div key={d.id} className=\"rounded-lg border border-slate-200 p-3\">\n <label className=\"flex items-center justify-between gap-2\">\n <span>\n <strong className=\"text-sm\">{d.label}</strong>\n {d.description && <span className=\"block text-xs text-slate-500\">{d.description}</span>}\n </span>\n <Switch checked={st.enabled} onCheckedChange={(v) => setModule(d.id, { enabled: v, config: st.config })} />\n </label>\n {st.enabled && d.settingsSchema && (\n <div className=\"mt-3\">\n <ModuleSettingsForm\n schema={d.settingsSchema}\n value={st.config}\n onChange={(config) => setModule(d.id, { enabled: st.enabled, config })}\n />\n </div>\n )}\n </div>\n );\n })}\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Formulier-e-mail</h3>\n <p className=\"mb-3 text-xs text-slate-500\">\n Een provider verstuurt alleen vanaf een geverifieerd domein. Kies de XiCMS-afzender\n (werkt direct) of het eigen domein van de klant (vereist domeinverificatie).\n </p>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Afzender-modus\">\n <Select\n value={forms.mode}\n onValueChange={(v) => setS({ ...s, forms: { ...forms, mode: v as \"xicms\" | \"client\" } })}\n options={[\n { value: \"xicms\", label: \"XiCMS-domein (aanbevolen)\" },\n { value: \"client\", label: \"Eigen domein van de klant\" },\n ]}\n />\n </Field>\n <Field label=\"Van (from) e-mail\" hint=\"Moet een geverifieerd domein zijn.\">\n <Input value={forms.fromEmail ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, fromEmail: e.target.value } })} />\n </Field>\n <Field label=\"Van (naam)\"><Input value={forms.fromName ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, fromName: e.target.value } })} /></Field>\n <Field label=\"Notificaties naar\" hint=\"Meestal het mailadres van de klant.\">\n <Input value={forms.notifyTo ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, notifyTo: e.target.value } })} />\n </Field>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Back-up & herstel</h3>\n <p className=\"mb-3 text-xs text-slate-500\">\n Sla alle content van deze site (pagina's, nieuws, formulieren, instellingen en je foto-galerij)\n op in één bestand. Bewaar dat veilig; je kunt het later weer terugzetten. De foto-bestanden\n zelf blijven in de cloud staan — de back-up bevat de teksten, instellingen en verwijzingen.\n </p>\n <div className=\"flex flex-wrap gap-2\">\n <Button variant=\"secondary\" onClick={() => void downloadBackup()} disabled={exporting || restoring}>\n {exporting ? \"Bezig…\" : \"Back-up downloaden\"}\n </Button>\n <Button variant=\"secondary\" onClick={() => restoreInput.current?.click()} disabled={restoring || exporting}>\n {restoring ? \"Terugzetten…\" : \"Back-up terugzetten…\"}\n </Button>\n <input\n ref={restoreInput}\n type=\"file\"\n accept=\"application/json,.json\"\n hidden\n onChange={(e) => {\n const f = e.target.files?.[0];\n if (f) void restoreFromFile(f);\n e.target.value = \"\";\n }}\n />\n </div>\n <p className=\"mt-2 text-xs text-slate-400\">\n Terugzetten vervangt de huidige content door de back-up. We maken automatisch eerst een\n back-up van de huidige stand, zodat je nooit iets kwijtraakt.\n </p>\n </Card>\n\n <SnapshotsCard siteId={siteId} />\n\n <div className=\"flex justify-end\">\n <Button onClick={save}>Instellingen opslaan</Button>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["ok"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/settings/SettingsPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { doc, onSnapshot, writeBatch } from \"firebase/firestore\";\nimport { getFunctions, httpsCallable } from \"firebase/functions\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport { functionsRegion } from \"../../firebase/region\";\nimport { SiteSettingsSchema, toPublicSettings, type SiteSettings } from \"../../schema/settings\";\nimport { useModules } from \"../../modules/context\";\nimport type { ModulesSiteConfig } from \"../../modules/types\";\nimport { builtinThemePresets } from \"../../theme/presets\";\nimport { Button, Card, Field, Input } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { Switch } from \"../ui/switch\";\nimport { ArrayEditor } from \"../ui/ArrayEditor\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { ModuleSettingsForm } from \"./moduleSettingsForm\";\nimport { SnapshotsCard } from \"./SnapshotsCard\";\nimport { MailSettings } from \"./MailSettings\";\n\nexport function SettingsPanel({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const { defs } = useModules();\n const [s, setS] = useState<SiteSettings>({});\n const [loaded, setLoaded] = useState(false);\n const [exporting, setExporting] = useState(false);\n const [restoring, setRestoring] = useState(false);\n const restoreInput = useRef<HTMLInputElement>(null);\n\n async function downloadBackup(silent = false): Promise<boolean> {\n const app = getFirebaseApp();\n if (!app) return false;\n setExporting(true);\n try {\n const fn = httpsCallable<{ siteId: string }, Record<string, unknown>>(\n getFunctions(app, functionsRegion()),\n \"exportSite\",\n );\n const res = await fn({ siteId });\n const blob = new Blob([JSON.stringify(res.data, null, 2)], { type: \"application/json\" });\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = `xicms-backup-${siteId}-${new Date().toISOString().slice(0, 10)}.json`;\n a.click();\n URL.revokeObjectURL(url);\n if (!silent) toast({ title: \"Back-up gedownload\", tone: \"success\" });\n return true;\n } catch (e) {\n toast({ title: \"Back-up mislukt\", description: (e as Error).message, tone: \"error\" });\n return false;\n } finally {\n setExporting(false);\n }\n }\n\n async function restoreFromFile(file: File) {\n let backup: Record<string, unknown>;\n try {\n backup = JSON.parse(await file.text()) as Record<string, unknown>;\n } catch {\n toast({ title: \"Ongeldig bestand\", description: \"Dit is geen geldig back-upbestand.\", tone: \"error\" });\n return;\n }\n if (backup._format !== \"xicms-export\") {\n toast({ title: \"Ongeldig bestand\", description: \"Dit lijkt geen XiCMS-back-up.\", tone: \"error\" });\n return;\n }\n if (backup.siteId && backup.siteId !== siteId) {\n const ok = await confirm({\n title: \"Back-up van een andere site\",\n description: `Deze back-up is van “${String(backup.siteId)}”, niet van deze site (${siteId}). Toch terugzetten?`,\n confirmLabel: \"Toch terugzetten\",\n tone: \"danger\",\n });\n if (!ok) return;\n }\n const ok = await confirm({\n title: \"Back-up terugzetten?\",\n description: `Dit VERVANGT alle huidige pagina's, nieuws, media en instellingen door de inhoud van de back-up${backup.exportedAt ? ` van ${new Date(String(backup.exportedAt)).toLocaleString(\"nl-NL\")}` : \"\"}. We maken eerst automatisch een back-up van de huidige stand.`,\n confirmLabel: \"Terugzetten\",\n tone: \"danger\",\n });\n if (!ok) return;\n setRestoring(true);\n try {\n // Veiligheid: eerst de huidige stand wegschrijven (als download) vóór we overschrijven.\n // Lukt die back-up niet, dan NIET doorgaan met de destructieve restore (anders ben je de\n // huidige stand kwijt zonder vangnet). De gebruiker kan het daarna opnieuw proberen.\n if (!(await downloadBackup(true))) {\n toast({ title: \"Terugzetten afgebroken\", description: \"De veiligheidsback-up van de huidige stand mislukte; er is niets overschreven.\", tone: \"error\" });\n return;\n }\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase niet geconfigureerd.\");\n const fn = httpsCallable(getFunctions(app, functionsRegion()), \"restoreSite\");\n await fn({ siteId, backup });\n toast({ title: \"Back-up teruggezet\", description: \"Herlaad de pagina om alles bij te werken.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Terugzetten mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setRestoring(false);\n }\n }\n\n useEffect(() => {\n const db = getDb();\n if (!db) {\n setLoaded(true);\n return;\n }\n return onSnapshot(doc(db, \"sites\", siteId, \"settings\", \"site\"), (snap) => {\n if (snap.exists()) setS(snap.data() as SiteSettings);\n setLoaded(true);\n });\n }, [siteId]);\n\n async function save() {\n const db = getDb();\n if (!db) return;\n const parsed = SiteSettingsSchema.safeParse(s);\n if (!parsed.success) {\n toast({ title: \"Ongeldige instellingen\", description: parsed.error.issues[0]?.message, tone: \"error\" });\n return;\n }\n // Volledige (rol-only) settings + de publiek-veilige subset die de live site leest —\n // atomair in één batch, zodat `site` en `public` nooit uit sync kunnen raken.\n try {\n const batch = writeBatch(db);\n batch.set(doc(db, \"sites\", siteId, \"settings\", \"site\"), parsed.data, { merge: true });\n batch.set(doc(db, \"sites\", siteId, \"settings\", \"public\"), toPublicSettings(parsed.data), { merge: true });\n await batch.commit();\n toast({ title: \"Opgeslagen\", description: \"Wijzigingen zijn direct live.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n if (!loaded) return <p className=\"text-sm text-slate-500\">Laden…</p>;\n\n const forms = s.forms ?? { mode: \"xicms\" as const };\n const theme = s.theme ?? { primary: \"#4f46e5\" };\n const footer = s.footer ?? {};\n\n // Hulpfuncties voor de module-instellingen.\n const moduleState = (id: string, defaultEnabled: boolean) => {\n const raw = (s.modules as ModulesSiteConfig | undefined)?.[id];\n if (typeof raw === \"boolean\") return { enabled: raw, config: {} as Record<string, unknown> };\n return { enabled: raw?.enabled ?? defaultEnabled, config: raw?.config ?? {} };\n };\n const setModule = (id: string, next: { enabled: boolean; config: Record<string, unknown> }) =>\n setS({ ...s, modules: { ...(s.modules as ModulesSiteConfig | undefined), [id]: next } });\n\n return (\n <div className=\"grid gap-4\">\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Algemeen</h3>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Sitenaam\"><Input value={s.name ?? \"\"} onChange={(e) => setS({ ...s, name: e.target.value })} /></Field>\n <Field label=\"Domein\"><Input value={s.domain ?? \"\"} onChange={(e) => setS({ ...s, domain: e.target.value })} /></Field>\n <Field label=\"Contact-e-mail\"><Input value={s.contactEmail ?? \"\"} onChange={(e) => setS({ ...s, contactEmail: e.target.value })} /></Field>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Huisstijl</h3>\n <div className=\"grid gap-3\">\n <Field label=\"Thema-preset\" hint=\"Vult kleuren, lettertype en afronding in één keer in.\">\n <Select\n value=\"\"\n placeholder=\"Kies een preset…\"\n onValueChange={(id) => {\n const preset = builtinThemePresets.find((p) => p.id === id);\n if (preset) setS({ ...s, theme: { ...theme, ...preset.theme } });\n }}\n options={builtinThemePresets.map((p) => ({ value: p.id, label: p.label }))}\n />\n </Field>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Primaire kleur\"><Input value={theme.primary} onChange={(e) => setS({ ...s, theme: { ...theme, primary: e.target.value } })} /></Field>\n <Field label=\"Accentkleur\"><Input value={theme.accent ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, accent: e.target.value } })} /></Field>\n <Field label=\"Achtergrondkleur\"><Input value={theme.background ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, background: e.target.value } })} placeholder=\"#ffffff\" /></Field>\n <Field label=\"Tekstkleur\"><Input value={theme.foreground ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, foreground: e.target.value } })} placeholder=\"#0f172a\" /></Field>\n </div>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">SEO (standaard)</h3>\n <div className=\"grid gap-3\">\n <Field label=\"Standaard-omschrijving\">\n <Input value={s.seoDefaults?.description ?? \"\"} onChange={(e) => setS({ ...s, seoDefaults: { ...s.seoDefaults, description: e.target.value } })} />\n </Field>\n <Field label=\"OG-afbeelding (URL)\">\n <Input value={s.seoDefaults?.ogImage ?? \"\"} onChange={(e) => setS({ ...s, seoDefaults: { ...s.seoDefaults, ogImage: e.target.value } })} />\n </Field>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Footer</h3>\n <p className=\"mb-3 text-xs text-slate-500\">Kolommen met links, social-links en copyright. Wijzigingen zijn direct live.</p>\n <div className=\"grid gap-4\">\n <div>\n <span className=\"text-sm font-medium\">Kolommen</span>\n <ArrayEditor\n items={footer.columns ?? []}\n onChange={(columns) => setS({ ...s, footer: { ...footer, columns } })}\n newItem={() => ({ heading: \"\", links: [] })}\n addLabel=\"+ Kolom\"\n emptyLabel=\"Nog geen kolommen.\"\n renderRow={(col, update) => (\n <div className=\"grid gap-2\">\n <Field label=\"Kop\"><Input value={col.heading ?? \"\"} onChange={(e) => update({ heading: e.target.value })} /></Field>\n <ArrayEditor\n items={col.links ?? []}\n onChange={(links) => update({ links })}\n newItem={() => ({ label: \"\", href: \"\" })}\n addLabel=\"+ Link\"\n renderRow={(link, updL) => (\n <div className=\"grid grid-cols-2 gap-2\">\n <Input value={link.label} placeholder=\"Label\" onChange={(e) => updL({ label: e.target.value })} />\n <Input value={link.href} placeholder=\"/pad of https://…\" onChange={(e) => updL({ href: e.target.value })} />\n </div>\n )}\n />\n </div>\n )}\n />\n </div>\n <div>\n <span className=\"text-sm font-medium\">Social-links</span>\n <ArrayEditor\n items={footer.social ?? []}\n onChange={(social) => setS({ ...s, footer: { ...footer, social } })}\n newItem={() => ({ platform: \"\", href: \"\" })}\n addLabel=\"+ Social-link\"\n renderRow={(link, update) => (\n <div className=\"grid grid-cols-2 gap-2\">\n <Input value={link.platform} placeholder=\"bv. Instagram\" onChange={(e) => update({ platform: e.target.value })} />\n <Input value={link.href} placeholder=\"https://…\" onChange={(e) => update({ href: e.target.value })} />\n </div>\n )}\n />\n </div>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Copyright (leeg = automatisch)\">\n <Input value={footer.copyright ?? \"\"} onChange={(e) => setS({ ...s, footer: { ...footer, copyright: e.target.value } })} />\n </Field>\n <label className=\"flex items-center gap-2 self-end text-sm\">\n <Switch checked={footer.showAdminLink !== false} onCheckedChange={(v) => setS({ ...s, footer: { ...footer, showAdminLink: v } })} />\n “Beheer”-link tonen\n </label>\n </div>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Modules</h3>\n <p className=\"mb-3 text-xs text-slate-500\">Zet functies aan/uit en stel ze in. Uitgezette modules verbergen hun blokken en beheer-paneel.</p>\n <div className=\"grid gap-3\">\n {defs\n .filter((d) => !d.core)\n .map((d) => {\n const st = moduleState(d.id, d.defaultEnabled ?? true);\n return (\n <div key={d.id} className=\"rounded-lg border border-slate-200 p-3\">\n <label className=\"flex items-center justify-between gap-2\">\n <span>\n <strong className=\"text-sm\">{d.label}</strong>\n {d.description && <span className=\"block text-xs text-slate-500\">{d.description}</span>}\n </span>\n <Switch checked={st.enabled} onCheckedChange={(v) => setModule(d.id, { enabled: v, config: st.config })} />\n </label>\n {st.enabled && d.settingsSchema && (\n <div className=\"mt-3\">\n <ModuleSettingsForm\n schema={d.settingsSchema}\n value={st.config}\n onChange={(config) => setModule(d.id, { enabled: st.enabled, config })}\n />\n </div>\n )}\n </div>\n );\n })}\n </div>\n </Card>\n\n {/* Site-brede mailprovider (formulieren én shop). Zie docs/SHARED-SETTINGS.md. */}\n <MailSettings siteId={siteId} />\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Formulier-afzender</h3>\n <p className=\"mb-3 text-xs text-slate-500\">\n De afzender-identiteit die formulier-notificaties gebruiken. Kies de XiCMS-afzender\n (werkt direct) of het eigen domein van de klant (vereist domeinverificatie). De provider\n waarmee daadwerkelijk verstuurd wordt, stel je hierboven bij <strong>E-mail</strong> in.\n </p>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Afzender-modus\">\n <Select\n value={forms.mode}\n onValueChange={(v) => setS({ ...s, forms: { ...forms, mode: v as \"xicms\" | \"client\" } })}\n options={[\n { value: \"xicms\", label: \"XiCMS-domein (aanbevolen)\" },\n { value: \"client\", label: \"Eigen domein van de klant\" },\n ]}\n />\n </Field>\n <Field label=\"Van (from) e-mail\" hint=\"Moet een geverifieerd domein zijn.\">\n <Input value={forms.fromEmail ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, fromEmail: e.target.value } })} />\n </Field>\n <Field label=\"Van (naam)\"><Input value={forms.fromName ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, fromName: e.target.value } })} /></Field>\n <Field label=\"Notificaties naar\" hint=\"Meestal het mailadres van de klant.\">\n <Input value={forms.notifyTo ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, notifyTo: e.target.value } })} />\n </Field>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Back-up & herstel</h3>\n <p className=\"mb-3 text-xs text-slate-500\">\n Sla alle content van deze site (pagina's, nieuws, formulieren, instellingen en je foto-galerij)\n op in één bestand. Bewaar dat veilig; je kunt het later weer terugzetten. De foto-bestanden\n zelf blijven in de cloud staan — de back-up bevat de teksten, instellingen en verwijzingen.\n </p>\n <div className=\"flex flex-wrap gap-2\">\n <Button variant=\"secondary\" onClick={() => void downloadBackup()} disabled={exporting || restoring}>\n {exporting ? \"Bezig…\" : \"Back-up downloaden\"}\n </Button>\n <Button variant=\"secondary\" onClick={() => restoreInput.current?.click()} disabled={restoring || exporting}>\n {restoring ? \"Terugzetten…\" : \"Back-up terugzetten…\"}\n </Button>\n <input\n ref={restoreInput}\n type=\"file\"\n accept=\"application/json,.json\"\n hidden\n onChange={(e) => {\n const f = e.target.files?.[0];\n if (f) void restoreFromFile(f);\n e.target.value = \"\";\n }}\n />\n </div>\n <p className=\"mt-2 text-xs text-slate-400\">\n Terugzetten vervangt de huidige content door de back-up. We maken automatisch eerst een\n back-up van de huidige stand, zodat je nooit iets kwijtraakt.\n </p>\n </Card>\n\n <SnapshotsCard siteId={siteId} />\n\n <div className=\"flex justify-end\">\n <Button onClick={save}>Instellingen opslaan</Button>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["ok"]}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Koppelingen — betaalprovider (Stripe of Mollie)
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Koppelingen — betaalprovider (Stripe of Mollie) voor de shop. Geheimen gaan uitsluitend via
|
|
3
|
+
* callables naar `sites/{siteId}/private/*` (deny-all rules); dit paneel toont alleen gemaskeerde
|
|
4
|
+
* status. Alleen een siteadmin kan opslaan.
|
|
5
|
+
*
|
|
6
|
+
* NB: de mailprovider (Resend/Postmark/SMTP) is een SITE-BREDE instelling — die staat niet meer
|
|
7
|
+
* hier maar onder Instellingen → E-mail (zie docs/SHARED-SETTINGS.md), zodat ook sites zónder shop
|
|
8
|
+
* hun formulier-mail kunnen instellen.
|
|
5
9
|
*/
|
|
6
10
|
export declare function ConnectionsTab({ siteId }: {
|
|
7
11
|
siteId: string;
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useEffect, useState } from "react";
|
|
4
4
|
import { Badge, Button, Card, Field, Input } from "../ui/primitives";
|
|
5
|
-
import { Switch } from "../ui/switch";
|
|
6
5
|
import { useToast } from "../ui/toast";
|
|
7
6
|
import { shopCallable } from "./lib";
|
|
8
7
|
function Segmented({
|
|
@@ -28,30 +27,16 @@ function ConnectionsTab({ siteId }) {
|
|
|
28
27
|
const [payProvider, setPayProvider] = useState("stripe");
|
|
29
28
|
const [stripeKey, setStripeKey] = useState("");
|
|
30
29
|
const [mollieKey, setMollieKey] = useState("");
|
|
31
|
-
const [mailProvider, setMailProvider] = useState("smtp");
|
|
32
|
-
const [resendKey, setResendKey] = useState("");
|
|
33
|
-
const [postmarkToken, setPostmarkToken] = useState("");
|
|
34
|
-
const [smtp, setSmtp] = useState({ host: "", port: "465", secure: true, user: "", pass: "" });
|
|
35
|
-
const [fromName, setFromName] = useState("");
|
|
36
|
-
const [fromEmail, setFromEmail] = useState("");
|
|
37
|
-
const [notifyTo, setNotifyTo] = useState("");
|
|
38
30
|
const refresh = useCallback(async () => {
|
|
39
31
|
try {
|
|
40
32
|
const raw = await shopCallable("getShopStatus")({ siteId });
|
|
41
33
|
const s = {
|
|
42
34
|
provider: raw.provider ?? "stripe",
|
|
43
35
|
stripe: raw.stripe ?? { configured: false, webhookConfigured: false },
|
|
44
|
-
mollie: raw.mollie ?? { configured: false }
|
|
45
|
-
mail: raw.mail ?? { configured: false }
|
|
36
|
+
mollie: raw.mollie ?? { configured: false }
|
|
46
37
|
};
|
|
47
38
|
setStatus(s);
|
|
48
39
|
setPayProvider(s.provider ?? "stripe");
|
|
49
|
-
const m = s.mail ?? {};
|
|
50
|
-
setMailProvider(m.provider ?? (m.host ? "smtp" : m.hasPostmarkToken ? "postmark" : m.hasResendKey ? "resend" : "smtp"));
|
|
51
|
-
setSmtp((p) => ({ ...p, host: m.host ?? p.host, port: String(m.port ?? 465), secure: m.secure !== false, user: m.user ?? p.user }));
|
|
52
|
-
setFromName(m.fromName ?? "");
|
|
53
|
-
setFromEmail(m.fromEmail ?? "");
|
|
54
|
-
setNotifyTo(m.notifyTo ?? "");
|
|
55
40
|
} catch {
|
|
56
41
|
setStatus(null);
|
|
57
42
|
}
|
|
@@ -100,42 +85,6 @@ function ConnectionsTab({ siteId }) {
|
|
|
100
85
|
}
|
|
101
86
|
}
|
|
102
87
|
}
|
|
103
|
-
async function saveMail() {
|
|
104
|
-
setBusy(true);
|
|
105
|
-
try {
|
|
106
|
-
const mail = { provider: mailProvider, fromName, fromEmail, notifyTo };
|
|
107
|
-
if (mailProvider === "resend") mail.resendApiKey = resendKey.trim() || void 0;
|
|
108
|
-
if (mailProvider === "postmark") mail.postmarkToken = postmarkToken.trim() || void 0;
|
|
109
|
-
if (mailProvider === "smtp") {
|
|
110
|
-
mail.host = smtp.host.trim();
|
|
111
|
-
mail.port = Number(smtp.port) || 465;
|
|
112
|
-
mail.secure = smtp.secure;
|
|
113
|
-
mail.user = smtp.user.trim();
|
|
114
|
-
if (smtp.pass) mail.pass = smtp.pass;
|
|
115
|
-
}
|
|
116
|
-
await shopCallable("saveMailSettings")({ siteId, mail });
|
|
117
|
-
toast({ title: "E-mailinstellingen opgeslagen", tone: "success" });
|
|
118
|
-
setResendKey("");
|
|
119
|
-
setPostmarkToken("");
|
|
120
|
-
setSmtp((p) => ({ ...p, pass: "" }));
|
|
121
|
-
await refresh();
|
|
122
|
-
} catch (e) {
|
|
123
|
-
toast({ title: "Opslaan mislukt", description: e instanceof Error ? e.message : void 0, tone: "error" });
|
|
124
|
-
} finally {
|
|
125
|
-
setBusy(false);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
async function sendTest() {
|
|
129
|
-
setBusy(true);
|
|
130
|
-
try {
|
|
131
|
-
const res = await shopCallable("sendShopTestMail")({ siteId });
|
|
132
|
-
toast({ title: "Testmail verstuurd", description: `Naar ${res.to} \u2014 check de inbox (en spam).`, tone: "success" });
|
|
133
|
-
} catch (e) {
|
|
134
|
-
toast({ title: "Testmail mislukt", description: e instanceof Error ? e.message : void 0, tone: "error" });
|
|
135
|
-
} finally {
|
|
136
|
-
setBusy(false);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
88
|
const payConfigured = payProvider === "stripe" ? status?.stripe.configured : status?.mollie.configured;
|
|
140
89
|
return /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
|
|
141
90
|
/* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
|
|
@@ -193,76 +142,19 @@ function ConnectionsTab({ siteId }) {
|
|
|
193
142
|
/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Button, { onClick: savePayment, disabled: busy || !mollieKey.trim(), children: "Koppel Mollie" }) })
|
|
194
143
|
] })
|
|
195
144
|
] }) }),
|
|
196
|
-
/* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
|
|
197
|
-
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [
|
|
198
|
-
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: "E-mail (bestelbevestigingen & formulieren)" }),
|
|
199
|
-
status && /* @__PURE__ */ jsx(Badge, { tone: status.mail.configured ? "green" : "amber", children: status.mail.configured ? `${mailProviderLabel(mailProvider)} actief` : "Nog niet ingesteld" })
|
|
200
|
-
] }),
|
|
201
|
-
/* @__PURE__ */ jsx(
|
|
202
|
-
Segmented,
|
|
203
|
-
{
|
|
204
|
-
value: mailProvider,
|
|
205
|
-
onChange: setMailProvider,
|
|
206
|
-
options: [
|
|
207
|
-
{ value: "smtp", label: "Eigen SMTP" },
|
|
208
|
-
{ value: "resend", label: "Resend" },
|
|
209
|
-
{ value: "postmark", label: "Postmark" }
|
|
210
|
-
]
|
|
211
|
-
}
|
|
212
|
-
),
|
|
213
|
-
mailProvider === "smtp" && /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
|
|
214
|
-
/* @__PURE__ */ jsxs("p", { className: "text-sm text-slate-600", children: [
|
|
215
|
-
"Via je eigen mailbox: bv. het adres bij je hostingpartij, of Gmail (smtp.gmail.com, poort 465, met een",
|
|
216
|
-
" ",
|
|
217
|
-
/* @__PURE__ */ jsx("a", { className: "underline", href: "https://myaccount.google.com/apppasswords", target: "_blank", rel: "noreferrer", children: "app-wachtwoord" }),
|
|
218
|
-
")."
|
|
219
|
-
] }),
|
|
220
|
-
/* @__PURE__ */ jsxs("div", { className: "grid gap-3 md:grid-cols-[1fr_110px]", children: [
|
|
221
|
-
/* @__PURE__ */ jsx(Field, { label: "SMTP-server", children: /* @__PURE__ */ jsx(Input, { value: smtp.host, onChange: (e) => setSmtp((p) => ({ ...p, host: e.target.value })), placeholder: "smtp.gmail.com" }) }),
|
|
222
|
-
/* @__PURE__ */ jsx(Field, { label: "Poort", children: /* @__PURE__ */ jsx(Input, { value: smtp.port, onChange: (e) => setSmtp((p) => ({ ...p, port: e.target.value })), placeholder: "465" }) })
|
|
223
|
-
] }),
|
|
224
|
-
/* @__PURE__ */ jsx(Switch, { checked: smtp.secure, onCheckedChange: (v) => setSmtp((p) => ({ ...p, secure: v })), label: "SSL (poort 465). Uit = STARTTLS (poort 587)" }),
|
|
225
|
-
/* @__PURE__ */ jsxs("div", { className: "grid gap-3 md:grid-cols-2", children: [
|
|
226
|
-
/* @__PURE__ */ jsx(Field, { label: "Gebruikersnaam", children: /* @__PURE__ */ jsx(Input, { value: smtp.user, onChange: (e) => setSmtp((p) => ({ ...p, user: e.target.value })), placeholder: "info@jouwdomein.nl" }) }),
|
|
227
|
-
/* @__PURE__ */ jsx(Field, { label: "Wachtwoord", hint: status?.mail.hasSmtpPass ? "Leeg laten = ongewijzigd" : void 0, children: /* @__PURE__ */ jsx(Input, { type: "password", value: smtp.pass, onChange: (e) => setSmtp((p) => ({ ...p, pass: e.target.value })) }) })
|
|
228
|
-
] })
|
|
229
|
-
] }),
|
|
230
|
-
mailProvider === "resend" && /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
|
|
231
|
-
/* @__PURE__ */ jsxs("p", { className: "text-sm text-slate-600", children: [
|
|
232
|
-
/* @__PURE__ */ jsx("a", { className: "underline", href: "https://resend.com", target: "_blank", rel: "noreferrer", children: "Resend" }),
|
|
233
|
-
" verstuurt vanaf een geverifieerd domein. Maak een API-sleutel (re_\u2026) en verifieer je afzenderdomein in het Resend-dashboard."
|
|
234
|
-
] }),
|
|
235
|
-
/* @__PURE__ */ jsx(Field, { label: "Resend API-sleutel (re_\u2026)", hint: status?.mail.hasResendKey ? "Leeg laten = ongewijzigd" : void 0, children: /* @__PURE__ */ jsx(Input, { type: "password", value: resendKey, onChange: (e) => setResendKey(e.target.value), placeholder: "re_\u2026" }) })
|
|
236
|
-
] }),
|
|
237
|
-
mailProvider === "postmark" && /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
|
|
238
|
-
/* @__PURE__ */ jsxs("p", { className: "text-sm text-slate-600", children: [
|
|
239
|
-
/* @__PURE__ */ jsx("a", { className: "underline", href: "https://postmarkapp.com", target: "_blank", rel: "noreferrer", children: "Postmark" }),
|
|
240
|
-
" verstuurt vanaf een geverifieerde afzender/domein (Sender Signature). Kopieer het Server-token."
|
|
241
|
-
] }),
|
|
242
|
-
/* @__PURE__ */ jsx(Field, { label: "Postmark server-token", hint: status?.mail.hasPostmarkToken ? "Leeg laten = ongewijzigd" : void 0, children: /* @__PURE__ */ jsx(Input, { type: "password", value: postmarkToken, onChange: (e) => setPostmarkToken(e.target.value), placeholder: "xxxxxxxx-xxxx-\u2026" }) })
|
|
243
|
-
] }),
|
|
244
|
-
/* @__PURE__ */ jsxs("div", { className: "grid gap-3 md:grid-cols-2", children: [
|
|
245
|
-
/* @__PURE__ */ jsx(Field, { label: "Afzendernaam", children: /* @__PURE__ */ jsx(Input, { value: fromName, onChange: (e) => setFromName(e.target.value), placeholder: "Christelijk Kraamcadeau" }) }),
|
|
246
|
-
/* @__PURE__ */ jsx(Field, { label: "Afzenderadres", children: /* @__PURE__ */ jsx(Input, { value: fromEmail, onChange: (e) => setFromEmail(e.target.value), placeholder: "info@jouwdomein.nl" }) })
|
|
247
|
-
] }),
|
|
248
|
-
/* @__PURE__ */ jsx(Field, { label: "Notificatie-adres", hint: "Hier ontvang je zelf een mail bij elke bestelling; ook het doel van de testmail", children: /* @__PURE__ */ jsx(Input, { value: notifyTo, onChange: (e) => setNotifyTo(e.target.value), placeholder: "bestellingen@jouwdomein.nl" }) }),
|
|
249
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
250
|
-
/* @__PURE__ */ jsx(Button, { onClick: saveMail, disabled: busy, children: "Opslaan" }),
|
|
251
|
-
/* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: sendTest, disabled: busy || !status?.mail.configured, children: "Stuur testmail" })
|
|
252
|
-
] })
|
|
253
|
-
] }) }),
|
|
254
145
|
/* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs("p", { className: "text-sm text-slate-600", children: [
|
|
255
|
-
"
|
|
256
|
-
/* @__PURE__ */ jsx("strong", { children: "
|
|
257
|
-
"
|
|
146
|
+
"De ",
|
|
147
|
+
/* @__PURE__ */ jsx("strong", { children: "e-mailprovider" }),
|
|
148
|
+
" (bestelbevestigingen \xE9n formulieren) stel je site-breed in onder ",
|
|
149
|
+
/* @__PURE__ */ jsx("strong", { children: "Instellingen \u2192 E-mail" }),
|
|
150
|
+
". Verzendkosten, valuta, ordernummer-prefix en bedrijfsgegevens (factuur) staan onder het tabblad ",
|
|
151
|
+
/* @__PURE__ */ jsx("strong", { children: "Instellingen" }),
|
|
152
|
+
"; mailteksten onder ",
|
|
258
153
|
/* @__PURE__ */ jsx("strong", { children: "E-mails" }),
|
|
259
154
|
"."
|
|
260
155
|
] }) })
|
|
261
156
|
] });
|
|
262
157
|
}
|
|
263
|
-
function mailProviderLabel(p) {
|
|
264
|
-
return p === "smtp" ? "Eigen SMTP" : p === "resend" ? "Resend" : "Postmark";
|
|
265
|
-
}
|
|
266
158
|
export {
|
|
267
159
|
ConnectionsTab
|
|
268
160
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/shop/ConnectionsTab.tsx"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useState } from \"react\";\nimport { Badge, Button, Card, Field, Input } from \"../ui/primitives\";\nimport { Switch } from \"../ui/switch\";\nimport { useToast } from \"../ui/toast\";\nimport { shopCallable } from \"./lib\";\n\ntype PayProvider = \"stripe\" | \"mollie\";\ntype MailProvider = \"resend\" | \"postmark\" | \"smtp\";\n\ninterface ShopStatus {\n provider: PayProvider;\n stripe: { configured: boolean; account?: string; webhookConfigured: boolean };\n mollie: { configured: boolean; profile?: string };\n mail: {\n provider?: MailProvider;\n configured: boolean;\n host?: string;\n port?: number;\n secure?: boolean;\n user?: string;\n fromName?: string;\n fromEmail?: string;\n notifyTo?: string;\n hasResendKey?: boolean;\n hasPostmarkToken?: boolean;\n hasSmtpPass?: boolean;\n };\n}\n\n/** Segmented control voor providerkeuze. */\nfunction Segmented<T extends string>({\n value,\n options,\n onChange,\n}: {\n value: T;\n options: { value: T; label: string }[];\n onChange: (v: T) => void;\n}) {\n return (\n <div className=\"inline-flex rounded-lg border border-slate-200 bg-slate-50 p-0.5\">\n {options.map((o) => (\n <button\n key={o.value}\n type=\"button\"\n onClick={() => onChange(o.value)}\n className={`rounded-md px-3 py-1.5 text-sm font-medium transition ${\n value === o.value ? \"bg-white text-brand-700 shadow-sm\" : \"text-slate-500 hover:text-slate-700\"\n }`}\n >\n {o.label}\n </button>\n ))}\n </div>\n );\n}\n\n/**\n * Koppelingen — betaalprovider (Stripe of Mollie) en mailprovider (Resend, Postmark of eigen SMTP).\n * Geheimen gaan uitsluitend via callables naar `sites/{siteId}/private/*` (deny-all rules); dit\n * paneel toont alleen gemaskeerde status. Alleen een siteadmin kan opslaan.\n */\nexport function ConnectionsTab({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const [status, setStatus] = useState<ShopStatus | null>(null);\n const [busy, setBusy] = useState(false);\n\n // Betaling\n const [payProvider, setPayProvider] = useState<PayProvider>(\"stripe\");\n const [stripeKey, setStripeKey] = useState(\"\");\n const [mollieKey, setMollieKey] = useState(\"\");\n\n // Mail\n const [mailProvider, setMailProvider] = useState<MailProvider>(\"smtp\");\n const [resendKey, setResendKey] = useState(\"\");\n const [postmarkToken, setPostmarkToken] = useState(\"\");\n const [smtp, setSmtp] = useState({ host: \"\", port: \"465\", secure: true, user: \"\", pass: \"\" });\n const [fromName, setFromName] = useState(\"\");\n const [fromEmail, setFromEmail] = useState(\"\");\n const [notifyTo, setNotifyTo] = useState(\"\");\n\n const refresh = useCallback(async () => {\n try {\n const raw = await shopCallable<{ siteId: string }, Partial<ShopStatus>>(\"getShopStatus\")({ siteId });\n // Normaliseer naar de volledige vorm zodat de render nooit op een ontbrekend deel-object\n // (bv. een oudere functions-versie zonder mollie/mail) kan crashen.\n const s: ShopStatus = {\n provider: raw.provider ?? \"stripe\",\n stripe: raw.stripe ?? { configured: false, webhookConfigured: false },\n mollie: raw.mollie ?? { configured: false },\n mail: raw.mail ?? { configured: false },\n };\n setStatus(s);\n setPayProvider(s.provider ?? \"stripe\");\n const m = s.mail ?? {};\n setMailProvider(m.provider ?? (m.host ? \"smtp\" : m.hasPostmarkToken ? \"postmark\" : m.hasResendKey ? \"resend\" : \"smtp\"));\n setSmtp((p) => ({ ...p, host: m.host ?? p.host, port: String(m.port ?? 465), secure: m.secure !== false, user: m.user ?? p.user }));\n setFromName(m.fromName ?? \"\");\n setFromEmail(m.fromEmail ?? \"\");\n setNotifyTo(m.notifyTo ?? \"\");\n } catch {\n setStatus(null);\n }\n }, [siteId]);\n\n useEffect(() => {\n void refresh();\n }, [refresh]);\n\n async function savePayment() {\n setBusy(true);\n try {\n const payload: Record<string, unknown> = { siteId, provider: payProvider };\n if (payProvider === \"stripe\") {\n if (!stripeKey.trim().startsWith(\"sk_\") && !stripeKey.trim().startsWith(\"rk_\")) {\n toast({ title: \"Ongeldige sleutel\", description: \"Plak een Stripe geheime sleutel (sk_…) of restricted key (rk_…).\", tone: \"error\" });\n setBusy(false);\n return;\n }\n payload.stripe = { secretKey: stripeKey.trim() };\n } else {\n if (!/^(test|live)_/.test(mollieKey.trim())) {\n toast({ title: \"Ongeldige sleutel\", description: \"Plak een Mollie API-sleutel (test_… of live_…).\", tone: \"error\" });\n setBusy(false);\n return;\n }\n payload.mollie = { apiKey: mollieKey.trim() };\n }\n const res = await shopCallable<Record<string, unknown>, { account?: string }>(\"saveShopSecrets\")(payload);\n toast({ title: `${payProvider === \"stripe\" ? \"Stripe\" : \"Mollie\"} gekoppeld`, description: res.account ? `Account: ${res.account}` : undefined, tone: \"success\" });\n setStripeKey(\"\");\n setMollieKey(\"\");\n await refresh();\n } catch (e) {\n toast({ title: \"Koppelen mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n async function switchActiveProvider(p: PayProvider) {\n setPayProvider(p);\n // Alleen de actieve provider wisselen als de andere al geconfigureerd is (anders vraagt het formulier eerst een sleutel).\n const otherConfigured = p === \"stripe\" ? status?.stripe.configured : status?.mollie.configured;\n if (otherConfigured) {\n try {\n await shopCallable<{ siteId: string; provider: PayProvider }, unknown>(\"saveShopSecrets\")({ siteId, provider: p });\n await refresh();\n } catch {\n /* stil — de gebruiker kan alsnog opslaan */\n }\n }\n }\n\n async function saveMail() {\n setBusy(true);\n try {\n const mail: Record<string, unknown> = { provider: mailProvider, fromName, fromEmail, notifyTo };\n if (mailProvider === \"resend\") mail.resendApiKey = resendKey.trim() || undefined;\n if (mailProvider === \"postmark\") mail.postmarkToken = postmarkToken.trim() || undefined;\n if (mailProvider === \"smtp\") {\n mail.host = smtp.host.trim();\n mail.port = Number(smtp.port) || 465;\n mail.secure = smtp.secure;\n mail.user = smtp.user.trim();\n if (smtp.pass) mail.pass = smtp.pass;\n }\n await shopCallable<{ siteId: string; mail: Record<string, unknown> }, { ok: boolean }>(\"saveMailSettings\")({ siteId, mail });\n toast({ title: \"E-mailinstellingen opgeslagen\", tone: \"success\" });\n setResendKey(\"\");\n setPostmarkToken(\"\");\n setSmtp((p) => ({ ...p, pass: \"\" }));\n await refresh();\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n async function sendTest() {\n setBusy(true);\n try {\n const res = await shopCallable<{ siteId: string }, { to: string }>(\"sendShopTestMail\")({ siteId });\n toast({ title: \"Testmail verstuurd\", description: `Naar ${res.to} — check de inbox (en spam).`, tone: \"success\" });\n } catch (e) {\n toast({ title: \"Testmail mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n const payConfigured = payProvider === \"stripe\" ? status?.stripe.configured : status?.mollie.configured;\n\n return (\n <div className=\"grid gap-4\">\n {/* ── Betalingen ─────────────────────────────────────────────── */}\n <Card>\n <div className=\"grid gap-4\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <h3 className=\"text-sm font-semibold\">Betalingen</h3>\n {status && (\n <Badge tone={payConfigured ? \"green\" : \"amber\"}>\n {payProvider === \"stripe\"\n ? status.stripe.configured\n ? `Stripe actief${status.stripe.account ? ` — ${status.stripe.account}` : \"\"}`\n : \"Stripe nog niet gekoppeld\"\n : status.mollie.configured\n ? `Mollie actief${status.mollie.profile ? ` — ${status.mollie.profile}` : \"\"}`\n : \"Mollie nog niet gekoppeld\"}\n </Badge>\n )}\n </div>\n\n <Segmented\n value={payProvider}\n onChange={(v) => void switchActiveProvider(v)}\n options={[\n { value: \"stripe\", label: \"Stripe\" },\n { value: \"mollie\", label: \"Mollie\" },\n ]}\n />\n\n {payProvider === \"stripe\" ? (\n <div className=\"grid gap-3\">\n <ol className=\"grid list-decimal gap-1 pl-5 text-sm text-slate-600\">\n <li>Maak een gratis account op <a className=\"underline\" href=\"https://dashboard.stripe.com/register\" target=\"_blank\" rel=\"noreferrer\">stripe.com</a> en activeer het (IBAN, KVK).</li>\n <li>Zet onder <em>Settings → Payment methods</em> iDEAL aan.</li>\n <li>Maak onder <em>Developers → API keys</em> een sleutel aan en plak hem hieronder — de webhook wordt automatisch aangemaakt.</li>\n </ol>\n <Field label=\"Stripe geheime sleutel (sk_live_… of rk_live_…)\" hint=\"Wordt veilig opgeslagen; nooit zichtbaar in de browser\">\n <Input type=\"password\" value={stripeKey} onChange={(e) => setStripeKey(e.target.value)} placeholder=\"sk_live_…\" />\n </Field>\n {status?.stripe.configured && !status.stripe.webhookConfigured && (\n <p className=\"rounded-lg bg-amber-50 px-3 py-2 text-sm text-amber-700\">\n Webhook nog niet geregistreerd — koppel opnieuw met een sleutel die webhook-rechten heeft.\n </p>\n )}\n <div><Button onClick={savePayment} disabled={busy || !stripeKey.trim()}>Koppel Stripe</Button></div>\n </div>\n ) : (\n <div className=\"grid gap-3\">\n <ol className=\"grid list-decimal gap-1 pl-5 text-sm text-slate-600\">\n <li>Maak een account op <a className=\"underline\" href=\"https://www.mollie.com/dashboard/signup\" target=\"_blank\" rel=\"noreferrer\">mollie.com</a> en doorloop de onboarding.</li>\n <li>Zet in het Mollie-dashboard de gewenste betaalmethoden (o.a. iDEAL) aan.</li>\n <li>Kopieer onder <em>Developers → API-keys</em> je live- of test-sleutel en plak hem hieronder. Mollie heeft geen aparte webhook-registratie nodig.</li>\n </ol>\n <Field label=\"Mollie API-sleutel (live_… of test_…)\" hint=\"Wordt veilig opgeslagen; nooit zichtbaar in de browser\">\n <Input type=\"password\" value={mollieKey} onChange={(e) => setMollieKey(e.target.value)} placeholder=\"live_…\" />\n </Field>\n <div><Button onClick={savePayment} disabled={busy || !mollieKey.trim()}>Koppel Mollie</Button></div>\n </div>\n )}\n </div>\n </Card>\n\n {/* ── E-mail ─────────────────────────────────────────────────── */}\n <Card>\n <div className=\"grid gap-4\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <h3 className=\"text-sm font-semibold\">E-mail (bestelbevestigingen & formulieren)</h3>\n {status && (\n <Badge tone={status.mail.configured ? \"green\" : \"amber\"}>\n {status.mail.configured ? `${mailProviderLabel(mailProvider)} actief` : \"Nog niet ingesteld\"}\n </Badge>\n )}\n </div>\n\n <Segmented\n value={mailProvider}\n onChange={setMailProvider}\n options={[\n { value: \"smtp\", label: \"Eigen SMTP\" },\n { value: \"resend\", label: \"Resend\" },\n { value: \"postmark\", label: \"Postmark\" },\n ]}\n />\n\n {mailProvider === \"smtp\" && (\n <div className=\"grid gap-3\">\n <p className=\"text-sm text-slate-600\">\n Via je eigen mailbox: bv. het adres bij je hostingpartij, of Gmail (smtp.gmail.com, poort 465, met een{\" \"}\n <a className=\"underline\" href=\"https://myaccount.google.com/apppasswords\" target=\"_blank\" rel=\"noreferrer\">app-wachtwoord</a>).\n </p>\n <div className=\"grid gap-3 md:grid-cols-[1fr_110px]\">\n <Field label=\"SMTP-server\"><Input value={smtp.host} onChange={(e) => setSmtp((p) => ({ ...p, host: e.target.value }))} placeholder=\"smtp.gmail.com\" /></Field>\n <Field label=\"Poort\"><Input value={smtp.port} onChange={(e) => setSmtp((p) => ({ ...p, port: e.target.value }))} placeholder=\"465\" /></Field>\n </div>\n <Switch checked={smtp.secure} onCheckedChange={(v) => setSmtp((p) => ({ ...p, secure: v }))} label=\"SSL (poort 465). Uit = STARTTLS (poort 587)\" />\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Gebruikersnaam\"><Input value={smtp.user} onChange={(e) => setSmtp((p) => ({ ...p, user: e.target.value }))} placeholder=\"info@jouwdomein.nl\" /></Field>\n <Field label=\"Wachtwoord\" hint={status?.mail.hasSmtpPass ? \"Leeg laten = ongewijzigd\" : undefined}>\n <Input type=\"password\" value={smtp.pass} onChange={(e) => setSmtp((p) => ({ ...p, pass: e.target.value }))} />\n </Field>\n </div>\n </div>\n )}\n\n {mailProvider === \"resend\" && (\n <div className=\"grid gap-3\">\n <p className=\"text-sm text-slate-600\">\n <a className=\"underline\" href=\"https://resend.com\" target=\"_blank\" rel=\"noreferrer\">Resend</a> verstuurt vanaf een geverifieerd domein.\n Maak een API-sleutel (re_…) en verifieer je afzenderdomein in het Resend-dashboard.\n </p>\n <Field label=\"Resend API-sleutel (re_…)\" hint={status?.mail.hasResendKey ? \"Leeg laten = ongewijzigd\" : undefined}>\n <Input type=\"password\" value={resendKey} onChange={(e) => setResendKey(e.target.value)} placeholder=\"re_…\" />\n </Field>\n </div>\n )}\n\n {mailProvider === \"postmark\" && (\n <div className=\"grid gap-3\">\n <p className=\"text-sm text-slate-600\">\n <a className=\"underline\" href=\"https://postmarkapp.com\" target=\"_blank\" rel=\"noreferrer\">Postmark</a> verstuurt vanaf een geverifieerde afzender/domein (Sender Signature).\n Kopieer het Server-token.\n </p>\n <Field label=\"Postmark server-token\" hint={status?.mail.hasPostmarkToken ? \"Leeg laten = ongewijzigd\" : undefined}>\n <Input type=\"password\" value={postmarkToken} onChange={(e) => setPostmarkToken(e.target.value)} placeholder=\"xxxxxxxx-xxxx-…\" />\n </Field>\n </div>\n )}\n\n {/* Gedeelde afzender-velden */}\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Afzendernaam\"><Input value={fromName} onChange={(e) => setFromName(e.target.value)} placeholder=\"Christelijk Kraamcadeau\" /></Field>\n <Field label=\"Afzenderadres\"><Input value={fromEmail} onChange={(e) => setFromEmail(e.target.value)} placeholder=\"info@jouwdomein.nl\" /></Field>\n </div>\n <Field label=\"Notificatie-adres\" hint=\"Hier ontvang je zelf een mail bij elke bestelling; ook het doel van de testmail\">\n <Input value={notifyTo} onChange={(e) => setNotifyTo(e.target.value)} placeholder=\"bestellingen@jouwdomein.nl\" />\n </Field>\n\n <div className=\"flex items-center gap-2\">\n <Button onClick={saveMail} disabled={busy}>Opslaan</Button>\n <Button variant=\"secondary\" onClick={sendTest} disabled={busy || !status?.mail.configured}>Stuur testmail</Button>\n </div>\n </div>\n </Card>\n\n <Card>\n <p className=\"text-sm text-slate-600\">\n Verzendkosten, valuta, ordernummer-prefix en bedrijfsgegevens (factuur) stel je in onder het tabblad\n <strong> Instellingen</strong>. Mailteksten pas je aan onder <strong>E-mails</strong>.\n </p>\n </Card>\n </div>\n );\n}\n\nfunction mailProviderLabel(p: MailProvider): string {\n return p === \"smtp\" ? \"Eigen SMTP\" : p === \"resend\" ? \"Resend\" : \"Postmark\";\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/shop/ConnectionsTab.tsx"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useState } from \"react\";\nimport { Badge, Button, Card, Field, Input } from \"../ui/primitives\";\nimport { useToast } from \"../ui/toast\";\nimport { shopCallable } from \"./lib\";\n\ntype PayProvider = \"stripe\" | \"mollie\";\n\ninterface ShopStatus {\n provider: PayProvider;\n stripe: { configured: boolean; account?: string; webhookConfigured: boolean };\n mollie: { configured: boolean; profile?: string };\n}\n\n/** Segmented control voor providerkeuze. */\nfunction Segmented<T extends string>({\n value,\n options,\n onChange,\n}: {\n value: T;\n options: { value: T; label: string }[];\n onChange: (v: T) => void;\n}) {\n return (\n <div className=\"inline-flex rounded-lg border border-slate-200 bg-slate-50 p-0.5\">\n {options.map((o) => (\n <button\n key={o.value}\n type=\"button\"\n onClick={() => onChange(o.value)}\n className={`rounded-md px-3 py-1.5 text-sm font-medium transition ${\n value === o.value ? \"bg-white text-brand-700 shadow-sm\" : \"text-slate-500 hover:text-slate-700\"\n }`}\n >\n {o.label}\n </button>\n ))}\n </div>\n );\n}\n\n/**\n * Koppelingen — betaalprovider (Stripe of Mollie) voor de shop. Geheimen gaan uitsluitend via\n * callables naar `sites/{siteId}/private/*` (deny-all rules); dit paneel toont alleen gemaskeerde\n * status. Alleen een siteadmin kan opslaan.\n *\n * NB: de mailprovider (Resend/Postmark/SMTP) is een SITE-BREDE instelling — die staat niet meer\n * hier maar onder Instellingen → E-mail (zie docs/SHARED-SETTINGS.md), zodat ook sites zónder shop\n * hun formulier-mail kunnen instellen.\n */\nexport function ConnectionsTab({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const [status, setStatus] = useState<ShopStatus | null>(null);\n const [busy, setBusy] = useState(false);\n\n // Betaling\n const [payProvider, setPayProvider] = useState<PayProvider>(\"stripe\");\n const [stripeKey, setStripeKey] = useState(\"\");\n const [mollieKey, setMollieKey] = useState(\"\");\n\n const refresh = useCallback(async () => {\n try {\n const raw = await shopCallable<{ siteId: string }, Partial<ShopStatus>>(\"getShopStatus\")({ siteId });\n // Normaliseer naar de volledige vorm zodat de render nooit op een ontbrekend deel-object\n // (bv. een oudere functions-versie zonder mollie) kan crashen.\n const s: ShopStatus = {\n provider: raw.provider ?? \"stripe\",\n stripe: raw.stripe ?? { configured: false, webhookConfigured: false },\n mollie: raw.mollie ?? { configured: false },\n };\n setStatus(s);\n setPayProvider(s.provider ?? \"stripe\");\n } catch {\n setStatus(null);\n }\n }, [siteId]);\n\n useEffect(() => {\n void refresh();\n }, [refresh]);\n\n async function savePayment() {\n setBusy(true);\n try {\n const payload: Record<string, unknown> = { siteId, provider: payProvider };\n if (payProvider === \"stripe\") {\n if (!stripeKey.trim().startsWith(\"sk_\") && !stripeKey.trim().startsWith(\"rk_\")) {\n toast({ title: \"Ongeldige sleutel\", description: \"Plak een Stripe geheime sleutel (sk_…) of restricted key (rk_…).\", tone: \"error\" });\n setBusy(false);\n return;\n }\n payload.stripe = { secretKey: stripeKey.trim() };\n } else {\n if (!/^(test|live)_/.test(mollieKey.trim())) {\n toast({ title: \"Ongeldige sleutel\", description: \"Plak een Mollie API-sleutel (test_… of live_…).\", tone: \"error\" });\n setBusy(false);\n return;\n }\n payload.mollie = { apiKey: mollieKey.trim() };\n }\n const res = await shopCallable<Record<string, unknown>, { account?: string }>(\"saveShopSecrets\")(payload);\n toast({ title: `${payProvider === \"stripe\" ? \"Stripe\" : \"Mollie\"} gekoppeld`, description: res.account ? `Account: ${res.account}` : undefined, tone: \"success\" });\n setStripeKey(\"\");\n setMollieKey(\"\");\n await refresh();\n } catch (e) {\n toast({ title: \"Koppelen mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n async function switchActiveProvider(p: PayProvider) {\n setPayProvider(p);\n // Alleen de actieve provider wisselen als de andere al geconfigureerd is (anders vraagt het formulier eerst een sleutel).\n const otherConfigured = p === \"stripe\" ? status?.stripe.configured : status?.mollie.configured;\n if (otherConfigured) {\n try {\n await shopCallable<{ siteId: string; provider: PayProvider }, unknown>(\"saveShopSecrets\")({ siteId, provider: p });\n await refresh();\n } catch {\n /* stil — de gebruiker kan alsnog opslaan */\n }\n }\n }\n\n const payConfigured = payProvider === \"stripe\" ? status?.stripe.configured : status?.mollie.configured;\n\n return (\n <div className=\"grid gap-4\">\n {/* ── Betalingen ─────────────────────────────────────────────── */}\n <Card>\n <div className=\"grid gap-4\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <h3 className=\"text-sm font-semibold\">Betalingen</h3>\n {status && (\n <Badge tone={payConfigured ? \"green\" : \"amber\"}>\n {payProvider === \"stripe\"\n ? status.stripe.configured\n ? `Stripe actief${status.stripe.account ? ` — ${status.stripe.account}` : \"\"}`\n : \"Stripe nog niet gekoppeld\"\n : status.mollie.configured\n ? `Mollie actief${status.mollie.profile ? ` — ${status.mollie.profile}` : \"\"}`\n : \"Mollie nog niet gekoppeld\"}\n </Badge>\n )}\n </div>\n\n <Segmented\n value={payProvider}\n onChange={(v) => void switchActiveProvider(v)}\n options={[\n { value: \"stripe\", label: \"Stripe\" },\n { value: \"mollie\", label: \"Mollie\" },\n ]}\n />\n\n {payProvider === \"stripe\" ? (\n <div className=\"grid gap-3\">\n <ol className=\"grid list-decimal gap-1 pl-5 text-sm text-slate-600\">\n <li>Maak een gratis account op <a className=\"underline\" href=\"https://dashboard.stripe.com/register\" target=\"_blank\" rel=\"noreferrer\">stripe.com</a> en activeer het (IBAN, KVK).</li>\n <li>Zet onder <em>Settings → Payment methods</em> iDEAL aan.</li>\n <li>Maak onder <em>Developers → API keys</em> een sleutel aan en plak hem hieronder — de webhook wordt automatisch aangemaakt.</li>\n </ol>\n <Field label=\"Stripe geheime sleutel (sk_live_… of rk_live_…)\" hint=\"Wordt veilig opgeslagen; nooit zichtbaar in de browser\">\n <Input type=\"password\" value={stripeKey} onChange={(e) => setStripeKey(e.target.value)} placeholder=\"sk_live_…\" />\n </Field>\n {status?.stripe.configured && !status.stripe.webhookConfigured && (\n <p className=\"rounded-lg bg-amber-50 px-3 py-2 text-sm text-amber-700\">\n Webhook nog niet geregistreerd — koppel opnieuw met een sleutel die webhook-rechten heeft.\n </p>\n )}\n <div><Button onClick={savePayment} disabled={busy || !stripeKey.trim()}>Koppel Stripe</Button></div>\n </div>\n ) : (\n <div className=\"grid gap-3\">\n <ol className=\"grid list-decimal gap-1 pl-5 text-sm text-slate-600\">\n <li>Maak een account op <a className=\"underline\" href=\"https://www.mollie.com/dashboard/signup\" target=\"_blank\" rel=\"noreferrer\">mollie.com</a> en doorloop de onboarding.</li>\n <li>Zet in het Mollie-dashboard de gewenste betaalmethoden (o.a. iDEAL) aan.</li>\n <li>Kopieer onder <em>Developers → API-keys</em> je live- of test-sleutel en plak hem hieronder. Mollie heeft geen aparte webhook-registratie nodig.</li>\n </ol>\n <Field label=\"Mollie API-sleutel (live_… of test_…)\" hint=\"Wordt veilig opgeslagen; nooit zichtbaar in de browser\">\n <Input type=\"password\" value={mollieKey} onChange={(e) => setMollieKey(e.target.value)} placeholder=\"live_…\" />\n </Field>\n <div><Button onClick={savePayment} disabled={busy || !mollieKey.trim()}>Koppel Mollie</Button></div>\n </div>\n )}\n </div>\n </Card>\n\n <Card>\n <p className=\"text-sm text-slate-600\">\n De <strong>e-mailprovider</strong> (bestelbevestigingen én formulieren) stel je site-breed in\n onder <strong>Instellingen → E-mail</strong>. Verzendkosten, valuta, ordernummer-prefix en\n bedrijfsgegevens (factuur) staan onder het tabblad <strong>Instellingen</strong>; mailteksten\n onder <strong>E-mails</strong>.\n </p>\n </Card>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Webwinkel-beheer: bestellingen, producten, categorieën, kortingscodes, e-mailtemplates,
|
|
3
|
-
* winkel-instellingen en koppelingen (Stripe/
|
|
3
|
+
* winkel-instellingen en koppelingen (betaalprovider Stripe/Mollie). De mailprovider is
|
|
4
|
+
* site-breed en staat onder Instellingen → E-mail (zie docs/SHARED-SETTINGS.md).
|
|
4
5
|
*/
|
|
5
6
|
export declare function ShopPanel({ siteId }: {
|
|
6
7
|
siteId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/shop/ShopPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"../ui/tabs\";\nimport { ProductsTab } from \"./ProductsTab\";\nimport { CategoriesTab } from \"./CategoriesTab\";\nimport { OrdersTab } from \"./OrdersTab\";\nimport { DiscountsTab } from \"./DiscountsTab\";\nimport { EmailsTab } from \"./EmailsTab\";\nimport { ShopSettingsTab } from \"./ShopSettingsTab\";\nimport { ConnectionsTab } from \"./ConnectionsTab\";\n\n/**\n * Webwinkel-beheer: bestellingen, producten, categorieën, kortingscodes, e-mailtemplates,\n * winkel-instellingen en koppelingen (Stripe/
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/shop/ShopPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"../ui/tabs\";\nimport { ProductsTab } from \"./ProductsTab\";\nimport { CategoriesTab } from \"./CategoriesTab\";\nimport { OrdersTab } from \"./OrdersTab\";\nimport { DiscountsTab } from \"./DiscountsTab\";\nimport { EmailsTab } from \"./EmailsTab\";\nimport { ShopSettingsTab } from \"./ShopSettingsTab\";\nimport { ConnectionsTab } from \"./ConnectionsTab\";\n\n/**\n * Webwinkel-beheer: bestellingen, producten, categorieën, kortingscodes, e-mailtemplates,\n * winkel-instellingen en koppelingen (betaalprovider Stripe/Mollie). De mailprovider is\n * site-breed en staat onder Instellingen → E-mail (zie docs/SHARED-SETTINGS.md).\n */\nexport function ShopPanel({ siteId }: { siteId: string }) {\n return (\n <Tabs defaultValue=\"orders\">\n <TabsList>\n <TabsTrigger value=\"orders\">Bestellingen</TabsTrigger>\n <TabsTrigger value=\"products\">Producten</TabsTrigger>\n <TabsTrigger value=\"categories\">Categorieën</TabsTrigger>\n <TabsTrigger value=\"discounts\">Kortingen</TabsTrigger>\n <TabsTrigger value=\"emails\">E-mails</TabsTrigger>\n <TabsTrigger value=\"settings\">Instellingen</TabsTrigger>\n <TabsTrigger value=\"connections\">Koppelingen</TabsTrigger>\n </TabsList>\n <TabsContent value=\"orders\">\n <OrdersTab siteId={siteId} />\n </TabsContent>\n <TabsContent value=\"products\">\n <ProductsTab siteId={siteId} />\n </TabsContent>\n <TabsContent value=\"categories\">\n <CategoriesTab siteId={siteId} />\n </TabsContent>\n <TabsContent value=\"discounts\">\n <DiscountsTab siteId={siteId} />\n </TabsContent>\n <TabsContent value=\"emails\">\n <EmailsTab siteId={siteId} />\n </TabsContent>\n <TabsContent value=\"settings\">\n <ShopSettingsTab siteId={siteId} />\n </TabsContent>\n <TabsContent value=\"connections\">\n <ConnectionsTab siteId={siteId} />\n </TabsContent>\n </Tabs>\n );\n}\n"],"mappings":";AAmBM,SACE,KADF;AAjBN,SAAS,MAAM,aAAa,UAAU,mBAAmB;AACzD,SAAS,mBAAmB;AAC5B,SAAS,qBAAqB;AAC9B,SAAS,iBAAiB;AAC1B,SAAS,oBAAoB;AAC7B,SAAS,iBAAiB;AAC1B,SAAS,uBAAuB;AAChC,SAAS,sBAAsB;AAOxB,SAAS,UAAU,EAAE,OAAO,GAAuB;AACxD,SACE,qBAAC,QAAK,cAAa,UACjB;AAAA,yBAAC,YACC;AAAA,0BAAC,eAAY,OAAM,UAAS,0BAAY;AAAA,MACxC,oBAAC,eAAY,OAAM,YAAW,uBAAS;AAAA,MACvC,oBAAC,eAAY,OAAM,cAAa,4BAAW;AAAA,MAC3C,oBAAC,eAAY,OAAM,aAAY,uBAAS;AAAA,MACxC,oBAAC,eAAY,OAAM,UAAS,qBAAO;AAAA,MACnC,oBAAC,eAAY,OAAM,YAAW,0BAAY;AAAA,MAC1C,oBAAC,eAAY,OAAM,eAAc,yBAAW;AAAA,OAC9C;AAAA,IACA,oBAAC,eAAY,OAAM,UACjB,8BAAC,aAAU,QAAgB,GAC7B;AAAA,IACA,oBAAC,eAAY,OAAM,YACjB,8BAAC,eAAY,QAAgB,GAC/B;AAAA,IACA,oBAAC,eAAY,OAAM,cACjB,8BAAC,iBAAc,QAAgB,GACjC;AAAA,IACA,oBAAC,eAAY,OAAM,aACjB,8BAAC,gBAAa,QAAgB,GAChC;AAAA,IACA,oBAAC,eAAY,OAAM,UACjB,8BAAC,aAAU,QAAgB,GAC7B;AAAA,IACA,oBAAC,eAAY,OAAM,YACjB,8BAAC,mBAAgB,QAAgB,GACnC;AAAA,IACA,oBAAC,eAAY,OAAM,eACjB,8BAAC,kBAAe,QAAgB,GAClC;AAAA,KACF;AAEJ;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiwebdesign/xicms-core",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.1.0",
|
|
4
4
|
"description": "XiCMS engine: gestructureerde content, pagina's, draft/publish, admin (TipTap+Radix), SEO en beveiliging voor klantsites.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|