@sprintup-cms/sdk 1.5.1 → 1.7.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
@@ -2,6 +2,45 @@
2
2
 
3
3
  All notable changes to `@sprintup-cms/sdk` will be documented here.
4
4
 
5
+ ## [1.7.0] — 2026-03-07
6
+
7
+ ### Added
8
+ - `ProductListBlock` — dynamic block that fetches products from an external API endpoint.
9
+ Supports configuration: `endpoint`, `title`, `subtitle`, `columns`, `showPrice`, `showRating`,
10
+ `limit`, `currency`. Includes loading skeleton, error state, and empty state. Normalises
11
+ both generic `{ id, price }` shape and iRead school product shape (`_id`, `plans[0].price`,
12
+ `type`, `duration`).
13
+ - `product-list` registered in the `BUILT_IN` renderer map — external apps now render
14
+ product list blocks out of the box without custom configuration.
15
+
16
+ ## [1.6.0] — 2026-03-07
17
+
18
+ ### Added
19
+ - `StructuredBlock` component: dedicated renderer for `__structured__` page type blocks.
20
+ Iterates nested `{ sectionName: { fieldName: value } }` content using the `pageType`
21
+ schema for correct field type rendering (richtext, image, boolean, url, arrays).
22
+ - `FieldValue` helper component for consistent field-level rendering across both
23
+ `StructuredBlock` and `SectionBlock`.
24
+ - `__structured__` registered in the `BUILT_IN` renderer map — structured page type
25
+ instances now render correctly on external apps without any custom configuration.
26
+
27
+ ### Fixed
28
+ - `SectionBlock` fallback no longer intercepts `__structured__` blocks.
29
+ - Arrays and nested objects in block data no longer render as `[object Object]`.
30
+
31
+ ## [1.5.2] — 2026-03-07
32
+
33
+ ### Fixed
34
+ - `CMSBlocks` now correctly renders `__structured__` blocks (structured page types).
35
+ Previously the block fell through to `SectionBlock`'s generic fallback, printing raw
36
+ `sectionName.fieldName: value` text. Now a dedicated `StructuredBlock` component
37
+ iterates the nested `{ sectionName: { fieldName: value } }` shape returned by the API,
38
+ using the `pageType` schema for field types when available, and rendering richtext
39
+ values via `dangerouslySetInnerHTML`, images as `<img>`, and arrays as comma-separated
40
+ text — instead of `[object Object]` or raw key dumps.
41
+ - `__structured__` registered in the `BUILT_IN` renderer map so custom renderers and the
42
+ `SectionBlock` fallback no longer intercept structured page content.
43
+
5
44
  ## [1.5.1] — 2026-03-07
6
45
 
7
46
  ### Fixed
@@ -460,52 +460,173 @@ function VideoBlock({ block }) {
460
460
  const embedUrl = url.replace("youtube.com/watch?v=", "youtube.com/embed/").replace("youtu.be/", "youtube.com/embed/").replace("vimeo.com/", "player.vimeo.com/video/");
461
461
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "aspect-video rounded-lg overflow-hidden border bg-muted", children: /* @__PURE__ */ jsxRuntime.jsx("iframe", { src: embedUrl, allow: "autoplay; fullscreen", allowFullScreen: true, className: "w-full h-full", title: d.title || "Video" }) });
462
462
  }
