feat: preview templates, thumbnail features, 2 new org templates

This commit is contained in:
Rotimi Best
2026-05-23 12:24:57 +01:00
parent 4fdd4115b9
commit 78ab13f54b
106 changed files with 8320 additions and 2040 deletions
@@ -0,0 +1,72 @@
<script lang="ts">
import type { CourseItem, OrgLandingPageLabels } from '../types';
interface Props {
course: CourseItem;
disableCourseLinks?: boolean;
labels?: OrgLandingPageLabels;
}
let { course, disableCourseLinks = false, labels }: Props = $props();
const href = $derived.by(() => {
if (disableCourseLinks) return undefined;
return course.link || (course.slug ? `/course/${course.slug}` : undefined);
});
function slugify(c: CourseItem): string {
if (c.slug) return c.slug;
return c.title
.toLowerCase()
.trim()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
function priceLabel(c: CourseItem): string {
if (c.price) return c.price;
if (!c.cost) return labels?.freeLabel ?? 'Free';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: c.currency ?? 'USD',
maximumFractionDigits: 0
}).format(c.cost);
}
</script>
<a
{href}
aria-disabled={disableCourseLinks}
tabindex={disableCourseLinks ? -1 : undefined}
class="ui:relative ui:flex ui:flex-col ui:gap-2.5 ui:p-6 ui:rounded-xl ui:no-underline ui:transition-colors ui:h-full ui:text-[var(--landing-fg)] {disableCourseLinks
? ''
: 'ui:cursor-pointer'}"
style="
background: linear-gradient(180deg, var(--landing-card) 0%, var(--landing-bg-section) 100%);
border: 1px solid var(--landing-border-soft);
min-height: 180px;
cursor: {disableCourseLinks ? 'default' : 'pointer'};
"
onmouseenter={(e) => !disableCourseLinks && (e.currentTarget.style.borderColor = 'var(--landing-border)')}
onmouseleave={(e) => (e.currentTarget.style.borderColor = 'var(--landing-border-soft)')}
>
<span class="ui:font-mono ui:text-[11px] ui:tracking-[0.04em]" style="color: var(--landing-accent);">
$ {slugify(course)}
</span>
<h3
class="ui:text-[17px] ui:font-semibold ui:leading-snug ui:m-0 ui:text-[var(--landing-fg)]"
style="letter-spacing: -0.01em;"
>
{course.title}
</h3>
<p class="ui:text-[13.5px] ui:leading-[1.5] ui:m-0 ui:flex-1 ui:line-clamp-3 ui:text-[var(--landing-fg-muted)]">
{course.description}
</p>
<div
class="ui:flex ui:items-center ui:justify-between ui:pt-3.5 ui:font-mono ui:text-[12px]"
style="border-top: 1px dashed var(--landing-border-soft);"
>
<span class="ui:text-[var(--landing-fg)]">{priceLabel(course)}</span>
<span aria-hidden="true" class="ui:text-[var(--landing-fg-muted)]">→</span>
</div>
</a>
@@ -2,17 +2,16 @@
import type { OrgLandingPageProps, CourseItem } from '../types';
import type { Snippet } from 'svelte';
import { Button } from '../../../base/button';
import { BlurFade } from '../../animation/blurfade';
import EditableLandingSection from '../editable-section.svelte';
interface Props {
orgName: OrgLandingPageProps['orgName'];
orgName?: OrgLandingPageProps['orgName'];
hero: OrgLandingPageProps['hero'];
courses: OrgLandingPageProps['courses'];
navigation: Snippet;
}
let { orgName, hero, courses, navigation }: Props = $props();
let { orgName = '', hero, courses = [], navigation }: Props = $props();
const programsCount = $derived(courses.length);
const learnersCount = $derived(courses.reduce((sum, course) => sum + (course.totalStudents ?? 0), 0));
@@ -63,221 +62,188 @@
<EditableLandingSection sectionKey="hero">
<section
class="ui:relative ui:overflow-hidden ui:px-6 ui:pt-16 ui:pb-16 ui:md:pt-20 ui:md:pb-20"
style="
background: #06070a;
--terminal-bg: #06070a;
--terminal-bg-soft: #0c0e13;
--terminal-bg-card: #0f1218;
--terminal-bg-elevated: #14171f;
--terminal-line: #1c1f28;
--terminal-line-soft: #161922;
--terminal-line-strong: #262a35;
--terminal-ink: #e9eaed;
--terminal-ink-dim: #9da1ab;
--terminal-ink-muted: #61656f;
--terminal-ink-faint: #3d4049;
"
class="ui:relative ui:overflow-hidden ui:px-6 ui:pt-16 ui:pb-16 ui:md:pt-20 ui:md:pb-20 ui:bg-[var(--landing-bg)]"
>
<div
class="ui:pointer-events-none ui:absolute ui:inset-x-0 ui:-top-32 ui:h-[680px] ui:opacity-90"
style="background: radial-gradient(ellipse 60% 70% at 50% 30%, color-mix(in oklab, var(--primary) 22%, transparent) 0%, transparent 65%);"
style="background: radial-gradient(ellipse 60% 70% at 50% 30%, color-mix(in oklab, var(--landing-accent) 22%, transparent) 0%, transparent 65%);"
aria-hidden="true"
></div>
<div class="ui:relative ui:max-w-[1120px] ui:mx-auto ui:text-center">
<BlurFade delay={0} once={true}>
<h1
class="ui:font-semibold ui:leading-[0.95] ui:mb-10 ui:max-w-[20ch] ui:mx-auto ui:text-balance"
style="
font-size: clamp(48px, 9vw, 128px);
letter-spacing: -0.04em;
background: linear-gradient(180deg,
color-mix(in oklab, var(--primary) 60%, #ffffff) 0%,
color-mix(in oklab, var(--primary) 35%, transparent) 45%,
rgba(8, 10, 14, 0.3) 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
text-shadow: 0 8px 60px color-mix(in oklab, var(--primary) 18%, transparent);
user-select: none;
"
>
{hero.heading}
</h1>
</BlurFade>
<h1
class="ui:font-semibold ui:leading-[0.95] ui:mb-10 ui:max-w-[20ch] ui:mx-auto ui:text-balance"
style="
font-size: clamp(48px, 9vw, 128px);
letter-spacing: -0.04em;
background: linear-gradient(180deg,
color-mix(in oklab, var(--landing-accent) 60%, #ffffff) 0%,
color-mix(in oklab, var(--landing-accent) 35%, transparent) 45%,
color-mix(in oklab, var(--landing-bg) 70%, transparent) 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
text-shadow: 0 8px 60px color-mix(in oklab, var(--landing-accent) 18%, transparent);
user-select: none;
"
>
{hero.heading}
</h1>
<BlurFade delay={0.12} once={true}>
<div class="ui:inline-flex ui:items-center ui:gap-2 ui:mb-8">
{#if hero.secondaryAction}
<Button
href={hero.secondaryAction.href}
size="sm"
variant="outline"
class="ui:rounded-full ui:px-4 ui:font-medium ui:bg-white/5 ui:border-[var(--terminal-line-strong)] ui:text-[var(--terminal-ink)] ui:hover:bg-white/10"
>
{hero.secondaryAction.label}
</Button>
{/if}
<div class="ui:inline-flex ui:items-center ui:gap-2 ui:mb-8">
{#if hero.secondaryAction}
<Button
href={hero.primaryAction.href}
disabled={hero.primaryAction.disabled ?? false}
href={hero.secondaryAction.href}
size="sm"
class="ui:rounded-full ui:px-4 ui:font-medium ui:bg-[#f4f5f7] ui:text-[#0a0b0e] ui:hover:bg-white"
variant="outline"
class="ui:rounded-full ui:px-4 ui:font-medium ui:bg-white/5 ui:border-[var(--landing-border)] ui:text-[var(--landing-fg)] ui:hover:bg-white/10"
>
{hero.primaryAction.label}
{hero.secondaryAction.label}
</Button>
</div>
</BlurFade>
{/if}
<Button
href={hero.primaryAction.href}
disabled={hero.primaryAction.disabled ?? false}
size="sm"
class="ui:rounded-full ui:px-4 ui:font-medium ui:bg-[#f4f5f7] ui:text-[#0a0b0e] ui:hover:bg-white"
>
{hero.primaryAction.label}
</Button>
</div>
<BlurFade delay={0.22} once={true}>
<p class="ui:text-[15px] ui:mb-7 ui:max-w-xl ui:mx-auto" style="color: var(--terminal-ink-dim);">
{hero.subheading}
</p>
</BlurFade>
<p class="ui:text-[15px] ui:mb-7 ui:max-w-xl ui:mx-auto ui:text-[var(--landing-fg-muted)]">
{hero.subheading}
</p>
{#if programsCount > 0}
<BlurFade delay={0.3} once={true}>
<span
class="ui:inline-flex ui:items-center ui:gap-3 ui:px-4 ui:py-2 ui:rounded-full ui:font-mono ui:text-[13px] ui:text-[var(--landing-fg-muted)]"
style="
border: 1px solid var(--landing-border);
background: rgba(255,255,255,0.02);
backdrop-filter: blur(8px);
"
>
<span style="color: var(--landing-accent);">&gt;</span>
<span
class="ui:inline-flex ui:items-center ui:gap-3 ui:px-4 ui:py-2 ui:rounded-full ui:font-mono ui:text-[13px]"
style="
border: 1px solid var(--terminal-line-strong);
background: rgba(255,255,255,0.02);
backdrop-filter: blur(8px);
color: var(--terminal-ink-dim);
"
><span class="ui:font-medium ui:text-[var(--landing-fg)]">{programsCount}</span>
{programsCount === 1 ? 'program' : 'programs'} running</span
>
<span style="color: var(--primary);">&gt;</span>
<span
><span style="color: var(--terminal-ink); font-weight: 500;">{programsCount}</span>
{programsCount === 1 ? 'program' : 'programs'} running</span
>
{#if learnersCount > 0}
<span class="ui:inline-block ui:w-px ui:h-3" style="background: var(--terminal-line-strong);"></span>
<span
><span style="color: var(--terminal-ink); font-weight: 500;">{learnersFormatted}</span> learners</span
>
{/if}
{#if tracksCount > 1}
<span class="ui:inline-block ui:w-px ui:h-3" style="background: var(--terminal-line-strong);"></span>
<span><span style="color: var(--terminal-ink); font-weight: 500;">{tracksCount}</span> tracks</span>
{/if}
</span>
</BlurFade>
{#if learnersCount > 0}
<span class="ui:inline-block ui:w-px ui:h-3 ui:bg-[var(--landing-border)]"></span>
<span><span class="ui:font-medium ui:text-[var(--landing-fg)]">{learnersFormatted}</span> learners</span>
{/if}
{#if tracksCount > 1}
<span class="ui:inline-block ui:w-px ui:h-3 ui:bg-[var(--landing-border)]"></span>
<span><span class="ui:font-medium ui:text-[var(--landing-fg)]">{tracksCount}</span> tracks</span>
{/if}
</span>
{/if}
{#if hero.image}
<BlurFade delay={0.4} once={true}>
<div
class="ui:mt-14 ui:max-w-[880px] ui:mx-auto ui:rounded-xl ui:overflow-hidden ui:text-left ui:bg-[var(--landing-card)]"
style="
border: 1px solid var(--landing-border);
box-shadow:
0 30px 90px -30px rgba(0,0,0,0.7),
0 0 0 1px color-mix(in oklab, var(--landing-accent) 6%, transparent);
"
>
<div
class="ui:mt-14 ui:max-w-[880px] ui:mx-auto ui:rounded-xl ui:overflow-hidden ui:text-left"
style="
border: 1px solid var(--terminal-line-strong);
background: var(--terminal-bg-card);
box-shadow:
0 30px 90px -30px rgba(0,0,0,0.7),
0 0 0 1px color-mix(in oklab, var(--primary) 6%, transparent);
"
class="ui:flex ui:items-center ui:gap-2 ui:px-4 ui:py-3 ui:bg-[var(--landing-card-soft)]"
style="border-bottom: 1px solid var(--landing-border-soft);"
>
<div
class="ui:flex ui:items-center ui:gap-2 ui:px-4 ui:py-3"
style="border-bottom: 1px solid var(--terminal-line); background: var(--terminal-bg-elevated);"
<span class="ui:flex ui:gap-1.5 ui:mr-2">
<span class="ui:size-[11px] ui:rounded-full" style="background: #ff5f57;"></span>
<span class="ui:size-[11px] ui:rounded-full" style="background: #febc2e;"></span>
<span class="ui:size-[11px] ui:rounded-full" style="background: #28c840;"></span>
</span>
<span
class="ui:flex-1 ui:text-center ui:font-mono ui:text-xs ui:truncate ui:text-[var(--landing-fg-muted)]"
>
<span class="ui:flex ui:gap-1.5 ui:mr-2">
<span class="ui:size-[11px] ui:rounded-full" style="background: #ff5f57;"></span>
<span class="ui:size-[11px] ui:rounded-full" style="background: #febc2e;"></span>
<span class="ui:size-[11px] ui:rounded-full" style="background: #28c840;"></span>
</span>
<span
class="ui:flex-1 ui:text-center ui:font-mono ui:text-xs ui:truncate"
style="color: var(--terminal-ink-dim);"
>
{orgSlug}/preview
</span>
</div>
<img src={hero.image} alt={orgName} class="ui:block ui:w-full ui:h-auto" />
{orgSlug}/preview
</span>
</div>
</BlurFade>
<img src={hero.image} alt={orgName} class="ui:block ui:w-full ui:h-auto" />
</div>
{:else if terminalRows.length > 0}
<BlurFade delay={0.4} once={true}>
<div
class="ui:mt-14 ui:max-w-[880px] ui:mx-auto ui:rounded-xl ui:overflow-hidden ui:text-left ui:bg-[var(--landing-card)]"
style="
border: 1px solid var(--landing-border);
box-shadow:
0 30px 90px -30px rgba(0,0,0,0.7),
0 0 0 1px color-mix(in oklab, var(--landing-accent) 6%, transparent);
"
>
<div
class="ui:mt-14 ui:max-w-[880px] ui:mx-auto ui:rounded-xl ui:overflow-hidden ui:text-left"
style="
border: 1px solid var(--terminal-line-strong);
background: var(--terminal-bg-card);
box-shadow:
0 30px 90px -30px rgba(0,0,0,0.7),
0 0 0 1px color-mix(in oklab, var(--primary) 6%, transparent);
"
class="ui:flex ui:items-center ui:gap-2 ui:px-4 ui:py-3 ui:bg-[var(--landing-card-soft)]"
style="border-bottom: 1px solid var(--landing-border-soft);"
>
<div
class="ui:flex ui:items-center ui:gap-2 ui:px-4 ui:py-3"
style="border-bottom: 1px solid var(--terminal-line); background: var(--terminal-bg-elevated);"
<span class="ui:flex ui:gap-1.5 ui:mr-2">
<span class="ui:size-[11px] ui:rounded-full" style="background: #ff5f57;"></span>
<span class="ui:size-[11px] ui:rounded-full" style="background: #febc2e;"></span>
<span class="ui:size-[11px] ui:rounded-full" style="background: #28c840;"></span>
</span>
<span
class="ui:flex-1 ui:text-center ui:font-mono ui:text-xs ui:truncate ui:text-[var(--landing-fg-muted)]"
>
<span class="ui:flex ui:gap-1.5 ui:mr-2">
<span class="ui:size-[11px] ui:rounded-full" style="background: #ff5f57;"></span>
<span class="ui:size-[11px] ui:rounded-full" style="background: #febc2e;"></span>
<span class="ui:size-[11px] ui:rounded-full" style="background: #28c840;"></span>
</span>
{orgSlug}/main · catalog
</span>
<span class="ui:font-mono ui:text-[11px] ui:text-[var(--landing-fg-faint)]">
{programsCount}
{programsCount === 1 ? 'program' : 'programs'}
</span>
</div>
<div class="ui:p-5 ui:font-mono ui:text-[13px] ui:text-[var(--landing-fg)]" style="line-height: 1.7;">
<div
class="ui:flex ui:items-baseline ui:gap-2 ui:px-3 ui:py-2 ui:rounded-md ui:mb-3"
style="background: rgba(255,255,255,0.02); border: 1px solid var(--landing-border-soft);"
>
<span style="color: var(--landing-accent);">&gt;</span>
<span>list programs --org {orgSlug}</span>
</div>
<div class="ui:py-1 ui:text-[12px] ui:text-[var(--landing-fg-faint)]">
● Loaded {programsCount}
{programsCount === 1 ? 'program' : 'programs'}
</div>
<div class="ui:mt-2 ui:flex ui:flex-col">
{#each terminalRows as row (row.slug)}
<div class="ui:flex ui:items-baseline ui:gap-4 ui:py-1.5 ui:text-[var(--landing-fg)]">
<span class="ui:text-[var(--landing-fg-faint)]">{row.n}</span>
<span class="ui:truncate ui:flex-1" style="color: var(--landing-accent);">{row.slug}</span>
<span class="ui:text-[12px] ui:text-[var(--landing-fg-muted)]">{row.duration}</span>
<span class="ui:text-[12px] ui:w-20 ui:text-right">{row.price}</span>
</div>
{/each}
</div>
<div class="ui:flex ui:items-baseline ui:gap-2 ui:mt-4">
<span style="color: var(--landing-accent);">&gt;</span>
<span
class="ui:flex-1 ui:text-center ui:font-mono ui:text-xs ui:truncate"
style="color: var(--terminal-ink-dim);"
>
{orgSlug}/main · catalog
</span>
<span class="ui:font-mono ui:text-[11px]" style="color: var(--terminal-ink-muted);">
{programsCount}
{programsCount === 1 ? 'program' : 'programs'}
</span>
</div>
<div class="ui:p-5 ui:font-mono ui:text-[13px]" style="color: var(--terminal-ink); line-height: 1.7;">
<div
class="ui:flex ui:items-baseline ui:gap-2 ui:px-3 ui:py-2 ui:rounded-md ui:mb-3"
style="background: rgba(255,255,255,0.02); border: 1px solid var(--terminal-line);"
>
<span style="color: var(--primary);">&gt;</span>
<span>list programs --org {orgSlug}</span>
</div>
<div class="ui:py-1 ui:text-[12px]" style="color: var(--terminal-ink-muted);">
● Loaded {programsCount}
{programsCount === 1 ? 'program' : 'programs'}
</div>
<div class="ui:mt-2 ui:flex ui:flex-col">
{#each terminalRows as row (row.slug)}
<div class="ui:flex ui:items-baseline ui:gap-4 ui:py-1.5" style="color: var(--terminal-ink);">
<span style="color: var(--terminal-ink-faint);">{row.n}</span>
<span class="ui:truncate ui:flex-1" style="color: var(--primary);">{row.slug}</span>
<span class="ui:text-[12px]" style="color: var(--terminal-ink-dim);">{row.duration}</span>
<span class="ui:text-[12px] ui:w-20 ui:text-right">{row.price}</span>
</div>
{/each}
</div>
<div class="ui:flex ui:items-baseline ui:gap-2 ui:mt-4">
<span style="color: var(--primary);">&gt;</span>
<span
class="ui:inline-block ui:w-2 ui:h-[14px] ui:align-middle"
style="background: var(--primary); animation: terminal-caret 1s steps(1) infinite;"
aria-hidden="true"
></span>
</div>
</div>
<div
class="ui:flex ui:items-center ui:justify-between ui:px-4 ui:py-2.5 ui:font-mono ui:text-[11px]"
style="border-top: 1px solid var(--terminal-line); background: var(--terminal-bg-soft); color: var(--terminal-ink-muted);"
>
<div class="ui:flex ui:gap-4">
<span>Enter enroll</span>
<span>↑/↓ navigate</span>
<span>^q quit</span>
</div>
<div style="color: var(--terminal-ink-dim);">{orgSlug}-cli</div>
class="ui:inline-block ui:w-2 ui:h-[14px] ui:align-middle"
style="background: var(--landing-accent); animation: terminal-caret 1s steps(1) infinite;"
aria-hidden="true"
></span>
</div>
</div>
</BlurFade>
<div
class="ui:flex ui:items-center ui:justify-between ui:px-4 ui:py-2.5 ui:font-mono ui:text-[11px] ui:bg-[var(--landing-bg-section)] ui:text-[var(--landing-fg-faint)]"
style="border-top: 1px solid var(--landing-border-soft);"
>
<div class="ui:flex ui:gap-4">
<span>Enter enroll</span>
<span>↑/↓ navigate</span>
<span>^q quit</span>
</div>
<div class="ui:text-[var(--landing-fg-muted)]">{orgSlug}-cli</div>
</div>
</div>
{/if}
</div>
</section>
@@ -17,15 +17,14 @@
</script>
<EditableLandingSection sectionKey="navigation">
<header class="ui:relative ui:z-50 ui:px-6" style="background: transparent;">
<header class="ui:relative ui:z-50 ui:px-6 ui:bg-transparent">
<div class="ui:max-w-[1120px] ui:mx-auto">
<div class="ui:flex ui:items-center ui:justify-between ui:h-16">
<div class="ui:flex ui:items-center ui:gap-8">
<a
href="/"
aria-label={orgName}
class="ui:flex ui:items-center ui:gap-2 ui:no-underline ui:text-foreground"
style="color: var(--terminal-ink, #e9eaed);"
class="ui:flex ui:items-center ui:gap-2 ui:no-underline ui:text-[var(--landing-fg)] ui:cursor-pointer"
>
{#if logoUrl}
<img src={logoUrl} alt={orgName} class="ui:h-6 ui:w-auto" />
@@ -47,16 +46,12 @@
{#each navItems as item (item.href + item.label)}
<a
href={item.href}
class="ui:text-[11px] ui:font-medium ui:tracking-[0.12em] ui:uppercase ui:no-underline ui:transition-colors"
style="color: var(--terminal-ink-dim, #9da1ab);"
onmouseenter={(e) => (e.currentTarget.style.color = 'var(--terminal-ink, #e9eaed)')}
onmouseleave={(e) => (e.currentTarget.style.color = 'var(--terminal-ink-dim, #9da1ab)')}
class="ui:text-[11px] ui:font-medium ui:tracking-[0.12em] ui:uppercase ui:no-underline ui:transition-colors ui:text-[var(--landing-fg-muted)] ui:hover:text-[var(--landing-fg)] ui:cursor-pointer"
>
{item.label}
</a>
{/each}
<span class="ui:select-none" style="color: var(--terminal-ink-faint, #3d4049);" aria-hidden="true">/</span
>
<span class="ui:select-none ui:text-[var(--landing-fg-faint)]" aria-hidden="true">/</span>
</nav>
{/if}
</div>
@@ -67,7 +62,7 @@
href={authAction.href}
size="sm"
variant="outline"
class="ui:rounded-full ui:px-4 ui:font-medium ui:bg-transparent ui:border-[var(--terminal-line-strong,#262a35)] ui:text-[var(--terminal-ink,#e9eaed)] ui:hover:bg-white/5"
class="ui:rounded-full ui:px-4 ui:font-medium ui:bg-transparent ui:border-[var(--landing-border)] ui:text-[var(--landing-fg)] ui:hover:bg-white/5"
>
{authAction.label}
</Button>