@reblu/site-blocks 0.24.0 → 0.26.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/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # @reblu/site-blocks
2
2
 
3
+ ## 0.26.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 05b1106: «Aviso legal» and «Desistimiento y devoluciones» pages, written by the tenant in nexus (REB-1201).
8
+ - `@reblu/site-contracts`: `Legal` gains optional `legalNotice` and `withdrawalPolicy` (Markdown or `null`). Absent from older servers.
9
+ - `@reblu/site-client`: `client.legal.get()` returns both documents.
10
+ - `@reblu/site-blocks`: new `LegalDocument` drop-in for any legal page: the SITE's title as the only `<h1>`, the tenant's Markdown sanitised below it, and the SITE's `fallback` when the page is not written yet. It never invents legal copy.
11
+
12
+ - 21b7413: Withdrawal form: a buyer communicates a withdrawal from the SITE (REB-1201).
13
+ - `@reblu/site-contracts`: new `WithdrawalRequestInput` and `WithdrawalRequestSubmission` types for `POST /api/site/withdrawal-requests`.
14
+ - `@reblu/site-client`: new `client.withdrawalRequests.submit(body, options?)`, returning `{ id, receivedAt, acknowledgementSent }`.
15
+ - `@reblu/site-blocks`: new `WithdrawalRequestForm` drop-in in `./client` (order number, optional name, email, items, optional reason). It decides nothing about whether the right applies; when no acknowledgement email could be sent, its receipt lists what the buyer communicated.
16
+
17
+ ### Patch Changes
18
+
19
+ - Updated dependencies [05b1106]
20
+ - Updated dependencies [21b7413]
21
+ - @reblu/site-contracts@1.26.0
22
+
23
+ ## 0.25.0
24
+
25
+ ### Minor Changes
26
+
27
+ - 1860554: New `CoursePurchaseTerms`: the drop-in for what a buyer must see before paying for a course (REB-1199). It shows the withdrawal waiver, the access duration and the refund guarantee from `AcademyCourseDetail`. Each term renders only when the course carries it; with none, nothing renders. It is a Server Component. The default copy is Spanish and can be replaced with `labels`. Also exports `DEFAULT_COURSE_PURCHASE_TERMS_LABELS`.
28
+
29
+ ### Patch Changes
30
+
31
+ - Updated dependencies [1860554]
32
+ - Updated dependencies [82ab415]
33
+ - @reblu/site-contracts@1.25.0
34
+
3
35
  ## 0.24.0
4
36
 
5
37
  ### Minor Changes
package/README.md CHANGED
@@ -25,6 +25,7 @@ colour on the red background. See `docs/theming/theming.md`.
25
25
  | `sanitizeHtml` | `.` | Sanitises an HTML string before `dangerouslySetInnerHTML` — the **HTML boundary**. See "Security boundaries" below. |
26
26
  | `MarkdownRenderer` | `.` | Canonical Blog renderer: `marked` → **hast-util-sanitize** (XSS) → component map, with a YouTube-domain allowlist. RSC-safe. |
27
27
  | `CourseDescription` | `.` | Drop-in for the rich-text course `description` (`client.academy.bySlug()`). Server Component. See "Rich-text descriptions". |
28
+ | `CoursePurchaseTerms`, `DEFAULT_COURSE_PURCHASE_TERMS_LABELS` | `.` | What the buyer sees before paying: withdrawal waiver, access duration, refund guarantee. Server Component. See "Course purchase terms". |
28
29
  | `LessonPreview`, `DEFAULT_LESSON_PREVIEW_LABELS` | `.` | A public free-preview lesson: hosted video player + sanitised body (`getLessonPreview()`). Server Component. See "Free-preview lesson". |
29
30
  | `Treatment`, `ParentPage`, `ProfileCard`, `ProductCard` | `.` | Presentational organisms over the SITE API contracts. RSC-safe. `Specialty`: deprecated alias, see below. |
30
31
  | `ProfessionalFields` | `.` | The tenant's own professional fields (`professional.fields`) on a team card or profile. RSC-safe. See below. |
