@reblu/site-blocks 0.19.0 → 0.20.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 +16 -0
- package/README.md +56 -2
- package/dist/client.cjs +237 -45
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +51 -2
- package/dist/client.d.ts +51 -2
- package/dist/client.js +234 -44
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +24 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -3
- package/dist/index.d.ts +24 -3
- package/dist/index.js +24 -9
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @reblu/site-blocks
|
|
2
2
|
|
|
3
|
+
## 0.20.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 4ca8816: New `CourseSyllabus` on `@reblu/site-blocks/client`: the programme of a public course page over `AcademyCourseDetail.modules`. The header shows the number of modules and lessons. Each module with lessons is an expandable button (`aria-expanded`, `aria-controls`) showing its lesson count and total duration, and lists its published lessons in order with their duration and free-preview badge. Modules without lessons render as a plain row. Spanish default copy, overridable through `labels` (`DEFAULT_COURSE_SYLLABUS_LABELS`).
|
|
8
|
+
- 25d8189: Automatic staff discount (REB-1131). When the buyer has a server-validated staff session, base-api applies the active «Solo personal» code that gives the largest discount, with no code typed. Buyers without a staff session see no change.
|
|
9
|
+
- `@reblu/site-contracts`: adds the `ShopStaffAutoDiscount` type, `{ code, label, amount }`. It is an optional `autoDiscount` field on `ShopCartValidation`, `ShopDiscountApplication` and `ShopCheckoutSession`, and is absent for everyone else.
|
|
10
|
+
- `@reblu/site-next`: `createShopValidateCartRoute()` now forwards the `sb-*` auth cookies, the same as checkout and apply-discount. Opt out with `{ forwardAuthCookies: false }`. `validateCart()` returns `outcome.autoDiscount` when the server applied one.
|
|
11
|
+
- `@reblu/site-blocks`: `CartSummary` and `CheckoutForm` accept `autoDiscount` and show «Descuento de personal aplicado» with its amount. `CheckoutForm` shows no remove action for it, and the code field stays available. The text can be changed with `labels.staffDiscountApplied`. Without the prop, both components render exactly as before.
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 6a0f8e6: `OrderConfirmation`: the default `foundDescription` now reads «Te hemos enviado un email con tu acceso.» Every shop buyer gets a customer account, and the order confirmation email carries a one-time link into it, where paid digital downloads live. The confirmation page never serves files. A SITE that passes its own `labels.foundDescription` is unaffected.
|
|
16
|
+
- Updated dependencies [25d8189]
|
|
17
|
+
- @reblu/site-contracts@1.20.0
|
|
18
|
+
|
|
3
19
|
## 0.19.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -39,6 +39,7 @@ colour on the red background. See `docs/theming/theming.md`.
|
|
|
39
39
|
| `AddToCartButton`, `CartIcon` | `./client` | Add-to-cart feedback (REB-837) — **moved here from `.`**, see below. |
|
|
40
40
|
| `DataRequestForm` | `./client` | Drop-in public GDPR rights form over `POST /api/site/data-requests` (REB-980). See "GDPR rights form" below. |
|
|
41
41
|
| `ProductCarousel` | `./client` | Scroll-snap product strip with prev/next arrows («Productos relacionados»). See "Product carousel" below. |
|
|
42
|
+
| `CourseSyllabus`, `DEFAULT_COURSE_SYLLABUS_LABELS` | `./client` | A course's programme: expandable modules listing their lessons (`client.academy.bySlug()`). See "Course programme" below. |
|
|
42
43
|
|
|
43
44
|
## Boundary
|
|
44
45
|
|
|
@@ -324,13 +325,31 @@ for the wiring steps and a full example app).
|
|
|
324
325
|
| `AddedToCartToast` | `.` | `productName`, `cartHref`, `status?`, `quantity?`, `imageUrl?` | Yes |
|
|
325
326
|
| `CartLineList` | `.` | `items`, `onQuantityChange?`, `onRemove?`, `liveMessage?`, `notice?`, `lineMessage?` | Yes |
|
|
326
327
|
| `CartAdjustmentsNotice` | `.` | `removed?`, `quantityChanges?`, `priceChanges?`, `labels?` | Yes |
|
|
327
|
-
| `CartSummary` | `.` | `subtotal`, `discount?`, `shipping?`, `shippingEnabled?`, `total`, `notice?`
|
|
328
|
+
| `CartSummary` | `.` | `subtotal`, `discount?`, `autoDiscount?`, `shipping?`, `shippingEnabled?`, `total`, `notice?` | Yes |
|
|
328
329
|
| `CheckoutLoginPrompt` | `.` | `open`, `email`, `onEmailChange`, `onClose`, `onSubmitEmail`, `onContinueAsGuest` | Yes (renders `null` when `open` is false) |
|
|
329
|
-
| `CheckoutForm` | `.` | `contact`, `onContactChange`, `discountCode`, `onDiscountCodeChange`, `onApplyDiscount`, `onRemoveDiscount`, `onSubmit` | Yes |
|
|
330
|
+
| `CheckoutForm` | `.` | `contact`, `onContactChange`, `discountCode`, `onDiscountCodeChange`, `onApplyDiscount`, `onRemoveDiscount`, `onSubmit`, `autoDiscount?` | Yes |
|
|
330
331
|
| `OrderConfirmation` | `.` | `status`, `order`, `cartHref?`, `onRecheck?` | Yes |
|
|
331
332
|
| `OrderInvoiceLink` | `.` | `invoice` (`order.invoice`), `labels?` | Yes (renders `null` without an invoice) |
|
|
332
333
|
| `FullInvoiceFields` | `.` | `value`, `onChange`, `overCap`, `errors?`, `countries?`, `labels?` | Yes (controlled, no hooks) |
|
|
333
334
|
|
|
335
|
+
**Automatic staff discount (REB-1131).** For a signed-in staff member,
|
|
336
|
+
base-api applies the best «Solo personal» code by itself and reports it as
|
|
337
|
+
`autoDiscount` on validate-cart, apply-discount and checkout. Pass it to both
|
|
338
|
+
components and they show «Descuento de personal aplicado» with its amount
|
|
339
|
+
(override with `labels.staffDiscountApplied`). `CheckoutForm` offers no remove
|
|
340
|
+
action for it and keeps the code field: a typed code replaces it only when the
|
|
341
|
+
server finds it larger. Without `autoDiscount`, both render exactly as before.
|
|
342
|
+
|
|
343
|
+
```tsx
|
|
344
|
+
const outcome = await validateCart() // @reblu/site-next/cart
|
|
345
|
+
// After an apply-discount call, prefer its `autoDiscount` (absent when the
|
|
346
|
+
// typed code won).
|
|
347
|
+
const autoDiscount = discountResponse?.autoDiscount ?? outcome.autoDiscount ?? null
|
|
348
|
+
|
|
349
|
+
<CartSummary subtotal={subtotal} autoDiscount={autoDiscount} total={subtotal - (autoDiscount?.amount ?? 0)} />
|
|
350
|
+
<CheckoutForm {...checkoutProps} autoDiscount={autoDiscount} />
|
|
351
|
+
```
|
|
352
|
+
|
|
334
353
|
**REB-837 breaking move**: `AddToCartButton` and `CartIcon` now carry real
|
|
335
354
|
internal state (the confirmed/error timer, the bump-on-add guard) and ship
|
|
336
355
|
from `@reblu/site-blocks/client` instead of the root `.` entry. Update the
|
|
@@ -887,6 +906,41 @@ It renders the package's `ProductCard` by default. To use your own card, pass
|
|
|
887
906
|
`headingClassName` replaces the heading's typography (see the className rule
|
|
888
907
|
above). It renders nothing for an empty list.
|
|
889
908
|
|
|
909
|
+
## Course programme — `CourseSyllabus` (REB-1134)
|
|
910
|
+
|
|
911
|
+
The programme of a public course page: every module, and inside each module the
|
|
912
|
+
lessons that make it up. The header reads «Programa» and «3 módulos · 12
|
|
913
|
+
lecciones». Each module row shows its lesson count and, when every lesson in it
|
|
914
|
+
has a duration, the module's total («4 lecciones · 1 h 20 min»). Opening a
|
|
915
|
+
module lists its lessons in order, with each lesson's duration and a «Vista
|
|
916
|
+
previa gratuita» badge on free-preview lessons.
|
|
917
|
+
|
|
918
|
+
```tsx
|
|
919
|
+
import { CourseSyllabus } from '@reblu/site-blocks/client'
|
|
920
|
+
|
|
921
|
+
// `course` is an `AcademyCourseDetail` from `client.academy.bySlug(slug)`.
|
|
922
|
+
// It works from a Server Component page: every prop is serialisable.
|
|
923
|
+
<CourseSyllabus modules={course.modules} />
|
|
924
|
+
```
|
|
925
|
+
|
|
926
|
+
- **Public data only.** It renders what the API serves. The API returns
|
|
927
|
+
published lessons only, and never a video, playback or attachment id, a
|
|
928
|
+
signed URL or the lesson content.
|
|
929
|
+
- **Accessibility.** The block is a `region` named by its heading. A module
|
|
930
|
+
with lessons is a `<button>` with `aria-expanded` and `aria-controls`
|
|
931
|
+
pointing at its lesson list. Tab reaches it; Enter and Space open and close
|
|
932
|
+
it. Modules open independently and start closed.
|
|
933
|
+
- **Modules without lessons** render as a plain row with the title, not a
|
|
934
|
+
button. A payload without `lessons` renders the same way.
|
|
935
|
+
- **Copy.** The defaults are Spanish (`DEFAULT_COURSE_SYLLABUS_LABELS`).
|
|
936
|
+
Override any of them with `labels={{ heading, moduleSingular, modulePlural,
|
|
937
|
+
lessonSingular, lessonPlural, freePreview, hours, minutes }}`.
|
|
938
|
+
- **Styling.** `headingLevel` (default `2`), `headingClassName` (replaces the
|
|
939
|
+
heading's typography, see the className rule above) and `className`. Style
|
|
940
|
+
the parts with `[data-slot="course-syllabus"]`, `course-syllabus-module`,
|
|
941
|
+
`course-syllabus-lessons` and `course-syllabus-lesson`. It renders nothing
|
|
942
|
+
for a course without modules.
|
|
943
|
+
|
|
890
944
|
## Page blocks
|
|
891
945
|
|
|
892
946
|
`PageBlocks` renders a page's registry blocks (`PageDetail.blocks` from
|
package/dist/client.cjs
CHANGED
|
@@ -34,8 +34,10 @@ __export(client_exports, {
|
|
|
34
34
|
AddToCartButton: () => AddToCartButton,
|
|
35
35
|
CartIcon: () => CartIcon,
|
|
36
36
|
Collection: () => Collection,
|
|
37
|
+
CourseSyllabus: () => CourseSyllabus,
|
|
37
38
|
DATA_REQUEST_FIELD_LIMITS: () => DATA_REQUEST_FIELD_LIMITS,
|
|
38
39
|
DATA_REQUEST_TYPES: () => import_site_contracts.DATA_REQUEST_TYPES,
|
|
40
|
+
DEFAULT_COURSE_SYLLABUS_LABELS: () => DEFAULT_COURSE_SYLLABUS_LABELS,
|
|
39
41
|
DEFAULT_DATA_REQUEST_FORM_LABELS: () => DEFAULT_DATA_REQUEST_FORM_LABELS,
|
|
40
42
|
DEFAULT_PRODUCT_CAROUSEL_LABELS: () => DEFAULT_PRODUCT_CAROUSEL_LABELS,
|
|
41
43
|
DataRequestForm: () => DataRequestForm,
|
|
@@ -460,7 +462,7 @@ var import_utils4 = require("@reblu/site-ui/utils");
|
|
|
460
462
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
461
463
|
var DEFAULT_LABELS4 = {
|
|
462
464
|
cart: "Carrito",
|
|
463
|
-
itemsSuffix: (
|
|
465
|
+
itemsSuffix: (count2) => `${count2} art\xEDculo${count2 === 1 ? "" : "s"}`
|
|
464
466
|
};
|
|
465
467
|
var BUMP_DURATION_MS = 400;
|
|
466
468
|
function CartIcon({ itemCount, href, hydrated, bumpOnAdd, labels, className }) {
|
|
@@ -732,11 +734,199 @@ function ProductCarousel(props) {
|
|
|
732
734
|
] });
|
|
733
735
|
}
|
|
734
736
|
|
|
735
|
-
// src/components/
|
|
737
|
+
// src/components/course-syllabus.tsx
|
|
736
738
|
var import_react5 = require("react");
|
|
739
|
+
var import_utils8 = require("@reblu/site-ui/utils");
|
|
740
|
+
|
|
741
|
+
// src/lib/slot-class-name.ts
|
|
737
742
|
var import_utils7 = require("@reblu/site-ui/utils");
|
|
738
|
-
var
|
|
743
|
+
var MERGE_SAFE_TEXT = /(?:^|:)!?text-(?:left|center|right|justify|start|end|wrap|nowrap|balance|pretty|ellipsis|clip)$/;
|
|
744
|
+
var TEXT_UTILITY = /(?:^|:)!?-?text-/;
|
|
745
|
+
function bypassesMerge(token) {
|
|
746
|
+
return TEXT_UTILITY.test(token) && !MERGE_SAFE_TEXT.test(token);
|
|
747
|
+
}
|
|
748
|
+
function slotClassName(structural, defaults, consumer) {
|
|
749
|
+
if (consumer === void 0 || consumer.trim() === "") return (0, import_utils7.cn)(structural, defaults);
|
|
750
|
+
const tokens = consumer.split(/\s+/).filter(Boolean);
|
|
751
|
+
const verbatim = tokens.filter(bypassesMerge);
|
|
752
|
+
const merged = (0, import_utils7.cn)(structural, tokens.filter((token) => !bypassesMerge(token)));
|
|
753
|
+
return [merged, ...verbatim].filter(Boolean).join(" ");
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// src/components/course-syllabus.tsx
|
|
739
757
|
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
758
|
+
var DEFAULT_COURSE_SYLLABUS_LABELS = {
|
|
759
|
+
heading: "Programa",
|
|
760
|
+
moduleSingular: "m\xF3dulo",
|
|
761
|
+
modulePlural: "m\xF3dulos",
|
|
762
|
+
lessonSingular: "lecci\xF3n",
|
|
763
|
+
lessonPlural: "lecciones",
|
|
764
|
+
freePreview: "Vista previa gratuita",
|
|
765
|
+
hours: "h",
|
|
766
|
+
minutes: "min"
|
|
767
|
+
};
|
|
768
|
+
var SEPARATOR = " \xB7 ";
|
|
769
|
+
function count(n, singular, plural) {
|
|
770
|
+
return `${n} ${n === 1 ? singular : plural}`;
|
|
771
|
+
}
|
|
772
|
+
function formatDuration(seconds, labels) {
|
|
773
|
+
const totalMinutes = Math.max(1, Math.round(seconds / 60));
|
|
774
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
775
|
+
const minutes = totalMinutes % 60;
|
|
776
|
+
if (hours === 0) return `${minutes} ${labels.minutes}`;
|
|
777
|
+
return minutes === 0 ? `${hours} ${labels.hours}` : `${hours} ${labels.hours} ${minutes} ${labels.minutes}`;
|
|
778
|
+
}
|
|
779
|
+
function moduleSeconds(lessons) {
|
|
780
|
+
let total = 0;
|
|
781
|
+
for (const l of lessons) {
|
|
782
|
+
if (typeof l.durationSeconds !== "number" || l.durationSeconds <= 0) return null;
|
|
783
|
+
total += l.durationSeconds;
|
|
784
|
+
}
|
|
785
|
+
return total;
|
|
786
|
+
}
|
|
787
|
+
function CourseSyllabus({
|
|
788
|
+
modules,
|
|
789
|
+
labels: labelOverrides,
|
|
790
|
+
headingLevel = 2,
|
|
791
|
+
headingClassName,
|
|
792
|
+
className
|
|
793
|
+
}) {
|
|
794
|
+
const labels = { ...DEFAULT_COURSE_SYLLABUS_LABELS, ...labelOverrides };
|
|
795
|
+
const baseId = (0, import_react5.useId)();
|
|
796
|
+
const [open, setOpen] = (0, import_react5.useState)(() => /* @__PURE__ */ new Set());
|
|
797
|
+
if (modules.length === 0) return null;
|
|
798
|
+
const totalLessons = modules.reduce((sum, m) => sum + (m.lessons?.length ?? 0), 0);
|
|
799
|
+
const summary = [count(modules.length, labels.moduleSingular, labels.modulePlural)];
|
|
800
|
+
if (totalLessons > 0)
|
|
801
|
+
summary.push(count(totalLessons, labels.lessonSingular, labels.lessonPlural));
|
|
802
|
+
const headingId = `${baseId}-heading`;
|
|
803
|
+
const Tag = `h${headingLevel}`;
|
|
804
|
+
function toggle(id) {
|
|
805
|
+
setOpen((prev) => {
|
|
806
|
+
const next = new Set(prev);
|
|
807
|
+
if (next.has(id)) next.delete(id);
|
|
808
|
+
else next.add(id);
|
|
809
|
+
return next;
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
813
|
+
"section",
|
|
814
|
+
{
|
|
815
|
+
"data-slot": "course-syllabus",
|
|
816
|
+
"aria-labelledby": headingId,
|
|
817
|
+
className: (0, import_utils8.cn)("space-y-4", className),
|
|
818
|
+
children: [
|
|
819
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "space-y-1", children: [
|
|
820
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
821
|
+
Tag,
|
|
822
|
+
{
|
|
823
|
+
id: headingId,
|
|
824
|
+
className: slotClassName("", "text-xl font-semibold", headingClassName),
|
|
825
|
+
children: labels.heading
|
|
826
|
+
}
|
|
827
|
+
),
|
|
828
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { "data-slot": "course-syllabus-summary", className: "text-sm text-muted-foreground", children: summary.join(SEPARATOR) })
|
|
829
|
+
] }),
|
|
830
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("ol", { className: "flex list-none flex-col gap-2.5 p-0", children: modules.map((module2, index) => {
|
|
831
|
+
const lessons = module2.lessons ?? [];
|
|
832
|
+
const number = /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
833
|
+
"span",
|
|
834
|
+
{
|
|
835
|
+
"aria-hidden": "true",
|
|
836
|
+
className: "flex size-7 shrink-0 items-center justify-center rounded-full bg-muted text-sm font-semibold text-primary",
|
|
837
|
+
children: index + 1
|
|
838
|
+
}
|
|
839
|
+
);
|
|
840
|
+
if (lessons.length === 0) {
|
|
841
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
842
|
+
"li",
|
|
843
|
+
{
|
|
844
|
+
"data-slot": "course-syllabus-module",
|
|
845
|
+
className: "flex items-center gap-3.5 rounded-lg border border-border bg-card px-4 py-3.5",
|
|
846
|
+
children: [
|
|
847
|
+
number,
|
|
848
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "font-semibold leading-snug", children: module2.title })
|
|
849
|
+
]
|
|
850
|
+
},
|
|
851
|
+
module2.id
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
const isOpen = open.has(module2.id);
|
|
855
|
+
const panelId = `${baseId}-module-${index}`;
|
|
856
|
+
const seconds = moduleSeconds(lessons);
|
|
857
|
+
const meta = [count(lessons.length, labels.lessonSingular, labels.lessonPlural)];
|
|
858
|
+
if (seconds !== null) meta.push(formatDuration(seconds, labels));
|
|
859
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
860
|
+
"li",
|
|
861
|
+
{
|
|
862
|
+
"data-slot": "course-syllabus-module",
|
|
863
|
+
className: "rounded-lg border border-border bg-card",
|
|
864
|
+
children: [
|
|
865
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
866
|
+
"button",
|
|
867
|
+
{
|
|
868
|
+
type: "button",
|
|
869
|
+
"aria-expanded": isOpen,
|
|
870
|
+
"aria-controls": panelId,
|
|
871
|
+
onClick: () => toggle(module2.id),
|
|
872
|
+
className: "flex w-full cursor-pointer items-center gap-3.5 rounded-lg px-4 py-3.5 text-left focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
|
|
873
|
+
children: [
|
|
874
|
+
number,
|
|
875
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [
|
|
876
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "font-semibold leading-snug", children: module2.title }),
|
|
877
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "text-sm text-muted-foreground", children: meta.join(SEPARATOR) })
|
|
878
|
+
] }),
|
|
879
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
880
|
+
"span",
|
|
881
|
+
{
|
|
882
|
+
"aria-hidden": "true",
|
|
883
|
+
className: (0, import_utils8.cn)(
|
|
884
|
+
"shrink-0 text-muted-foreground transition-transform motion-reduce:transition-none",
|
|
885
|
+
isOpen && "rotate-180"
|
|
886
|
+
),
|
|
887
|
+
children: "\u25BE"
|
|
888
|
+
}
|
|
889
|
+
)
|
|
890
|
+
]
|
|
891
|
+
}
|
|
892
|
+
),
|
|
893
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
894
|
+
"ol",
|
|
895
|
+
{
|
|
896
|
+
id: panelId,
|
|
897
|
+
hidden: !isOpen,
|
|
898
|
+
"data-slot": "course-syllabus-lessons",
|
|
899
|
+
className: "list-none space-y-2 border-t border-border px-4 py-3 pl-[3.75rem]",
|
|
900
|
+
children: lessons.map((lesson) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
901
|
+
"li",
|
|
902
|
+
{
|
|
903
|
+
"data-slot": "course-syllabus-lesson",
|
|
904
|
+
className: "flex flex-wrap items-baseline gap-x-3 gap-y-1 text-sm",
|
|
905
|
+
children: [
|
|
906
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { "data-slot": "course-syllabus-lesson-title", className: "flex-1", children: lesson.title }),
|
|
907
|
+
lesson.isFreePreview && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "rounded-full bg-muted px-2 py-0.5 text-xs font-medium", children: labels.freePreview }),
|
|
908
|
+
typeof lesson.durationSeconds === "number" && lesson.durationSeconds > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "text-muted-foreground tabular-nums", children: formatDuration(lesson.durationSeconds, labels) })
|
|
909
|
+
]
|
|
910
|
+
},
|
|
911
|
+
lesson.id
|
|
912
|
+
))
|
|
913
|
+
}
|
|
914
|
+
)
|
|
915
|
+
]
|
|
916
|
+
},
|
|
917
|
+
module2.id
|
|
918
|
+
);
|
|
919
|
+
}) })
|
|
920
|
+
]
|
|
921
|
+
}
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// src/components/data-request-form.tsx
|
|
926
|
+
var import_react6 = require("react");
|
|
927
|
+
var import_utils9 = require("@reblu/site-ui/utils");
|
|
928
|
+
var import_site_contracts = require("@reblu/site-contracts");
|
|
929
|
+
var import_jsx_runtime9 = require("react/jsx-runtime");
|
|
740
930
|
var DATA_REQUEST_FIELD_LIMITS = Object.freeze({ name: 200, email: 320, details: 5e3 });
|
|
741
931
|
var DEFAULT_DATA_REQUEST_FORM_LABELS = {
|
|
742
932
|
requestTypeLabel: "Derecho que quieres ejercer",
|
|
@@ -762,7 +952,7 @@ var DEFAULT_DATA_REQUEST_FORM_LABELS = {
|
|
|
762
952
|
nameRequired: "Indica tu nombre.",
|
|
763
953
|
emailInvalid: "Introduce un email v\xE1lido.",
|
|
764
954
|
consentRequired: "Debes aceptar el tratamiento de tus datos para gestionar la solicitud.",
|
|
765
|
-
success: (deadline) => /* @__PURE__ */ (0,
|
|
955
|
+
success: (deadline) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
|
|
766
956
|
"Hemos recibido tu solicitud. Te responderemos como m\xE1ximo el ",
|
|
767
957
|
deadline,
|
|
768
958
|
"."
|
|
@@ -811,18 +1001,18 @@ function DataRequestForm({
|
|
|
811
1001
|
...labels,
|
|
812
1002
|
requestTypes: { ...DEFAULT_DATA_REQUEST_FORM_LABELS.requestTypes, ...labels?.requestTypes }
|
|
813
1003
|
};
|
|
814
|
-
const id = (0,
|
|
815
|
-
const [requestType, setRequestType] = (0,
|
|
816
|
-
const [name, setName] = (0,
|
|
817
|
-
const [email, setEmail] = (0,
|
|
818
|
-
const [details, setDetails] = (0,
|
|
819
|
-
const [consent, setConsent] = (0,
|
|
820
|
-
const [errors, setErrors] = (0,
|
|
821
|
-
const [formError, setFormError] = (0,
|
|
822
|
-
const [submitting, setSubmitting] = (0,
|
|
823
|
-
const [ack, setAck] = (0,
|
|
824
|
-
const statusRef = (0,
|
|
825
|
-
(0,
|
|
1004
|
+
const id = (0, import_react6.useId)();
|
|
1005
|
+
const [requestType, setRequestType] = (0, import_react6.useState)("");
|
|
1006
|
+
const [name, setName] = (0, import_react6.useState)("");
|
|
1007
|
+
const [email, setEmail] = (0, import_react6.useState)("");
|
|
1008
|
+
const [details, setDetails] = (0, import_react6.useState)("");
|
|
1009
|
+
const [consent, setConsent] = (0, import_react6.useState)(false);
|
|
1010
|
+
const [errors, setErrors] = (0, import_react6.useState)({});
|
|
1011
|
+
const [formError, setFormError] = (0, import_react6.useState)(null);
|
|
1012
|
+
const [submitting, setSubmitting] = (0, import_react6.useState)(false);
|
|
1013
|
+
const [ack, setAck] = (0, import_react6.useState)(null);
|
|
1014
|
+
const statusRef = (0, import_react6.useRef)(null);
|
|
1015
|
+
(0, import_react6.useEffect)(() => {
|
|
826
1016
|
if (ack) statusRef.current?.focus();
|
|
827
1017
|
}, [ack]);
|
|
828
1018
|
async function handleSubmit(event) {
|
|
@@ -873,21 +1063,21 @@ function DataRequestForm({
|
|
|
873
1063
|
dateStyle: "long",
|
|
874
1064
|
timeStyle: "short"
|
|
875
1065
|
}).format(new Date(ack.slaDueAt));
|
|
876
|
-
return /* @__PURE__ */ (0,
|
|
1066
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
877
1067
|
"div",
|
|
878
1068
|
{
|
|
879
1069
|
ref: statusRef,
|
|
880
1070
|
tabIndex: -1,
|
|
881
1071
|
role: "status",
|
|
882
1072
|
"data-slot": "data-request-form-success",
|
|
883
|
-
className: (0,
|
|
1073
|
+
className: (0, import_utils9.cn)("rounded-lg border border-border p-6 text-sm", className),
|
|
884
1074
|
children: t.success(deadline)
|
|
885
1075
|
}
|
|
886
1076
|
);
|
|
887
1077
|
}
|
|
888
1078
|
const errorId = (field) => errors[field] ? `${id}-${field}-error` : void 0;
|
|
889
|
-
const fieldError = (field) => errors[field] ? /* @__PURE__ */ (0,
|
|
890
|
-
return /* @__PURE__ */ (0,
|
|
1079
|
+
const fieldError = (field) => errors[field] ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { id: `${id}-${field}-error`, className: "text-xs text-destructive", children: errors[field] }) : null;
|
|
1080
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
|
|
891
1081
|
"form",
|
|
892
1082
|
{
|
|
893
1083
|
noValidate: true,
|
|
@@ -895,11 +1085,11 @@ function DataRequestForm({
|
|
|
895
1085
|
onFocusCapture: onInteract,
|
|
896
1086
|
onPointerDown: onInteract,
|
|
897
1087
|
"data-slot": "data-request-form",
|
|
898
|
-
className: (0,
|
|
1088
|
+
className: (0, import_utils9.cn)("space-y-4", className),
|
|
899
1089
|
children: [
|
|
900
|
-
/* @__PURE__ */ (0,
|
|
901
|
-
/* @__PURE__ */ (0,
|
|
902
|
-
/* @__PURE__ */ (0,
|
|
1090
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "space-y-1", children: [
|
|
1091
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("label", { htmlFor: `${id}-type`, className: "text-sm font-medium", children: t.requestTypeLabel }),
|
|
1092
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
|
|
903
1093
|
"select",
|
|
904
1094
|
{
|
|
905
1095
|
id: `${id}-type`,
|
|
@@ -907,18 +1097,18 @@ function DataRequestForm({
|
|
|
907
1097
|
onChange: (e) => setRequestType(e.target.value),
|
|
908
1098
|
"aria-invalid": errors.requestType ? true : void 0,
|
|
909
1099
|
"aria-describedby": errorId("requestType"),
|
|
910
|
-
className: (0,
|
|
1100
|
+
className: (0, import_utils9.cn)(FIELD_CLASS, errors.requestType ? "border-destructive" : "border-border"),
|
|
911
1101
|
children: [
|
|
912
|
-
/* @__PURE__ */ (0,
|
|
913
|
-
requestTypes.map((type) => /* @__PURE__ */ (0,
|
|
1102
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("option", { value: "", children: t.requestTypePlaceholder }),
|
|
1103
|
+
requestTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("option", { value: type, children: t.requestTypes[type] }, type))
|
|
914
1104
|
]
|
|
915
1105
|
}
|
|
916
1106
|
),
|
|
917
1107
|
fieldError("requestType")
|
|
918
1108
|
] }),
|
|
919
|
-
/* @__PURE__ */ (0,
|
|
920
|
-
/* @__PURE__ */ (0,
|
|
921
|
-
/* @__PURE__ */ (0,
|
|
1109
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "space-y-1", children: [
|
|
1110
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("label", { htmlFor: `${id}-name`, className: "text-sm font-medium", children: t.nameLabel }),
|
|
1111
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
922
1112
|
"input",
|
|
923
1113
|
{
|
|
924
1114
|
id: `${id}-name`,
|
|
@@ -929,14 +1119,14 @@ function DataRequestForm({
|
|
|
929
1119
|
onChange: (e) => setName(e.target.value),
|
|
930
1120
|
"aria-invalid": errors.name ? true : void 0,
|
|
931
1121
|
"aria-describedby": errorId("name"),
|
|
932
|
-
className: (0,
|
|
1122
|
+
className: (0, import_utils9.cn)(FIELD_CLASS, errors.name ? "border-destructive" : "border-border")
|
|
933
1123
|
}
|
|
934
1124
|
),
|
|
935
1125
|
fieldError("name")
|
|
936
1126
|
] }),
|
|
937
|
-
/* @__PURE__ */ (0,
|
|
938
|
-
/* @__PURE__ */ (0,
|
|
939
|
-
/* @__PURE__ */ (0,
|
|
1127
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "space-y-1", children: [
|
|
1128
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("label", { htmlFor: `${id}-email`, className: "text-sm font-medium", children: t.emailLabel }),
|
|
1129
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
940
1130
|
"input",
|
|
941
1131
|
{
|
|
942
1132
|
id: `${id}-email`,
|
|
@@ -947,14 +1137,14 @@ function DataRequestForm({
|
|
|
947
1137
|
onChange: (e) => setEmail(e.target.value),
|
|
948
1138
|
"aria-invalid": errors.email ? true : void 0,
|
|
949
1139
|
"aria-describedby": errorId("email"),
|
|
950
|
-
className: (0,
|
|
1140
|
+
className: (0, import_utils9.cn)(FIELD_CLASS, errors.email ? "border-destructive" : "border-border")
|
|
951
1141
|
}
|
|
952
1142
|
),
|
|
953
1143
|
fieldError("email")
|
|
954
1144
|
] }),
|
|
955
|
-
/* @__PURE__ */ (0,
|
|
956
|
-
/* @__PURE__ */ (0,
|
|
957
|
-
/* @__PURE__ */ (0,
|
|
1145
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "space-y-1", children: [
|
|
1146
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("label", { htmlFor: `${id}-details`, className: "text-sm font-medium", children: t.detailsLabel }),
|
|
1147
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
958
1148
|
"textarea",
|
|
959
1149
|
{
|
|
960
1150
|
id: `${id}-details`,
|
|
@@ -963,13 +1153,13 @@ function DataRequestForm({
|
|
|
963
1153
|
value: details,
|
|
964
1154
|
onChange: (e) => setDetails(e.target.value),
|
|
965
1155
|
placeholder: t.detailsPlaceholder,
|
|
966
|
-
className: (0,
|
|
1156
|
+
className: (0, import_utils9.cn)(FIELD_CLASS, "border-border")
|
|
967
1157
|
}
|
|
968
1158
|
)
|
|
969
1159
|
] }),
|
|
970
|
-
/* @__PURE__ */ (0,
|
|
971
|
-
/* @__PURE__ */ (0,
|
|
972
|
-
/* @__PURE__ */ (0,
|
|
1160
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "space-y-1", children: [
|
|
1161
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex items-start gap-2", children: [
|
|
1162
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
973
1163
|
"input",
|
|
974
1164
|
{
|
|
975
1165
|
id: `${id}-consent`,
|
|
@@ -981,12 +1171,12 @@ function DataRequestForm({
|
|
|
981
1171
|
className: "mt-1"
|
|
982
1172
|
}
|
|
983
1173
|
),
|
|
984
|
-
/* @__PURE__ */ (0,
|
|
1174
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("label", { htmlFor: `${id}-consent`, className: "text-sm text-foreground/80", children: t.consent })
|
|
985
1175
|
] }),
|
|
986
1176
|
fieldError("consent")
|
|
987
1177
|
] }),
|
|
988
|
-
formError && /* @__PURE__ */ (0,
|
|
989
|
-
/* @__PURE__ */ (0,
|
|
1178
|
+
formError && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { role: "alert", className: "text-sm text-destructive", children: formError }),
|
|
1179
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
990
1180
|
"button",
|
|
991
1181
|
{
|
|
992
1182
|
type: "submit",
|
|
@@ -1004,8 +1194,10 @@ function DataRequestForm({
|
|
|
1004
1194
|
AddToCartButton,
|
|
1005
1195
|
CartIcon,
|
|
1006
1196
|
Collection,
|
|
1197
|
+
CourseSyllabus,
|
|
1007
1198
|
DATA_REQUEST_FIELD_LIMITS,
|
|
1008
1199
|
DATA_REQUEST_TYPES,
|
|
1200
|
+
DEFAULT_COURSE_SYLLABUS_LABELS,
|
|
1009
1201
|
DEFAULT_DATA_REQUEST_FORM_LABELS,
|
|
1010
1202
|
DEFAULT_PRODUCT_CAROUSEL_LABELS,
|
|
1011
1203
|
DataRequestForm,
|