@reblu/site-blocks 0.25.0 → 0.27.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 +38 -0
- package/README.md +111 -3
- package/dist/client.cjs +277 -0
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +86 -2
- package/dist/client.d.ts +86 -2
- package/dist/client.js +274 -0
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +397 -334
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -5
- package/dist/index.d.ts +48 -5
- package/dist/index.js +397 -335
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# @reblu/site-blocks
|
|
2
2
|
|
|
3
|
+
## 0.27.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3f76060: `CoursePurchaseTerms` shows the consent box that the course's modality calls for (REB-1240). It reads `AcademyCourseDetail.withdrawalConsent`:
|
|
8
|
+
- Digital content: the withdrawal waiver. It keeps `data-slot="course-purchase-terms-withdrawal-waiver"`, as in REB-1199.
|
|
9
|
+
- A training service: the express request to start, which does not say the withdrawal right is lost. It uses the new `data-slot="course-purchase-terms-service-start-request"`.
|
|
10
|
+
- A course in person or by webinar: no box.
|
|
11
|
+
|
|
12
|
+
Both items carry `data-kind`. Against an API that predates `withdrawalConsent`, the block still reads `withdrawalWaiverRequired`.
|
|
13
|
+
|
|
14
|
+
**Visible change:** each box is now followed by the tenant's exact text, «Texto de la casilla: «…»». Replace it, or hide it with an empty node, through the new `consentText` label. `serviceStartRequest` is also a new label.
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- Updated dependencies [143cef1]
|
|
19
|
+
- @reblu/site-contracts@1.27.0
|
|
20
|
+
|
|
21
|
+
## 0.26.0
|
|
22
|
+
|
|
23
|
+
### Minor Changes
|
|
24
|
+
|
|
25
|
+
- 05b1106: «Aviso legal» and «Desistimiento y devoluciones» pages, written by the tenant in nexus (REB-1201).
|
|
26
|
+
- `@reblu/site-contracts`: `Legal` gains optional `legalNotice` and `withdrawalPolicy` (Markdown or `null`). Absent from older servers.
|
|
27
|
+
- `@reblu/site-client`: `client.legal.get()` returns both documents.
|
|
28
|
+
- `@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.
|
|
29
|
+
|
|
30
|
+
- 21b7413: Withdrawal form: a buyer communicates a withdrawal from the SITE (REB-1201).
|
|
31
|
+
- `@reblu/site-contracts`: new `WithdrawalRequestInput` and `WithdrawalRequestSubmission` types for `POST /api/site/withdrawal-requests`.
|
|
32
|
+
- `@reblu/site-client`: new `client.withdrawalRequests.submit(body, options?)`, returning `{ id, receivedAt, acknowledgementSent }`.
|
|
33
|
+
- `@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.
|
|
34
|
+
|
|
35
|
+
### Patch Changes
|
|
36
|
+
|
|
37
|
+
- Updated dependencies [05b1106]
|
|
38
|
+
- Updated dependencies [21b7413]
|
|
39
|
+
- @reblu/site-contracts@1.26.0
|
|
40
|
+
|
|
3
41
|
## 0.25.0
|
|
4
42
|
|
|
5
43
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -25,7 +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:
|
|
28
|
+
| `CoursePurchaseTerms`, `DEFAULT_COURSE_PURCHASE_TERMS_LABELS` | `.` | What the buyer sees before paying: consent box by course modality, access duration, refund guarantee. Server Component. See "Course purchase terms". |
|
|
29
29
|
| `LessonPreview`, `DEFAULT_LESSON_PREVIEW_LABELS` | `.` | A public free-preview lesson: hosted video player + sanitised body (`getLessonPreview()`). Server Component. See "Free-preview lesson". |
|
|
30
30
|
| `Treatment`, `ParentPage`, `ProfileCard`, `ProductCard` | `.` | Presentational organisms over the SITE API contracts. RSC-safe. `Specialty`: deprecated alias, see below. |
|
|
31
31
|
| `ProfessionalFields` | `.` | The tenant's own professional fields (`professional.fields`) on a team card or profile. RSC-safe. See below. |
|
|
@@ -1128,11 +1128,16 @@ export default async function Page({ params }) {
|
|
|
1128
1128
|
`lesson-preview-badge`, `lesson-preview-duration`, `lesson-preview-player`,
|
|
1129
1129
|
`lesson-preview-content`.
|
|
1130
1130
|
|
|
1131
|
-
## Course purchase terms — `CoursePurchaseTerms` (REB-1199)
|
|
1131
|
+
## Course purchase terms — `CoursePurchaseTerms` (REB-1199, REB-1240)
|
|
1132
1132
|
|
|
1133
1133
|
What a buyer must see before paying for a course, from `client.academy.bySlug()`:
|
|
1134
1134
|
|
|
1135
|
-
- **
|
|
1135
|
+
- **Consent box** (`withdrawalConsent`, REB-1240): which box checkout asks for, as set by the course's modality (`deliveryModality`), followed by the tenant's exact text:
|
|
1136
|
+
- `DIGITAL_CONTENT_WAIVER`, for digital content: the buyer requests immediate access and waives the 14-day withdrawal right (`labels.withdrawalWaiver`);
|
|
1137
|
+
- `SERVICE_START_REQUEST`, for a training service: the buyer expressly asks for the training to start within the 14 days. It does not say the right is lost (`labels.serviceStartRequest`);
|
|
1138
|
+
- `null`, for example in person or webinar: nothing is shown.
|
|
1139
|
+
|
|
1140
|
+
Against an API that predates `withdrawalConsent`, the block falls back to `withdrawalWaiverRequired` (waiver line only).
|
|
1136
1141
|
- **Access duration** (`accessDurationMonths`): months of access after enrolment. `null` means no end date, and nothing is shown.
|
|
1137
1142
|
- **Refund guarantee** (`refundGuarantee`): the course's terms. `null` means none, and nothing is shown.
|
|
1138
1143
|
|
|
@@ -1150,11 +1155,15 @@ const course = await client.academy.bySlug(slug)
|
|
|
1150
1155
|
course={course}
|
|
1151
1156
|
labels={{
|
|
1152
1157
|
withdrawalWaiver: 'Immediate access: you waive the 14-day withdrawal right.',
|
|
1158
|
+
serviceStartRequest: 'You ask the training to start within the 14-day withdrawal period.',
|
|
1159
|
+
consentText: (text) => ` Box text: “${text}”`,
|
|
1153
1160
|
accessDuration: (months) => `Access for ${months} months.`,
|
|
1154
1161
|
}}
|
|
1155
1162
|
/>
|
|
1156
1163
|
```
|
|
1157
1164
|
|
|
1165
|
+
The waiver item keeps `data-slot='course-purchase-terms-withdrawal-waiver'` (as in REB-1199); the express request to start uses `data-slot='course-purchase-terms-service-start-request'`. Both carry `data-kind` with the kind.
|
|
1166
|
+
|
|
1158
1167
|
## Page blocks
|
|
1159
1168
|
|
|
1160
1169
|
`PageBlocks` renders a page's registry blocks (`PageDetail.blocks` from
|
|
@@ -1315,6 +1324,45 @@ renderer (`headingOffset` demotes its headings under yours).
|
|
|
1315
1324
|
`markdownToPlainText` drops images and anything the sanitiser strips, and cuts
|
|
1316
1325
|
at a word boundary with `…` within `maxLength`.
|
|
1317
1326
|
|
|
1327
|
+
## Legal pages — `LegalDocument` (REB-1201)
|
|
1328
|
+
|
|
1329
|
+
Every legal page is Markdown the tenant writes in nexus › Configuración › Legal
|
|
1330
|
+
and `client.legal.get()` serves: `legalNotice` («Aviso legal»),
|
|
1331
|
+
`withdrawalPolicy` («Desistimiento y devoluciones», one page with a section per
|
|
1332
|
+
purchase modality), `privacyPolicy`, `termsAndConditions`, `cookiePolicy`. Drop
|
|
1333
|
+
each one into its own route; link them from your footer, and the withdrawal page
|
|
1334
|
+
from your checkout:
|
|
1335
|
+
|
|
1336
|
+
```tsx
|
|
1337
|
+
// app/desistimiento/page.tsx
|
|
1338
|
+
import { LegalDocument } from '@reblu/site-blocks'
|
|
1339
|
+
|
|
1340
|
+
export default async function WithdrawalPolicyPage() {
|
|
1341
|
+
const legal = await client.legal.get()
|
|
1342
|
+
return (
|
|
1343
|
+
<main>
|
|
1344
|
+
<LegalDocument
|
|
1345
|
+
title='Desistimiento y devoluciones'
|
|
1346
|
+
content={legal.withdrawalPolicy}
|
|
1347
|
+
fallback={<p>El contenido de esta página aún no ha sido configurado.</p>}
|
|
1348
|
+
/>
|
|
1349
|
+
</main>
|
|
1350
|
+
)
|
|
1351
|
+
}
|
|
1352
|
+
```
|
|
1353
|
+
|
|
1354
|
+
`LegalDocument` (Server Component, RSC-safe):
|
|
1355
|
+
|
|
1356
|
+
- renders `title` as the page's only `<h1>` and lands the document's shallowest
|
|
1357
|
+
heading at `<h2>`, keeping the relative depth of every section below it;
|
|
1358
|
+
- runs the Markdown through the same sanitised pipeline as `MarkdownRenderer`;
|
|
1359
|
+
- for `null` (not written yet, or an older server without the field), blank, or
|
|
1360
|
+
sanitised-away content renders your `fallback` under the title — or nothing
|
|
1361
|
+
without one. It never invents legal copy;
|
|
1362
|
+
- styling hooks: `className`, `titleClassName` (replaces the default
|
|
1363
|
+
`text-3xl font-bold`), `contentClassName`, `titleId`, and
|
|
1364
|
+
`data-slot='legal-document'` on the `<article>`.
|
|
1365
|
+
|
|
1318
1366
|
## Contact form interest question — `ContactInterestField` (REB-759)
|
|
1319
1367
|
|
|
1320
1368
|
The tenant's own optional question on the contact form, e.g. «¿Qué tratamiento
|
|
@@ -1418,6 +1466,66 @@ statuses this form maps — see the `@reblu/site-next` README.
|
|
|
1418
1466
|
`locale`, and the offered rights with `requestTypes`.
|
|
1419
1467
|
- `onSubmitted(ack)` fires after success — e.g. for analytics.
|
|
1420
1468
|
|
|
1469
|
+
## Withdrawal form — `WithdrawalRequestForm` (REB-1201)
|
|
1470
|
+
|
|
1471
|
+
Drop-in form for the buyer to communicate a withdrawal: order number, optional
|
|
1472
|
+
name, email, what they withdraw from and an optional reason. base-api stores
|
|
1473
|
+
it, emails the tenant team (the contact-form recipients, else the contact
|
|
1474
|
+
email) and sends the buyer an acknowledgement — their copy on a durable
|
|
1475
|
+
medium. Put it on your withdrawal page, under the tenant's text
|
|
1476
|
+
(`LegalDocument` over `legal.withdrawalPolicy`):
|
|
1477
|
+
|
|
1478
|
+
```tsx
|
|
1479
|
+
'use client'
|
|
1480
|
+
import { useCallback, useState } from 'react'
|
|
1481
|
+
import { WithdrawalRequestForm } from '@reblu/site-blocks/client'
|
|
1482
|
+
import {
|
|
1483
|
+
DeferredRecaptchaProvider,
|
|
1484
|
+
useDeferredRecaptchaToken,
|
|
1485
|
+
} from '@reblu/site-next/recaptcha-provider'
|
|
1486
|
+
|
|
1487
|
+
function Form({ onArm }: { onArm: () => void }) {
|
|
1488
|
+
const getCaptchaToken = useDeferredRecaptchaToken('withdrawal_request', { onArm })
|
|
1489
|
+
return <WithdrawalRequestForm getCaptchaToken={getCaptchaToken} onInteract={onArm} />
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
export function WithdrawalRequestSection({ siteKey }: { siteKey: string }) {
|
|
1493
|
+
const [armed, setArmed] = useState(false)
|
|
1494
|
+
const arm = useCallback(() => setArmed(true), [])
|
|
1495
|
+
return (
|
|
1496
|
+
<DeferredRecaptchaProvider siteKey={siteKey} armed={armed}>
|
|
1497
|
+
<Form onArm={arm} />
|
|
1498
|
+
</DeferredRecaptchaProvider>
|
|
1499
|
+
)
|
|
1500
|
+
}
|
|
1501
|
+
```
|
|
1502
|
+
|
|
1503
|
+
```ts
|
|
1504
|
+
// app/api/withdrawal-requests/route.ts — server side, the API key never reaches the browser
|
|
1505
|
+
import { createProxyRoute } from '@reblu/site-next/route-handlers'
|
|
1506
|
+
|
|
1507
|
+
export const POST = createProxyRoute('/api/site/withdrawal-requests', { method: 'POST' })
|
|
1508
|
+
```
|
|
1509
|
+
|
|
1510
|
+
- **It decides nothing.** The form does not check the order and excludes no
|
|
1511
|
+
product or service; whether the right applies is the tenant team's call,
|
|
1512
|
+
explained on the tenant's own withdrawal page.
|
|
1513
|
+
- **The receipt.** On success the form is replaced by a `role="status"` receipt
|
|
1514
|
+
with the received-at date. When `acknowledgementSent` is `false` (the tenant
|
|
1515
|
+
has no email delivery configured, or it failed) the receipt also lists the
|
|
1516
|
+
order and the items, so the buyer can keep it.
|
|
1517
|
+
- **Gate it on the reCAPTCHA key alone**, like `DataRequestForm`: base-api
|
|
1518
|
+
rejects a request without a token, and the channel must stay open even with
|
|
1519
|
+
the contact form off.
|
|
1520
|
+
- **No server text reaches the visitor.** `400` → `validationError`, `422` →
|
|
1521
|
+
`captchaError`, `429` → `rateLimitError`, anything else → `genericError`.
|
|
1522
|
+
- **Limits.** The inputs carry `maxLength` at the server limits
|
|
1523
|
+
(`WITHDRAWAL_REQUEST_FIELD_LIMITS`: order 100, email 320, name 200, items
|
|
1524
|
+
2000, reason 2000).
|
|
1525
|
+
- **Copy** defaults to Spanish; override any string with `labels` (`notice:
|
|
1526
|
+
null` hides the small print), the date locale with `locale`. Pass `submit` for
|
|
1527
|
+
your own transport, `endpoint` for another route path.
|
|
1528
|
+
|
|
1421
1529
|
## Security boundaries
|
|
1422
1530
|
|
|
1423
1531
|
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
|