@@ -1127,6 +1128,33 @@ export default async function Page({ params }) {
1127
1128
  `lesson-preview-badge`, `lesson-preview-duration`, `lesson-preview-player`,
1128
1129
  `lesson-preview-content`.
1129
1130
 
1131
+ ## Course purchase terms — `CoursePurchaseTerms` (REB-1199)
1132
+
1133
+ What a buyer must see before paying for a course, from `client.academy.bySlug()`:
1134
+
1135
+ - **Withdrawal waiver** (`withdrawalWaiverRequired`): checkout asks for a separate box in which the buyer requests immediate access and waives the 14-day withdrawal right.
1136
+ - **Access duration** (`accessDurationMonths`): months of access after enrolment. `null` means no end date, and nothing is shown.
1137
+ - **Refund guarantee** (`refundGuarantee`): the course's terms. `null` means none, and nothing is shown.
1138
+
1139
+ Each term renders only when the course carries it. A course with none renders nothing, not even the heading. It is a Server Component, RSC-safe. The copy is Spanish and neutral; replace any text with `labels`.
1140
+
1141
+ ```tsx
1142
+ import { CoursePurchaseTerms } from '@reblu/site-blocks'
1143
+
1144
+ const course = await client.academy.bySlug(slug)
1145
+
1146
+ <CoursePurchaseTerms course={course} heading='Antes de comprar' />
1147
+
1148
+ // In another language:
1149
+ <CoursePurchaseTerms
1150
+ course={course}
1151
+ labels={{
1152
+ withdrawalWaiver: 'Immediate access: you waive the 14-day withdrawal right.',
1153
+ accessDuration: (months) => `Access for ${months} months.`,
1154
+ }}
1155
+ />
1156
+ ```
1157
+
1130
1158
  ## Page blocks
1131
1159
 
1132
1160
  `PageBlocks` renders a page's registry blocks (`PageDetail.blocks` from
@@ -1287,6 +1315,45 @@ renderer (`headingOffset` demotes its headings under yours).
1287
1315
  `markdownToPlainText` drops images and anything the sanitiser strips, and cuts
1288
1316
  at a word boundary with `…` within `maxLength`.
1289
1317
 
1318
+ ## Legal pages — `LegalDocument` (REB-1201)
1319
+
1320
+ Every legal page is Markdown the tenant writes in nexus › Configuración › Legal
1321
+ and `client.legal.get()` serves: `legalNotice` («Aviso legal»),
1322
+ `withdrawalPolicy` («Desistimiento y devoluciones», one page with a section per
1323
+ purchase modality), `privacyPolicy`, `termsAndConditions`, `cookiePolicy`. Drop
1324
+ each one into its own route; link them from your footer, and the withdrawal page
1325
+ from your checkout:
1326
+
1327
+ ```tsx
1328
+ // app/desistimiento/page.tsx
1329
+ import { LegalDocument } from '@reblu/site-blocks'
1330
+
1331
+ export default async function WithdrawalPolicyPage() {
1332
+ const legal = await client.legal.get()
1333
+ return (
1334
+ <main>
1335
+ <LegalDocument
1336
+ title='Desistimiento y devoluciones'
1337
+ content={legal.withdrawalPolicy}
1338
+ fallback={<p>El contenido de esta página aún no ha sido configurado.</p>}
1339
+ />
1340
+ </main>
1341
+ )
1342
+ }
1343
+ ```
1344
+
1345
+ `LegalDocument` (Server Component, RSC-safe):
1346
+
1347
+ - renders `title` as the page's only `<h1>` and lands the document's shallowest
1348
+ heading at `<h2>`, keeping the relative depth of every section below it;
1349
+ - runs the Markdown through the same sanitised pipeline as `MarkdownRenderer`;
1350
+ - for `null` (not written yet, or an older server without the field), blank, or
1351
+ sanitised-away content renders your `fallback` under the title — or nothing
1352
+ without one. It never invents legal copy;
1353
+ - styling hooks: `className`, `titleClassName` (replaces the default
1354
+ `text-3xl font-bold`), `contentClassName`, `titleId`, and
1355
+ `data-slot='legal-document'` on the `<article>`.
1356
+
1290
1357
  ## Contact form interest question — `ContactInterestField` (REB-759)
1291
1358
 
1292
1359
  The tenant's own optional question on the contact form, e.g. «¿Qué tratamiento
@@ -1390,6 +1457,66 @@ statuses this form maps — see the `@reblu/site-next` README.
1390
1457
  `locale`, and the offered rights with `requestTypes`.
1391
1458
  - `onSubmitted(ack)` fires after success — e.g. for analytics.
1392
1459
 
1460
+ ## Withdrawal form — `WithdrawalRequestForm` (REB-1201)
1461
+
1462
+ Drop-in form for the buyer to communicate a withdrawal: order number, optional
1463
+ name, email, what they withdraw from and an optional reason. base-api stores
1464
+ it, emails the tenant team (the contact-form recipients, else the contact
1465
+ email) and sends the buyer an acknowledgement — their copy on a durable
1466
+ medium. Put it on your withdrawal page, under the tenant's text
1467
+ (`LegalDocument` over `legal.withdrawalPolicy`):
1468
+
1469
+ ```tsx
1470
+ 'use client'
1471
+ import { useCallback, useState } from 'react'
1472
+ import { WithdrawalRequestForm } from '@reblu/site-blocks/client'
1473
+ import {
1474
+ DeferredRecaptchaProvider,
1475
+ useDeferredRecaptchaToken,
1476
+ } from '@reblu/site-next/recaptcha-provider'
1477
+
1478
+ function Form({ onArm }: { onArm: () => void }) {
1479
+ const getCaptchaToken = useDeferredRecaptchaToken('withdrawal_request', { onArm })
1480
+ return <WithdrawalRequestForm getCaptchaToken={getCaptchaToken} onInteract={onArm} />
1481
+ }
1482
+
1483
+ export function WithdrawalRequestSection({ siteKey }: { siteKey: string }) {
1484
+ const [armed, setArmed] = useState(false)
1485
+ const arm = useCallback(() => setArmed(true), [])
1486
+ return (
1487
+ <DeferredRecaptchaProvider siteKey={siteKey} armed={armed}>
1488
+ <Form onArm={arm} />
1489
+ </DeferredRecaptchaProvider>
1490
+ )
1491
+ }
1492
+ ```
1493
+
1494
+ ```ts
1495
+ // app/api/withdrawal-requests/route.ts — server side, the API key never reaches the browser
1496
+ import { createProxyRoute } from '@reblu/site-next/route-handlers'
1497
+
1498
+ export const POST = createProxyRoute('/api/site/withdrawal-requests', { method: 'POST' })
1499
+ ```
1500
+
1501
+ - **It decides nothing.** The form does not check the order and excludes no
1502
+ product or service; whether the right applies is the tenant team's call,
1503
+ explained on the tenant's own withdrawal page.
1504
+ - **The receipt.** On success the form is replaced by a `role="status"` receipt
1505
+ with the received-at date. When `acknowledgementSent` is `false` (the tenant
1506
+ has no email delivery configured, or it failed) the receipt also lists the
1507
+ order and the items, so the buyer can keep it.
1508
+ - **Gate it on the reCAPTCHA key alone**, like `DataRequestForm`: base-api
1509
+ rejects a request without a token, and the channel must stay open even with
1510
+ the contact form off.
1511
+ - **No server text reaches the visitor.** `400` → `validationError`, `422` →
1512
+ `captchaError`, `429` → `rateLimitError`, anything else → `genericError`.
1513
+ - **Limits.** The inputs carry `maxLength` at the server limits
1514
+ (`WITHDRAWAL_REQUEST_FIELD_LIMITS`: order 100, email 320, name 200, items
1515
+ 2000, reason 2000).
1516
+ - **Copy** defaults to Spanish; override any string with `labels` (`notice:
1517
+ null` hides the small print), the date locale with `locale`. Pass `submit` for
1518
+ your own transport, `endpoint` for another route path.
1519
+
1393
1520
  ## Security boundaries
1394
1521
 
1395
1522
  Two sinks, two different helpers, in two different packages — do not reach for
package/dist/client.cjs CHANGED
@@ -40,9 +40,12 @@ __export(client_exports, {
40
40
  DEFAULT_COURSE_SYLLABUS_LABELS: () => DEFAULT_COURSE_SYLLABUS_LABELS,
41
41
  DEFAULT_DATA_REQUEST_FORM_LABELS: () => DEFAULT_DATA_REQUEST_FORM_LABELS,
42
42
  DEFAULT_PRODUCT_CAROUSEL_LABELS: () => DEFAULT_PRODUCT_CAROUSEL_LABELS,
43
+ DEFAULT_WITHDRAWAL_REQUEST_FORM_LABELS: () => DEFAULT_WITHDRAWAL_REQUEST_FORM_LABELS,
43
44
  DataRequestForm: () => DataRequestForm,
44
45
  ProductCarousel: () => ProductCarousel,
45
46
  ShareButtons: () => ShareButtons,
47
+ WITHDRAWAL_REQUEST_FIELD_LIMITS: () => WITHDRAWAL_REQUEST_FIELD_LIMITS,
48
+ WithdrawalRequestForm: () => WithdrawalRequestForm,
46
49
  useCollection: () => useCollection
47
50
  });
48
51
  module.exports = __toCommonJS(client_exports);
@@ -1324,6 +1327,277 @@ function DataRequestForm({
1324
1327
  }
1325
1328
  );
1326
1329
  }