463
+ function normaliseProduct(raw) {
464
+ if (raw.id !== void 0 && raw.price !== void 0) return raw;
465
+ const firstPlan = Array.isArray(raw.plans) && raw.plans.length > 0 ? raw.plans[0] : null;
466
+ return {
467
+ id: raw._id ?? raw.id ?? String(Math.random()),
468
+ name: raw.name ?? "Unnamed",
469
+ description: raw.description,
470
+ price: firstPlan?.price ?? 0,
471
+ image: raw.image ?? raw.thumbnail ?? void 0,
472
+ rating: raw.rating ?? void 0,
473
+ category: raw.type ? raw.type.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()) : void 0,
474
+ badge: raw.duration ? `${raw.duration} months` : void 0
475
+ };
476
+ }
477
+ function ProductListBlock({ block }) {
478
+ const d = getData(block);
479
+ const { title, subtitle, endpoint, columns = 3, showPrice = true, showRating = true, limit = 6, currency = "$" } = d;
480
+ const cols = parseInt(String(columns), 10);
481
+ const [state, setState] = React__default.default.useState({
482
+ data: null,
483
+ loading: true,
484
+ error: null
485
+ });
486
+ React__default.default.useEffect(() => {
487
+ if (!endpoint) {
488
+ setState({ data: [], loading: false, error: null });
489
+ return;
490
+ }
491
+ let cancelled = false;
492
+ async function fetchData() {
493
+ try {
494
+ const res = await fetch(endpoint);
495
+ if (!res.ok) throw new Error(`Failed to fetch: ${res.status}`);
496
+ const json = await res.json();
497
+ if (cancelled) return;
498
+ const items = Array.isArray(json) ? json : json.data ?? json.products ?? [];
499
+ setState({ data: items.map(normaliseProduct), loading: false, error: null });
500
+ } catch (err) {
501
+ if (!cancelled) setState({ data: null, loading: false, error: err.message });
502
+ }
503
+ }
504
+ fetchData();
505
+ return () => {
506
+ cancelled = true;
507
+ };
508
+ }, [endpoint]);
509
+ const gridCols = cols === 2 ? "sm:grid-cols-2" : cols === 4 ? "sm:grid-cols-2 lg:grid-cols-4" : "sm:grid-cols-2 lg:grid-cols-3";
510
+ if (state.loading) {
511
+ return /* @__PURE__ */ jsxRuntime.jsx("section", { className: "py-12", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-w-6xl mx-auto px-4", children: [
512
+ (title || subtitle) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center mb-8", children: [
513
+ title && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-8 w-64 bg-muted animate-pulse rounded mx-auto" }),
514
+ subtitle && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-4 w-96 max-w-full bg-muted/60 animate-pulse rounded mx-auto mt-2" })
515
+ ] }),
516
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: `grid gap-6 grid-cols-1 ${gridCols}`, children: Array.from({ length: Math.min(limit, 6) }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border rounded-lg overflow-hidden", children: [
517
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "aspect-square bg-muted animate-pulse" }),
518
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 space-y-3", children: [
519
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-4 w-20 bg-muted/60 animate-pulse rounded" }),
520
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-5 w-full bg-muted animate-pulse rounded" }),
521
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-6 w-16 bg-muted animate-pulse rounded" })
522
+ ] })
523
+ ] }, i)) })
524
+ ] }) });
525
+ }
526
+ if (state.error) {
527
+ return /* @__PURE__ */ jsxRuntime.jsx("section", { className: "py-12", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "max-w-6xl mx-auto px-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center py-12 border border-dashed border-border rounded-lg bg-muted/20", children: [
528
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium text-muted-foreground", children: "Unable to load products" }),
529
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground/70 mt-1", children: state.error })
530
+ ] }) }) });
531
+ }
532
+ const products = (state.data || []).slice(0, limit);
533
+ if (products.length === 0) {
534
+ return /* @__PURE__ */ jsxRuntime.jsx("section", { className: "py-12", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "max-w-6xl mx-auto px-4", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center py-12 border border-dashed border-border rounded-lg bg-muted/10", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: "No products available" }) }) }) });
535
+ }
536
+ return /* @__PURE__ */ jsxRuntime.jsx("section", { className: "py-12", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-w-6xl mx-auto px-4", children: [
537
+ (title || subtitle) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center mb-8", children: [
538
+ title && /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-2xl md:text-3xl font-semibold tracking-tight", children: title }),
539
+ subtitle && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground mt-2", children: subtitle })
540
+ ] }),
541
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: `grid gap-6 grid-cols-1 ${gridCols}`, children: products.map((product) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border rounded-lg overflow-hidden group hover:shadow-md transition-shadow", children: [
542
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "aspect-square relative bg-muted overflow-hidden", children: [
543
+ product.image ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: product.image, alt: product.name, className: "w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-full h-full flex items-center justify-center text-muted-foreground/30", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-12 h-12", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1.5, d: "M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" }) }) }),
544
+ product.badge && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "absolute top-3 left-3 text-xs px-2 py-1 rounded bg-primary text-primary-foreground", children: product.badge })
545
+ ] }),
546
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 space-y-2", children: [
547
+ product.category && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground uppercase tracking-wide", children: product.category }),
548
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "font-medium text-sm line-clamp-2", children: product.name }),
549
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between pt-1", children: [
550
+ showPrice && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-semibold", children: [
551
+ currency,
552
+ typeof product.price === "number" ? product.price.toFixed(2) : product.price
553
+ ] }),
554
+ showRating && product.rating && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 text-sm text-muted-foreground", children: [
555
+ /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-3.5 h-3.5 fill-yellow-400 text-yellow-400", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" }) }),
556
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: product.rating })
557
+ ] })
558
+ ] })
559
+ ] })
560
+ ] }, product.id)) })
561
+ ] }) });
562
+ }
563
+ function FieldValue({ value, fieldType }) {
564
+ if (value === null || value === void 0 || value === "") return null;
565
+ const isHtml = (v) => typeof v === "string" && /<[a-z][\s\S]*>/i.test(v);
566
+ if (fieldType === "richtext" || isHtml(value)) {
567
+ return /* @__PURE__ */ jsxRuntime.jsx(
568
+ "div",
569
+ {
570
+ className: "prose prose-neutral dark:prose-invert max-w-none",
571
+ dangerouslySetInnerHTML: { __html: String(value) }
572
+ }
573
+ );
574
+ }
575
+ if (fieldType === "image") {
576
+ return /* @__PURE__ */ jsxRuntime.jsx("img", { src: String(value), alt: "", className: "rounded-lg border max-h-96 object-cover w-full" });
577
+ }
578
+ if (fieldType === "boolean") {
579
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: value ? "Yes" : "No" });
580
+ }
581
+ if (fieldType === "url") {
582
+ return /* @__PURE__ */ jsxRuntime.jsx("a", { href: String(value), className: "text-primary underline underline-offset-2 break-all", children: String(value) });
583
+ }
584
+ if (Array.isArray(value)) {
585
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: value.join(", ") });
586
+ }
587
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: String(value) });
588
+ }
589
+ function StructuredBlock({ block, pageType }) {
590
+ const nested = block.content ?? block.data ?? {};
591
+ if (pageType?.sections?.length) {
592
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-8", children: pageType.sections.map((section) => {
593
+ const sectionData = nested[section.name] ?? {};
594
+ const filledFields = section.fields.filter((f) => {
595
+ const v = sectionData[f.name];
596
+ return v !== void 0 && v !== null && v !== "";
597
+ });
598
+ if (filledFields.length === 0) return null;
599
+ return /* @__PURE__ */ jsxRuntime.jsx("section", { className: "space-y-4", children: filledFields.map((field) => {
600
+ const value = sectionData[field.name];
601
+ if (value === void 0 || value === null || value === "") return null;
602
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(FieldValue, { value, fieldType: field.fieldType }) }, field.id);
603
+ }) }, section.id);
604
+ }) });
605
+ }
606
+ const isHtml = (v) => typeof v === "string" && /<[a-z][\s\S]*>/i.test(v);
607
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-8", children: Object.entries(nested).map(([sectionName, fields]) => {
608
+ if (!fields || typeof fields !== "object" || Array.isArray(fields)) return null;
609
+ const entries = Object.entries(fields).filter(([, v]) => v !== "" && v !== null && v !== void 0);
610
+ if (entries.length === 0) return null;
611
+ return /* @__PURE__ */ jsxRuntime.jsx("section", { className: "space-y-4", children: entries.map(([fieldName, value]) => {
612
+ if (typeof value === "object" && !Array.isArray(value)) return null;
613
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { children: isHtml(value) ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "prose prose-neutral dark:prose-invert max-w-none", dangerouslySetInnerHTML: { __html: String(value) } }) : Array.isArray(value) ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground", children: value.join(", ") }) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground", children: String(value) }) }, fieldName);
614
+ }) }, sectionName);
615
+ }) });
616
+ }
463
617
  function SectionBlock({ block, pageType }) {
464
618
  const d = getData(block);
465
619
  const section = pageType?.sections.find((s) => s.name === block.type);
466
620
  if (section) {
467
- const filledFields = section.fields.filter((f) => d[f.name] !== void 0 && d[f.name] !== "" && d[f.name] !== null);
621
+ const filledFields = section.fields.filter((f) => {
622
+ const v = d[f.name];
623
+ return v !== void 0 && v !== null && v !== "";
624
+ });
468
625
  if (filledFields.length === 0) return null;
469
- return /* @__PURE__ */ jsxRuntime.jsx("section", { className: "py-4 space-y-4 border-b border-border last:border-0", children: filledFields.map((field) => {
626
+ return /* @__PURE__ */ jsxRuntime.jsx("section", { className: "space-y-4", children: filledFields.map((field) => {
470
627
  const value = d[field.name];
471
- if (!value && value !== 0 && value !== false) return null;
472
- if (field.fieldType === "richtext") {
473
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
474
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-1", children: field.label }),
475
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "prose prose-neutral dark:prose-invert max-w-none text-sm", dangerouslySetInnerHTML: { __html: String(value) } })
476
- ] }, field.id);
477
- }
478
- if (field.fieldType === "image") {
479
- return /* @__PURE__ */ jsxRuntime.jsx("figure", { children: /* @__PURE__ */ jsxRuntime.jsx("img", { src: String(value), alt: field.label, className: "rounded-lg border max-h-96 object-cover" }) }, field.id);
480
- }
481
- if (field.fieldType === "boolean") {
482
- return /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm", children: [
483
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-medium", children: [
484
- field.label,
485
- ":"
486
- ] }),
487
- " ",
488
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: value ? "Yes" : "No" })
489
- ] }, field.id);
490
- }
491
- if (field.fieldType === "url") {
492
- return /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm", children: [
493
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-medium", children: [
494
- field.label,
495
- ":"
496
- ] }),
497
- " ",
498
- /* @__PURE__ */ jsxRuntime.jsx("a", { href: String(value), className: "text-primary underline underline-offset-2 break-all", children: String(value) })
499
- ] }, field.id);
500
- }
501
- return /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm", children: [
502
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-medium", children: [
503
- field.label,
504
- ":"
505
- ] }),
506
- " ",
507
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: String(value) })
508
- ] }, field.id);
628
+ if (value === void 0 || value === null || value === "") return null;
629
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(FieldValue, { value, fieldType: field.fieldType }) }, field.id);
509
630
  }) });
