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 <rotimi-best@users.noreply.github.com> * 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 <rotimi-best@users.noreply.github.com> * 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 <rotimi-best@users.noreply.github.com> * 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 <rotimi-best@users.noreply.github.com> * 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 <rotimi-best@users.noreply.github.com> * 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 <rotimi-best@users.noreply.github.com> ---------
This commit is contained in:
co-authored by
Rotimi Best
parent
8a22ee60bc
commit
bce38aa530
@@ -7,6 +7,7 @@
|
||||
import BoldNav from './nav.svelte';
|
||||
import BoldHero from './hero.svelte';
|
||||
import BoldCourseCard from './course-card.svelte';
|
||||
import OrgLandingPageCoursesEmpty from '../courses-empty.svelte';
|
||||
import { Button } from '../../../base/button';
|
||||
import { DotPattern } from '../../animation/dot-pattern';
|
||||
import { themeStyle } from '../theme-style';
|
||||
@@ -19,6 +20,7 @@
|
||||
hero,
|
||||
courses,
|
||||
hasMoreCourses = false,
|
||||
coursesLoaded = true,
|
||||
disableCourseLinks = false,
|
||||
embed,
|
||||
callout,
|
||||
@@ -43,23 +45,27 @@
|
||||
<div class="ui:flex ui:items-end ui:justify-between ui:mb-12">
|
||||
<h2 class="ui:text-4xl ui:font-black ui:tracking-tight">{labels?.catalogHeading ?? 'Latest Courses'}</h2>
|
||||
</div>
|
||||
<div class="ui:grid ui:grid-cols-1 ui:md:grid-cols-2 ui:gap-8">
|
||||
{#each courses as course, index (course.id)}
|
||||
<BoldCourseCard {course} {disableCourseLinks} {labels} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if hasMoreCourses}
|
||||
<div class="ui:mt-12 ui:flex ui:justify-center">
|
||||
<Button
|
||||
href={disableCourseLinks ? undefined : '/courses'}
|
||||
size="lg"
|
||||
class="ui:rounded-xl ui:px-8"
|
||||
disabled={disableCourseLinks}
|
||||
>
|
||||
{labels?.browseCoursesLabel ?? 'View more courses'}
|
||||
</Button>
|
||||
{#if coursesLoaded && courses.length === 0}
|
||||
<OrgLandingPageCoursesEmpty {labels} />
|
||||
{:else}
|
||||
<div class="ui:grid ui:grid-cols-1 ui:md:grid-cols-2 ui:gap-8">
|
||||
{#each courses as course, index (course.id)}
|
||||
<BoldCourseCard {course} {disableCourseLinks} {labels} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if hasMoreCourses}
|
||||
<div class="ui:mt-12 ui:flex ui:justify-center">
|
||||
<Button
|
||||
href={disableCourseLinks ? undefined : '/courses'}
|
||||
size="lg"
|
||||
class="ui:rounded-xl ui:px-8"
|
||||
disabled={disableCourseLinks}
|
||||
>
|
||||
{labels?.browseCoursesLabel ?? 'View more courses'}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import ClassicNav from './nav.svelte';
|
||||
import ClassicHero from './hero.svelte';
|
||||
import ClassicCourseCard 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,
|
||||
@@ -44,24 +46,28 @@
|
||||
<div class="ui:mt-6 ui:h-px ui:w-24 ui:bg-[var(--landing-border)] ui:mx-auto"></div>
|
||||
</div>
|
||||
|
||||
<div class="ui:grid ui:grid-cols-1 ui:sm:grid-cols-2 ui:lg:grid-cols-3 ui:gap-8 ui:justify-items-center">
|
||||
{#each courses as course, index (course.id)}
|
||||
<ClassicCourseCard {course} {disableCourseLinks} {labels} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if hasMoreCourses}
|
||||
<div class="ui:mt-12 ui:flex ui:justify-center">
|
||||
<Button
|
||||
href={disableCourseLinks ? undefined : '/courses'}
|
||||
variant="outline"
|
||||
size="lg"
|
||||
class="ui:px-8"
|
||||
disabled={disableCourseLinks}
|
||||
>
|
||||
{labels?.browseCoursesLabel ?? 'View more courses'}
|
||||
</Button>
|
||||
{#if coursesLoaded && courses.length === 0}
|
||||
<OrgLandingPageCoursesEmpty {labels} />
|
||||
{:else}
|
||||
<div class="ui:grid ui:grid-cols-1 ui:sm:grid-cols-2 ui:lg:grid-cols-3 ui:gap-8 ui:justify-items-center">
|
||||
{#each courses as course, index (course.id)}
|
||||
<ClassicCourseCard {course} {disableCourseLinks} {labels} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if hasMoreCourses}
|
||||
<div class="ui:mt-12 ui:flex ui:justify-center">
|
||||
<Button
|
||||
href={disableCourseLinks ? undefined : '/courses'}
|
||||
variant="outline"
|
||||
size="lg"
|
||||
class="ui:px-8"
|
||||
disabled={disableCourseLinks}
|
||||
>
|
||||
{labels?.browseCoursesLabel ?? 'View more courses'}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import CorporateNav from './nav.svelte';
|
||||
import CorporateHero from './hero.svelte';
|
||||
import CorporateCourseCard 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}
|
||||
</CorporateHero>
|
||||
|
||||
{#if courses.length > 0}
|
||||
<section class="ui:py-24 ui:px-6 ui:border-t ui:border-[var(--landing-border)]">
|
||||
<div class="ui:max-w-[1120px] ui:mx-auto">
|
||||
<div class="ui:flex ui:items-end ui:justify-between ui:flex-wrap ui:gap-4 ui:mb-10">
|
||||
<div>
|
||||
<p
|
||||
class="ui:text-xs ui:font-semibold ui:tracking-widest ui:uppercase ui:text-[var(--landing-fg)] ui:mb-3"
|
||||
>
|
||||
{labels?.catalogEyebrow ?? 'Catalog'}
|
||||
</p>
|
||||
<h2 class="ui:text-3xl ui:lg:text-4xl ui:font-semibold ui:tracking-tight ui:m-0 ui:max-w-xl">
|
||||
{labels?.catalogHeading ?? 'Courses your teams are taking'}
|
||||
</h2>
|
||||
</div>
|
||||
{#if hasMoreCourses}
|
||||
<Button
|
||||
href={disableCourseLinks ? undefined : '/courses'}
|
||||
variant="outline"
|
||||
class="ui:rounded-none ui:font-medium"
|
||||
disabled={disableCourseLinks}
|
||||
>
|
||||
{labels?.browseCoursesLabel ?? 'Browse catalog →'}
|
||||
</Button>
|
||||
{/if}
|
||||
<section class="ui:py-24 ui:px-6 ui:border-t ui:border-[var(--landing-border)]">
|
||||
<div class="ui:max-w-[1120px] ui:mx-auto">
|
||||
<div class="ui:flex ui:items-end ui:justify-between ui:flex-wrap ui:gap-4 ui:mb-10">
|
||||
<div>
|
||||
<p class="ui:text-xs ui:font-semibold ui:tracking-widest ui:uppercase ui:text-[var(--landing-fg)] ui:mb-3">
|
||||
{labels?.catalogEyebrow ?? 'Catalog'}
|
||||
</p>
|
||||
<h2 class="ui:text-3xl ui:lg:text-4xl ui:font-semibold ui:tracking-tight ui:m-0 ui:max-w-xl">
|
||||
{labels?.catalogHeading ?? 'Courses your teams are taking'}
|
||||
</h2>
|
||||
</div>
|
||||
{#if hasMoreCourses && courses.length > 0}
|
||||
<Button
|
||||
href={disableCourseLinks ? undefined : '/courses'}
|
||||
variant="outline"
|
||||
class="ui:rounded-none ui:font-medium"
|
||||
disabled={disableCourseLinks}
|
||||
>
|
||||
{labels?.browseCoursesLabel ?? 'Browse catalog →'}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if coursesLoaded && courses.length === 0}
|
||||
<OrgLandingPageCoursesEmpty {labels} />
|
||||
{:else}
|
||||
<div
|
||||
class="ui:grid ui:grid-cols-1 ui:md:grid-cols-2 ui:lg:grid-cols-3 ui:border-t ui:border-l ui:border-[var(--landing-border)]"
|
||||
>
|
||||
@@ -71,9 +73,9 @@
|
||||
<CorporateCourseCard {course} {disableCourseLinks} {labels} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<OrgLandingPageLinks {links} {labels} variant="corporate" />
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import LibraryBigIcon from '@lucide/svelte/icons/library-big';
|
||||
import { Empty } from '../empty';
|
||||
import type { OrgLandingPageLabels } from './types';
|
||||
|
||||
interface Props {
|
||||
labels?: OrgLandingPageLabels;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let { labels, class: className = '' }: Props = $props();
|
||||
|
||||
const title = $derived(labels?.catalogEmptyTitle ?? 'No courses published yet');
|
||||
const description = $derived(labels?.catalogEmptyDescription ?? 'Check back soon for new programs.');
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="ui:[&_[data-slot=empty-title]]:text-[var(--landing-fg)] ui:[&_[data-slot=empty-description]]:text-[var(--landing-fg-muted)] {className}"
|
||||
>
|
||||
<Empty
|
||||
icon={LibraryBigIcon}
|
||||
{title}
|
||||
{description}
|
||||
variant="page"
|
||||
class="ui:border-[var(--landing-border)] ui:bg-[var(--landing-card-soft)]/40"
|
||||
iconClass="ui:text-[var(--landing-fg-muted)]"
|
||||
/>
|
||||
</div>
|
||||
@@ -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}
|
||||
</EditorialHero>
|
||||
|
||||
{#if courses.length > 0}
|
||||
<section id="courses" class="ui:py-10 ui:px-6 ui:md:px-8 ui:bg-[var(--landing-bg)]">
|
||||
<div class="ui:max-w-[1240px] ui:mx-auto">
|
||||
<div class="ui:flex ui:flex-col ui:md:flex-row ui:md:items-end ui:justify-between ui:gap-6 ui:mb-8">
|
||||
<div>
|
||||
<p class="ui:text-[13px] ui:m-0 ui:mb-2 ui:text-[var(--landing-fg-muted)]">
|
||||
{labels?.catalogEyebrow ?? 'Catalog'}
|
||||
</p>
|
||||
<h2
|
||||
class="ui:text-3xl ui:md:text-[40px] ui:font-medium ui:tracking-tight ui:m-0 ui:leading-[1.1] ui:text-[var(--landing-fg)]"
|
||||
style="letter-spacing: -0.025em;"
|
||||
>
|
||||
{labels?.catalogHeading ?? 'Courses starting this season.'}
|
||||
</h2>
|
||||
<p class="ui:text-base ui:mt-3 ui:max-w-xl ui:m-0 ui:text-[var(--landing-fg-muted)]">
|
||||
{labels?.catalogDescription ??
|
||||
'Cohort-based and self-paced — every course ends with a reviewed project and a certificate of completion.'}
|
||||
</p>
|
||||
</div>
|
||||
{#if hasMoreCourses}
|
||||
<Button
|
||||
href={disableCourseLinks ? undefined : '/courses'}
|
||||
variant="outline"
|
||||
class="ui:rounded-full ui:px-5 ui:font-medium ui:bg-transparent ui:border-[var(--landing-border)] ui:text-[var(--landing-fg)] ui:hover:bg-[var(--landing-bg-section)]"
|
||||
disabled={disableCourseLinks}
|
||||
>
|
||||
{labels?.browseCoursesLabel ?? 'Browse all courses →'}
|
||||
</Button>
|
||||
{/if}
|
||||
<section id="courses" class="ui:py-10 ui:px-6 ui:md:px-8 ui:bg-[var(--landing-bg)]">
|
||||
<div class="ui:max-w-[1240px] ui:mx-auto">
|
||||
<div class="ui:flex ui:flex-col ui:md:flex-row ui:md:items-end ui:justify-between ui:gap-6 ui:mb-8">
|
||||
<div>
|
||||
<p class="ui:text-[13px] ui:m-0 ui:mb-2 ui:text-[var(--landing-fg-muted)]">
|
||||
{labels?.catalogEyebrow ?? 'Catalog'}
|
||||
</p>
|
||||
<h2
|
||||
class="ui:text-3xl ui:md:text-[40px] ui:font-medium ui:tracking-tight ui:m-0 ui:leading-[1.1] ui:text-[var(--landing-fg)]"
|
||||
style="letter-spacing: -0.025em;"
|
||||
>
|
||||
{labels?.catalogHeading ?? 'Courses starting this season.'}
|
||||
</h2>
|
||||
<p class="ui:text-base ui:mt-3 ui:max-w-xl ui:m-0 ui:text-[var(--landing-fg-muted)]">
|
||||
{labels?.catalogDescription ??
|
||||
'Cohort-based and self-paced — every course ends with a reviewed project and a certificate of completion.'}
|
||||
</p>
|
||||
</div>
|
||||
{#if hasMoreCourses && courses.length > 0}
|
||||
<Button
|
||||
href={disableCourseLinks ? undefined : '/courses'}
|
||||
variant="outline"
|
||||
class="ui:rounded-full ui:px-5 ui:font-medium ui:bg-transparent ui:border-[var(--landing-border)] ui:text-[var(--landing-fg)] ui:hover:bg-[var(--landing-bg-section)]"
|
||||
disabled={disableCourseLinks}
|
||||
>
|
||||
{labels?.browseCoursesLabel ?? 'Browse all courses →'}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if coursesLoaded && courses.length === 0}
|
||||
<OrgLandingPageCoursesEmpty {labels} />
|
||||
{:else}
|
||||
{#if visibleFilterDefs.length > 1}
|
||||
<div class="ui:flex ui:flex-wrap ui:items-center ui:gap-1.5 ui:mb-8">
|
||||
{#each visibleFilterDefs as def (def.key)}
|
||||
@@ -169,9 +173,9 @@
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<OrgLandingPageLinks {links} {labels} variant="editorial" />
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 @@
|
||||
<section class="ui:py-20 ui:px-4">
|
||||
<div class="ui:max-w-[1200px] ui:mx-auto">
|
||||
<h2 class="ui:text-2xl ui:font-semibold ui:mb-8">{labels?.catalogHeading ?? 'Our Courses'}</h2>
|
||||
<div class="ui:grid ui:grid-cols-1 ui:md:grid-cols-2 ui:gap-6">
|
||||
{#each courses as course, index (course.id)}
|
||||
<MinimalCourseCard {course} {disableCourseLinks} {labels} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if hasMoreCourses}
|
||||
<div class="ui:mt-10 ui:flex ui:justify-center">
|
||||
<Button
|
||||
href={disableCourseLinks ? undefined : '/courses'}
|
||||
variant="outline"
|
||||
size="lg"
|
||||
disabled={disableCourseLinks}
|
||||
>
|
||||
{labels?.browseCoursesLabel ?? 'View more courses'}
|
||||
</Button>
|
||||
{#if coursesLoaded && courses.length === 0}
|
||||
<OrgLandingPageCoursesEmpty {labels} />
|
||||
{:else}
|
||||
<div class="ui:grid ui:grid-cols-1 ui:md:grid-cols-2 ui:gap-6">
|
||||
{#each courses as course, index (course.id)}
|
||||
<MinimalCourseCard {course} {disableCourseLinks} {labels} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if hasMoreCourses}
|
||||
<div class="ui:mt-10 ui:flex ui:justify-center">
|
||||
<Button
|
||||
href={disableCourseLinks ? undefined : '/courses'}
|
||||
variant="outline"
|
||||
size="lg"
|
||||
disabled={disableCourseLinks}
|
||||
>
|
||||
{labels?.browseCoursesLabel ?? 'View more courses'}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -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}
|
||||
</SaasHero>
|
||||
|
||||
{#if courses.length > 0}
|
||||
<div class="plus-row"></div>
|
||||
<div class="plus-row"></div>
|
||||
|
||||
<section class="ui:py-12 ui:md:py-20 ui:px-4 ui:md:px-6">
|
||||
<div class="ui:max-w-[1180px] ui:mx-auto">
|
||||
<div
|
||||
class="ui:flex ui:flex-col ui:md:flex-row ui:md:items-end ui:md:justify-between ui:gap-4 ui:mb-8 ui:md:mb-12"
|
||||
>
|
||||
<div>
|
||||
<p
|
||||
class="ui:text-xs ui:font-semibold ui:tracking-widest ui:uppercase ui:text-[var(--landing-accent)] ui:mb-3"
|
||||
>
|
||||
{labels?.catalogEyebrow ?? 'Catalog'}
|
||||
</p>
|
||||
<h2 class="ui:text-3xl ui:md:text-4xl ui:font-bold ui:tracking-tight">
|
||||
{labels?.catalogHeading ?? 'Featured courses.'}
|
||||
</h2>
|
||||
</div>
|
||||
{#if hasMoreCourses}
|
||||
<Button
|
||||
href={disableCourseLinks ? undefined : '/courses'}
|
||||
variant="outline"
|
||||
class="ui:rounded-full ui:self-start ui:md:self-auto"
|
||||
disabled={disableCourseLinks}
|
||||
>
|
||||
{labels?.browseCoursesLabel ?? 'Browse catalog'}
|
||||
</Button>
|
||||
{/if}
|
||||
<section class="ui:py-12 ui:md:py-20 ui:px-4 ui:md:px-6">
|
||||
<div class="ui:max-w-[1180px] ui:mx-auto">
|
||||
<div
|
||||
class="ui:flex ui:flex-col ui:md:flex-row ui:md:items-end ui:md:justify-between ui:gap-4 ui:mb-8 ui:md:mb-12"
|
||||
>
|
||||
<div>
|
||||
<p
|
||||
class="ui:text-xs ui:font-semibold ui:tracking-widest ui:uppercase ui:text-[var(--landing-accent)] ui:mb-3"
|
||||
>
|
||||
{labels?.catalogEyebrow ?? 'Catalog'}
|
||||
</p>
|
||||
<h2 class="ui:text-3xl ui:md:text-4xl ui:font-bold ui:tracking-tight">
|
||||
{labels?.catalogHeading ?? 'Featured courses.'}
|
||||
</h2>
|
||||
</div>
|
||||
{#if hasMoreCourses && courses.length > 0}
|
||||
<Button
|
||||
href={disableCourseLinks ? undefined : '/courses'}
|
||||
variant="outline"
|
||||
class="ui:rounded-full ui:self-start ui:md:self-auto"
|
||||
disabled={disableCourseLinks}
|
||||
>
|
||||
{labels?.browseCoursesLabel ?? 'Browse catalog'}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if coursesLoaded && courses.length === 0}
|
||||
<OrgLandingPageCoursesEmpty {labels} />
|
||||
{:else}
|
||||
<div class="course-grid ui:relative">
|
||||
{#each courses as course, index (course.id)}
|
||||
<SaasCourseCard {course} {disableCourseLinks} {labels} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{#if links && links.cards.length > 0}
|
||||
<div class="plus-row"></div>
|
||||
|
||||
@@ -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}
|
||||
</StudioHero>
|
||||
|
||||
{#if courses.length > 0}
|
||||
<section class="ui:py-24 ui:px-6 ui:border-t ui:border-[var(--landing-border)]">
|
||||
<div class="ui:max-w-[1080px] ui:mx-auto">
|
||||
<div class="ui:grid ui:grid-cols-1 ui:md:grid-cols-2 ui:gap-12 ui:items-end ui:mb-12">
|
||||
<div>
|
||||
<p class="ui:text-sm ui:text-[var(--landing-fg-muted)] ui:mb-1.5 ui:inline-flex ui:items-center ui:gap-2">
|
||||
<span class="ui:size-1.5 ui:rounded-full ui:bg-[var(--landing-accent)]"></span>
|
||||
{labels?.catalogEyebrow ?? 'Catalog'}
|
||||
</p>
|
||||
<h2 class="ui:text-3xl ui:lg:text-4xl ui:font-semibold ui:tracking-tight ui:m-0">
|
||||
{labels?.catalogHeading ?? 'Courses your team can actually finish.'}
|
||||
</h2>
|
||||
</div>
|
||||
{#if hasMoreCourses}
|
||||
<div class="ui:flex ui:justify-start ui:md:justify-end">
|
||||
<Button
|
||||
href={disableCourseLinks ? undefined : '/courses'}
|
||||
variant="outline"
|
||||
class="ui:rounded-md"
|
||||
disabled={disableCourseLinks}
|
||||
>
|
||||
{labels?.browseCoursesLabel ?? 'Browse all →'}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
<section class="ui:py-24 ui:px-6 ui:border-t ui:border-[var(--landing-border)]">
|
||||
<div class="ui:max-w-[1080px] ui:mx-auto">
|
||||
<div class="ui:grid ui:grid-cols-1 ui:md:grid-cols-2 ui:gap-12 ui:items-end ui:mb-12">
|
||||
<div>
|
||||
<p class="ui:text-sm ui:text-[var(--landing-fg-muted)] ui:mb-1.5 ui:inline-flex ui:items-center ui:gap-2">
|
||||
<span class="ui:size-1.5 ui:rounded-full ui:bg-[var(--landing-accent)]"></span>
|
||||
{labels?.catalogEyebrow ?? 'Catalog'}
|
||||
</p>
|
||||
<h2 class="ui:text-3xl ui:lg:text-4xl ui:font-semibold ui:tracking-tight ui:m-0">
|
||||
{labels?.catalogHeading ?? 'Courses your team can actually finish.'}
|
||||
</h2>
|
||||
</div>
|
||||
{#if hasMoreCourses && courses.length > 0}
|
||||
<div class="ui:flex ui:justify-start ui:md:justify-end">
|
||||
<Button
|
||||
href={disableCourseLinks ? undefined : '/courses'}
|
||||
variant="outline"
|
||||
class="ui:rounded-md"
|
||||
disabled={disableCourseLinks}
|
||||
>
|
||||
{labels?.browseCoursesLabel ?? 'Browse all →'}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if coursesLoaded && courses.length === 0}
|
||||
<OrgLandingPageCoursesEmpty {labels} />
|
||||
{:else}
|
||||
<div class="ui:grid ui:grid-cols-1 ui:md:grid-cols-2 ui:lg:grid-cols-3 ui:gap-3">
|
||||
{#each courses as course, index (course.id)}
|
||||
<StudioCourseCard {course} {disableCourseLinks} {labels} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<OrgLandingPageLinks {links} {labels} variant="studio" />
|
||||
|
||||
@@ -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}
|
||||
</TechHero>
|
||||
|
||||
{#if courses.length > 0}
|
||||
<section class="ui:py-24 ui:px-6">
|
||||
<div class="ui:max-w-[1280px] ui:mx-auto">
|
||||
<div class="ui:flex ui:items-end ui:justify-between ui:flex-wrap ui:gap-4 ui:mb-14">
|
||||
<div>
|
||||
<p
|
||||
class="ui:font-mono ui:text-xs ui:tracking-widest ui:uppercase ui:text-[var(--landing-accent)] ui:mb-3"
|
||||
>
|
||||
{labels?.catalogEyebrow ?? '// catalog'}
|
||||
</p>
|
||||
<h2 class="ui:text-4xl ui:lg:text-5xl ui:font-extrabold ui:tracking-tight ui:m-0">
|
||||
{labels?.catalogHeading ?? 'Latest courses'}
|
||||
</h2>
|
||||
</div>
|
||||
{#if hasMoreCourses}
|
||||
<Button
|
||||
href={disableCourseLinks ? undefined : '/courses'}
|
||||
variant="outline"
|
||||
class="ui:rounded-none ui:font-mono ui:text-sm"
|
||||
disabled={disableCourseLinks}
|
||||
>
|
||||
{labels?.browseCoursesLabel ?? 'view all →'}
|
||||
</Button>
|
||||
{/if}
|
||||
<section class="ui:py-24 ui:px-6">
|
||||
<div class="ui:max-w-[1280px] ui:mx-auto">
|
||||
<div class="ui:flex ui:items-end ui:justify-between ui:flex-wrap ui:gap-4 ui:mb-14">
|
||||
<div>
|
||||
<p class="ui:font-mono ui:text-xs ui:tracking-widest ui:uppercase ui:text-[var(--landing-accent)] ui:mb-3">
|
||||
{labels?.catalogEyebrow ?? '// catalog'}
|
||||
</p>
|
||||
<h2 class="ui:text-4xl ui:lg:text-5xl ui:font-extrabold ui:tracking-tight ui:m-0">
|
||||
{labels?.catalogHeading ?? 'Latest courses'}
|
||||
</h2>
|
||||
</div>
|
||||
{#if hasMoreCourses && courses.length > 0}
|
||||
<Button
|
||||
href={disableCourseLinks ? undefined : '/courses'}
|
||||
variant="outline"
|
||||
class="ui:rounded-none ui:font-mono ui:text-sm"
|
||||
disabled={disableCourseLinks}
|
||||
>
|
||||
{labels?.browseCoursesLabel ?? 'view all →'}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if coursesLoaded && courses.length === 0}
|
||||
<OrgLandingPageCoursesEmpty {labels} />
|
||||
{:else}
|
||||
<div
|
||||
class="ui:grid ui:grid-cols-1 ui:md:grid-cols-2 ui:lg:grid-cols-3 ui:border-t ui:border-l ui:border-[var(--landing-border)]"
|
||||
>
|
||||
@@ -71,9 +73,9 @@
|
||||
<TechCourseCard {course} {disableCourseLinks} {labels} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<OrgLandingPageLinks {links} {labels} variant="tech" />
|
||||
|
||||
@@ -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}
|
||||
</TerminalHero>
|
||||
|
||||
{#if courses.length > 0}
|
||||
<section
|
||||
class="ui:py-24 ui:px-6 ui:bg-[var(--landing-bg)]"
|
||||
style="border-top: 1px solid var(--landing-border-soft);"
|
||||
>
|
||||
<div class="ui:max-w-[1120px] ui:mx-auto">
|
||||
<p
|
||||
class="ui:font-mono ui:text-[11px] ui:tracking-[0.12em] ui:uppercase ui:mb-3 ui:inline-flex ui:items-center ui:gap-2"
|
||||
style="color: var(--landing-accent);"
|
||||
>
|
||||
<span
|
||||
class="ui:size-1.5 ui:rounded-full"
|
||||
style="background: var(--landing-accent); box-shadow: 0 0 12px var(--landing-accent);"
|
||||
></span>
|
||||
{labels?.catalogEyebrow ?? 'Catalog'}
|
||||
</p>
|
||||
<section
|
||||
class="ui:py-24 ui:px-6 ui:bg-[var(--landing-bg)]"
|
||||
style="border-top: 1px solid var(--landing-border-soft);"
|
||||
>
|
||||
<div class="ui:max-w-[1120px] ui:mx-auto">
|
||||
<p
|
||||
class="ui:font-mono ui:text-[11px] ui:tracking-[0.12em] ui:uppercase ui:mb-3 ui:inline-flex ui:items-center ui:gap-2"
|
||||
style="color: var(--landing-accent);"
|
||||
>
|
||||
<span
|
||||
class="ui:size-1.5 ui:rounded-full"
|
||||
style="background: var(--landing-accent); box-shadow: 0 0 12px var(--landing-accent);"
|
||||
></span>
|
||||
{labels?.catalogEyebrow ?? 'Catalog'}
|
||||
</p>
|
||||
|
||||
<h2
|
||||
class="ui:text-4xl ui:lg:text-[40px] ui:font-semibold ui:tracking-tight ui:leading-[1.08] ui:m-0 ui:mb-3 ui:max-w-[700px] ui:text-[var(--landing-fg)]"
|
||||
>
|
||||
{labels?.catalogHeading ?? 'Courses your team can actually finish.'}
|
||||
</h2>
|
||||
<h2
|
||||
class="ui:text-4xl ui:lg:text-[40px] ui:font-semibold ui:tracking-tight ui:leading-[1.08] ui:m-0 ui:mb-3 ui:max-w-[700px] ui:text-[var(--landing-fg)]"
|
||||
>
|
||||
{labels?.catalogHeading ?? 'Courses your team can actually finish.'}
|
||||
</h2>
|
||||
|
||||
<p class="ui:text-[15px] ui:max-w-xl ui:m-0 ui:mb-8 ui:text-[var(--landing-fg-muted)]">
|
||||
{labels?.catalogDescription ??
|
||||
'Tracks built around real-world artifacts — pull requests, postmortems, and runbooks. No filler.'}
|
||||
</p>
|
||||
<p class="ui:text-[15px] ui:max-w-xl ui:m-0 ui:mb-8 ui:text-[var(--landing-fg-muted)]">
|
||||
{labels?.catalogDescription ??
|
||||
'Tracks built around real-world artifacts — pull requests, postmortems, and runbooks. No filler.'}
|
||||
</p>
|
||||
|
||||
{#if coursesLoaded && courses.length === 0}
|
||||
<OrgLandingPageCoursesEmpty {labels} />
|
||||
{:else}
|
||||
{#if tabs.length > 1}
|
||||
<div
|
||||
class="ui:inline-flex ui:items-center ui:gap-0.5 ui:p-1 ui:rounded-full ui:mb-8 ui:bg-[var(--landing-card)]"
|
||||
@@ -151,9 +155,9 @@
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<OrgLandingPageLinks {links} {labels} variant="terminal" />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
</VibrantHero>
|
||||
|
||||
{#if featured}
|
||||
<section
|
||||
class="ui:bg-[var(--landing-accent)] ui:text-[var(--landing-accent-fg)] ui:px-6 ui:md:px-8 ui:py-24 ui:md:py-32"
|
||||
>
|
||||
{#if coursesLoaded && courses.length === 0}
|
||||
<section id="courses" class="ui:px-6 ui:md:px-8 ui:py-24 ui:md:py-28 ui:bg-[var(--landing-bg)]">
|
||||
<div class="ui:max-w-[1320px] ui:mx-auto">
|
||||
<div class="ui:grid ui:grid-cols-1 ui:lg:grid-cols-[1.15fr_1fr] ui:gap-12 ui:lg:gap-20 ui:items-center">
|
||||
<div>
|
||||
{#if featuredTypeMeta}
|
||||
<span
|
||||
class="ui:inline-flex ui:items-center ui:gap-1.5 ui:rounded-full ui:px-3 ui:py-1 ui:text-[13px] ui:font-medium ui:mb-6 ui:text-[var(--landing-accent-fg)]"
|
||||
style="background: rgba(255,255,255,0.18);"
|
||||
<OrgLandingPageCoursesEmpty {labels} />
|
||||
</div>
|
||||
</section>
|
||||
{:else}
|
||||
{#if featured}
|
||||
<section
|
||||
class="ui:bg-[var(--landing-accent)] ui:text-[var(--landing-accent-fg)] ui:px-6 ui:md:px-8 ui:py-24 ui:md:py-32"
|
||||
>
|
||||
<div class="ui:max-w-[1320px] ui:mx-auto">
|
||||
<div class="ui:grid ui:grid-cols-1 ui:lg:grid-cols-[1.15fr_1fr] ui:gap-12 ui:lg:gap-20 ui:items-center">
|
||||
<div>
|
||||
{#if featuredTypeMeta}
|
||||
<span
|
||||
class="ui:inline-flex ui:items-center ui:gap-1.5 ui:rounded-full ui:px-3 ui:py-1 ui:text-[13px] ui:font-medium ui:mb-6 ui:text-[var(--landing-accent-fg)]"
|
||||
style="background: rgba(255,255,255,0.18);"
|
||||
>
|
||||
{labels?.featuredLabel ?? 'Featured'} · {featuredTypeMeta.label}
|
||||
</span>
|
||||
{/if}
|
||||
<h2
|
||||
class="ui:text-4xl ui:md:text-5xl ui:lg:text-[56px] ui:font-medium ui:tracking-tight ui:m-0 ui:mb-6 ui:max-w-[520px]"
|
||||
style="line-height: 1.05; letter-spacing: -0.025em;"
|
||||
>
|
||||
{labels?.featuredLabel ?? 'Featured'} · {featuredTypeMeta.label}
|
||||
</span>
|
||||
{/if}
|
||||
<h2
|
||||
class="ui:text-4xl ui:md:text-5xl ui:lg:text-[56px] ui:font-medium ui:tracking-tight ui:m-0 ui:mb-6 ui:max-w-[520px]"
|
||||
style="line-height: 1.05; letter-spacing: -0.025em;"
|
||||
>
|
||||
{featured.title}
|
||||
</h2>
|
||||
<p class="ui:text-lg ui:m-0 ui:mb-4 ui:max-w-[480px] ui:opacity-90" style="line-height: 1.5;">
|
||||
{featured.description}
|
||||
</p>
|
||||
<div
|
||||
class="ui:flex ui:flex-wrap ui:items-center ui:gap-x-5 ui:gap-y-2 ui:text-[14.5px] ui:mb-8 ui:opacity-85"
|
||||
>
|
||||
{#if featured.lessonCount}
|
||||
<span>{resolvedLessonsLabel(featured.lessonCount)}</span>
|
||||
{/if}
|
||||
{#if featured.exerciseCount}
|
||||
<span aria-hidden="true">·</span>
|
||||
<span>{resolvedExercisesLabel(featured.exerciseCount)}</span>
|
||||
{/if}
|
||||
{#if featured.duration}
|
||||
<span aria-hidden="true">·</span>
|
||||
<span>{featured.duration}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="ui:flex ui:flex-wrap ui:items-center ui:gap-3">
|
||||
<Button
|
||||
href={courseHref(featured)}
|
||||
disabled={disableCourseLinks}
|
||||
class="ui:rounded-md ui:px-6 ui:py-3 ui:text-base ui:font-medium ui:bg-[var(--landing-bg)] ui:text-[var(--landing-fg)] ui:hover:opacity-90"
|
||||
{featured.title}
|
||||
</h2>
|
||||
<p class="ui:text-lg ui:m-0 ui:mb-4 ui:max-w-[480px] ui:opacity-90" style="line-height: 1.5;">
|
||||
{featured.description}
|
||||
</p>
|
||||
<div
|
||||
class="ui:flex ui:flex-wrap ui:items-center ui:gap-x-5 ui:gap-y-2 ui:text-[14.5px] ui:mb-8 ui:opacity-85"
|
||||
>
|
||||
{labels?.startCourseLabel ?? 'Start this course'}
|
||||
</Button>
|
||||
<span class="ui:text-base ui:font-medium ui:opacity-95">
|
||||
{featured.price || formatCost(featured.cost, featured.currency)}
|
||||
</span>
|
||||
{#if featured.lessonCount}
|
||||
<span>{resolvedLessonsLabel(featured.lessonCount)}</span>
|
||||
{/if}
|
||||
{#if featured.exerciseCount}
|
||||
<span aria-hidden="true">·</span>
|
||||
<span>{resolvedExercisesLabel(featured.exerciseCount)}</span>
|
||||
{/if}
|
||||
{#if featured.duration}
|
||||
<span aria-hidden="true">·</span>
|
||||
<span>{featured.duration}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="ui:flex ui:flex-wrap ui:items-center ui:gap-3">
|
||||
<Button
|
||||
href={courseHref(featured)}
|
||||
disabled={disableCourseLinks}
|
||||
class="ui:rounded-md ui:px-6 ui:py-3 ui:text-base ui:font-medium ui:bg-[var(--landing-bg)] ui:text-[var(--landing-fg)] ui:hover:opacity-90"
|
||||
>
|
||||
{labels?.startCourseLabel ?? 'Start this course'}
|
||||
</Button>
|
||||
<span class="ui:text-base ui:font-medium ui:opacity-95">
|
||||
{featured.price || formatCost(featured.cost, featured.currency)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="ui:relative ui:overflow-hidden ui:rounded-3xl ui:h-[440px] ui:md:h-[480px]"
|
||||
style="
|
||||
<div
|
||||
class="ui:relative ui:overflow-hidden ui:rounded-3xl ui:h-[440px] ui:md:h-[480px]"
|
||||
style="
|
||||
background:
|
||||
radial-gradient(50% 60% at 40% 40%, rgba(255,255,255,0.55) 0%, transparent 70%),
|
||||
radial-gradient(60% 70% at 70% 65%, rgba(255,255,255,0.32) 0%, transparent 70%),
|
||||
radial-gradient(40% 50% at 25% 75%, rgba(255,235,210,0.5) 0%, transparent 70%),
|
||||
linear-gradient(135deg, #b0a8a0 0%, #d4cabd 50%, #c8c0b5 100%);
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="ui:absolute ui:left-1/2 ui:top-1/2 ui:w-[80%] ui:overflow-hidden ui:rounded-2xl ui:p-6"
|
||||
style="
|
||||
>
|
||||
<div
|
||||
class="ui:absolute ui:left-1/2 ui:top-1/2 ui:w-[80%] ui:overflow-hidden ui:rounded-2xl ui:p-6"
|
||||
style="
|
||||
transform: translate(-50%, -50%);
|
||||
background: #0c1320;
|
||||
color: #fff;
|
||||
box-shadow: 0 30px 60px -20px rgba(0,0,0,0.35);
|
||||
"
|
||||
>
|
||||
<p class="ui:m-0 ui:mb-1 ui:text-[16px] ui:font-semibold">Course outline</p>
|
||||
<p class="ui:m-0 ui:mb-5 ui:text-[13px]" style="color: #8a93a0;">
|
||||
Pick up where you left off — anytime.
|
||||
</p>
|
||||
<div class="ui:flex ui:flex-col ui:gap-2">
|
||||
{#each Array.from({ length: Math.min(featured.lessonCount ?? 4, 4) }) as _, idx (idx)}
|
||||
<div
|
||||
class="ui:flex ui:items-center ui:gap-3 ui:rounded-lg ui:px-3.5 ui:py-3"
|
||||
style="background: #131c2c;"
|
||||
>
|
||||
<span
|
||||
class="ui:inline-flex ui:items-center ui:justify-center ui:size-6 ui:rounded ui:text-[11px] ui:font-semibold ui:tabular-nums"
|
||||
style="background: #2a3548; color: #fff;"
|
||||
>
|
||||
<p class="ui:m-0 ui:mb-1 ui:text-[16px] ui:font-semibold">Course outline</p>
|
||||
<p class="ui:m-0 ui:mb-5 ui:text-[13px]" style="color: #8a93a0;">
|
||||
Pick up where you left off — anytime.
|
||||
</p>
|
||||
<div class="ui:flex ui:flex-col ui:gap-2">
|
||||
{#each Array.from({ length: Math.min(featured.lessonCount ?? 4, 4) }) as _, idx (idx)}
|
||||
<div
|
||||
class="ui:flex ui:items-center ui:gap-3 ui:rounded-lg ui:px-3.5 ui:py-3"
|
||||
style="background: #131c2c;"
|
||||
>
|
||||
{String(idx + 1).padStart(2, '0')}
|
||||
</span>
|
||||
<span class="ui:flex-1 ui:text-[13.5px] ui:font-medium" style="color: #fff;">
|
||||
{idx === 0
|
||||
? 'Foundations & setup'
|
||||
: idx === 1
|
||||
? 'Core concepts'
|
||||
: idx === 2
|
||||
? 'Hands-on practice'
|
||||
: 'Capstone project'}
|
||||
</span>
|
||||
<span class="ui:text-[11.5px] ui:tabular-nums" style="color: #6c7585;">
|
||||
{idx === 0 ? '42 min' : idx === 1 ? '1h 12m' : idx === 2 ? '58 min' : '2h 30m'}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
<span
|
||||
class="ui:inline-flex ui:items-center ui:justify-center ui:size-6 ui:rounded ui:text-[11px] ui:font-semibold ui:tabular-nums"
|
||||
style="background: #2a3548; color: #fff;"
|
||||
>
|
||||
{String(idx + 1).padStart(2, '0')}
|
||||
</span>
|
||||
<span class="ui:flex-1 ui:text-[13.5px] ui:font-medium" style="color: #fff;">
|
||||
{idx === 0
|
||||
? 'Foundations & setup'
|
||||
: idx === 1
|
||||
? 'Core concepts'
|
||||
: idx === 2
|
||||
? 'Hands-on practice'
|
||||
: 'Capstone project'}
|
||||
</span>
|
||||
<span class="ui:text-[11.5px] ui:tabular-nums" style="color: #6c7585;">
|
||||
{idx === 0 ? '42 min' : idx === 1 ? '1h 12m' : idx === 2 ? '58 min' : '2h 30m'}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if remainingCourses.length > 0 || (!featured && courses.length > 0)}
|
||||
{@const catalogCourses = featured ? remainingCourses : courses}
|
||||
<section id="courses" class="ui:px-6 ui:md:px-8 ui:py-24 ui:md:py-28 ui:bg-[var(--landing-bg)]">
|
||||
<div class="ui:max-w-[1320px] ui:mx-auto">
|
||||
<div class="ui:flex ui:flex-col ui:md:flex-row ui:md:items-end ui:justify-between ui:gap-6 ui:mb-12">
|
||||
<div>
|
||||
<p
|
||||
class="ui:text-[13px] ui:font-medium ui:tracking-[0.08em] ui:uppercase ui:text-[var(--landing-fg-muted)] ui:m-0 ui:mb-3"
|
||||
>
|
||||
{labels?.catalogEyebrow ?? 'Catalog'}
|
||||
</p>
|
||||
<h2
|
||||
class="ui:text-4xl ui:md:text-5xl ui:font-medium ui:tracking-tight ui:m-0 ui:text-[var(--landing-fg)] ui:max-w-2xl"
|
||||
style="line-height: 1.05; letter-spacing: -0.025em;"
|
||||
>
|
||||
{labels?.catalogHeading ?? 'More courses to take next.'}
|
||||
</h2>
|
||||
{#if remainingCourses.length > 0 || (!featured && courses.length > 0)}
|
||||
{@const catalogCourses = featured ? remainingCourses : courses}
|
||||
<section id="courses" class="ui:px-6 ui:md:px-8 ui:py-24 ui:md:py-28 ui:bg-[var(--landing-bg)]">
|
||||
<div class="ui:max-w-[1320px] ui:mx-auto">
|
||||
<div class="ui:flex ui:flex-col ui:md:flex-row ui:md:items-end ui:justify-between ui:gap-6 ui:mb-12">
|
||||
<div>
|
||||
<p
|
||||
class="ui:text-[13px] ui:font-medium ui:tracking-[0.08em] ui:uppercase ui:text-[var(--landing-fg-muted)] ui:m-0 ui:mb-3"
|
||||
>
|
||||
{labels?.catalogEyebrow ?? 'Catalog'}
|
||||
</p>
|
||||
<h2
|
||||
class="ui:text-4xl ui:md:text-5xl ui:font-medium ui:tracking-tight ui:m-0 ui:text-[var(--landing-fg)] ui:max-w-2xl"
|
||||
style="line-height: 1.05; letter-spacing: -0.025em;"
|
||||
>
|
||||
{labels?.catalogHeading ?? 'More courses to take next.'}
|
||||
</h2>
|
||||
</div>
|
||||
{#if hasMoreCourses}
|
||||
<Button
|
||||
href={disableCourseLinks ? undefined : '/courses'}
|
||||
variant="outline"
|
||||
class="ui:rounded-md ui:px-5 ui:py-2.5 ui:font-medium ui:border-[var(--landing-border)] ui:text-[var(--landing-fg)] ui:hover:bg-[var(--landing-card-soft)]"
|
||||
disabled={disableCourseLinks}
|
||||
>
|
||||
{labels?.browseCoursesLabel ?? 'Browse all →'}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if hasMoreCourses}
|
||||
<Button
|
||||
href={disableCourseLinks ? undefined : '/courses'}
|
||||
variant="outline"
|
||||
class="ui:rounded-md ui:px-5 ui:py-2.5 ui:font-medium ui:border-[var(--landing-border)] ui:text-[var(--landing-fg)] ui:hover:bg-[var(--landing-card-soft)]"
|
||||
disabled={disableCourseLinks}
|
||||
>
|
||||
{labels?.browseCoursesLabel ?? 'Browse all →'}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="ui:grid ui:grid-cols-1 ui:md:grid-cols-2 ui:lg:grid-cols-3 ui:gap-5">
|
||||
{#each catalogCourses as course, index (course.id)}
|
||||
<VibrantCourseCard {course} {disableCourseLinks} {labels} />
|
||||
{/each}
|
||||
<div class="ui:grid ui:grid-cols-1 ui:md:grid-cols-2 ui:lg:grid-cols-3 ui:gap-5">
|
||||
{#each catalogCourses as course, index (course.id)}
|
||||
<VibrantCourseCard {course} {disableCourseLinks} {labels} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
{/if}
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user