1330
+
1331
+ // src/components/withdrawal-request-form.tsx
1332
+ var import_react7 = require("react");
1333
+ var import_utils10 = require("@reblu/site-ui/utils");
1334
+ var import_jsx_runtime10 = require("react/jsx-runtime");
1335
+ var WITHDRAWAL_REQUEST_FIELD_LIMITS = Object.freeze({
1336
+ orderReference: 100,
1337
+ email: 320,
1338
+ name: 200,
1339
+ items: 2e3,
1340
+ reason: 2e3
1341
+ });
1342
+ var DEFAULT_WITHDRAWAL_REQUEST_FORM_LABELS = {
1343
+ orderReferenceLabel: "N\xFAmero de pedido",
1344
+ nameLabel: "Nombre y apellidos (opcional)",
1345
+ emailLabel: "Email",
1346
+ itemsLabel: "Qu\xE9 productos o servicios quieres devolver o cancelar",
1347
+ itemsPlaceholder: "Indica los productos, bonos o cursos y las unidades",
1348
+ reasonLabel: "Motivo (opcional)",
1349
+ notice: "Usaremos estos datos solo para gestionar tu desistimiento.",
1350
+ submit: "Enviar desistimiento",
1351
+ submitting: "Enviando\u2026",
1352
+ orderReferenceRequired: "Indica el n\xFAmero de pedido.",
1353
+ emailInvalid: "Introduce un email v\xE1lido.",
1354
+ itemsRequired: "Indica de qu\xE9 productos o servicios desistes.",
1355
+ success: (receivedAt) => /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
1356
+ "Hemos recibido tu desistimiento el ",
1357
+ receivedAt,
1358
+ ". Te hemos enviado una copia por email."
1359
+ ] }),
1360
+ successWithoutEmail: (receivedAt) => /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
1361
+ "Hemos recibido tu desistimiento el ",
1362
+ receivedAt,
1363
+ ". Guarda esta pantalla como justificante de lo que nos has comunicado:"
1364
+ ] }),
1365
+ receiptOrderReference: "Pedido",
1366
+ receiptItems: "Productos o servicios",
1367
+ captchaError: "No hemos podido verificar que no eres un robot. Recarga la p\xE1gina e int\xE9ntalo de nuevo.",
1368
+ validationError: "Revisa los datos del formulario e int\xE9ntalo de nuevo.",
1369
+ rateLimitError: "Has enviado demasiadas solicitudes. Espera unos minutos e int\xE9ntalo de nuevo.",
1370
+ genericError: "No hemos podido enviar tu desistimiento. Int\xE9ntalo de nuevo m\xE1s tarde."
1371
+ };
1372
+ var FIELD_ORDER2 = ["orderReference", "email", "items"];
1373
+ var EMAIL_PATTERN2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1374
+ async function postToRoute2(endpoint, input) {
1375
+ try {
1376
+ const res = await fetch(endpoint, {
1377
+ method: "POST",
1378
+ headers: { "Content-Type": "application/json" },
1379
+ body: JSON.stringify(input)
1380
+ });
1381
+ if (!res.ok) return { ok: false, status: res.status };
1382
+ const json = await res.json();
1383
+ return json.data ? { ok: true, data: json.data } : { ok: false, status: res.status };
1384
+ } catch {
1385
+ return { ok: false, status: 0 };
1386
+ }
1387
+ }
1388
+ function rejectionMessage2(status, t) {
1389
+ if (status === 400) return t.validationError;
1390
+ if (status === 422) return t.captchaError;
1391
+ if (status === 429) return t.rateLimitError;
1392
+ return t.genericError;
1393
+ }
1394
+ var FIELD_CLASS2 = "w-full rounded-lg border bg-background px-3 py-2 text-sm transition focus:outline-none focus:ring-2 focus:ring-primary/20";
1395
+ function WithdrawalRequestForm({
1396
+ getCaptchaToken,
1397
+ submit,
1398
+ endpoint = "/api/withdrawal-requests",
1399
+ onInteract,
1400
+ onSubmitted,
1401
+ labels,
1402
+ locale = "es-ES",
1403
+ className
1404
+ }) {
1405
+ const t = { ...DEFAULT_WITHDRAWAL_REQUEST_FORM_LABELS, ...labels };
1406
+ const id = (0, import_react7.useId)();
1407
+ const [orderReference, setOrderReference] = (0, import_react7.useState)("");
1408
+ const [name, setName] = (0, import_react7.useState)("");
1409
+ const [email, setEmail] = (0, import_react7.useState)("");
1410
+ const [items, setItems] = (0, import_react7.useState)("");
1411
+ const [reason, setReason] = (0, import_react7.useState)("");
1412
+ const [errors, setErrors] = (0, import_react7.useState)({});
1413
+ const [formError, setFormError] = (0, import_react7.useState)(null);
1414
+ const [submitting, setSubmitting] = (0, import_react7.useState)(false);
1415
+ const [receipt, setReceipt] = (0, import_react7.useState)(null);
1416
+ const statusRef = (0, import_react7.useRef)(null);
1417
+ (0, import_react7.useEffect)(() => {
1418
+ if (receipt) statusRef.current?.focus();
1419
+ }, [receipt]);
1420
+ async function handleSubmit(event) {
1421
+ event.preventDefault();
1422
+ if (submitting) return;
1423
+ setFormError(null);
1424
+ const next = {};
1425
+ if (!orderReference.trim()) next.orderReference = t.orderReferenceRequired;
1426
+ if (!EMAIL_PATTERN2.test(email.trim())) next.email = t.emailInvalid;
1427
+ if (!items.trim()) next.items = t.itemsRequired;
1428
+ setErrors(next);
1429
+ const firstInvalid = FIELD_ORDER2.find((field) => next[field]);
1430
+ if (firstInvalid) {
1431
+ document.getElementById(`${id}-${firstInvalid}`)?.focus();
1432
+ return;
1433
+ }
1434
+ setSubmitting(true);
1435
+ try {
1436
+ const captcha = await getCaptchaToken().catch(() => void 0);
1437
+ if (!captcha) {
1438
+ setFormError(t.captchaError);
1439
+ return;
1440
+ }
1441
+ const input = {
1442
+ orderReference: orderReference.trim(),
1443
+ email: email.trim(),
1444
+ ...name.trim() ? { name: name.trim() } : {},
1445
+ items: items.trim(),
1446
+ ...reason.trim() ? { reason: reason.trim() } : {},
1447
+ captcha
1448
+ };
1449
+ const result = await (submit ?? ((body) => postToRoute2(endpoint, body)))(input).catch(
1450
+ () => ({ ok: false, status: 0 })
1451
+ );
1452
+ if (!result.ok) {
1453
+ setFormError(rejectionMessage2(result.status, t));
1454
+ return;
1455
+ }
1456
+ setReceipt({ ack: result.data, orderReference: input.orderReference, items: input.items });
1457
+ onSubmitted?.(result.data);
1458
+ } finally {
1459
+ setSubmitting(false);
1460
+ }
1461
+ }
1462
+ if (receipt) {
1463
+ const receivedAt = new Intl.DateTimeFormat(locale, {
1464
+ dateStyle: "long",
1465
+ timeStyle: "short"
1466
+ }).format(new Date(receipt.ack.receivedAt));
1467
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1468
+ "div",
1469
+ {
1470
+ ref: statusRef,
1471
+ tabIndex: -1,
1472
+ role: "status",
1473
+ "data-slot": "withdrawal-request-form-success",
1474
+ className: (0, import_utils10.cn)("space-y-3 rounded-lg border border-border p-6 text-sm", className),
1475
+ children: receipt.ack.acknowledgementSent ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { children: t.success(receivedAt) }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
1476
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { children: t.successWithoutEmail(receivedAt) }),
1477
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("dl", { className: "space-y-1", children: [
1478
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { children: [
1479
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("dt", { className: "font-medium", children: t.receiptOrderReference }),
1480
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("dd", { children: receipt.orderReference })
1481
+ ] }),
1482
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { children: [
1483
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("dt", { className: "font-medium", children: t.receiptItems }),
1484
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("dd", { className: "whitespace-pre-line", children: receipt.items })
1485
+ ] })
1486
+ ] })
1487
+ ] })
1488
+ }
1489
+ );
1490
+ }
1491
+ const errorId = (field) => errors[field] ? `${id}-${field}-error` : void 0;
1492
+ const fieldError = (field) => errors[field] ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { id: `${id}-${field}-error`, className: "text-xs text-destructive", children: errors[field] }) : null;
1493
+ const borderFor = (field) => errors[field] ? "border-destructive" : "border-border";
1494
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
1495
+ "form",
1496
+ {
1497
+ noValidate: true,
1498
+ onSubmit: handleSubmit,
1499
+ onFocusCapture: onInteract,
1500
+ onPointerDown: onInteract,
1501
+ "data-slot": "withdrawal-request-form",
1502
+ className: (0, import_utils10.cn)("space-y-4", className),
1503
+ children: [
1504
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "space-y-1", children: [
1505
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("label", { htmlFor: `${id}-orderReference`, className: "text-sm font-medium", children: t.orderReferenceLabel }),
1506
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1507
+ "input",
1508
+ {
1509
+ id: `${id}-orderReference`,
1510
+ type: "text",
1511
+ maxLength: WITHDRAWAL_REQUEST_FIELD_LIMITS.orderReference,
1512
+ value: orderReference,
1513
+ onChange: (e) => setOrderReference(e.target.value),
1514
+ "aria-invalid": errors.orderReference ? true : void 0,
1515
+ "aria-describedby": errorId("orderReference"),
1516
+ className: (0, import_utils10.cn)(FIELD_CLASS2, borderFor("orderReference"))
1517
+ }
1518
+ ),
1519
+ fieldError("orderReference")
1520
+ ] }),
1521
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "space-y-1", children: [
1522
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("label", { htmlFor: `${id}-name`, className: "text-sm font-medium", children: t.nameLabel }),
1523
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1524
+ "input",
1525
+ {
1526
+ id: `${id}-name`,
1527
+ type: "text",
1528
+ autoComplete: "name",
1529
+ maxLength: WITHDRAWAL_REQUEST_FIELD_LIMITS.name,
1530
+ value: name,
1531
+ onChange: (e) => setName(e.target.value),
1532
+ className: (0, import_utils10.cn)(FIELD_CLASS2, "border-border")
1533
+ }
1534
+ )
1535
+ ] }),
1536
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "space-y-1", children: [
1537
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("label", { htmlFor: `${id}-email`, className: "text-sm font-medium", children: t.emailLabel }),
1538
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1539
+ "input",
1540
+ {
1541
+ id: `${id}-email`,
1542
+ type: "email",
1543
+ autoComplete: "email",
1544
+ maxLength: WITHDRAWAL_REQUEST_FIELD_LIMITS.email,
1545
+ value: email,
1546
+ onChange: (e) => setEmail(e.target.value),
1547
+ "aria-invalid": errors.email ? true : void 0,
1548
+ "aria-describedby": errorId("email"),
1549
+ className: (0, import_utils10.cn)(FIELD_CLASS2, borderFor("email"))
1550
+ }
1551
+ ),
1552
+ fieldError("email")
1553
+ ] }),
1554
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "space-y-1", children: [
1555
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("label", { htmlFor: `${id}-items`, className: "text-sm font-medium", children: t.itemsLabel }),
1556
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1557
+ "textarea",
1558
+ {
1559
+ id: `${id}-items`,
1560
+ rows: 3,
1561
+ maxLength: WITHDRAWAL_REQUEST_FIELD_LIMITS.items,
1562
+ value: items,
1563
+ onChange: (e) => setItems(e.target.value),
1564
+ placeholder: t.itemsPlaceholder,
1565
+ "aria-invalid": errors.items ? true : void 0,
1566
+ "aria-describedby": errorId("items"),
1567
+ className: (0, import_utils10.cn)(FIELD_CLASS2, borderFor("items"))
1568
+ }
1569
+ ),
1570
+ fieldError("items")
1571
+ ] }),
1572
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "space-y-1", children: [
1573
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("label", { htmlFor: `${id}-reason`, className: "text-sm font-medium", children: t.reasonLabel }),
1574
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1575
+ "textarea",
1576
+ {
1577
+ id: `${id}-reason`,
1578
+ rows: 3,
1579
+ maxLength: WITHDRAWAL_REQUEST_FIELD_LIMITS.reason,
1580
+ value: reason,
1581
+ onChange: (e) => setReason(e.target.value),
1582
+ className: (0, import_utils10.cn)(FIELD_CLASS2, "border-border")
1583
+ }
1584
+ )
1585
+ ] }),
1586
+ t.notice ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "text-xs text-muted-foreground", children: t.notice }) : null,
1587
+ formError && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { role: "alert", className: "text-sm text-destructive", children: formError }),
1588
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1589
+ "button",
1590
+ {
1591
+ type: "submit",
1592
+ disabled: submitting,
1593
+ className: "cursor-pointer rounded-lg bg-primary px-6 py-2 text-sm font-semibold text-primary-foreground disabled:cursor-not-allowed disabled:opacity-60",
1594
+ children: submitting ? t.submitting : t.submit
1595
+ }
1596
+ )
1597
+ ]
1598
+ }
1599
+ );
1600
+ }
1327
1601
  // Annotate the CommonJS export names for ESM import in node:
1328
1602
  0 && (module.exports = {
1329
1603
  AddToCartButton,
@@ -1335,9 +1609,12 @@ function DataRequestForm({
1335
1609
  DEFAULT_COURSE_SYLLABUS_LABELS,
1336
1610
  DEFAULT_DATA_REQUEST_FORM_LABELS,
1337
1611
  DEFAULT_PRODUCT_CAROUSEL_LABELS,
1612
+ DEFAULT_WITHDRAWAL_REQUEST_FORM_LABELS,
1338
1613
  DataRequestForm,
1339
1614
  ProductCarousel,
1340
1615
  ShareButtons,
1616
+ WITHDRAWAL_REQUEST_FIELD_LIMITS,
1617
+ WithdrawalRequestForm,
1341
1618
  useCollection
1342
1619
  });
1343
1620
  //# sourceMappingURL=client.cjs.map