@reblu/site-blocks 0.19.0 → 0.21.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,34 @@
1
1
  # @reblu/site-blocks
2
2
 
3
+ ## 0.21.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 76e64f3: Free-preview lessons for the public, and a redesigned course programme (REB-1135).
8
+ - New RSC-safe `LessonPreview`: the hosted video player in an `<iframe>` plus the lesson body through the sanitising Markdown pipeline, over `getLessonPreview()` from `@reblu/site-next/academy-server`. Spanish copy overridable via `DEFAULT_LESSON_PREVIEW_LABELS`. The SITE's CSP needs `frame-src https://player.mux.com`.
9
+ - `CourseSyllabus` redesign: rotating chevron icon, a video/reading icon per lesson, lesson durations as «m:ss» / «h:mm:ss» aligned right, the course total in the summary, «Expandir todo / Contraer todo», and «Vista previa gratuita» as a call to action through the new `getPreviewHref`, `previewHrefTemplate` or `onPreview` props. Every existing `data-slot` is kept; new slots: `course-syllabus-header`, `course-syllabus-toggle-all`, `course-syllabus-chevron`, `course-syllabus-lesson-icon`, `course-syllabus-lesson-duration`, `course-syllabus-preview`. New labels: `expandAll`, `collapseAll`, `videoLesson`, `textLesson`. Visible change: lesson durations read «10:00» instead of «10 min».
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies [76e64f3]
14
+ - @reblu/site-contracts@1.21.0
15
+
16
+ ## 0.20.0
17
+
18
+ ### Minor Changes
19
+
20
+ - 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`).
21
+ - 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.
22
+ - `@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.
23
+ - `@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.
24
+ - `@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.
25
+
26
+ ### Patch Changes
27
+
28
+ - 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.
29
+ - Updated dependencies [25d8189]
30
+ - @reblu/site-contracts@1.20.0
31
+
3
32
  ## 0.19.0
4
33
 
5
34
  ### Minor Changes
package/README.md CHANGED
@@ -25,6 +25,7 @@ colour on the red background. See `docs/theming/theming.md`.
25
25
  | `sanitizeHtml` | `.` | Sanitises an HTML string before `dangerouslySetInnerHTML` — the **HTML boundary**. See "Security boundaries" below. |
26
26
  | `MarkdownRenderer` | `.` | Canonical Blog renderer: `marked` → **hast-util-sanitize** (XSS) → component map, with a YouTube-domain allowlist. RSC-safe. |
27
27
  | `CourseDescription` | `.` | Drop-in for the rich-text course `description` (`client.academy.bySlug()`). Server Component. See "Rich-text descriptions". |
28
+ | `LessonPreview`, `DEFAULT_LESSON_PREVIEW_LABELS` | `.` | A public free-preview lesson: hosted video player + sanitised body (`getLessonPreview()`). Server Component. See "Free-preview lesson". |
28
29
  | `Treatment`, `ParentPage`, `ProfileCard`, `ProductCard` | `.` | Presentational organisms over the SITE API contracts. RSC-safe. `Specialty`: deprecated alias, see below. |
29
30
  | `ProfessionalFields` | `.` | The tenant's own professional fields (`professional.fields`) on a team card or profile. RSC-safe. See below. |
30
31
  | `ShopFilters` | `.` | Plug-and-play shop filter chrome over `GET /api/site/shop/facets`. RSC-safe. See "Shop filters" below. |
@@ -39,6 +40,7 @@ colour on the red background. See `docs/theming/theming.md`.
39
40
  | `AddToCartButton`, `CartIcon` | `./client` | Add-to-cart feedback (REB-837) — **moved here from `.`**, see below. |
40
41
  | `DataRequestForm` | `./client` | Drop-in public GDPR rights form over `POST /api/site/data-requests` (REB-980). See "GDPR rights form" below. |
41
42
  | `ProductCarousel` | `./client` | Scroll-snap product strip with prev/next arrows («Productos relacionados»). See "Product carousel" below. |
43
+ | `CourseSyllabus`, `DEFAULT_COURSE_SYLLABUS_LABELS` | `./client` | A course's programme: expandable modules listing their lessons (`client.academy.bySlug()`). See "Course programme" below. |
42
44
 
43
45
  ## Boundary
44
46
 
@@ -324,13 +326,31 @@ for the wiring steps and a full example app).
324
326
  | `AddedToCartToast` | `.` | `productName`, `cartHref`, `status?`, `quantity?`, `imageUrl?` | Yes |
325
327
  | `CartLineList` | `.` | `items`, `onQuantityChange?`, `onRemove?`, `liveMessage?`, `notice?`, `lineMessage?` | Yes |
326
328
  | `CartAdjustmentsNotice` | `.` | `removed?`, `quantityChanges?`, `priceChanges?`, `labels?` | Yes |
