@reblu/site-blocks 0.18.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 +33 -0
- package/README.md +179 -2
- package/dist/_heading-BsNbc1wc.d.cts +4 -0
- package/dist/_heading-BsNbc1wc.d.ts +4 -0
- package/dist/client.cjs +449 -50
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +109 -2
- package/dist/client.d.ts +109 -2
- package/dist/client.js +446 -51
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +374 -309
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +51 -8
- package/dist/index.d.ts +51 -8
- package/dist/index.js +373 -309
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/client.cjs
CHANGED
|
@@ -34,10 +34,14 @@ __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,
|
|
42
|
+
DEFAULT_PRODUCT_CAROUSEL_LABELS: () => DEFAULT_PRODUCT_CAROUSEL_LABELS,
|
|
40
43
|
DataRequestForm: () => DataRequestForm,
|
|
44
|
+
ProductCarousel: () => ProductCarousel,
|
|
41
45
|
ShareButtons: () => ShareButtons,
|
|
42
46
|
useCollection: () => useCollection
|
|
43
47
|
});
|
|
@@ -226,7 +230,7 @@ function Collection({
|
|
|
226
230
|
] })
|
|
227
231
|
] });
|
|
228
232
|
}
|
|
229
|
-
var PAGE_BTN_CLASS = "inline-flex h-9 items-center rounded-md border border-border px-4 text-sm font-medium transition-colors hover:bg-muted disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring";
|
|
233
|
+
var PAGE_BTN_CLASS = "inline-flex h-9 cursor-pointer items-center rounded-md border border-border px-4 text-sm font-medium transition-colors hover:bg-muted disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring";
|
|
230
234
|
function FacetChip({ label, pressed, color, onClick }) {
|
|
231
235
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
232
236
|
"button",
|
|
@@ -236,7 +240,7 @@ function FacetChip({ label, pressed, color, onClick }) {
|
|
|
236
240
|
onClick,
|
|
237
241
|
style: color ? { "--facet-color": color } : void 0,
|
|
238
242
|
className: (0, import_utils.cn)(
|
|
239
|
-
"inline-flex items-center rounded-full border px-3 py-1 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
243
|
+
"inline-flex cursor-pointer items-center rounded-full border px-3 py-1 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
240
244
|
pressed ? color ? "border-transparent bg-[var(--facet-color)] text-white" : "border-transparent bg-primary text-primary-foreground" : "border-border text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
241
245
|
),
|
|
242
246
|
children: label
|
|
@@ -321,7 +325,7 @@ function ShareButtons({ url, title, className, labels }) {
|
|
|
321
325
|
{
|
|
322
326
|
type: "button",
|
|
323
327
|
onClick: handleCopy,
|
|
324
|
-
className: (0, import_utils2.cn)(LINK_CLASS, "w-auto gap-2 px-3", copied && "border-primary text-primary"),
|
|
328
|
+
className: (0, import_utils2.cn)(LINK_CLASS, "w-auto cursor-pointer gap-2 px-3", copied && "border-primary text-primary"),
|
|
325
329
|
"aria-label": t.copy,
|
|
326
330
|
children: [
|
|
327
331
|
copied ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CheckIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CopyIcon, {}),
|
|
@@ -442,7 +446,7 @@ function AddToCartButton({
|
|
|
442
446
|
"data-slot": "add-to-cart-button",
|
|
443
447
|
"data-status": status,
|
|
444
448
|
className: (0, import_utils3.cn)(
|
|
445
|
-
"inline-flex items-center justify-center gap-2 rounded-xl px-4 py-2.5 text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
449
|
+
"inline-flex cursor-pointer items-center justify-center gap-2 rounded-xl px-4 py-2.5 text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
446
450
|
isDisabled ? "cursor-not-allowed bg-muted text-muted-foreground" : "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
447
451
|
status === "error" && "bg-destructive text-destructive-foreground",
|
|
448
452
|
className
|
|
@@ -458,7 +462,7 @@ var import_utils4 = require("@reblu/site-ui/utils");
|
|
|
458
462
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
459
463
|
var DEFAULT_LABELS4 = {
|
|
460
464
|
cart: "Carrito",
|
|
461
|
-
itemsSuffix: (
|
|
465
|
+
itemsSuffix: (count2) => `${count2} art\xEDculo${count2 === 1 ? "" : "s"}`
|
|
462
466
|
};
|
|
463
467
|
var BUMP_DURATION_MS = 400;
|
|
464
468
|
function CartIcon({ itemCount, href, hydrated, bumpOnAdd, labels, className }) {
|
|
@@ -527,11 +531,402 @@ function CartIcon({ itemCount, href, hydrated, bumpOnAdd, labels, className }) {
|
|
|
527
531
|
);
|
|
528
532
|
}
|
|
529
533
|
|
|
530
|
-
// src/components/
|
|
534
|
+
// src/components/product-carousel.tsx
|
|
531
535
|
var import_react4 = require("react");
|
|
536
|
+
var import_button_variants = require("@reblu/site-ui/button-variants");
|
|
537
|
+
var import_utils6 = require("@reblu/site-ui/utils");
|
|
538
|
+
|
|
539
|
+
// src/components/_heading.tsx
|
|
540
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
541
|
+
function Heading({
|
|
542
|
+
level,
|
|
543
|
+
className,
|
|
544
|
+
children
|
|
545
|
+
}) {
|
|
546
|
+
const Tag = `h${level}`;
|
|
547
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Tag, { className, children });
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/components/product-card.tsx
|
|
532
551
|
var import_utils5 = require("@reblu/site-ui/utils");
|
|
552
|
+
|
|
553
|
+
// src/lib/format-price.ts
|
|
554
|
+
function formatPrice(cents, currency = "EUR", locale = "es-ES") {
|
|
555
|
+
return new Intl.NumberFormat(locale, {
|
|
556
|
+
style: "currency",
|
|
557
|
+
currency
|
|
558
|
+
}).format(cents / 100);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// src/components/product-card.tsx
|
|
562
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
563
|
+
function ProductCard({ product, href, currencyCode = "EUR", locale, className }) {
|
|
564
|
+
const image = product.images.find((i) => i.isPrimary) ?? product.images[0];
|
|
565
|
+
const hasDiscount = product.comparePrice != null && product.comparePrice > product.price;
|
|
566
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
567
|
+
"article",
|
|
568
|
+
{
|
|
569
|
+
className: (0, import_utils5.cn)(
|
|
570
|
+
"group flex flex-col overflow-hidden rounded-lg border bg-card transition-shadow hover:shadow-lg",
|
|
571
|
+
className
|
|
572
|
+
),
|
|
573
|
+
"data-slot": "product-card",
|
|
574
|
+
children: [
|
|
575
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "relative aspect-square overflow-hidden bg-muted", children: image ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
576
|
+
"img",
|
|
577
|
+
{
|
|
578
|
+
src: image.url,
|
|
579
|
+
alt: image.alt ?? product.name,
|
|
580
|
+
loading: "lazy",
|
|
581
|
+
className: "h-full w-full object-cover transition-transform group-hover:scale-105"
|
|
582
|
+
}
|
|
583
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex h-full items-center justify-center text-4xl text-muted-foreground/30", "aria-hidden": "true", children: "\u{1F6CD}\uFE0F" }) }),
|
|
584
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-1 flex-col gap-2 p-4", children: [
|
|
585
|
+
product.category && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "inline-flex w-fit items-center rounded-full bg-secondary px-2.5 py-0.5 text-xs font-medium text-secondary-foreground", children: product.category.name }),
|
|
586
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h3", { className: "text-base font-semibold leading-tight", children: href ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("a", { href, className: "transition-colors hover:text-primary", children: product.name }) : product.name }),
|
|
587
|
+
product.shortDescription && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "line-clamp-2 text-sm text-muted-foreground", children: product.shortDescription }),
|
|
588
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "mt-auto flex items-baseline gap-2 pt-2", children: [
|
|
589
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-lg font-bold text-foreground", children: formatPrice(product.price, currencyCode, locale) }),
|
|
590
|
+
hasDiscount && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-sm text-muted-foreground line-through", children: formatPrice(product.comparePrice, currencyCode, locale) })
|
|
591
|
+
] })
|
|
592
|
+
] })
|
|
593
|
+
]
|
|
594
|
+
}
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// src/components/product-carousel.tsx
|
|
599
|
+
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
600
|
+
var DEFAULT_PRODUCT_CAROUSEL_LABELS = {
|
|
601
|
+
region: "Productos relacionados",
|
|
602
|
+
roleDescription: "carrusel",
|
|
603
|
+
previous: "Anterior",
|
|
604
|
+
next: "Siguiente"
|
|
605
|
+
};
|
|
606
|
+
var EDGE_EPSILON = 1;
|
|
607
|
+
function prefersReducedMotion() {
|
|
608
|
+
return typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)").matches : false;
|
|
609
|
+
}
|
|
610
|
+
function ChevronIcon({ direction }) {
|
|
611
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
612
|
+
"svg",
|
|
613
|
+
{
|
|
614
|
+
"aria-hidden": "true",
|
|
615
|
+
viewBox: "0 0 24 24",
|
|
616
|
+
fill: "none",
|
|
617
|
+
stroke: "currentColor",
|
|
618
|
+
strokeWidth: 2,
|
|
619
|
+
strokeLinecap: "round",
|
|
620
|
+
strokeLinejoin: "round",
|
|
621
|
+
className: "rtl:-scale-x-100",
|
|
622
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: direction === "left" ? "m15 18-6-6 6-6" : "m9 18 6-6-6-6" })
|
|
623
|
+
}
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
function ProductCarousel(props) {
|
|
627
|
+
const { products, heading, headingLevel = 2, headingClassName, labels, className, itemClassName } = props;
|
|
628
|
+
const t = { ...DEFAULT_PRODUCT_CAROUSEL_LABELS, ...labels };
|
|
629
|
+
const trackId = (0, import_react4.useId)();
|
|
630
|
+
const headingId = (0, import_react4.useId)();
|
|
631
|
+
const trackRef = (0, import_react4.useRef)(null);
|
|
632
|
+
const [canPrev, setCanPrev] = (0, import_react4.useState)(false);
|
|
633
|
+
const [canNext, setCanNext] = (0, import_react4.useState)(false);
|
|
634
|
+
const update = (0, import_react4.useCallback)(() => {
|
|
635
|
+
const el = trackRef.current;
|
|
636
|
+
if (!el) return;
|
|
637
|
+
const max = el.scrollWidth - el.clientWidth;
|
|
638
|
+
const pos = Math.abs(el.scrollLeft);
|
|
639
|
+
setCanPrev(pos > EDGE_EPSILON);
|
|
640
|
+
setCanNext(pos < max - EDGE_EPSILON);
|
|
641
|
+
}, []);
|
|
642
|
+
(0, import_react4.useEffect)(() => {
|
|
643
|
+
const el = trackRef.current;
|
|
644
|
+
if (!el) return;
|
|
645
|
+
update();
|
|
646
|
+
el.addEventListener("scroll", update, { passive: true });
|
|
647
|
+
let observer;
|
|
648
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
649
|
+
observer = new ResizeObserver(update);
|
|
650
|
+
observer.observe(el);
|
|
651
|
+
} else {
|
|
652
|
+
window.addEventListener("resize", update);
|
|
653
|
+
}
|
|
654
|
+
return () => {
|
|
655
|
+
el.removeEventListener("scroll", update);
|
|
656
|
+
if (observer) observer.disconnect();
|
|
657
|
+
else window.removeEventListener("resize", update);
|
|
658
|
+
};
|
|
659
|
+
}, [update, products.length]);
|
|
660
|
+
const scrollByPage = (direction) => {
|
|
661
|
+
const el = trackRef.current;
|
|
662
|
+
if (!el) return;
|
|
663
|
+
const rtl = typeof getComputedStyle === "function" && getComputedStyle(el).direction === "rtl";
|
|
664
|
+
el.scrollBy({
|
|
665
|
+
left: direction * el.clientWidth * (rtl ? -1 : 1),
|
|
666
|
+
behavior: prefersReducedMotion() ? "auto" : "smooth"
|
|
667
|
+
});
|
|
668
|
+
};
|
|
669
|
+
if (products.length === 0) return null;
|
|
670
|
+
const scrollable = canPrev || canNext;
|
|
671
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("section", { "data-slot": "product-carousel", className: (0, import_utils6.cn)("flex flex-col gap-4", className), children: [
|
|
672
|
+
(heading || scrollable) && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex items-center justify-between gap-4", children: [
|
|
673
|
+
heading ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { id: headingId, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Heading, { level: headingLevel, className: headingClassName ?? "text-xl font-semibold", children: heading }) }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", {}),
|
|
674
|
+
scrollable && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "hidden gap-2 sm:flex", "data-slot": "product-carousel-controls", children: [
|
|
675
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
676
|
+
"button",
|
|
677
|
+
{
|
|
678
|
+
type: "button",
|
|
679
|
+
className: (0, import_utils6.cn)((0, import_button_variants.buttonVariants)({ variant: "outline", size: "icon" }), "rounded-full"),
|
|
680
|
+
"aria-label": t.previous,
|
|
681
|
+
"aria-controls": trackId,
|
|
682
|
+
disabled: !canPrev,
|
|
683
|
+
onClick: () => scrollByPage(-1),
|
|
684
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ChevronIcon, { direction: "left" })
|
|
685
|
+
}
|
|
686
|
+
),
|
|
687
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
688
|
+
"button",
|
|
689
|
+
{
|
|
690
|
+
type: "button",
|
|
691
|
+
className: (0, import_utils6.cn)((0, import_button_variants.buttonVariants)({ variant: "outline", size: "icon" }), "rounded-full"),
|
|
692
|
+
"aria-label": t.next,
|
|
693
|
+
"aria-controls": trackId,
|
|
694
|
+
disabled: !canNext,
|
|
695
|
+
onClick: () => scrollByPage(1),
|
|
696
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ChevronIcon, { direction: "right" })
|
|
697
|
+
}
|
|
698
|
+
)
|
|
699
|
+
] })
|
|
700
|
+
] }),
|
|
701
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
702
|
+
"div",
|
|
703
|
+
{
|
|
704
|
+
ref: trackRef,
|
|
705
|
+
id: trackId,
|
|
706
|
+
role: "region",
|
|
707
|
+
"aria-roledescription": t.roleDescription,
|
|
708
|
+
...heading ? { "aria-labelledby": headingId } : { "aria-label": t.region },
|
|
709
|
+
tabIndex: 0,
|
|
710
|
+
"data-slot": "product-carousel-track",
|
|
711
|
+
className: "snap-x snap-mandatory overflow-x-auto overscroll-x-contain scroll-smooth rounded-lg pb-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:scroll-auto",
|
|
712
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("ul", { role: "list", className: "m-0 flex list-none gap-4 p-0", children: products.map((product, index) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
713
|
+
"li",
|
|
714
|
+
{
|
|
715
|
+
className: (0, import_utils6.cn)(
|
|
716
|
+
"shrink-0 snap-start basis-[calc((100%-1rem)/1.5)] sm:basis-[calc((100%-2rem)/3)] lg:basis-[calc((100%-3rem)/4)]",
|
|
717
|
+
itemClassName
|
|
718
|
+
),
|
|
719
|
+
children: props.renderProduct ? props.renderProduct(product, index) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
720
|
+
ProductCard,
|
|
721
|
+
{
|
|
722
|
+
product,
|
|
723
|
+
href: props.productHref?.(product),
|
|
724
|
+
currencyCode: props.currencyCode,
|
|
725
|
+
locale: props.locale,
|
|
726
|
+
className: "h-full"
|
|
727
|
+
}
|
|
728
|
+
)
|
|
729
|
+
},
|
|
730
|
+
product.id
|
|
731
|
+
)) })
|
|
732
|
+
}
|
|
733
|
+
)
|
|
734
|
+
] });
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// src/components/course-syllabus.tsx
|
|
738
|
+
var import_react5 = require("react");
|
|
739
|
+
var import_utils8 = require("@reblu/site-ui/utils");
|
|
740
|
+
|
|
741
|
+
// src/lib/slot-class-name.ts
|
|
742
|
+
var import_utils7 = require("@reblu/site-ui/utils");
|
|
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
|
|
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");
|
|
533
928
|
var import_site_contracts = require("@reblu/site-contracts");
|
|
534
|
-
var
|
|
929
|
+
var import_jsx_runtime9 = require("react/jsx-runtime");
|
|
535
930
|
var DATA_REQUEST_FIELD_LIMITS = Object.freeze({ name: 200, email: 320, details: 5e3 });
|
|
536
931
|
var DEFAULT_DATA_REQUEST_FORM_LABELS = {
|
|
537
932
|
requestTypeLabel: "Derecho que quieres ejercer",
|
|
@@ -557,7 +952,7 @@ var DEFAULT_DATA_REQUEST_FORM_LABELS = {
|
|
|
557
952
|
nameRequired: "Indica tu nombre.",
|
|
558
953
|
emailInvalid: "Introduce un email v\xE1lido.",
|
|
559
954
|
consentRequired: "Debes aceptar el tratamiento de tus datos para gestionar la solicitud.",
|
|
560
|
-
success: (deadline) => /* @__PURE__ */ (0,
|
|
955
|
+
success: (deadline) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
|
|
561
956
|
"Hemos recibido tu solicitud. Te responderemos como m\xE1ximo el ",
|
|
562
957
|
deadline,
|
|
563
958
|
"."
|
|
@@ -606,18 +1001,18 @@ function DataRequestForm({
|
|
|
606
1001
|
...labels,
|
|
607
1002
|
requestTypes: { ...DEFAULT_DATA_REQUEST_FORM_LABELS.requestTypes, ...labels?.requestTypes }
|
|
608
1003
|
};
|
|
609
|
-
const id = (0,
|
|
610
|
-
const [requestType, setRequestType] = (0,
|
|
611
|
-
const [name, setName] = (0,
|
|
612
|
-
const [email, setEmail] = (0,
|
|
613
|
-
const [details, setDetails] = (0,
|
|
614
|
-
const [consent, setConsent] = (0,
|
|
615
|
-
const [errors, setErrors] = (0,
|
|
616
|
-
const [formError, setFormError] = (0,
|
|
617
|
-
const [submitting, setSubmitting] = (0,
|
|
618
|
-
const [ack, setAck] = (0,
|
|
619
|
-
const statusRef = (0,
|
|
620
|
-
(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)(() => {
|
|
621
1016
|
if (ack) statusRef.current?.focus();
|
|
622
1017
|
}, [ack]);
|
|
623
1018
|
async function handleSubmit(event) {
|
|
@@ -668,21 +1063,21 @@ function DataRequestForm({
|
|
|
668
1063
|
dateStyle: "long",
|
|
669
1064
|
timeStyle: "short"
|
|
670
1065
|
}).format(new Date(ack.slaDueAt));
|
|
671
|
-
return /* @__PURE__ */ (0,
|
|
1066
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
672
1067
|
"div",
|
|
673
1068
|
{
|
|
674
1069
|
ref: statusRef,
|
|
675
1070
|
tabIndex: -1,
|
|
676
1071
|
role: "status",
|
|
677
1072
|
"data-slot": "data-request-form-success",
|
|
678
|
-
className: (0,
|
|
1073
|
+
className: (0, import_utils9.cn)("rounded-lg border border-border p-6 text-sm", className),
|
|
679
1074
|
children: t.success(deadline)
|
|
680
1075
|
}
|
|
681
1076
|
);
|
|
682
1077
|
}
|
|
683
1078
|
const errorId = (field) => errors[field] ? `${id}-${field}-error` : void 0;
|
|
684
|
-
const fieldError = (field) => errors[field] ? /* @__PURE__ */ (0,
|
|
685
|
-
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)(
|
|
686
1081
|
"form",
|
|
687
1082
|
{
|
|
688
1083
|
noValidate: true,
|
|
@@ -690,11 +1085,11 @@ function DataRequestForm({
|
|
|
690
1085
|
onFocusCapture: onInteract,
|
|
691
1086
|
onPointerDown: onInteract,
|
|
692
1087
|
"data-slot": "data-request-form",
|
|
693
|
-
className: (0,
|
|
1088
|
+
className: (0, import_utils9.cn)("space-y-4", className),
|
|
694
1089
|
children: [
|
|
695
|
-
/* @__PURE__ */ (0,
|
|
696
|
-
/* @__PURE__ */ (0,
|
|
697
|
-
/* @__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)(
|
|
698
1093
|
"select",
|
|
699
1094
|
{
|
|
700
1095
|
id: `${id}-type`,
|
|
@@ -702,18 +1097,18 @@ function DataRequestForm({
|
|
|
702
1097
|
onChange: (e) => setRequestType(e.target.value),
|
|
703
1098
|
"aria-invalid": errors.requestType ? true : void 0,
|
|
704
1099
|
"aria-describedby": errorId("requestType"),
|
|
705
|
-
className: (0,
|
|
1100
|
+
className: (0, import_utils9.cn)(FIELD_CLASS, errors.requestType ? "border-destructive" : "border-border"),
|
|
706
1101
|
children: [
|
|
707
|
-
/* @__PURE__ */ (0,
|
|
708
|
-
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))
|
|
709
1104
|
]
|
|
710
1105
|
}
|
|
711
1106
|
),
|
|
712
1107
|
fieldError("requestType")
|
|
713
1108
|
] }),
|
|
714
|
-
/* @__PURE__ */ (0,
|
|
715
|
-
/* @__PURE__ */ (0,
|
|
716
|
-
/* @__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)(
|
|
717
1112
|
"input",
|
|
718
1113
|
{
|
|
719
1114
|
id: `${id}-name`,
|
|
@@ -724,14 +1119,14 @@ function DataRequestForm({
|
|
|
724
1119
|
onChange: (e) => setName(e.target.value),
|
|
725
1120
|
"aria-invalid": errors.name ? true : void 0,
|
|
726
1121
|
"aria-describedby": errorId("name"),
|
|
727
|
-
className: (0,
|
|
1122
|
+
className: (0, import_utils9.cn)(FIELD_CLASS, errors.name ? "border-destructive" : "border-border")
|
|
728
1123
|
}
|
|
729
1124
|
),
|
|
730
1125
|
fieldError("name")
|
|
731
1126
|
] }),
|
|
732
|
-
/* @__PURE__ */ (0,
|
|
733
|
-
/* @__PURE__ */ (0,
|
|
734
|
-
/* @__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)(
|
|
735
1130
|
"input",
|
|
736
1131
|
{
|
|
737
1132
|
id: `${id}-email`,
|
|
@@ -742,14 +1137,14 @@ function DataRequestForm({
|
|
|
742
1137
|
onChange: (e) => setEmail(e.target.value),
|
|
743
1138
|
"aria-invalid": errors.email ? true : void 0,
|
|
744
1139
|
"aria-describedby": errorId("email"),
|
|
745
|
-
className: (0,
|
|
1140
|
+
className: (0, import_utils9.cn)(FIELD_CLASS, errors.email ? "border-destructive" : "border-border")
|
|
746
1141
|
}
|
|
747
1142
|
),
|
|
748
1143
|
fieldError("email")
|
|
749
1144
|
] }),
|
|
750
|
-
/* @__PURE__ */ (0,
|
|
751
|
-
/* @__PURE__ */ (0,
|
|
752
|
-
/* @__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)(
|
|
753
1148
|
"textarea",
|
|
754
1149
|
{
|
|
755
1150
|
id: `${id}-details`,
|
|
@@ -758,13 +1153,13 @@ function DataRequestForm({
|
|
|
758
1153
|
value: details,
|
|
759
1154
|
onChange: (e) => setDetails(e.target.value),
|
|
760
1155
|
placeholder: t.detailsPlaceholder,
|
|
761
|
-
className: (0,
|
|
1156
|
+
className: (0, import_utils9.cn)(FIELD_CLASS, "border-border")
|
|
762
1157
|
}
|
|
763
1158
|
)
|
|
764
1159
|
] }),
|
|
765
|
-
/* @__PURE__ */ (0,
|
|
766
|
-
/* @__PURE__ */ (0,
|
|
767
|
-
/* @__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)(
|
|
768
1163
|
"input",
|
|
769
1164
|
{
|
|
770
1165
|
id: `${id}-consent`,
|
|
@@ -776,17 +1171,17 @@ function DataRequestForm({
|
|
|
776
1171
|
className: "mt-1"
|
|
777
1172
|
}
|
|
778
1173
|
),
|
|
779
|
-
/* @__PURE__ */ (0,
|
|
1174
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("label", { htmlFor: `${id}-consent`, className: "text-sm text-foreground/80", children: t.consent })
|
|
780
1175
|
] }),
|
|
781
1176
|
fieldError("consent")
|
|
782
1177
|
] }),
|
|
783
|
-
formError && /* @__PURE__ */ (0,
|
|
784
|
-
/* @__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)(
|
|
785
1180
|
"button",
|
|
786
1181
|
{
|
|
787
1182
|
type: "submit",
|
|
788
1183
|
disabled: submitting,
|
|
789
|
-
className: "rounded-lg bg-primary px-6 py-2 text-sm font-semibold text-primary-foreground disabled:opacity-60",
|
|
1184
|
+
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",
|
|
790
1185
|
children: submitting ? t.submitting : t.submit
|
|
791
1186
|
}
|
|
792
1187
|
)
|
|
@@ -799,10 +1194,14 @@ function DataRequestForm({
|
|
|
799
1194
|
AddToCartButton,
|
|
800
1195
|
CartIcon,
|
|
801
1196
|
Collection,
|
|
1197
|
+
CourseSyllabus,
|
|
802
1198
|
DATA_REQUEST_FIELD_LIMITS,
|
|
803
1199
|
DATA_REQUEST_TYPES,
|
|
1200
|
+
DEFAULT_COURSE_SYLLABUS_LABELS,
|
|
804
1201
|
DEFAULT_DATA_REQUEST_FORM_LABELS,
|
|
1202
|
+
DEFAULT_PRODUCT_CAROUSEL_LABELS,
|
|
805
1203
|
DataRequestForm,
|
|
1204
|
+
ProductCarousel,
|
|
806
1205
|
ShareButtons,
|
|
807
1206
|
useCollection
|
|
808
1207
|
});
|