feat: certificates, landing pages, ai tutor, website update, compliance

This commit is contained in:
Rotimi Best
2026-05-18 03:47:22 +01:00
parent c31ad63955
commit ac3a2c14a7
644 changed files with 56050 additions and 5439 deletions
@@ -0,0 +1,297 @@
<script lang="ts">
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'];
hero: OrgLandingPageProps['hero'];
courses: OrgLandingPageProps['courses'];
navigation: Snippet;
}
let { orgName, hero, courses, navigation }: Props = $props();
const programsCount = $derived(courses.length);
const learnersCount = $derived(courses.reduce((sum, course) => sum + (course.totalStudents ?? 0), 0));
const tracksCount = $derived(new Set(courses.map((course) => course.type).filter(Boolean)).size);
const numberFormatter = new Intl.NumberFormat('en-US');
const learnersFormatted = $derived(numberFormatter.format(learnersCount));
function priceLabel(course: CourseItem): string {
if (course.price) return course.price;
if (!course.cost) return 'Free';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: course.currency ?? 'USD',
maximumFractionDigits: 0
}).format(course.cost);
}
function durationLabel(course: CourseItem): string {
if (course.duration) return course.duration;
if (course.lessonCount && course.lessonCount > 0) {
return `${course.lessonCount} ${course.lessonCount === 1 ? 'lesson' : 'lessons'}`;
}
return '—';
}
const terminalRows = $derived(
courses.slice(0, 5).map((course, index) => ({
n: String(index + 1).padStart(2, '0'),
slug: course.slug ?? course.id,
price: priceLabel(course),
duration: durationLabel(course)
}))
);
const orgSlug = $derived(
orgName
.toLowerCase()
.trim()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '') || 'org'
);
</script>
{@render navigation()}
<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;
"
>
<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%);"
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>
<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}
<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>
<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>
{#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]"
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 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}
{#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"
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);
"
>
<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"
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" />
</div>
</BlurFade>
{: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"
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);
"
>
<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"
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>
</div>
</div>
</BlurFade>
{/if}
</div>
</section>
</EditableLandingSection>
<style>
@keyframes terminal-caret {
50% {
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
[style*='terminal-caret'] {
animation: none !important;
}
}
</style>
@@ -0,0 +1,79 @@
<script lang="ts">
import type { NavItem } from '../types';
import { Button } from '../../../base/button';
import EditableLandingSection from '../editable-section.svelte';
interface Props {
orgName: string;
logoUrl?: string;
navItems: NavItem[];
authAction?: {
label: string;
href: string;
};
}
let { orgName, logoUrl, navItems, authAction }: Props = $props();
</script>
<EditableLandingSection sectionKey="navigation">
<header class="ui:relative ui:z-50 ui:px-6" style="background: 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);"
>
{#if logoUrl}
<img src={logoUrl} alt={orgName} class="ui:h-6 ui:w-auto" />
{:else}
<svg viewBox="0 0 16 16" fill="none" class="ui:size-5">
<path
d="M2 2 L8 8 L2 14 M14 2 L8 8 L14 14"
stroke="currentColor"
stroke-width="1.6"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
{/if}
</a>
{#if navItems.length > 0}
<nav class="ui:hidden ui:md:flex ui:items-center ui:gap-6">
{#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)')}
>
{item.label}
</a>
{/each}
<span class="ui:select-none" style="color: var(--terminal-ink-faint, #3d4049);" aria-hidden="true">/</span
>
</nav>
{/if}
</div>
<div class="ui:flex ui:items-center ui:gap-3">
{#if authAction}
<Button
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"
>
{authAction.label}
</Button>
{/if}
</div>
</div>
</div>
</header>
</EditableLandingSection>