510
631
  }
511
632
  const isHtml = (v) => typeof v === "string" && /<[a-z][\s\S]*>/i.test(v);
@@ -513,21 +634,13 @@ function SectionBlock({ block, pageType }) {
513
634
  ([, v]) => v !== "" && v !== null && v !== void 0 && typeof v !== "object" && !Array.isArray(v)
514
635
  );
515
636
  if (entries.length === 0) return null;
516
- return /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "py-4 space-y-2 border-b border-border last:border-0", children: [
517
- block.label && /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "font-semibold text-sm text-muted-foreground uppercase tracking-wide", children: block.label }),
518
- entries.map(
519
- ([key, value]) => isHtml(value) ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "prose prose-neutral dark:prose-invert max-w-none text-sm", dangerouslySetInnerHTML: { __html: String(value) } }, key) : /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm", children: [
520
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-medium capitalize", children: [
521
- key.replace(/_/g, " "),
522
- ":"
523
- ] }),
524
- " ",
525
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: String(value) })
526
- ] }, key)
527
- )
528
- ] });
637
+ return /* @__PURE__ */ jsxRuntime.jsx("section", { className: "space-y-2", children: entries.map(
638
+ ([key, value]) => isHtml(value) ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "prose prose-neutral dark:prose-invert max-w-none", dangerouslySetInnerHTML: { __html: String(value) } }, key) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground", children: String(value) }, key)
639
+ ) });
529
640
  }