327
- | `CartSummary` | `.` | `subtotal`, `discount?`, `shipping?`, `shippingEnabled?`, `total`, `notice?` | Yes |
329
+ | `CartSummary` | `.` | `subtotal`, `discount?`, `autoDiscount?`, `shipping?`, `shippingEnabled?`, `total`, `notice?` | Yes |
328
330
  | `CheckoutLoginPrompt` | `.` | `open`, `email`, `onEmailChange`, `onClose`, `onSubmitEmail`, `onContinueAsGuest` | Yes (renders `null` when `open` is false) |
329
- | `CheckoutForm` | `.` | `contact`, `onContactChange`, `discountCode`, `onDiscountCodeChange`, `onApplyDiscount`, `onRemoveDiscount`, `onSubmit` | Yes |
331
+ | `CheckoutForm` | `.` | `contact`, `onContactChange`, `discountCode`, `onDiscountCodeChange`, `onApplyDiscount`, `onRemoveDiscount`, `onSubmit`, `autoDiscount?` | Yes |
330
332
  | `OrderConfirmation` | `.` | `status`, `order`, `cartHref?`, `onRecheck?` | Yes |
331
333
  | `OrderInvoiceLink` | `.` | `invoice` (`order.invoice`), `labels?` | Yes (renders `null` without an invoice) |
332
334
  | `FullInvoiceFields` | `.` | `value`, `onChange`, `overCap`, `errors?`, `countries?`, `labels?` | Yes (controlled, no hooks) |
333
335
 
336
+ **Automatic staff discount (REB-1131).** For a signed-in staff member,
337
+ base-api applies the best «Solo personal» code by itself and reports it as
338
+ `autoDiscount` on validate-cart, apply-discount and checkout. Pass it to both
339
+ components and they show «Descuento de personal aplicado» with its amount
340
+ (override with `labels.staffDiscountApplied`). `CheckoutForm` offers no remove
341
+ action for it and keeps the code field: a typed code replaces it only when the
342
+ server finds it larger. Without `autoDiscount`, both render exactly as before.
343
+
344
+ ```tsx
345
+ const outcome = await validateCart() // @reblu/site-next/cart
346
+ // After an apply-discount call, prefer its `autoDiscount` (absent when the
347
+ // typed code won).
348
+ const autoDiscount = discountResponse?.autoDiscount ?? outcome.autoDiscount ?? null
349
+
350
+ <CartSummary subtotal={subtotal} autoDiscount={autoDiscount} total={subtotal - (autoDiscount?.amount ?? 0)} />
351
+ <CheckoutForm {...checkoutProps} autoDiscount={autoDiscount} />
352
+ ```
353
+
334
354
  **REB-837 breaking move**: `AddToCartButton` and `CartIcon` now carry real
335
355
  internal state (the confirmed/error timer, the bump-on-add guard) and ship
336
356
  from `@reblu/site-blocks/client` instead of the root `.` entry. Update the
@@ -887,6 +907,110 @@ It renders the package's `ProductCard` by default. To use your own card, pass
887
907
  `headingClassName` replaces the heading's typography (see the className rule
888
908
  above). It renders nothing for an empty list.
889
909
 
