From bce38aa53092664793eecf259a342b0b8c8f91ed Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Mon, 6 Jul 2026 20:51:17 +0100 Subject: [PATCH] fix(dashboard): show course cards in landing page settings preview (#697) * fix(dashboard): show course cards in landing page settings preview Fetch public courses for the org and pass them to the theme preview on the landing page settings screen, matching the editor preview. Co-authored-by: Rotimi Best * refactor(dashboard): skip redundant public course fetches Add loadPublicCoursesIfNeeded so settings, editor, and live landing page reuse cached courses for the same org site instead of clearing and refetching on every mount. Co-authored-by: Rotimi Best * fix(dashboard): always refetch courses on live org landing page Keep loadPublicCoursesIfNeeded for settings preview and editor only. The public-facing landing page should fetch fresh on each visit. Co-authored-by: Rotimi Best * feat(ui): add shared empty state for org landing page catalogs Introduce OrgLandingPageCoursesEmpty using the dashboard Empty pattern with landing-theme colors. Show it across all ten org landing templates when no courses are published, and wire translated labels through buildOrgLandingPageProps. Co-authored-by: Rotimi Best * fix(dashboard): address Greptile review on public courses cache - Add coursesLoaded prop so empty catalog state waits for fetch - Deduplicate concurrent public course requests via in-flight promise - Refresh courses on settings mount; reuse cache in editor only - Expose invalidatePublicCourses for explicit cache clearing Co-authored-by: Rotimi Best * fix(dashboard): load landing preview courses when org store hydrates Replace onMount course fetch with a reactive effect keyed on the route slug and currentOrg.siteName so public courses load after app init. Mark the fetch as complete in finally so failed requests show the empty state instead of a permanently blank catalog section. Co-authored-by: Rotimi Best --------- --- .../src/lib/features/org/api/org.svelte.ts | 65 ++++- .../landing-page/landing-page.svelte | 3 +- .../lib/features/org/utils/landing-page.ts | 15 +- .../settings/pages/landingpage.svelte | 17 +- .../src/lib/utils/translations/da.json | 4 + .../src/lib/utils/translations/de.json | 4 + .../src/lib/utils/translations/en.json | 4 + .../src/lib/utils/translations/es.json | 4 + .../src/lib/utils/translations/fr.json | 4 + .../src/lib/utils/translations/hi.json | 4 + .../src/lib/utils/translations/pl.json | 4 + .../src/lib/utils/translations/pt.json | 4 + .../src/lib/utils/translations/ru.json | 4 + .../src/lib/utils/translations/vi.json | 4 + .../settings/landingpage/edit/+page.svelte | 21 +- .../custom/org-landing-page/bold/org.svelte | 38 +-- .../org-landing-page/classic/org.svelte | 40 +-- .../org-landing-page/corporate/org.svelte | 56 ++-- .../org-landing-page/courses-empty.svelte | 28 ++ .../org-landing-page/editorial/org.svelte | 68 ++--- .../ui/src/custom/org-landing-page/index.ts | 1 + .../org-landing-page/minimal/org.svelte | 38 +-- .../custom/org-landing-page/saas/org.svelte | 64 ++--- .../custom/org-landing-page/studio/org.svelte | 60 +++-- .../custom/org-landing-page/tech/org.svelte | 56 ++-- .../org-landing-page/terminal/org.svelte | 60 +++-- .../ui/src/custom/org-landing-page/types.ts | 6 + .../org-landing-page/vibrant/org.svelte | 254 +++++++++--------- 28 files changed, 571 insertions(+), 359 deletions(-) create mode 100644 packages/ui/src/custom/org-landing-page/courses-empty.svelte diff --git a/apps/dashboard/src/lib/features/org/api/org.svelte.ts b/apps/dashboard/src/lib/features/org/api/org.svelte.ts index 2a58287c..be9ebe2d 100644 --- a/apps/dashboard/src/lib/features/org/api/org.svelte.ts +++ b/apps/dashboard/src/lib/features/org/api/org.svelte.ts @@ -70,8 +70,11 @@ class OrgApi extends BaseApiWithErrors { audiencePagination = $state(null); publicCourses: OrgPublicCourses = $state([]); hasMorePublicCourses = $state(false); + publicCoursesLoadedSiteName: string | null = $state(null); isFetchingOrgPublicCourses = $state(false); + private activePublicCoursesFetch: Promise | null = null; + private activePublicCoursesFetchSiteName: string | null = null; private activeAudienceRequestController: AbortController | null = null; cancelAudienceRequest() { @@ -155,15 +158,52 @@ class OrgApi extends BaseApiWithErrors { return response; } + invalidatePublicCourses() { + this.publicCoursesLoadedSiteName = null; + } + + /** + * Refetches public courses for a site, clearing any cached settings preview data. + */ + async refreshPublicCourses(siteName: string) { + if (!siteName) { + return; + } + + this.invalidatePublicCourses(); + await this.fetchPublicCoursesBySiteName(siteName); + } + + /** + * Loads public courses for a site when they have not been fetched yet. + * Skips the request when courses are already in memory for the same site. + */ + async loadPublicCoursesIfNeeded(siteName: string) { + if (!siteName || this.publicCoursesLoadedSiteName === siteName) { + return; + } + + await this.fetchPublicCoursesBySiteName(siteName); + } + /** * Gets public courses by organization siteName (for landing pages) * @param siteName Organization site name * @returns Published courses array */ async getPublicCoursesBySiteName(siteName: string) { + this.invalidatePublicCourses(); + await this.fetchPublicCoursesBySiteName(siteName); + } + + private fetchPublicCoursesBySiteName(siteName: string): Promise { + if (this.activePublicCoursesFetch && this.activePublicCoursesFetchSiteName === siteName) { + return this.activePublicCoursesFetch; + } + this.isFetchingOrgPublicCourses = true; - await this.execute({ + const fetchPromise = this.execute({ requestFn: () => classroomio.organization.courses.public.$get({ query: { siteName } @@ -172,10 +212,29 @@ class OrgApi extends BaseApiWithErrors { onSuccess: (response) => { this.publicCourses = response.data.courses; this.hasMorePublicCourses = response.data.hasMoreCourses; + this.publicCoursesLoadedSiteName = siteName; } - }); + }) + .then(() => undefined) + .finally(() => { + this.isFetchingOrgPublicCourses = false; - this.isFetchingOrgPublicCourses = false; + if (this.publicCoursesLoadedSiteName !== siteName) { + this.publicCourses = []; + this.hasMorePublicCourses = false; + this.publicCoursesLoadedSiteName = siteName; + } + + if (this.activePublicCoursesFetchSiteName === siteName) { + this.activePublicCoursesFetch = null; + this.activePublicCoursesFetchSiteName = null; + } + }); + + this.activePublicCoursesFetch = fetchPromise; + this.activePublicCoursesFetchSiteName = siteName; + + return fetchPromise; } /** diff --git a/apps/dashboard/src/lib/features/org/components/landing-page/landing-page.svelte b/apps/dashboard/src/lib/features/org/components/landing-page/landing-page.svelte index 5b0ade71..bca0b339 100644 --- a/apps/dashboard/src/lib/features/org/components/landing-page/landing-page.svelte +++ b/apps/dashboard/src/lib/features/org/components/landing-page/landing-page.svelte @@ -49,7 +49,8 @@ normalizeLandingPageSettings(org.landingpage), orgApi.publicCourses, orgApi.hasMorePublicCourses, - authAction + authAction, + { coursesLoaded: hasLoadedCourses } ) ); diff --git a/apps/dashboard/src/lib/features/org/utils/landing-page.ts b/apps/dashboard/src/lib/features/org/utils/landing-page.ts index 7576f1fb..06866f30 100644 --- a/apps/dashboard/src/lib/features/org/utils/landing-page.ts +++ b/apps/dashboard/src/lib/features/org/utils/landing-page.ts @@ -18,6 +18,7 @@ import { labelMatchesSocialPlatform, resolveFooterSocialPlatform } from '@cio/ui/custom/org-landing-page/footer-social-platform'; +import { t } from '$lib/utils/functions/translations'; export const landingPageThemes = [ 'minimal', @@ -689,12 +690,20 @@ export function mapPublicCoursesToLandingPageCourses(courses: OrgPublicCourses): }); } +export function buildOrgLandingPageLabels(): OrgLandingPageProps['labels'] { + return { + catalogEmptyTitle: t.get('public_courses.catalog_empty.title'), + catalogEmptyDescription: t.get('public_courses.catalog_empty.description') + }; +} + export function buildOrgLandingPageProps( org: AccountOrg, landingpage: unknown, courses: OrgPublicCourses, hasMoreCourses = false, - authAction?: OrgLandingPageProps['authAction'] + authAction?: OrgLandingPageProps['authAction'], + options?: { coursesLoaded?: boolean } ): OrgLandingPageProps { const normalizedLandingPage = normalizeLandingPageSettings(landingpage); @@ -704,7 +713,9 @@ export function buildOrgLandingPageProps( authAction, ...normalizedLandingPage, courses: mapPublicCoursesToLandingPageCourses(courses), - hasMoreCourses + hasMoreCourses, + coursesLoaded: options?.coursesLoaded ?? true, + labels: buildOrgLandingPageLabels() }; } diff --git a/apps/dashboard/src/lib/features/settings/pages/landingpage.svelte b/apps/dashboard/src/lib/features/settings/pages/landingpage.svelte index 4b844783..3cc4c4c7 100644 --- a/apps/dashboard/src/lib/features/settings/pages/landingpage.svelte +++ b/apps/dashboard/src/lib/features/settings/pages/landingpage.svelte @@ -1,5 +1,6 @@ + +
+ +
diff --git a/packages/ui/src/custom/org-landing-page/editorial/org.svelte b/packages/ui/src/custom/org-landing-page/editorial/org.svelte index efe69f77..96dd8ccd 100644 --- a/packages/ui/src/custom/org-landing-page/editorial/org.svelte +++ b/packages/ui/src/custom/org-landing-page/editorial/org.svelte @@ -7,6 +7,7 @@ import EditorialNav from './nav.svelte'; import EditorialHero from './hero.svelte'; import EditorialCourseCard from './course-card.svelte'; + import OrgLandingPageCoursesEmpty from '../courses-empty.svelte'; import { themeStyle } from '../theme-style'; import { Button } from '../../../base/button'; import { getCourseTypeLandingMeta } from '../landing-page-utils'; @@ -19,6 +20,7 @@ hero, courses, hasMoreCourses = false, + coursesLoaded = true, disableCourseLinks = false, embed, callout, @@ -96,37 +98,39 @@ {/snippet} - {#if courses.length > 0} -
-
-
-
-

- {labels?.catalogEyebrow ?? 'Catalog'} -

-

- {labels?.catalogHeading ?? 'Courses starting this season.'} -

-

- {labels?.catalogDescription ?? - 'Cohort-based and self-paced — every course ends with a reviewed project and a certificate of completion.'} -

-
- {#if hasMoreCourses} - - {/if} +
+
+
+
+

+ {labels?.catalogEyebrow ?? 'Catalog'} +

+

+ {labels?.catalogHeading ?? 'Courses starting this season.'} +

+

+ {labels?.catalogDescription ?? + 'Cohort-based and self-paced — every course ends with a reviewed project and a certificate of completion.'} +

+ {#if hasMoreCourses && courses.length > 0} + + {/if} +
+ {#if coursesLoaded && courses.length === 0} + + {:else} {#if visibleFilterDefs.length > 1}
{#each visibleFilterDefs as def (def.key)} @@ -169,9 +173,9 @@
{/if} -
-
- {/if} + {/if} +
+
diff --git a/packages/ui/src/custom/org-landing-page/index.ts b/packages/ui/src/custom/org-landing-page/index.ts index dfabbbc7..be653e00 100644 --- a/packages/ui/src/custom/org-landing-page/index.ts +++ b/packages/ui/src/custom/org-landing-page/index.ts @@ -69,6 +69,7 @@ export { courseCard as VibrantLandingCourseCard } from './vibrant'; +export { default as OrgLandingPageCoursesEmpty } from './courses-empty.svelte'; export { default as LandingButton } from './landing-button.svelte'; export { default as OrgLandingPageEmbed } from './embed.svelte'; export { default as OrgLandingPageCallout } from './callout.svelte'; diff --git a/packages/ui/src/custom/org-landing-page/minimal/org.svelte b/packages/ui/src/custom/org-landing-page/minimal/org.svelte index d026312a..4c4f2298 100644 --- a/packages/ui/src/custom/org-landing-page/minimal/org.svelte +++ b/packages/ui/src/custom/org-landing-page/minimal/org.svelte @@ -7,6 +7,7 @@ import MinimalNav from './nav.svelte'; import MinimalHero from './hero.svelte'; import MinimalCourseCard from './course-card.svelte'; + import OrgLandingPageCoursesEmpty from '../courses-empty.svelte'; import { Button } from '../../../base/button'; import { themeStyle } from '../theme-style'; @@ -18,6 +19,7 @@ hero, courses, hasMoreCourses = false, + coursesLoaded = true, disableCourseLinks = false, embed, callout, @@ -41,23 +43,27 @@

{labels?.catalogHeading ?? 'Our Courses'}

-
- {#each courses as course, index (course.id)} - - {/each} -
- - {#if hasMoreCourses} -
- + {#if coursesLoaded && courses.length === 0} + + {:else} +
+ {#each courses as course, index (course.id)} + + {/each}
+ + {#if hasMoreCourses} +
+ +
+ {/if} {/if}
diff --git a/packages/ui/src/custom/org-landing-page/saas/org.svelte b/packages/ui/src/custom/org-landing-page/saas/org.svelte index 84944599..9f59293c 100644 --- a/packages/ui/src/custom/org-landing-page/saas/org.svelte +++ b/packages/ui/src/custom/org-landing-page/saas/org.svelte @@ -7,6 +7,7 @@ import SaasNav from './nav.svelte'; import SaasHero from './hero.svelte'; import SaasCourseCard from './course-card.svelte'; + import OrgLandingPageCoursesEmpty from '../courses-empty.svelte'; import { Button } from '../../../base/button'; import { themeStyle } from '../theme-style'; @@ -18,6 +19,7 @@ hero, courses, hasMoreCourses = false, + coursesLoaded = true, disableCourseLinks = false, embed, callout, @@ -38,44 +40,46 @@ {/snippet} - {#if courses.length > 0} -
+
-
-
-
-
-

- {labels?.catalogEyebrow ?? 'Catalog'} -

-

- {labels?.catalogHeading ?? 'Featured courses.'} -

-
- {#if hasMoreCourses} - - {/if} +
+
+
+
+

+ {labels?.catalogEyebrow ?? 'Catalog'} +

+

+ {labels?.catalogHeading ?? 'Featured courses.'} +

+ {#if hasMoreCourses && courses.length > 0} + + {/if} +
+ {#if coursesLoaded && courses.length === 0} + + {:else}
{#each courses as course, index (course.id)} {/each}
-
-
- {/if} + {/if} +
+
{#if links && links.cards.length > 0}
diff --git a/packages/ui/src/custom/org-landing-page/studio/org.svelte b/packages/ui/src/custom/org-landing-page/studio/org.svelte index 4db903da..da1483fe 100644 --- a/packages/ui/src/custom/org-landing-page/studio/org.svelte +++ b/packages/ui/src/custom/org-landing-page/studio/org.svelte @@ -7,6 +7,7 @@ import StudioNav from './nav.svelte'; import StudioHero from './hero.svelte'; import StudioCourseCard from './course-card.svelte'; + import OrgLandingPageCoursesEmpty from '../courses-empty.svelte'; import { Button } from '../../../base/button'; import { themeStyle } from '../theme-style'; @@ -18,6 +19,7 @@ hero, courses, hasMoreCourses = false, + coursesLoaded = true, disableCourseLinks = false, embed, callout, @@ -38,41 +40,43 @@ {/snippet} - {#if courses.length > 0} -
-
-
-
-

- - {labels?.catalogEyebrow ?? 'Catalog'} -

-

- {labels?.catalogHeading ?? 'Courses your team can actually finish.'} -

-
- {#if hasMoreCourses} -
- -
- {/if} +
+
+
+
+

+ + {labels?.catalogEyebrow ?? 'Catalog'} +

+

+ {labels?.catalogHeading ?? 'Courses your team can actually finish.'} +

+ {#if hasMoreCourses && courses.length > 0} +
+ +
+ {/if} +
+ {#if coursesLoaded && courses.length === 0} + + {:else}
{#each courses as course, index (course.id)} {/each}
-
-
- {/if} + {/if} +
+
diff --git a/packages/ui/src/custom/org-landing-page/tech/org.svelte b/packages/ui/src/custom/org-landing-page/tech/org.svelte index f431e320..97131e02 100644 --- a/packages/ui/src/custom/org-landing-page/tech/org.svelte +++ b/packages/ui/src/custom/org-landing-page/tech/org.svelte @@ -7,6 +7,7 @@ import TechNav from './nav.svelte'; import TechHero from './hero.svelte'; import TechCourseCard from './course-card.svelte'; + import OrgLandingPageCoursesEmpty from '../courses-empty.svelte'; import { Button } from '../../../base/button'; import { themeStyle } from '../theme-style'; @@ -18,6 +19,7 @@ hero, courses, hasMoreCourses = false, + coursesLoaded = true, disableCourseLinks = false, embed, callout, @@ -38,32 +40,32 @@ {/snippet} - {#if courses.length > 0} -
-
-
-
-

- {labels?.catalogEyebrow ?? '// catalog'} -

-

- {labels?.catalogHeading ?? 'Latest courses'} -

-
- {#if hasMoreCourses} - - {/if} +
+
+
+
+

+ {labels?.catalogEyebrow ?? '// catalog'} +

+

+ {labels?.catalogHeading ?? 'Latest courses'} +

+ {#if hasMoreCourses && courses.length > 0} + + {/if} +
+ {#if coursesLoaded && courses.length === 0} + + {:else}
@@ -71,9 +73,9 @@ {/each}
-
-
- {/if} + {/if} +
+
diff --git a/packages/ui/src/custom/org-landing-page/terminal/org.svelte b/packages/ui/src/custom/org-landing-page/terminal/org.svelte index 05361395..fab0eb04 100644 --- a/packages/ui/src/custom/org-landing-page/terminal/org.svelte +++ b/packages/ui/src/custom/org-landing-page/terminal/org.svelte @@ -7,6 +7,7 @@ import TerminalNav from './nav.svelte'; import TerminalHero from './hero.svelte'; import TerminalCourseCard from './course-card.svelte'; + import OrgLandingPageCoursesEmpty from '../courses-empty.svelte'; import { Button } from '../../../base/button'; import { themeStyle } from '../theme-style'; import { getCourseTypeLandingMeta } from '../landing-page-utils'; @@ -19,6 +20,7 @@ hero, courses, hasMoreCourses = false, + coursesLoaded = true, disableCourseLinks = false, embed, callout, @@ -81,34 +83,36 @@ {/snippet} - {#if courses.length > 0} -
-
-

- - {labels?.catalogEyebrow ?? 'Catalog'} -

+
+
+

+ + {labels?.catalogEyebrow ?? 'Catalog'} +

-

- {labels?.catalogHeading ?? 'Courses your team can actually finish.'} -

+

+ {labels?.catalogHeading ?? 'Courses your team can actually finish.'} +

-

- {labels?.catalogDescription ?? - 'Tracks built around real-world artifacts — pull requests, postmortems, and runbooks. No filler.'} -

+

+ {labels?.catalogDescription ?? + 'Tracks built around real-world artifacts — pull requests, postmortems, and runbooks. No filler.'} +

+ {#if coursesLoaded && courses.length === 0} + + {:else} {#if tabs.length > 1}
{/if} -
-
- {/if} + {/if} +
+
diff --git a/packages/ui/src/custom/org-landing-page/types.ts b/packages/ui/src/custom/org-landing-page/types.ts index 7013f8d3..3cc70261 100644 --- a/packages/ui/src/custom/org-landing-page/types.ts +++ b/packages/ui/src/custom/org-landing-page/types.ts @@ -178,6 +178,10 @@ export interface OrgLandingPageLabels { calloutEyebrow?: string; /** Card footer CTA on the resources/links section. Default per template ("Learn more", "Visit"). Separate from the existing per-template `boldVisitLabel` / `classicLearnMoreLabel` on `LandingPageLinks`, which still apply to those specific templates. */ learnMoreLabel?: string; + /** Empty catalog title when no courses are published. Default: "No courses published yet". */ + catalogEmptyTitle?: string; + /** Empty catalog description when no courses are published. Default: "Check back soon for new programs." */ + catalogEmptyDescription?: string; } export type CourseCurriculumLesson = { @@ -342,6 +346,8 @@ export interface OrgLandingPageProps { }; courses: CourseItem[]; hasMoreCourses?: boolean; + /** When false, templates suppress the empty catalog state while courses are still loading. */ + coursesLoaded?: boolean; disableCourseLinks?: boolean; embed?: LandingPageEmbed; callout?: LandingPageCallout; diff --git a/packages/ui/src/custom/org-landing-page/vibrant/org.svelte b/packages/ui/src/custom/org-landing-page/vibrant/org.svelte index 0299f5bc..27d7f359 100644 --- a/packages/ui/src/custom/org-landing-page/vibrant/org.svelte +++ b/packages/ui/src/custom/org-landing-page/vibrant/org.svelte @@ -7,6 +7,7 @@ import VibrantNav from './nav.svelte'; import VibrantHero from './hero.svelte'; import VibrantCourseCard from './course-card.svelte'; + import OrgLandingPageCoursesEmpty from '../courses-empty.svelte'; import { themeStyle } from '../theme-style'; import { Button } from '../../../base/button'; import { getCourseTypeLandingMeta, defaultLessonsLabel, defaultExercisesLabel } from '../landing-page-utils'; @@ -19,6 +20,7 @@ hero, courses, hasMoreCourses = false, + coursesLoaded = true, disableCourseLinks = false, embed, callout, @@ -56,153 +58,161 @@ {/snippet} - {#if featured} -
+ {#if coursesLoaded && courses.length === 0} +
-
-
- {#if featuredTypeMeta} - +
+
+ {:else} + {#if featured} +
+
+
+
+ {#if featuredTypeMeta} + + {labels?.featuredLabel ?? 'Featured'} · {featuredTypeMeta.label} + + {/if} +

- {labels?.featuredLabel ?? 'Featured'} · {featuredTypeMeta.label} - - {/if} -

- {featured.title} -

-

- {featured.description} -

-
- {#if featured.lessonCount} - {resolvedLessonsLabel(featured.lessonCount)} - {/if} - {#if featured.exerciseCount} - - {resolvedExercisesLabel(featured.exerciseCount)} - {/if} - {#if featured.duration} - - {featured.duration} - {/if} -
-
- - - {featured.price || formatCost(featured.cost, featured.currency)} - + {#if featured.lessonCount} + {resolvedLessonsLabel(featured.lessonCount)} + {/if} + {#if featured.exerciseCount} + + {resolvedExercisesLabel(featured.exerciseCount)} + {/if} + {#if featured.duration} + + {featured.duration} + {/if} +
+
+ + + {featured.price || formatCost(featured.cost, featured.currency)} + +
-
-
-
-

Course outline

-

- Pick up where you left off — anytime. -

-
- {#each Array.from({ length: Math.min(featured.lessonCount ?? 4, 4) }) as _, idx (idx)} -
- +

Course outline

+

+ Pick up where you left off — anytime. +

+
+ {#each Array.from({ length: Math.min(featured.lessonCount ?? 4, 4) }) as _, idx (idx)} +
- {String(idx + 1).padStart(2, '0')} - - - {idx === 0 - ? 'Foundations & setup' - : idx === 1 - ? 'Core concepts' - : idx === 2 - ? 'Hands-on practice' - : 'Capstone project'} - - - {idx === 0 ? '42 min' : idx === 1 ? '1h 12m' : idx === 2 ? '58 min' : '2h 30m'} - -
- {/each} + + {String(idx + 1).padStart(2, '0')} + + + {idx === 0 + ? 'Foundations & setup' + : idx === 1 + ? 'Core concepts' + : idx === 2 + ? 'Hands-on practice' + : 'Capstone project'} + + + {idx === 0 ? '42 min' : idx === 1 ? '1h 12m' : idx === 2 ? '58 min' : '2h 30m'} + +
+ {/each} +
- -
- {/if} +
+ {/if} - {#if remainingCourses.length > 0 || (!featured && courses.length > 0)} - {@const catalogCourses = featured ? remainingCourses : courses} -
-
-
-
-

- {labels?.catalogEyebrow ?? 'Catalog'} -

-

- {labels?.catalogHeading ?? 'More courses to take next.'} -

+ {#if remainingCourses.length > 0 || (!featured && courses.length > 0)} + {@const catalogCourses = featured ? remainingCourses : courses} +
+
+
+
+

+ {labels?.catalogEyebrow ?? 'Catalog'} +

+

+ {labels?.catalogHeading ?? 'More courses to take next.'} +

+
+ {#if hasMoreCourses} + + {/if}
- {#if hasMoreCourses} - - {/if} -
-
- {#each catalogCourses as course, index (course.id)} - - {/each} +
+ {#each catalogCourses as course, index (course.id)} + + {/each} +
-
-
+ + {/if} {/if}