@reblu/site-blocks 0.20.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 +13 -0
- package/README.md +84 -14
- package/dist/client.cjs +218 -83
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +40 -13
- package/dist/client.d.ts +40 -13
- package/dist/client.js +221 -86
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +421 -301
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +45 -2
- package/dist/index.d.ts +45 -2
- package/dist/index.js +419 -301
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
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
|
+
|
|
3
16
|
## 0.20.0
|
|
4
17
|
|
|
5
18
|
### 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. |
|
|
@@ -906,40 +907,109 @@ It renders the package's `ProductCard` by default. To use your own card, pass
|
|
|
906
907
|
`headingClassName` replaces the heading's typography (see the className rule
|
|
907
908
|
above). It renders nothing for an empty list.
|
|
908
909
|
|
|
909
|
-
## Course programme — `CourseSyllabus` (REB-1134)
|
|
910
|
+
## Course programme — `CourseSyllabus` (REB-1134, redesigned in REB-1135)
|
|
910
911
|
|
|
911
912
|
The programme of a public course page: every module, and inside each module the
|
|
912
913
|
lessons that make it up. The header reads «Programa» and «3 módulos · 12
|
|
913
|
-
lecciones
|
|
914
|
-
|
|
915
|
-
module
|
|
916
|
-
|
|
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.
|
|
917
920
|
|
|
918
921
|
```tsx
|
|
919
922
|
import { CourseSyllabus } from '@reblu/site-blocks/client'
|
|
920
923
|
|
|
921
924
|
// `course` is an `AcademyCourseDetail` from `client.academy.bySlug(slug)`.
|
|
922
|
-
//
|
|
923
|
-
<CourseSyllabus
|
|
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
|
+
/>
|
|
924
937
|
```
|
|
925
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").
|
|
926
945
|
- **Public data only.** It renders what the API serves. The API returns
|
|
927
946
|
published lessons only, and never a video, playback or attachment id, a
|
|
928
947
|
signed URL or the lesson content.
|
|
929
948
|
- **Accessibility.** The block is a `region` named by its heading. A module
|
|
930
949
|
with lessons is a `<button>` with `aria-expanded` and `aria-controls`
|
|
931
|
-
pointing at its lesson list. Tab reaches
|
|
932
|
-
|
|
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">`.
|
|
933
955
|
- **Modules without lessons** render as a plain row with the title, not a
|
|
934
956
|
button. A payload without `lessons` renders the same way.
|
|
935
957
|
- **Copy.** The defaults are Spanish (`DEFAULT_COURSE_SYLLABUS_LABELS`).
|
|
936
958
|
Override any of them with `labels={{ heading, moduleSingular, modulePlural,
|
|
937
|
-
lessonSingular, lessonPlural, freePreview, hours, minutes
|
|
938
|
-
|
|
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
|
|
939
963
|
heading's typography, see the className rule above) and `className`. Style
|
|
940
|
-
the parts with `[data-slot="course-syllabus"]`, `course-syllabus-
|
|
941
|
-
`course-syllabus-
|
|
942
|
-
|
|
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`.
|
|
943
1013
|
|
|
944
1014
|
## Page blocks
|
|
945
1015
|
|
package/dist/client.cjs
CHANGED
|
@@ -763,42 +763,104 @@ var DEFAULT_COURSE_SYLLABUS_LABELS = {
|
|
|
763
763
|
lessonPlural: "lecciones",
|
|
764
764
|
freePreview: "Vista previa gratuita",
|
|
765
765
|
hours: "h",
|
|
766
|
-
minutes: "min"
|
|
766
|
+
minutes: "min",
|
|
767
|
+
expandAll: "Expandir todo",
|
|
768
|
+
collapseAll: "Contraer todo",
|
|
769
|
+
videoLesson: "V\xEDdeo",
|
|
770
|
+
textLesson: "Lectura"
|
|
767
771
|
};
|
|
768
772
|
var SEPARATOR = " \xB7 ";
|
|
769
773
|
function count(n, singular, plural) {
|
|
770
774
|
return `${n} ${n === 1 ? singular : plural}`;
|
|
771
775
|
}
|
|
772
|
-
function
|
|
776
|
+
function formatTotal(seconds, labels) {
|
|
773
777
|
const totalMinutes = Math.max(1, Math.round(seconds / 60));
|
|
774
778
|
const hours = Math.floor(totalMinutes / 60);
|
|
775
779
|
const minutes = totalMinutes % 60;
|
|
776
780
|
if (hours === 0) return `${minutes} ${labels.minutes}`;
|
|
777
781
|
return minutes === 0 ? `${hours} ${labels.hours}` : `${hours} ${labels.hours} ${minutes} ${labels.minutes}`;
|
|
778
782
|
}
|
|
779
|
-
function
|
|
783
|
+
function splitSeconds(seconds) {
|
|
784
|
+
const whole = Math.round(seconds);
|
|
785
|
+
return [Math.floor(whole / 3600), Math.floor(whole % 3600 / 60), whole % 60];
|
|
786
|
+
}
|
|
787
|
+
function formatClock(seconds) {
|
|
788
|
+
const [h, m, s] = splitSeconds(seconds);
|
|
789
|
+
const ss = String(s).padStart(2, "0");
|
|
790
|
+
return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${ss}` : `${m}:${ss}`;
|
|
791
|
+
}
|
|
792
|
+
function isoDuration(seconds) {
|
|
793
|
+
const [h, m, s] = splitSeconds(seconds);
|
|
794
|
+
return `PT${h > 0 ? `${h}H` : ""}${m}M${s}S`;
|
|
795
|
+
}
|
|
796
|
+
function hasDuration(lesson) {
|
|
797
|
+
return typeof lesson.durationSeconds === "number" && lesson.durationSeconds > 0;
|
|
798
|
+
}
|
|
799
|
+
function totalSeconds(lessons) {
|
|
800
|
+
if (lessons.length === 0) return null;
|
|
780
801
|
let total = 0;
|
|
781
802
|
for (const l of lessons) {
|
|
782
|
-
if (
|
|
803
|
+
if (!hasDuration(l)) return null;
|
|
783
804
|
total += l.durationSeconds;
|
|
784
805
|
}
|
|
785
806
|
return total;
|
|
786
807
|
}
|
|
808
|
+
function fillTemplate(template, lesson) {
|
|
809
|
+
return template.replaceAll("{lessonSlug}", encodeURIComponent(lesson.slug)).replaceAll("{lessonId}", encodeURIComponent(lesson.id));
|
|
810
|
+
}
|
|
811
|
+
var ICON_PROPS = {
|
|
812
|
+
"aria-hidden": true,
|
|
813
|
+
viewBox: "0 0 24 24",
|
|
814
|
+
fill: "none",
|
|
815
|
+
stroke: "currentColor",
|
|
816
|
+
strokeWidth: 2,
|
|
817
|
+
strokeLinecap: "round",
|
|
818
|
+
strokeLinejoin: "round"
|
|
819
|
+
};
|
|
820
|
+
function LessonIcon({ type }) {
|
|
821
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
822
|
+
"svg",
|
|
823
|
+
{
|
|
824
|
+
...ICON_PROPS,
|
|
825
|
+
"data-slot": "course-syllabus-lesson-icon",
|
|
826
|
+
"data-type": type,
|
|
827
|
+
className: "size-4 shrink-0 text-muted-foreground",
|
|
828
|
+
children: type === "video" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
|
|
829
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("circle", { cx: "12", cy: "12", r: "9" }),
|
|
830
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "m10 8.5 5 3.5-5 3.5z" })
|
|
831
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
|
|
832
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z" }),
|
|
833
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M14 3v5h5M9 13h6M9 17h4" })
|
|
834
|
+
] })
|
|
835
|
+
}
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
function PlayIcon() {
|
|
839
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("svg", { ...ICON_PROPS, className: "size-3.5 shrink-0", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "m7 4 13 8-13 8z", fill: "currentColor" }) });
|
|
840
|
+
}
|
|
841
|
+
var PREVIEW_CTA = "inline-flex items-center gap-1.5 rounded-full border border-primary/40 bg-primary/10 px-3 py-1 text-xs font-semibold text-primary transition-colors hover:bg-primary hover:text-primary-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring motion-reduce:transition-none";
|
|
787
842
|
function CourseSyllabus({
|
|
788
843
|
modules,
|
|
789
844
|
labels: labelOverrides,
|
|
790
845
|
headingLevel = 2,
|
|
791
846
|
headingClassName,
|
|
792
|
-
className
|
|
847
|
+
className,
|
|
848
|
+
getPreviewHref,
|
|
849
|
+
previewHrefTemplate,
|
|
850
|
+
onPreview
|
|
793
851
|
}) {
|
|
794
852
|
const labels = { ...DEFAULT_COURSE_SYLLABUS_LABELS, ...labelOverrides };
|
|
795
853
|
const baseId = (0, import_react5.useId)();
|
|
796
854
|
const [open, setOpen] = (0, import_react5.useState)(() => /* @__PURE__ */ new Set());
|
|
797
855
|
if (modules.length === 0) return null;
|
|
798
|
-
const
|
|
856
|
+
const expandable = modules.filter((m) => (m.lessons?.length ?? 0) > 0).map((m) => m.id);
|
|
857
|
+
const allLessons = modules.flatMap((m) => m.lessons ?? []);
|
|
858
|
+
const courseSeconds = totalSeconds(allLessons);
|
|
799
859
|
const summary = [count(modules.length, labels.moduleSingular, labels.modulePlural)];
|
|
800
|
-
if (
|
|
801
|
-
summary.push(count(
|
|
860
|
+
if (allLessons.length > 0)
|
|
861
|
+
summary.push(count(allLessons.length, labels.lessonSingular, labels.lessonPlural));
|
|
862
|
+
if (courseSeconds !== null) summary.push(formatTotal(courseSeconds, labels));
|
|
863
|
+
const allOpen = expandable.length > 0 && expandable.every((id) => open.has(id));
|
|
802
864
|
const headingId = `${baseId}-heading`;
|
|
803
865
|
const Tag = `h${headingLevel}`;
|
|
804
866
|
function toggle(id) {
|
|
@@ -809,6 +871,44 @@ function CourseSyllabus({
|
|
|
809
871
|
return next;
|
|
810
872
|
});
|
|
811
873
|
}
|
|
874
|
+
function previewHref(lesson) {
|
|
875
|
+
if (getPreviewHref) return getPreviewHref(lesson);
|
|
876
|
+
return previewHrefTemplate ? fillTemplate(previewHrefTemplate, lesson) : void 0;
|
|
877
|
+
}
|
|
878
|
+
function renderPreview(lesson) {
|
|
879
|
+
const name = `${labels.freePreview}: ${lesson.title}`;
|
|
880
|
+
const href = previewHref(lesson);
|
|
881
|
+
if (href) {
|
|
882
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("a", { href, "aria-label": name, "data-slot": "course-syllabus-preview", className: PREVIEW_CTA, children: [
|
|
883
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PlayIcon, {}),
|
|
884
|
+
labels.freePreview
|
|
885
|
+
] });
|
|
886
|
+
}
|
|
887
|
+
if (onPreview) {
|
|
888
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
889
|
+
"button",
|
|
890
|
+
{
|
|
891
|
+
type: "button",
|
|
892
|
+
"aria-label": name,
|
|
893
|
+
"data-slot": "course-syllabus-preview",
|
|
894
|
+
onClick: () => onPreview(lesson),
|
|
895
|
+
className: (0, import_utils8.cn)(PREVIEW_CTA, "cursor-pointer"),
|
|
896
|
+
children: [
|
|
897
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PlayIcon, {}),
|
|
898
|
+
labels.freePreview
|
|
899
|
+
]
|
|
900
|
+
}
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
904
|
+
"span",
|
|
905
|
+
{
|
|
906
|
+
"data-slot": "course-syllabus-preview",
|
|
907
|
+
className: "rounded-full bg-muted px-2.5 py-0.5 text-xs font-medium text-muted-foreground",
|
|
908
|
+
children: labels.freePreview
|
|
909
|
+
}
|
|
910
|
+
);
|
|
911
|
+
}
|
|
812
912
|
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
813
913
|
"section",
|
|
814
914
|
{
|
|
@@ -816,24 +916,43 @@ function CourseSyllabus({
|
|
|
816
916
|
"aria-labelledby": headingId,
|
|
817
917
|
className: (0, import_utils8.cn)("space-y-4", className),
|
|
818
918
|
children: [
|
|
819
|
-
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
children:
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
919
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
920
|
+
"div",
|
|
921
|
+
{
|
|
922
|
+
"data-slot": "course-syllabus-header",
|
|
923
|
+
className: "flex flex-wrap items-end justify-between gap-x-4 gap-y-2",
|
|
924
|
+
children: [
|
|
925
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "space-y-1", children: [
|
|
926
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
927
|
+
Tag,
|
|
928
|
+
{
|
|
929
|
+
id: headingId,
|
|
930
|
+
className: slotClassName("", "text-xl font-semibold", headingClassName),
|
|
931
|
+
children: labels.heading
|
|
932
|
+
}
|
|
933
|
+
),
|
|
934
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { "data-slot": "course-syllabus-summary", className: "text-sm text-muted-foreground", children: summary.join(SEPARATOR) })
|
|
935
|
+
] }),
|
|
936
|
+
expandable.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
937
|
+
"button",
|
|
938
|
+
{
|
|
939
|
+
type: "button",
|
|
940
|
+
"data-slot": "course-syllabus-toggle-all",
|
|
941
|
+
onClick: () => setOpen(allOpen ? /* @__PURE__ */ new Set() : new Set(expandable)),
|
|
942
|
+
className: "cursor-pointer rounded-md px-2 py-1 text-sm font-semibold text-primary underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
|
|
943
|
+
children: allOpen ? labels.collapseAll : labels.expandAll
|
|
944
|
+
}
|
|
945
|
+
)
|
|
946
|
+
]
|
|
947
|
+
}
|
|
948
|
+
),
|
|
949
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("ol", { className: "list-none divide-y divide-border overflow-hidden rounded-xl border border-border bg-card p-0", children: modules.map((module2, index) => {
|
|
831
950
|
const lessons = module2.lessons ?? [];
|
|
832
951
|
const number = /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
833
952
|
"span",
|
|
834
953
|
{
|
|
835
954
|
"aria-hidden": "true",
|
|
836
|
-
className: "flex size-
|
|
955
|
+
className: "flex size-8 shrink-0 items-center justify-center rounded-full bg-primary/10 text-sm font-semibold text-primary tabular-nums",
|
|
837
956
|
children: index + 1
|
|
838
957
|
}
|
|
839
958
|
);
|
|
@@ -842,7 +961,7 @@ function CourseSyllabus({
|
|
|
842
961
|
"li",
|
|
843
962
|
{
|
|
844
963
|
"data-slot": "course-syllabus-module",
|
|
845
|
-
className: "flex items-center gap-
|
|
964
|
+
className: "flex items-center gap-4 px-4 py-4 sm:px-5",
|
|
846
965
|
children: [
|
|
847
966
|
number,
|
|
848
967
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "font-semibold leading-snug", children: module2.title })
|
|
@@ -853,69 +972,85 @@ function CourseSyllabus({
|
|
|
853
972
|
}
|
|
854
973
|
const isOpen = open.has(module2.id);
|
|
855
974
|
const panelId = `${baseId}-module-${index}`;
|
|
856
|
-
const seconds =
|
|
975
|
+
const seconds = totalSeconds(lessons);
|
|
857
976
|
const meta = [count(lessons.length, labels.lessonSingular, labels.lessonPlural)];
|
|
858
|
-
if (seconds !== null) meta.push(
|
|
859
|
-
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
-
}
|
|
977
|
+
if (seconds !== null) meta.push(formatTotal(seconds, labels));
|
|
978
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("li", { "data-slot": "course-syllabus-module", children: [
|
|
979
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
980
|
+
"button",
|
|
981
|
+
{
|
|
982
|
+
type: "button",
|
|
983
|
+
"aria-expanded": isOpen,
|
|
984
|
+
"aria-controls": panelId,
|
|
985
|
+
onClick: () => toggle(module2.id),
|
|
986
|
+
className: (0, import_utils8.cn)(
|
|
987
|
+
"flex w-full cursor-pointer items-center gap-4 px-4 py-4 text-left transition-colors hover:bg-muted/60 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring motion-reduce:transition-none sm:px-5",
|
|
988
|
+
isOpen && "bg-muted/40"
|
|
892
989
|
),
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
{
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
990
|
+
children: [
|
|
991
|
+
number,
|
|
992
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [
|
|
993
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "font-semibold leading-snug", children: module2.title }),
|
|
994
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "text-sm text-muted-foreground", children: meta.join(SEPARATOR) })
|
|
995
|
+
] }),
|
|
996
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
997
|
+
"svg",
|
|
998
|
+
{
|
|
999
|
+
...ICON_PROPS,
|
|
1000
|
+
"data-slot": "course-syllabus-chevron",
|
|
1001
|
+
className: (0, import_utils8.cn)(
|
|
1002
|
+
"size-5 shrink-0 text-muted-foreground transition-transform duration-200 motion-reduce:transition-none",
|
|
1003
|
+
isOpen && "rotate-180"
|
|
1004
|
+
),
|
|
1005
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "m6 9 6 6 6-6" })
|
|
1006
|
+
}
|
|
1007
|
+
)
|
|
1008
|
+
]
|
|
1009
|
+
}
|
|
1010
|
+
),
|
|
1011
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
1012
|
+
"ol",
|
|
1013
|
+
{
|
|
1014
|
+
id: panelId,
|
|
1015
|
+
hidden: !isOpen,
|
|
1016
|
+
"data-slot": "course-syllabus-lessons",
|
|
1017
|
+
className: "list-none border-t border-border p-0 py-1",
|
|
1018
|
+
children: lessons.map((lesson) => {
|
|
1019
|
+
const type = lesson.hasVideo ? "video" : "text";
|
|
1020
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
|
|
1021
|
+
"li",
|
|
1022
|
+
{
|
|
1023
|
+
"data-slot": "course-syllabus-lesson",
|
|
1024
|
+
className: "flex flex-wrap items-center gap-x-3 gap-y-1.5 px-4 py-2.5 text-sm sm:pl-[4.25rem] sm:pr-5",
|
|
1025
|
+
children: [
|
|
1026
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(LessonIcon, { type }),
|
|
1027
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "sr-only", children: type === "video" ? labels.videoLesson : labels.textLesson }),
|
|
1028
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
1029
|
+
"span",
|
|
1030
|
+
{
|
|
1031
|
+
"data-slot": "course-syllabus-lesson-title",
|
|
1032
|
+
className: "min-w-0 flex-1 leading-snug",
|
|
1033
|
+
children: lesson.title
|
|
1034
|
+
}
|
|
1035
|
+
),
|
|
1036
|
+
lesson.isFreePreview && renderPreview(lesson),
|
|
1037
|
+
hasDuration(lesson) && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
1038
|
+
"time",
|
|
1039
|
+
{
|
|
1040
|
+
"data-slot": "course-syllabus-lesson-duration",
|
|
1041
|
+
dateTime: isoDuration(lesson.durationSeconds),
|
|
1042
|
+
className: "ml-auto shrink-0 text-muted-foreground tabular-nums",
|
|
1043
|
+
children: formatClock(lesson.durationSeconds)
|
|
1044
|
+
}
|
|
1045
|
+
)
|
|
1046
|
+
]
|
|
1047
|
+
},
|
|
1048
|
+
lesson.id
|
|
1049
|
+
);
|
|
1050
|
+
})
|
|
1051
|
+
}
|
|
1052
|
+
)
|
|
1053
|
+
] }, module2.id);
|
|
919
1054
|
}) })
|
|
920
1055
|
]
|
|
921
1056
|
}
|