910
+ ## Course programme — `CourseSyllabus` (REB-1134, redesigned in REB-1135)
911
+
912
+ The programme of a public course page: every module, and inside each module the
913
+ lessons that make it up. The header reads «Programa» and «3 módulos · 12
914
+ lecciones · 2 h 40 min», with an «Expandir todo / Contraer todo» control. Each
915
+ module row shows a number badge, its lesson count and, when every lesson in it
916
+ has a duration, the module's total («4 lecciones · 1 h 20 min»), and a chevron
917
+ that rotates when open. Opening a module lists its lessons in order: a video or
918
+ reading icon, the title, the duration right-aligned as «m:ss» / «h:mm:ss», and on
919
+ free-preview lessons «Vista previa gratuita» as a call to action.
920
+
921
+ ```tsx
922
+ import { CourseSyllabus } from '@reblu/site-blocks/client'
923
+
924
+ // `course` is an `AcademyCourseDetail` from `client.academy.bySlug(slug)`.
925
+ // From a Server Component page, use the serialisable template:
926
+ <CourseSyllabus
927
+ modules={course.modules}
928
+ previewHrefTemplate={`/formacion/${course.slug}/preview/{lessonSlug}`}
929
+ />
930
+
931
+ // From a Client Component you can pass functions instead:
932
+ <CourseSyllabus
933
+ modules={course.modules}
934
+ getPreviewHref={(lesson) => `/formacion/${course.slug}/preview/${lesson.slug}`}
935
+ // or: onPreview={(lesson) => openPreviewModal(lesson.slug)}
936
+ />
937
+ ```
938
+
939
+ - **Free preview call to action.** `getPreviewHref(lesson)` (or the
940
+ serialisable `previewHrefTemplate`, with `{lessonSlug}` and `{lessonId}`)
941
+ renders a link; `onPreview(lesson)` renders a button (e.g. to open a modal).
942
+ Both apply only to lessons with `isFreePreview`. With neither, the flag is a
943
+ plain label. The link/button is named «Vista previa gratuita: {título}». Point
944
+ it at a page that renders `LessonPreview` (see "Free-preview lesson").
945
+ - **Public data only.** It renders what the API serves. The API returns
946
+ published lessons only, and never a video, playback or attachment id, a
947
+ signed URL or the lesson content.
948
+ - **Accessibility.** The block is a `region` named by its heading. A module
949
+ with lessons is a `<button>` with `aria-expanded` and `aria-controls`
950
+ pointing at its lesson list. Tab reaches «Expandir todo», then each module;
951
+ Enter and Space open and close it, and an open module's preview links follow
952
+ it in the tab order. Modules open independently and start closed. Icons are
953
+ `aria-hidden`; each lesson's type is announced as «Vídeo» / «Lectura».
954
+ Durations are `<time datetime="PT10M0S">`.
955
+ - **Modules without lessons** render as a plain row with the title, not a
956
+ button. A payload without `lessons` renders the same way.
957
+ - **Copy.** The defaults are Spanish (`DEFAULT_COURSE_SYLLABUS_LABELS`).
958
+ Override any of them with `labels={{ heading, moduleSingular, modulePlural,
959
+ lessonSingular, lessonPlural, freePreview, hours, minutes, expandAll,
960
+ collapseAll, videoLesson, textLesson }}`.
961
+ - **Styling.** Uses only the SDK tokens (`border`, `card`, `muted`, `primary`,
962
+ `ring`). `headingLevel` (default `2`), `headingClassName` (replaces the
963
+ heading's typography, see the className rule above) and `className`. Style
964
+ the parts with `[data-slot="course-syllabus"]`, `course-syllabus-header`,
965
+ `course-syllabus-summary`, `course-syllabus-toggle-all`,
966
+ `course-syllabus-module`, `course-syllabus-chevron`, `course-syllabus-lessons`,
967
+ `course-syllabus-lesson`, `course-syllabus-lesson-icon` (`data-type="video" |
968
+ "text"`), `course-syllabus-lesson-title`, `course-syllabus-lesson-duration`
969
+ and `course-syllabus-preview`. No slot was renamed in REB-1135. It renders
970
+ nothing for a course without modules.
971
+
972
+ ## Free-preview lesson — `LessonPreview` (REB-1135)
973
+
974
+ A lesson the owner marked «Vista previa gratuita», for visitors who have not
975
+ paid or enrolled: the video and the lesson body. Fetch it on the server with
976
+ `getLessonPreview` (`@reblu/site-next/academy-server`), which returns `null`
977
+ for any lesson the public may not see.
978
+
979
+ ```tsx
980
+ // app/formacion/[slug]/preview/[lessonSlug]/page.tsx — a Server Component
981
+ import { notFound } from 'next/navigation'
982
+ import { LessonPreview } from '@reblu/site-blocks'
983
+ import { getLessonPreview } from '@reblu/site-next/academy-server'
984
+ import { client } from '@/lib/reblu' // your server-side RebluClient
985
+
986
+ export const dynamic = 'force-dynamic' // the video token is short-lived: never ISR
987
+
988
+ export default async function Page({ params }) {
989
+ const { slug, lessonSlug } = await params
990
+ const lesson = await getLessonPreview(client, slug, lessonSlug)
991
+ if (!lesson) notFound()
992
+ return <LessonPreview lesson={lesson} headingLevel={1} />
993
+ }
994
+ ```
995
+
996
+ - **Player.** `video.embedUrl` is the provider's hosted player, already carrying
997
+ its short-lived token, rendered in an `<iframe>` named «Vídeo: {título}» —
998
+ no video library in the SDK. Only `https:` URLs are embedded. Add
999
+ `frame-src https://player.mux.com` to the SITE's Content-Security-Policy.
1000
+ `video` is `null` for a text-only lesson (no player is rendered).
1001
+ - **Body.** `content` is Markdown, rendered through the same sanitising
1002
+ pipeline as `CourseDescription`: no raw HTML, no scripts, no `javascript:`
1003
+ links. Its headings sit one level below the lesson title. An empty body
1004
+ renders nothing.
1005
+ - **Semantics.** An `<article>` named by the title; the duration is a
1006
+ `<time>`. Server Component (RSC-safe), from the root entry.
1007
+ - **Copy.** Spanish defaults (`DEFAULT_LESSON_PREVIEW_LABELS`), override with
1008
+ `labels={{ badge, player, duration }}`.
1009
+ - **Styling.** `headingLevel` (default `2`), `headingId`, `headingClassName`,
1010
+ `contentClassName`, `className`; slots `lesson-preview`,
1011
+ `lesson-preview-badge`, `lesson-preview-duration`, `lesson-preview-player`,
1012
+ `lesson-preview-content`.
1013
+
890
1014
  ## Page blocks
891
1015
 
892
1016
  `PageBlocks` renders a page's registry blocks (`PageDetail.blocks` from