@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 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?` | Yes |
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: (count) => `${count} art\xEDculo${count === 1 ? "" : "s"}`
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/data-request-form.tsx
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 import_site_contracts = require("@reblu/site-contracts");
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, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
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, import_react5.useId)();
815
- const [requestType, setRequestType] = (0, import_react5.useState)("");
816
- const [name, setName] = (0, import_react5.useState)("");
817
- const [email, setEmail] = (0, import_react5.useState)("");
818
- const [details, setDetails] = (0, import_react5.useState)("");
819
- const [consent, setConsent] = (0, import_react5.useState)(false);
820
- const [errors, setErrors] = (0, import_react5.useState)({});
821
- const [formError, setFormError] = (0, import_react5.useState)(null);
822
- const [submitting, setSubmitting] = (0, import_react5.useState)(false);
823
- const [ack, setAck] = (0, import_react5.useState)(null);
824
- const statusRef = (0, import_react5.useRef)(null);
825
- (0, import_react5.useEffect)(() => {
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, import_jsx_runtime8.jsx)(
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, import_utils7.cn)("rounded-lg border border-border p-6 text-sm", className),
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, import_jsx_runtime8.jsx)("p", { id: `${id}-${field}-error`, className: "text-xs text-destructive", children: errors[field] }) : null;
890
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
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, import_utils7.cn)("space-y-4", className),
1088
+ className: (0, import_utils9.cn)("space-y-4", className),
899
1089
  children: [
900
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "space-y-1", children: [
901
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("label", { htmlFor: `${id}-type`, className: "text-sm font-medium", children: t.requestTypeLabel }),
902
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
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, import_utils7.cn)(FIELD_CLASS, errors.requestType ? "border-destructive" : "border-border"),
1100
+ className: (0, import_utils9.cn)(FIELD_CLASS, errors.requestType ? "border-destructive" : "border-border"),
911
1101
  children: [
912
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: "", children: t.requestTypePlaceholder }),
913
- requestTypes.map((type) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: type, children: t.requestTypes[type] }, type))
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, import_jsx_runtime8.jsxs)("div", { className: "space-y-1", children: [
920
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("label", { htmlFor: `${id}-name`, className: "text-sm font-medium", children: t.nameLabel }),
921
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
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, import_utils7.cn)(FIELD_CLASS, errors.name ? "border-destructive" : "border-border")
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, import_jsx_runtime8.jsxs)("div", { className: "space-y-1", children: [
938
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("label", { htmlFor: `${id}-email`, className: "text-sm font-medium", children: t.emailLabel }),
939
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
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, import_utils7.cn)(FIELD_CLASS, errors.email ? "border-destructive" : "border-border")
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, import_jsx_runtime8.jsxs)("div", { className: "space-y-1", children: [
956
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("label", { htmlFor: `${id}-details`, className: "text-sm font-medium", children: t.detailsLabel }),
957
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
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, import_utils7.cn)(FIELD_CLASS, "border-border")
1156
+ className: (0, import_utils9.cn)(FIELD_CLASS, "border-border")
967
1157
  }
968
1158
  )
969
1159
  ] }),
970
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "space-y-1", children: [
971
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex items-start gap-2", children: [
972
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
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, import_jsx_runtime8.jsx)("label", { htmlFor: `${id}-consent`, className: "text-sm text-foreground/80", children: t.consent })
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, import_jsx_runtime8.jsx)("p", { role: "alert", className: "text-sm text-destructive", children: formError }),
989
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
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,