530
641
  var BUILT_IN = {
642
+ // Structured page type — content is nested { sectionName: { fieldName: value } }
643
+ "__structured__": (b, pt) => /* @__PURE__ */ jsxRuntime.jsx(StructuredBlock, { block: b, pageType: pt }),
531
644
  heading: (b) => /* @__PURE__ */ jsxRuntime.jsx(HeadingBlock, { block: b }),
532
645
  text: (b) => /* @__PURE__ */ jsxRuntime.jsx(TextBlock, { block: b }),
533
646
  richtext: (b) => /* @__PURE__ */ jsxRuntime.jsx(RichTextBlock, { block: b }),
@@ -542,7 +655,8 @@ var BUILT_IN = {
542
655
  alert: (b) => /* @__PURE__ */ jsxRuntime.jsx(AlertBlock, { block: b }),
543
656
  divider: () => /* @__PURE__ */ jsxRuntime.jsx(DividerBlock, {}),
544
657
  spacer: (b) => /* @__PURE__ */ jsxRuntime.jsx(SpacerBlock, { block: b }),
545
- video: (b) => /* @__PURE__ */ jsxRuntime.jsx(VideoBlock, { block: b })
658
+ video: (b) => /* @__PURE__ */ jsxRuntime.jsx(VideoBlock, { block: b }),
659
+ "product-list": (b) => /* @__PURE__ */ jsxRuntime.jsx(ProductListBlock, { block: b })
546
660
  };
547
661
  function CMSBlocks({ blocks, pageType, className = "", custom = {} }) {
548
662
  if (!blocks?.length) return null;