@charleslemaux/cms 1.0.0-rc.1

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/react.cjs ADDED
@@ -0,0 +1,665 @@
1
+ 'use client';
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/react/index.ts
22
+ var react_exports = {};
23
+ __export(react_exports, {
24
+ ArticleBody: () => ArticleBody,
25
+ NewsletterForm: () => NewsletterForm,
26
+ NewsletterPage: () => NewsletterPage,
27
+ newsletterFormLabels: () => newsletterFormLabels,
28
+ newsletterPageLabels: () => newsletterPageLabels,
29
+ useArticle: () => useArticle,
30
+ useArticles: () => useArticles,
31
+ useZone: () => useZone
32
+ });
33
+ module.exports = __toCommonJS(react_exports);
34
+
35
+ // src/react/ArticleBody.tsx
36
+ var import_react = require("react");
37
+ var import_jsx_runtime = require("react/jsx-runtime");
38
+ function ArticleBody({ html, sanitize, as: Tag = "div", ...rest }) {
39
+ const cleanHtml = (0, import_react.useMemo)(() => sanitize ? sanitize(html ?? "") : html ?? "", [html, sanitize]);
40
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Tag, { ...rest, dangerouslySetInnerHTML: { __html: cleanHtml } });
41
+ }
42
+
43
+ // src/react/hooks.ts
44
+ var import_react2 = require("react");
45
+
46
+ // src/errors.ts
47
+ var CmsError = class extends Error {
48
+ constructor(status, code, message, retryAfter) {
49
+ super(message);
50
+ this.name = "CmsError";
51
+ this.status = status;
52
+ this.code = code;
53
+ if (retryAfter !== void 0) this.retryAfter = retryAfter;
54
+ }
55
+ };
56
+ function isCmsError(value) {
57
+ return value instanceof Error && value.name === "CmsError" && typeof value.status === "number" && typeof value.code === "string";
58
+ }
59
+
60
+ // src/react/hooks.ts
61
+ function toCmsError(error) {
62
+ if (isCmsError(error)) return error;
63
+ return new CmsError(0, "network_error", error instanceof Error ? error.message : String(error));
64
+ }
65
+ function useCmsQuery(load, deps) {
66
+ const key = JSON.stringify(deps);
67
+ const [state, setState] = (0, import_react2.useState)(() => ({
68
+ key,
69
+ value: { data: void 0, error: void 0, loading: load !== null }
70
+ }));
71
+ (0, import_react2.useEffect)(() => {
72
+ if (!load) {
73
+ setState({ key, value: { data: void 0, error: void 0, loading: false } });
74
+ return void 0;
75
+ }
76
+ const controller = new AbortController();
77
+ setState({ key, value: { data: void 0, error: void 0, loading: true } });
78
+ load(controller.signal).then(
79
+ (data) => {
80
+ if (!controller.signal.aborted) setState({ key, value: { data, error: void 0, loading: false } });
81
+ },
82
+ (error) => {
83
+ if (!controller.signal.aborted) {
84
+ setState({ key, value: { data: void 0, error: toCmsError(error), loading: false } });
85
+ }
86
+ }
87
+ );
88
+ return () => controller.abort();
89
+ }, deps);
90
+ return state.key === key ? state.value : { data: void 0, error: void 0, loading: load !== null };
91
+ }
92
+ function useZone(client, options = {}) {
93
+ const { lang } = options;
94
+ return useCmsQuery((signal) => client.getZone({ lang, signal }), [client.baseUrl, client.zone, lang]);
95
+ }
96
+ function useArticles(client, params = {}) {
97
+ const { lang, category, tag, page, limit } = params;
98
+ return useCmsQuery(
99
+ (signal) => client.listArticles({ lang, category, tag, page, limit, signal }),
100
+ [client.baseUrl, client.zone, lang, category, tag, page, limit]
101
+ );
102
+ }
103
+ function useArticle(client, slug, options = {}) {
104
+ const { lang } = options;
105
+ return useCmsQuery(
106
+ slug ? (signal) => client.getArticle(slug, { lang, signal }) : null,
107
+ [client.baseUrl, client.zone, slug, lang]
108
+ );
109
+ }
110
+
111
+ // src/react/labels.ts
112
+ var FORM_LABELS = {
113
+ en: {
114
+ email: "Email address",
115
+ emailPlaceholder: "you@example.com",
116
+ consent: "I agree to receive this newsletter and can unsubscribe at any time.",
117
+ submit: "Subscribe",
118
+ sending: "Sending\u2026",
119
+ sent: "Almost done: open the email we just sent you and click its link to confirm your subscription.",
120
+ invalidEmail: "This email address is not valid.",
121
+ consentRequired: "Please tick the box to agree.",
122
+ captchaRequired: "Please complete the verification.",
123
+ captchaFailed: "The verification failed. Please try again.",
124
+ rateLimited: "Too many attempts. Please try again later.",
125
+ unavailable: "The newsletter is unavailable right now. Please try again later."
126
+ },
127
+ fr: {
128
+ email: "Adresse e-mail",
129
+ emailPlaceholder: "vous@exemple.com",
130
+ consent: "J\u2019accepte de recevoir cette newsletter et je peux me d\xE9sinscrire \xE0 tout moment.",
131
+ submit: "S\u2019inscrire",
132
+ sending: "Envoi\u2026",
133
+ sent: "Presque termin\xE9 : ouvrez l\u2019e-mail que nous venons de vous envoyer et cliquez sur son lien pour confirmer votre inscription.",
134
+ invalidEmail: "Cette adresse e-mail n\u2019est pas valide.",
135
+ consentRequired: "Cochez la case pour donner votre accord.",
136
+ captchaRequired: "Veuillez compl\xE9ter la v\xE9rification.",
137
+ captchaFailed: "La v\xE9rification a \xE9chou\xE9. Veuillez r\xE9essayer.",
138
+ rateLimited: "Trop de tentatives. Veuillez r\xE9essayer plus tard.",
139
+ unavailable: "La newsletter est indisponible pour le moment. Veuillez r\xE9essayer plus tard."
140
+ },
141
+ de: {
142
+ email: "E-Mail-Adresse",
143
+ emailPlaceholder: "sie@beispiel.de",
144
+ consent: "Ich m\xF6chte diesen Newsletter erhalten und kann mich jederzeit abmelden.",
145
+ submit: "Abonnieren",
146
+ sending: "Wird gesendet\u2026",
147
+ sent: "Fast geschafft: \xD6ffnen Sie die E-Mail, die wir Ihnen gerade geschickt haben, und klicken Sie auf den Link, um Ihr Abonnement zu best\xE4tigen.",
148
+ invalidEmail: "Diese E-Mail-Adresse ist ung\xFCltig.",
149
+ consentRequired: "Bitte setzen Sie das H\xE4kchen, um zuzustimmen.",
150
+ captchaRequired: "Bitte schlie\xDFen Sie die \xDCberpr\xFCfung ab.",
151
+ captchaFailed: "Die \xDCberpr\xFCfung ist fehlgeschlagen. Bitte versuchen Sie es erneut.",
152
+ rateLimited: "Zu viele Versuche. Bitte versuchen Sie es sp\xE4ter erneut.",
153
+ unavailable: "Der Newsletter ist derzeit nicht verf\xFCgbar. Bitte versuchen Sie es sp\xE4ter erneut."
154
+ },
155
+ es: {
156
+ email: "Correo electr\xF3nico",
157
+ emailPlaceholder: "usted@ejemplo.com",
158
+ consent: "Acepto recibir este bolet\xEDn y puedo darme de baja en cualquier momento.",
159
+ submit: "Suscribirme",
160
+ sending: "Enviando\u2026",
161
+ sent: "Casi listo: abra el correo que le acabamos de enviar y haga clic en su enlace para confirmar su suscripci\xF3n.",
162
+ invalidEmail: "Esta direcci\xF3n de correo electr\xF3nico no es v\xE1lida.",
163
+ consentRequired: "Marque la casilla para dar su consentimiento.",
164
+ captchaRequired: "Complete la verificaci\xF3n.",
165
+ captchaFailed: "La verificaci\xF3n ha fallado. Int\xE9ntelo de nuevo.",
166
+ rateLimited: "Demasiados intentos. Int\xE9ntelo de nuevo m\xE1s tarde.",
167
+ unavailable: "El bolet\xEDn no est\xE1 disponible en este momento. Int\xE9ntelo de nuevo m\xE1s tarde."
168
+ }
169
+ };
170
+ var PAGE_LABELS = {
171
+ en: {
172
+ confirmIntro: "Confirm your subscription to our newsletter.",
173
+ confirmButton: "Confirm my subscription",
174
+ confirming: "Confirming\u2026",
175
+ confirmed: "Thank you! Your subscription is confirmed.",
176
+ unsubscribeIntro: "Unsubscribe from our newsletter?",
177
+ unsubscribeButton: "Unsubscribe",
178
+ unsubscribing: "Unsubscribing\u2026",
179
+ unsubscribed: "You are unsubscribed. You will not receive our newsletter anymore.",
180
+ invalidLink: "This link is invalid or has expired.",
181
+ noLink: "Open this page from the link in our email.",
182
+ failed: "Something went wrong. Please try again later."
183
+ },
184
+ fr: {
185
+ confirmIntro: "Confirmez votre inscription \xE0 notre newsletter.",
186
+ confirmButton: "Confirmer mon inscription",
187
+ confirming: "Confirmation\u2026",
188
+ confirmed: "Merci ! Votre inscription est confirm\xE9e.",
189
+ unsubscribeIntro: "Vous d\xE9sinscrire de notre newsletter ?",
190
+ unsubscribeButton: "Me d\xE9sinscrire",
191
+ unsubscribing: "D\xE9sinscription\u2026",
192
+ unsubscribed: "Votre d\xE9sinscription est confirm\xE9e. Vous ne recevrez plus notre newsletter.",
193
+ invalidLink: "Ce lien n\u2019est pas valide ou a expir\xE9.",
194
+ noLink: "Ouvrez cette page depuis le lien de notre e-mail.",
195
+ failed: "Une erreur s\u2019est produite. Veuillez r\xE9essayer plus tard."
196
+ },
197
+ de: {
198
+ confirmIntro: "Best\xE4tigen Sie Ihr Abonnement unseres Newsletters.",
199
+ confirmButton: "Abonnement best\xE4tigen",
200
+ confirming: "Wird best\xE4tigt\u2026",
201
+ confirmed: "Vielen Dank! Ihr Abonnement ist best\xE4tigt.",
202
+ unsubscribeIntro: "Von unserem Newsletter abmelden?",
203
+ unsubscribeButton: "Abmelden",
204
+ unsubscribing: "Wird abgemeldet\u2026",
205
+ unsubscribed: "Sie sind abgemeldet und erhalten unseren Newsletter nicht mehr.",
206
+ invalidLink: "Dieser Link ist ung\xFCltig oder abgelaufen.",
207
+ noLink: "\xD6ffnen Sie diese Seite \xFCber den Link in unserer E-Mail.",
208
+ failed: "Etwas ist schiefgelaufen. Bitte versuchen Sie es sp\xE4ter erneut."
209
+ },
210
+ es: {
211
+ confirmIntro: "Confirme su suscripci\xF3n a nuestro bolet\xEDn.",
212
+ confirmButton: "Confirmar mi suscripci\xF3n",
213
+ confirming: "Confirmando\u2026",
214
+ confirmed: "\xA1Gracias! Su suscripci\xF3n est\xE1 confirmada.",
215
+ unsubscribeIntro: "\xBFDesea darse de baja de nuestro bolet\xEDn?",
216
+ unsubscribeButton: "Darme de baja",
217
+ unsubscribing: "Dando de baja\u2026",
218
+ unsubscribed: "Se ha dado de baja. Ya no recibir\xE1 nuestro bolet\xEDn.",
219
+ invalidLink: "Este enlace no es v\xE1lido o ha caducado.",
220
+ noLink: "Abra esta p\xE1gina desde el enlace de nuestro correo electr\xF3nico.",
221
+ failed: "Algo ha fallado. Int\xE9ntelo de nuevo m\xE1s tarde."
222
+ }
223
+ };
224
+ function pickLocale(lang) {
225
+ const base = (lang ?? "").toLowerCase().split(/[-_]/)[0];
226
+ return base === "fr" || base === "de" || base === "es" ? base : "en";
227
+ }
228
+ function withoutUndefined(values) {
229
+ if (!values) return {};
230
+ return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== void 0));
231
+ }
232
+ function newsletterFormLabels(lang, overrides) {
233
+ return { ...FORM_LABELS[pickLocale(lang)], ...withoutUndefined(overrides) };
234
+ }
235
+ function newsletterPageLabels(lang, overrides) {
236
+ return { ...PAGE_LABELS[pickLocale(lang)], ...withoutUndefined(overrides) };
237
+ }
238
+
239
+ // src/react/NewsletterForm.tsx
240
+ var import_react3 = require("react");
241
+
242
+ // src/react/safe-callback.ts
243
+ function callSafely(fn, ...args) {
244
+ if (!fn) return;
245
+ try {
246
+ fn(...args);
247
+ } catch (error) {
248
+ if (typeof queueMicrotask === "function") {
249
+ queueMicrotask(() => {
250
+ throw error;
251
+ });
252
+ } else {
253
+ setTimeout(() => {
254
+ throw error;
255
+ });
256
+ }
257
+ }
258
+ }
259
+
260
+ // src/react/turnstile.ts
261
+ var TURNSTILE_SCRIPT_URL = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
262
+ var pending = null;
263
+ function loadTurnstile() {
264
+ const win = window;
265
+ if (win.turnstile) return Promise.resolve(win.turnstile);
266
+ if (!pending) {
267
+ pending = new Promise((resolve, reject) => {
268
+ const script = document.createElement("script");
269
+ const fail = (message) => {
270
+ pending = null;
271
+ script.remove();
272
+ reject(new Error(message));
273
+ };
274
+ script.src = TURNSTILE_SCRIPT_URL;
275
+ script.async = true;
276
+ script.addEventListener("load", () => {
277
+ if (win.turnstile) resolve(win.turnstile);
278
+ else fail("Turnstile loaded without its API.");
279
+ });
280
+ script.addEventListener("error", () => fail("Turnstile could not be loaded."));
281
+ document.head.appendChild(script);
282
+ });
283
+ }
284
+ return pending;
285
+ }
286
+
287
+ // src/react/NewsletterForm.tsx
288
+ var import_jsx_runtime2 = require("react/jsx-runtime");
289
+ var HONEYPOT_STYLE = {
290
+ position: "absolute",
291
+ left: "-10000px",
292
+ top: "auto",
293
+ width: "1px",
294
+ height: "1px",
295
+ overflow: "hidden"
296
+ };
297
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
298
+ function problemOf(error) {
299
+ switch (error.code) {
300
+ case "invalid_email":
301
+ return "invalidEmail";
302
+ case "consent_required":
303
+ return "consentRequired";
304
+ case "captcha_failed":
305
+ return "captchaFailed";
306
+ case "rate_limited":
307
+ return "rateLimited";
308
+ default:
309
+ return "unavailable";
310
+ }
311
+ }
312
+ function captchaLanguage(lang) {
313
+ return (lang ?? "").toLowerCase().split(/[-_]/)[0] || "auto";
314
+ }
315
+ function NewsletterForm({
316
+ client,
317
+ lang,
318
+ settings: givenSettings,
319
+ labels: overrides,
320
+ className,
321
+ captchaTheme = "auto",
322
+ onSuccess,
323
+ onError
324
+ }) {
325
+ const labels = newsletterFormLabels(lang, overrides);
326
+ const zone = useCmsQuery(
327
+ givenSettings ? null : (signal) => client.getZone({ signal }),
328
+ [givenSettings === void 0, client.baseUrl, client.zone]
329
+ );
330
+ const settings = givenSettings ?? zone.data?.newsletter;
331
+ const siteKey = settings?.enabled ? settings.captcha?.siteKey : void 0;
332
+ const id = (0, import_react3.useId)();
333
+ const [email, setEmail] = (0, import_react3.useState)("");
334
+ const [consent, setConsent] = (0, import_react3.useState)(false);
335
+ const [status, setStatus] = (0, import_react3.useState)("idle");
336
+ const [problem, setProblem] = (0, import_react3.useState)(null);
337
+ const [token, setToken] = (0, import_react3.useState)(null);
338
+ const [captchaBroken, setCaptchaBroken] = (0, import_react3.useState)(false);
339
+ const honeypotRef = (0, import_react3.useRef)(null);
340
+ const captchaRef = (0, import_react3.useRef)(null);
341
+ const widgetRef = (0, import_react3.useRef)(null);
342
+ const sendingRef = (0, import_react3.useRef)(false);
343
+ const showFields = status !== "sent";
344
+ (0, import_react3.useEffect)(() => {
345
+ const container = captchaRef.current;
346
+ if (!siteKey || !showFields || !container) return void 0;
347
+ let cancelled = false;
348
+ setCaptchaBroken(false);
349
+ const fail = () => {
350
+ if (cancelled) return;
351
+ setCaptchaBroken(true);
352
+ setProblem("unavailable");
353
+ };
354
+ loadTurnstile().then(
355
+ (api) => {
356
+ if (cancelled) return;
357
+ try {
358
+ const widgetId = api.render(container, {
359
+ sitekey: siteKey,
360
+ language: captchaLanguage(lang),
361
+ theme: captchaTheme,
362
+ callback: (value) => {
363
+ setToken(value);
364
+ setProblem((current) => current === "captchaRequired" || current === "captchaFailed" ? null : current);
365
+ },
366
+ "expired-callback": () => setToken(null),
367
+ "error-callback": () => setToken(null)
368
+ });
369
+ if (widgetId) widgetRef.current = { api, id: widgetId };
370
+ else fail();
371
+ } catch {
372
+ fail();
373
+ }
374
+ },
375
+ fail
376
+ );
377
+ return () => {
378
+ cancelled = true;
379
+ if (widgetRef.current) {
380
+ widgetRef.current.api.remove(widgetRef.current.id);
381
+ widgetRef.current = null;
382
+ }
383
+ setToken(null);
384
+ };
385
+ }, [siteKey, showFields, lang, captchaTheme]);
386
+ function clearProblem(which) {
387
+ if (problem === which) {
388
+ setProblem(null);
389
+ setStatus("idle");
390
+ }
391
+ }
392
+ async function handleSubmit(event) {
393
+ event.preventDefault();
394
+ if (sendingRef.current || !settings) return;
395
+ const address = email.trim();
396
+ let local = null;
397
+ if (!EMAIL_RE.test(address) || address.length > 254) local = "invalidEmail";
398
+ else if (!consent) local = "consentRequired";
399
+ else if (siteKey && captchaBroken) local = "unavailable";
400
+ else if (siteKey && !token) local = "captchaRequired";
401
+ if (local) {
402
+ setStatus("error");
403
+ setProblem(local);
404
+ return;
405
+ }
406
+ sendingRef.current = true;
407
+ setStatus("sending");
408
+ setProblem(null);
409
+ let cmsError;
410
+ try {
411
+ await client.subscribe({
412
+ email: address,
413
+ lang,
414
+ consent: true,
415
+ captchaToken: token ?? void 0,
416
+ website: honeypotRef.current?.value ?? ""
417
+ });
418
+ } catch (error) {
419
+ cmsError = isCmsError(error) ? error : new CmsError(0, "network_error", String(error));
420
+ } finally {
421
+ sendingRef.current = false;
422
+ if (widgetRef.current) widgetRef.current.api.reset(widgetRef.current.id);
423
+ setToken(null);
424
+ }
425
+ if (cmsError) {
426
+ setStatus("error");
427
+ setProblem(problemOf(cmsError));
428
+ callSafely(onError, cmsError);
429
+ } else {
430
+ setStatus("sent");
431
+ callSafely(onSuccess, address);
432
+ }
433
+ }
434
+ if (settings && !settings.enabled) return null;
435
+ const failedToLoad = !givenSettings && zone.error !== void 0;
436
+ let message = "";
437
+ if (status === "sent") message = labels.sent;
438
+ else if (problem) message = labels[problem];
439
+ else if (failedToLoad) message = labels.unavailable;
440
+ const displayStatus = problem || failedToLoad ? "error" : status === "error" ? "idle" : status;
441
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
442
+ "form",
443
+ {
444
+ className: ["cms-newsletter-form", className].filter(Boolean).join(" "),
445
+ "data-status": displayStatus,
446
+ "data-captcha": !siteKey ? "none" : token ? "ready" : "pending",
447
+ noValidate: true,
448
+ onSubmit: handleSubmit,
449
+ children: [
450
+ showFields && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
451
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "cms-newsletter-form__field", children: [
452
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { className: "cms-newsletter-form__label", htmlFor: `${id}-email`, children: labels.email }),
453
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
454
+ "input",
455
+ {
456
+ id: `${id}-email`,
457
+ className: "cms-newsletter-form__email",
458
+ type: "email",
459
+ name: "email",
460
+ autoComplete: "email",
461
+ required: true,
462
+ placeholder: labels.emailPlaceholder,
463
+ value: email,
464
+ disabled: status === "sending",
465
+ "aria-invalid": problem === "invalidEmail" || void 0,
466
+ onChange: (event) => {
467
+ setEmail(event.target.value);
468
+ clearProblem("invalidEmail");
469
+ }
470
+ }
471
+ )
472
+ ] }),
473
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "cms-newsletter-form__consent", children: [
474
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
475
+ "input",
476
+ {
477
+ id: `${id}-consent`,
478
+ className: "cms-newsletter-form__checkbox",
479
+ type: "checkbox",
480
+ name: "consent",
481
+ checked: consent,
482
+ disabled: status === "sending",
483
+ "aria-invalid": problem === "consentRequired" || void 0,
484
+ onChange: (event) => {
485
+ setConsent(event.target.checked);
486
+ clearProblem("consentRequired");
487
+ }
488
+ }
489
+ ),
490
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { className: "cms-newsletter-form__consent-label", htmlFor: `${id}-consent`, children: labels.consent })
491
+ ] }),
492
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "cms-newsletter-form__honeypot", "aria-hidden": "true", style: HONEYPOT_STYLE, children: [
493
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { htmlFor: `${id}-website`, children: "Website" }),
494
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
495
+ "input",
496
+ {
497
+ id: `${id}-website`,
498
+ ref: honeypotRef,
499
+ type: "text",
500
+ name: "website",
501
+ tabIndex: -1,
502
+ autoComplete: "off",
503
+ defaultValue: ""
504
+ }
505
+ )
506
+ ] }),
507
+ siteKey && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "cms-newsletter-form__captcha", ref: captchaRef }),
508
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
509
+ "button",
510
+ {
511
+ className: "cms-newsletter-form__submit",
512
+ type: "submit",
513
+ disabled: status === "sending" || !settings || captchaBroken,
514
+ children: status === "sending" ? labels.sending : labels.submit
515
+ }
516
+ )
517
+ ] }),
518
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "cms-newsletter-form__message", role: "status", "aria-live": "polite", children: message })
519
+ ]
520
+ }
521
+ );
522
+ }
523
+
524
+ // src/react/NewsletterPage.tsx
525
+ var import_react4 = require("react");
526
+
527
+ // src/newsletter-action.ts
528
+ var ACTIONS = ["confirm", "unsubscribe"];
529
+ var TOKEN_RE = /^[A-Za-z0-9_-]{16,512}$/;
530
+ function readNewsletterAction(source) {
531
+ if (source == null) return null;
532
+ const params = toSearchParams(source);
533
+ let found = null;
534
+ for (const action of ACTIONS) {
535
+ const values = params.getAll(action);
536
+ if (values.length === 0) continue;
537
+ if (values.length > 1 || found) return null;
538
+ found = { action, token: values[0] ?? "" };
539
+ }
540
+ return found && TOKEN_RE.test(found.token) ? found : null;
541
+ }
542
+ function toSearchParams(source) {
543
+ if (typeof source === "string") {
544
+ const query = source.includes("?") ? source.slice(source.indexOf("?") + 1) : source;
545
+ return new URLSearchParams(query.split("#")[0]);
546
+ }
547
+ if (isSearchParams(source)) return source;
548
+ if (isUrl(source)) return source.searchParams;
549
+ const params = new URLSearchParams();
550
+ for (const [key, value] of Object.entries(source)) {
551
+ const values = Array.isArray(value) ? value : value === void 0 ? [] : [value];
552
+ for (const item of values) params.append(key, item);
553
+ }
554
+ return params;
555
+ }
556
+ function isSearchParams(value) {
557
+ return typeof value.getAll === "function" && typeof value.append === "function";
558
+ }
559
+ function isUrl(value) {
560
+ const url = value;
561
+ return typeof url.href === "string" && typeof url.searchParams === "object" && isSearchParams(url.searchParams);
562
+ }
563
+
564
+ // src/react/NewsletterPage.tsx
565
+ var import_jsx_runtime3 = require("react/jsx-runtime");
566
+ function NewsletterPage({
567
+ client,
568
+ search,
569
+ lang,
570
+ labels: overrides,
571
+ className,
572
+ onDone,
573
+ cleanUrl = true
574
+ }) {
575
+ const labels = newsletterPageLabels(lang, overrides);
576
+ const readsAddressBar = search === void 0;
577
+ const [fromAddressBar, setFromAddressBar] = (0, import_react4.useState)(void 0);
578
+ const [phase, setPhase] = (0, import_react4.useState)("ready");
579
+ const workingRef = (0, import_react4.useRef)(false);
580
+ (0, import_react4.useEffect)(() => {
581
+ if (readsAddressBar) setFromAddressBar(readNewsletterAction(window.location.href));
582
+ }, [readsAddressBar]);
583
+ const action = readsAddressBar ? fromAddressBar : readNewsletterAction(search);
584
+ async function run() {
585
+ if (!action || workingRef.current) return;
586
+ workingRef.current = true;
587
+ setPhase("working");
588
+ let result;
589
+ let failedPhase;
590
+ try {
591
+ result = action.action === "confirm" ? await client.confirm(action.token) : await client.unsubscribe(action.token);
592
+ } catch (error) {
593
+ failedPhase = isCmsError(error) && error.code === "invalid_token" ? "invalid" : "failed";
594
+ } finally {
595
+ workingRef.current = false;
596
+ }
597
+ if (result) {
598
+ setPhase("done");
599
+ if (cleanUrl) removeTokenFromAddressBar();
600
+ callSafely(onDone, result);
601
+ } else if (failedPhase) {
602
+ setPhase(failedPhase);
603
+ }
604
+ }
605
+ let intro = null;
606
+ let button = null;
607
+ let message = "";
608
+ if (action === null) {
609
+ message = labels.noLink;
610
+ } else if (action) {
611
+ const confirming = action.action === "confirm";
612
+ if (phase === "done") {
613
+ message = confirming ? labels.confirmed : labels.unsubscribed;
614
+ } else if (phase === "invalid") {
615
+ message = labels.invalidLink;
616
+ } else {
617
+ intro = confirming ? labels.confirmIntro : labels.unsubscribeIntro;
618
+ if (phase === "working") button = confirming ? labels.confirming : labels.unsubscribing;
619
+ else button = confirming ? labels.confirmButton : labels.unsubscribeButton;
620
+ if (phase === "failed") message = labels.failed;
621
+ }
622
+ }
623
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
624
+ "div",
625
+ {
626
+ className: ["cms-newsletter-page", className].filter(Boolean).join(" "),
627
+ "data-action": action ? action.action : action === null ? "none" : "unknown",
628
+ "data-status": phase,
629
+ children: [
630
+ intro && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "cms-newsletter-page__intro", children: intro }),
631
+ button && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "cms-newsletter-page__button", type: "button", disabled: phase === "working", onClick: run, children: button }),
632
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "cms-newsletter-page__message", role: "status", "aria-live": "polite", children: message })
633
+ ]
634
+ }
635
+ );
636
+ }
637
+ function removeTokenFromAddressBar() {
638
+ const url = new URL(window.location.href);
639
+ if (!url.searchParams.has("confirm") && !url.searchParams.has("unsubscribe")) return;
640
+ url.searchParams.delete("confirm");
641
+ url.searchParams.delete("unsubscribe");
642
+ window.history.replaceState(
643
+ withoutNextRouterKeys(window.history.state),
644
+ "",
645
+ `${url.pathname}${url.search}${url.hash}`
646
+ );
647
+ }
648
+ function withoutNextRouterKeys(state) {
649
+ if (state === null || typeof state !== "object") return state;
650
+ const clone = { ...state };
651
+ delete clone.__NA;
652
+ delete clone._N;
653
+ return clone;
654
+ }
655
+ // Annotate the CommonJS export names for ESM import in node:
656
+ 0 && (module.exports = {
657
+ ArticleBody,
658
+ NewsletterForm,
659
+ NewsletterPage,
660
+ newsletterFormLabels,
661
+ newsletterPageLabels,
662
+ useArticle,
663
+ useArticles,
664
+ useZone
665
+ });