feat: certificates, landing pages, ai tutor, website update, compliance
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
<script>
|
||||
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
|
||||
import LayoutTemplate from '@lucide/svelte/icons/layout-template';
|
||||
import MessagesSquare from '@lucide/svelte/icons/messages-square';
|
||||
import Terminal from '@lucide/svelte/icons/terminal';
|
||||
|
||||
const lanes = [
|
||||
{
|
||||
Icon: LayoutTemplate,
|
||||
title: 'Pick a template',
|
||||
description: 'Product 101, onboarding, or expert track. Fill three fields, get a course draft.'
|
||||
},
|
||||
{
|
||||
Icon: MessagesSquare,
|
||||
title: 'Chat with the builder',
|
||||
description: 'Claude lives in the editor. It plans, drafts, and writes quizzes — you approve each step.'
|
||||
},
|
||||
{
|
||||
Icon: Terminal,
|
||||
title: 'Use your own agent',
|
||||
description: 'Drive course authoring from Cursor, Claude Code, Codex, or OpenCode via MCP.'
|
||||
}
|
||||
];
|
||||
|
||||
const agents = [
|
||||
{ name: 'Cursor', logo: '/cursor-logo.png' },
|
||||
{ name: 'Claude Code', logo: '/claude-logo.svg' },
|
||||
{ name: 'Codex', logo: '/codex-logo.png' },
|
||||
{ name: 'OpenCode', logo: '/opencode-logo.svg' }
|
||||
];
|
||||
</script>
|
||||
|
||||
<section class="bg-gray-50 px-6 py-12 lg:px-12 lg:py-16">
|
||||
<div class="mx-auto max-w-[1100px]">
|
||||
<BlurFade once>
|
||||
<div class="mb-2 text-xs font-medium tracking-widest text-blue-700 uppercase">AI authoring</div>
|
||||
<h2 class="max-w-[680px] text-[clamp(1.75rem,3vw,2.6rem)] leading-[1.1] font-medium tracking-tight text-gray-950">
|
||||
Build entire courses with AI.
|
||||
</h2>
|
||||
<p class="mt-3 max-w-[560px] text-sm leading-relaxed text-gray-500">
|
||||
Generate the structure, lessons, and quizzes from your docs — then review and approve every step before it
|
||||
ships. Three ways in. Pick whichever fits your team.
|
||||
</p>
|
||||
</BlurFade>
|
||||
|
||||
<BlurFade delay={0.1} once>
|
||||
<div class="mx-auto mt-10 w-full max-w-[1100px]">
|
||||
<div class="overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-gray-200">
|
||||
<img
|
||||
src="/ai-course-builder.jpeg"
|
||||
alt="Course builder — plan, approve, implement. Templates, in-editor chat, and MCP."
|
||||
class="block h-auto w-full"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</BlurFade>
|
||||
|
||||
<div class="mt-10 grid grid-cols-1 gap-8 md:grid-cols-3">
|
||||
{#each lanes as lane, i}
|
||||
<BlurFade delay={0.05 * i + 0.15} once>
|
||||
<div>
|
||||
<div class="text-gray-950">
|
||||
<lane.Icon size={20} strokeWidth={1.6} />
|
||||
</div>
|
||||
<h3 class="mt-3 text-sm font-medium text-gray-950 lg:text-base">{lane.title}</h3>
|
||||
<p class="mt-1.5 max-w-[280px] text-xs leading-relaxed text-gray-500 lg:text-sm">{lane.description}</p>
|
||||
</div>
|
||||
</BlurFade>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<BlurFade delay={0.3} once>
|
||||
<div class="mt-10 flex flex-wrap items-center gap-x-6 gap-y-3 border-t border-gray-200 pt-6">
|
||||
<span class="text-[10px] font-medium tracking-widest text-gray-500 uppercase">Works with</span>
|
||||
{#each agents as agent}
|
||||
<div class="flex items-center gap-1.5">
|
||||
<img src={agent.logo} alt="{agent.name} logo" class="h-4 w-4 rounded-full" />
|
||||
<span class="text-xs text-gray-600">{agent.name}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</BlurFade>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,6 +1,23 @@
|
||||
<script>
|
||||
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
|
||||
import { BorderBeam } from '@cio/ui/custom/animation/border-beam';
|
||||
import CodeBlock from './code-block.svelte';
|
||||
|
||||
// mcp.json — sky-300 keys · emerald-300 strings · slate-500 punctuation
|
||||
// 2-space indent (real spaces; body uses whitespace-pre).
|
||||
const mcpJsonLines = [
|
||||
'<span class="text-slate-500">{</span>',
|
||||
' <span class="text-sky-300">"mcpServers"</span><span class="text-slate-500">: {</span>',
|
||||
' <span class="text-sky-300">"classroomio"</span><span class="text-slate-500">: {</span>',
|
||||
' <span class="text-sky-300">"command"</span><span class="text-slate-500">:</span> <span class="text-emerald-300">"npx"</span><span class="text-slate-500">,</span>',
|
||||
' <span class="text-sky-300">"args"</span><span class="text-slate-500">: [</span><span class="text-emerald-300">"-y"</span><span class="text-slate-500">,</span> <span class="text-emerald-300">"@classroomio/mcp"</span><span class="text-slate-500">],</span>',
|
||||
' <span class="text-sky-300">"env"</span><span class="text-slate-500">: {</span>',
|
||||
' <span class="text-sky-300">"CLASSROOMIO_API_URL"</span><span class="text-slate-500">:</span> <span class="text-emerald-300">"https://api.classroomio.com"</span><span class="text-slate-500">,</span>',
|
||||
' <span class="text-sky-300">"CLASSROOMIO_API_KEY"</span><span class="text-slate-500">:</span> <span class="text-emerald-300">"<your-mcp-key>"</span>',
|
||||
' <span class="text-slate-500">}</span>',
|
||||
' <span class="text-slate-500">}</span>',
|
||||
' <span class="text-slate-500">}</span>',
|
||||
'<span class="text-slate-500">}</span>'
|
||||
];
|
||||
|
||||
const aiFeatures = [
|
||||
{
|
||||
@@ -72,29 +89,12 @@
|
||||
|
||||
<BlurFade delay={0.2} once>
|
||||
<div>
|
||||
<!-- Code Block -->
|
||||
<div class="relative overflow-hidden rounded-2xl border border-gray-800 bg-[#0d0d0d]">
|
||||
<BorderBeam size={200} duration={10} colorFrom="#0233BD" colorTo="#6b8fff" />
|
||||
<div class="flex items-center gap-2 border-b border-gray-800 bg-gray-900 px-5 py-3.5">
|
||||
<div class="h-2.5 w-2.5 rounded-full bg-red-400"></div>
|
||||
<div class="h-2.5 w-2.5 rounded-full bg-yellow-400"></div>
|
||||
<div class="h-2.5 w-2.5 rounded-full bg-green-500"></div>
|
||||
<span class="ml-2 text-xs text-gray-600">mcp.json</span>
|
||||
</div>
|
||||
<div class="p-6 font-mono text-xs leading-loose whitespace-pre text-gray-400 lg:text-sm">
|
||||
{'{'}<br />{' '}<span class="text-blue-400">"mcpServers"</span>: {'{'}<br />{' '}<span
|
||||
class="text-blue-400">"classroomio"</span
|
||||
>: {'{'}<br />{' '}<span class="text-blue-400">"command"</span>:
|
||||
<span class="text-green-400">"npx"</span>,<br />{' '}<span class="text-blue-400">"args"</span>:
|
||||
[<span class="text-green-400">"-y"</span>, <span class="text-green-400">"@classroomio/mcp"</span>],<br
|
||||
/>{' '}<span class="text-blue-400">"env"</span>: {'{'}<br />{' '}<span
|
||||
class="text-blue-400">"CLASSROOMIO_API_URL"</span
|
||||
>: <span class="text-green-400">"https://api.classroomio.com"</span>,<br />{' '}<span
|
||||
class="text-blue-400">"CLASSROOMIO_API_KEY"</span
|
||||
>: <span class="text-green-400">"<your-mcp-key>"</span><br />{' }'}<br />{' }'}<br
|
||||
/>{' }'}<br />{'}'}
|
||||
</div>
|
||||
</div>
|
||||
<CodeBlock
|
||||
fileName="mcp.json"
|
||||
lang="JSON"
|
||||
codeLines={mcpJsonLines}
|
||||
statusBar="paste into ~/.claude/mcp.json · restart your client"
|
||||
/>
|
||||
|
||||
<!-- Supported Agents -->
|
||||
<div class="mt-6">
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
<script>
|
||||
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
|
||||
import { BotIcon, BotOffIcon } from '@cio/ui/custom/moving-icons';
|
||||
import ImagePlaceholder from './image-placeholder.svelte';
|
||||
import ArrowRight from '@lucide/svelte/icons/arrow-right';
|
||||
|
||||
/**
|
||||
* AI teaching assistant section — centered, light theme, logo on top, gradient accent,
|
||||
* large heading, short subhead, optional CTA, and an image placeholder underneath.
|
||||
* Used on the homepage + each solution page with variant copy.
|
||||
*
|
||||
* @typedef {Object} Props
|
||||
* @property {string} [accent] Small gradient line above the heading (e.g. "Academy", "Compliance AI")
|
||||
* @property {string} [title] Main heading
|
||||
* @property {string} description Subhead paragraph (1–3 sentences)
|
||||
* @property {string} [ctaLabel] CTA button label (optional). Omit to hide the button.
|
||||
* @property {string} [ctaHref] CTA destination
|
||||
* @property {string} [imagePath] Suggested static file path for the image placeholder
|
||||
* @property {string} [imageCaption] Caption shown inside the placeholder
|
||||
* @property {boolean} [showImage] Show the image placeholder (default true)
|
||||
* @property {string} [imageSrc] Real image path. When set, renders a real <img> (takes priority over vimeoVideoId).
|
||||
* @property {string} [imageAlt] Alt text for the real image
|
||||
* @property {string} [vimeoVideoId] Vimeo video id. When provided (and no imageSrc), replaces the image placeholder.
|
||||
* @property {string} [videoAspect] CSS padding-top percentage controlling the video aspect ratio (default "54.68%")
|
||||
* @property {string} [videoTitle] Accessible title for the iframe
|
||||
* @property {string} [bgClass] Section background (default bg-white)
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let {
|
||||
accent = 'Academy',
|
||||
title = 'AI-powered Teaching Assistant',
|
||||
description,
|
||||
ctaLabel,
|
||||
ctaHref = '/customer-education',
|
||||
imagePath = '/static/ai-tutor-stack.png',
|
||||
imageCaption = 'AI tutor in a lesson — lesson-aware chat, follow-ups, translation, summarisation',
|
||||
showImage = true,
|
||||
imageSrc,
|
||||
imageAlt = '',
|
||||
vimeoVideoId,
|
||||
videoAspect = '54.68%',
|
||||
videoTitle = 'AI Teaching Assistant on ClassroomIO.',
|
||||
bgClass = 'bg-white'
|
||||
} = $props();
|
||||
|
||||
/** @type {HTMLElement | null} */
|
||||
let sectionEl = $state(null);
|
||||
let inZone = $state(true);
|
||||
|
||||
const inZoneBorder = 'border-blue-500';
|
||||
const outZoneBorder = 'border-yellow-500';
|
||||
const inZoneBot = '#2563eb';
|
||||
const outZoneBot = '#eab308';
|
||||
</script>
|
||||
|
||||
<section
|
||||
bind:this={sectionEl}
|
||||
role="region"
|
||||
aria-label={title}
|
||||
onmouseenter={() => (inZone = true)}
|
||||
onmouseleave={() => (inZone = false)}
|
||||
class="{bgClass} {inZone
|
||||
? inZoneBorder
|
||||
: outZoneBorder} border-t-2 border-dashed px-6 py-12 transition-colors duration-200 lg:px-12 lg:py-16"
|
||||
>
|
||||
<div class="mx-auto max-w-[1100px] text-center">
|
||||
<BlurFade once>
|
||||
<div class="relative mx-auto mb-2 h-12 w-12">
|
||||
<div
|
||||
class="absolute inset-0 flex items-center justify-center transition-opacity duration-300 ease-in-out"
|
||||
style="opacity: {inZone ? 1 : 0}; pointer-events: {inZone ? 'auto' : 'none'};"
|
||||
aria-hidden={!inZone}
|
||||
>
|
||||
<BotIcon
|
||||
size={48}
|
||||
strokeWidth={1.6}
|
||||
color={inZoneBot}
|
||||
trackElement={sectionEl}
|
||||
trackRadius={420}
|
||||
maxEyeOffset={2.2}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="absolute inset-0 flex items-center justify-center transition-opacity duration-300 ease-in-out"
|
||||
style="opacity: {inZone ? 0 : 1}; pointer-events: {inZone ? 'none' : 'auto'};"
|
||||
aria-hidden={inZone}
|
||||
>
|
||||
<BotOffIcon size={48} strokeWidth={1.6} color={outZoneBot} />
|
||||
</div>
|
||||
</div>
|
||||
<p class="mb-6 text-[11px] text-gray-400">(you have full control over what i say and do)</p>
|
||||
</BlurFade>
|
||||
|
||||
{#if accent}
|
||||
<BlurFade delay={0.05} once>
|
||||
<p
|
||||
class="bg-gradient-to-r from-blue-600 via-fuchsia-500 to-orange-400 bg-clip-text text-[clamp(1.4rem,2.2vw,2rem)] leading-[1.1] font-medium tracking-tight text-transparent"
|
||||
>
|
||||
{accent}
|
||||
</p>
|
||||
</BlurFade>
|
||||
{/if}
|
||||
|
||||
<BlurFade delay={0.1} once>
|
||||
<h2
|
||||
class="mx-auto max-w-[820px] text-[clamp(1.75rem,3.4vw,2.6rem)] leading-[1.1] font-medium tracking-tight text-gray-950"
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
</BlurFade>
|
||||
|
||||
<BlurFade delay={0.2} once>
|
||||
<p class="mx-auto mt-4 max-w-[600px] text-sm leading-relaxed text-gray-500">
|
||||
{description}
|
||||
</p>
|
||||
</BlurFade>
|
||||
|
||||
{#if ctaLabel}
|
||||
<BlurFade delay={0.3} once>
|
||||
<div class="mt-6 flex justify-center">
|
||||
<a
|
||||
href={ctaHref}
|
||||
class="inline-flex items-center gap-2 rounded-full bg-gray-950 px-5 py-2.5 text-sm font-medium text-white no-underline transition-all hover:-translate-y-0.5 hover:bg-blue-700"
|
||||
>
|
||||
{ctaLabel}
|
||||
<ArrowRight size={14} />
|
||||
</a>
|
||||
</div>
|
||||
</BlurFade>
|
||||
{/if}
|
||||
|
||||
{#if !imageSrc && !vimeoVideoId && showImage}
|
||||
<BlurFade delay={0.35} once>
|
||||
<div class="mx-auto mt-10 w-full max-w-[820px]">
|
||||
<ImagePlaceholder suggestedFile={imagePath} caption={imageCaption} aspect="aspect-[16/9]" />
|
||||
</div>
|
||||
</BlurFade>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if imageSrc}
|
||||
<BlurFade delay={0.35} once>
|
||||
<div class="mx-auto mt-10 w-full max-w-[1100px]">
|
||||
<div class="overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-gray-200">
|
||||
<img src={imageSrc} alt={imageAlt} class="block h-auto w-full" loading="lazy" decoding="async" />
|
||||
</div>
|
||||
</div>
|
||||
</BlurFade>
|
||||
{:else if vimeoVideoId}
|
||||
<BlurFade delay={0.35} once>
|
||||
<div class="mx-auto mt-10 w-full max-w-[960px] overflow-hidden rounded-lg">
|
||||
<div style="padding:{videoAspect} 0 0 0;position:relative;">
|
||||
<iframe
|
||||
src="https://player.vimeo.com/video/{vimeoVideoId}?badge=0&autopause=0&player_id=0&app_id=58479&autoplay=1&muted=1&loop=1"
|
||||
frameborder="0"
|
||||
allow="autoplay; fullscreen; picture-in-picture; clipboard-write; encrypted-media; web-share"
|
||||
referrerpolicy="strict-origin-when-cross-origin"
|
||||
style="position:absolute;top:0;left:0;width:100%;height:100%;"
|
||||
title={videoTitle}
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</BlurFade>
|
||||
{/if}
|
||||
</section>
|
||||
@@ -7,16 +7,14 @@
|
||||
'Full API access to every object in your LMS. Create users, enroll learners, pull completions. All programmatically, at scale.'
|
||||
},
|
||||
{
|
||||
icon: '⚡',
|
||||
title: 'Zapier Integration',
|
||||
comingSoon: true,
|
||||
icon: '🤖',
|
||||
title: 'MCP server',
|
||||
description:
|
||||
'Connect to 6,000+ apps without writing a line of code. Auto-enroll from your CRM, send Slack alerts on completion, update your spreadsheet.'
|
||||
'Drive course authoring from Cursor, Claude Code, Codex, or OpenCode. The MCP server exposes the API surface as agent tools — list, create, update, publish.'
|
||||
},
|
||||
{
|
||||
icon: '🪝',
|
||||
title: 'Webhooks',
|
||||
comingSoon: true,
|
||||
description:
|
||||
'Real-time event notifications for every action. Course enrolled, lesson completed, certificate issued. Push to any endpoint instantly.'
|
||||
}
|
||||
@@ -40,15 +38,7 @@
|
||||
class="rounded-2xl border border-gray-200 bg-white p-8 transition-all duration-200 hover:border-gray-950 hover:shadow-lg hover:shadow-black/5"
|
||||
>
|
||||
<div class="mb-4 text-3xl">{item.icon}</div>
|
||||
<h3 class="mb-2 text-base font-medium">
|
||||
{item.title}
|
||||
{#if item.comingSoon}
|
||||
<span
|
||||
class="ml-1 rounded-full bg-blue-700 px-1.5 py-0.5 align-middle text-[0.6rem] tracking-wide text-white uppercase"
|
||||
>Soon</span
|
||||
>
|
||||
{/if}
|
||||
</h3>
|
||||
<h3 class="mb-2 text-base font-medium">{item.title}</h3>
|
||||
<p class="text-sm leading-relaxed text-gray-500">{item.description}</p>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<script>
|
||||
import BoxedGrid from './boxed-grid.svelte';
|
||||
import Webhook from '@lucide/svelte/icons/webhook';
|
||||
import Code2 from '@lucide/svelte/icons/code-2';
|
||||
import Sparkles from '@lucide/svelte/icons/sparkles';
|
||||
|
||||
const lanes = [
|
||||
{
|
||||
Icon: Code2,
|
||||
title: 'REST API',
|
||||
description: 'Every object, reachable over HTTP. Provision users, pull completions, build your own tools.'
|
||||
},
|
||||
{
|
||||
Icon: Webhook,
|
||||
title: 'Webhooks',
|
||||
description: 'Signed events on enrollment, completion, certificate. Retries on failure.'
|
||||
},
|
||||
{
|
||||
Icon: Sparkles,
|
||||
title: 'MCP server',
|
||||
description: 'Course authoring as agent tools. Works with Cursor, Claude Code, Codex, OpenCode.'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<section class="bg-gray-50 px-6 py-12 lg:px-12 lg:py-16">
|
||||
<div class="mx-auto max-w-[1100px]">
|
||||
<div class="mb-8 max-w-[640px]">
|
||||
<div class="mb-2 text-xs font-medium tracking-widest text-blue-700 uppercase">For developers</div>
|
||||
<h2 class="text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight">API, Webhooks, MCP.</h2>
|
||||
<p class="mt-3 text-sm leading-relaxed text-gray-500">
|
||||
No middleware in between. Wire it into the stack you already have.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<BoxedGrid items={lanes} cols={3} padding="p-5 lg:p-6">
|
||||
{#snippet cell(lane)}
|
||||
<div class="mb-3 flex h-9 w-9 items-center justify-center rounded-xl bg-blue-50 text-blue-700">
|
||||
<lane.Icon size={16} strokeWidth={1.75} />
|
||||
</div>
|
||||
<h3 class="text-sm font-medium text-gray-950 lg:text-base">{lane.title}</h3>
|
||||
<p class="mt-1.5 text-xs leading-relaxed text-gray-500 lg:text-sm">{lane.description}</p>
|
||||
{/snippet}
|
||||
</BoxedGrid>
|
||||
|
||||
<div class="mt-6">
|
||||
<a
|
||||
href="/automation"
|
||||
class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-5 py-2.5 text-sm font-medium text-gray-950 no-underline transition-all hover:border-gray-950"
|
||||
>
|
||||
See the developer reference →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script>
|
||||
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
|
||||
|
||||
/**
|
||||
* Boxed grid of cards — one outer rounded border, internal dividers between cells.
|
||||
* Reused by RoiBand, AutomationBand, customer-ed outcomes, etc.
|
||||
*
|
||||
* @typedef {Object} Props
|
||||
* @property {any[]} items
|
||||
* @property {import('svelte').Snippet<[any, number]>} cell Snippet that renders one cell's content
|
||||
* @property {2 | 3 | 4} [cols] Desktop column count (default 3)
|
||||
* @property {string} [bgClass] Background class for the container (default bg-white)
|
||||
* @property {string} [padding] Cell padding (default "p-6 lg:p-8")
|
||||
* @property {boolean} [animate] Wrap each cell in BlurFade (default true)
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let { items, cell, cols = 3, bgClass = 'bg-white', padding = 'p-6 lg:p-8', animate = true } = $props();
|
||||
|
||||
const colsClass = {
|
||||
2: 'sm:grid-cols-2',
|
||||
3: 'sm:grid-cols-3',
|
||||
4: 'sm:grid-cols-4'
|
||||
}[cols];
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-1 overflow-hidden rounded-2xl border border-gray-200 {bgClass} {colsClass}">
|
||||
{#each items as item, i (i)}
|
||||
{#if animate}
|
||||
<BlurFade delay={0.05 * i} once>
|
||||
<div
|
||||
class="h-full border-gray-200 {padding}"
|
||||
class:border-t={i > 0}
|
||||
class:sm:border-l={i % cols !== 0}
|
||||
class:sm:border-t-0={i < cols}
|
||||
class:sm:border-t={i >= cols}
|
||||
>
|
||||
{@render cell(item, i)}
|
||||
</div>
|
||||
</BlurFade>
|
||||
{:else}
|
||||
<div
|
||||
class="h-full border-gray-200 {padding}"
|
||||
class:border-t={i > 0}
|
||||
class:sm:border-l={i % cols !== 0}
|
||||
class:sm:border-t-0={i < cols}
|
||||
class:sm:border-t={i >= cols}
|
||||
>
|
||||
{@render cell(item, i)}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script>
|
||||
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
|
||||
import ImagePlaceholder from './image-placeholder.svelte';
|
||||
</script>
|
||||
|
||||
<section class="bg-white px-6 py-16 lg:px-12 lg:py-24">
|
||||
<div class="mx-auto max-w-[1100px]">
|
||||
<BlurFade delay={0} once>
|
||||
<div class="mb-10 max-w-[620px]">
|
||||
<div class="mb-3 text-xs font-medium tracking-widest text-blue-700 uppercase">Certificate templates</div>
|
||||
<h2 class="text-[clamp(2rem,3vw,2.8rem)] leading-[1.15] font-medium tracking-tight">
|
||||
Design certificates that
|
||||
<em class="text-blue-700 not-italic">look like yours.</em>
|
||||
</h2>
|
||||
<p class="mt-5 text-base leading-relaxed text-gray-500">
|
||||
A real design canvas — drag elements, swap brand colours and fonts, configure the ID format. Issue PDFs and
|
||||
PNGs learners actually want to share.
|
||||
</p>
|
||||
</div>
|
||||
</BlurFade>
|
||||
|
||||
<BlurFade delay={0.1} once>
|
||||
<ImagePlaceholder
|
||||
suggestedFile="/static/certificate-editor.png"
|
||||
caption="Screenshot of the certificate template editor"
|
||||
aspect="aspect-[16/9]"
|
||||
/>
|
||||
</BlurFade>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,69 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { renderCertificateDocument, DEFAULT_CERTIFICATE_DESIGN } from '@cio/certificates';
|
||||
|
||||
/**
|
||||
* Renders the default ClassroomIO certificate template (classique) in an iframe.
|
||||
* Used on customer-education and compliance-training pages.
|
||||
*
|
||||
* @typedef {Object} Props
|
||||
* @property {string} recipient Person's name
|
||||
* @property {string} achievement Course or program name
|
||||
* @property {string} issued Issue date (e.g. "11 May 2026")
|
||||
* @property {string} verifyValue Certificate ID rendered in the seal
|
||||
* @property {string} [orgName] Org name shown on the top tag (default "ClassroomIO")
|
||||
* @property {string} [courseDescription] Body description (default derived from achievement)
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let {
|
||||
recipient,
|
||||
achievement,
|
||||
issued,
|
||||
verifyValue,
|
||||
orgName = 'ClassroomIO',
|
||||
courseDescription = `For successfully completing ${achievement}.`
|
||||
} = $props();
|
||||
|
||||
const srcdoc = renderCertificateDocument(DEFAULT_CERTIFICATE_DESIGN, {
|
||||
recipientName: recipient,
|
||||
courseName: achievement,
|
||||
courseDescription,
|
||||
orgName,
|
||||
date: issued,
|
||||
certificateId: verifyValue
|
||||
});
|
||||
|
||||
const NATIVE_W = 1100;
|
||||
const NATIVE_H = 780;
|
||||
|
||||
/** @type {HTMLDivElement | null} */
|
||||
let stageEl = $state(null);
|
||||
let scale = $state(0.5);
|
||||
|
||||
onMount(() => {
|
||||
if (!stageEl || typeof ResizeObserver === 'undefined') return;
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
if (!stageEl) return;
|
||||
const rect = stageEl.getBoundingClientRect();
|
||||
if (rect.width > 0) scale = rect.width / NATIVE_W;
|
||||
});
|
||||
observer.observe(stageEl);
|
||||
return () => observer.disconnect();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={stageEl}
|
||||
class="relative overflow-hidden rounded-2xl border border-gray-200 shadow-2xl/10"
|
||||
style="aspect-ratio: {NATIVE_W} / {NATIVE_H};"
|
||||
>
|
||||
<iframe
|
||||
title="Certificate preview"
|
||||
sandbox="allow-same-origin"
|
||||
{srcdoc}
|
||||
class="absolute top-0 left-0 border-0"
|
||||
style="width: {NATIVE_W}px; height: {NATIVE_H}px; transform: scale({scale}); transform-origin: top left;"
|
||||
></iframe>
|
||||
</div>
|
||||
@@ -0,0 +1,89 @@
|
||||
<script>
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {string} fileName Filename shown in chrome (e.g. "mcp.json")
|
||||
* @property {string[]} codeLines HTML-formatted source lines (one entry per visual line)
|
||||
* @property {string} [lang] Optional language badge (e.g. "JSON", "HTML", "TypeScript")
|
||||
* @property {string} [statusBar] Optional status bar text
|
||||
* @property {boolean} [copyable] Show a Copy button (default true)
|
||||
* @property {boolean} [showLineNumbers] Show line-number gutter (default false)
|
||||
* @property {string} [class] Extra wrapper classes
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let {
|
||||
fileName,
|
||||
codeLines,
|
||||
lang = '',
|
||||
statusBar = '',
|
||||
copyable = true,
|
||||
showLineNumbers = false,
|
||||
class: className = ''
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="relative overflow-hidden rounded-2xl bg-[#0d1117] shadow-[0_20px_60px_-20px_rgba(0,0,0,0.6)] ring-1 ring-white/[0.08] {className}"
|
||||
>
|
||||
<!-- Editor chrome -->
|
||||
<div class="flex items-center justify-between border-b border-white/[0.06] bg-[#0a0e14] px-4 py-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-2.5 w-2.5 rounded-full bg-[#ff5f57]"></div>
|
||||
<div class="h-2.5 w-2.5 rounded-full bg-[#febc2e]"></div>
|
||||
<div class="h-2.5 w-2.5 rounded-full bg-[#28c840]"></div>
|
||||
<span class="cio-code-font ml-3 text-[12px] text-slate-500">{fileName}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if lang}
|
||||
<span
|
||||
class="cio-code-font rounded-md bg-white/[0.04] px-2 py-0.5 text-[10px] font-medium tracking-wider text-slate-400 uppercase"
|
||||
>{lang}</span
|
||||
>
|
||||
{/if}
|
||||
{#if copyable}
|
||||
<span
|
||||
class="cio-code-font cursor-pointer rounded-md bg-blue-500/10 px-2 py-0.5 text-[10px] font-medium tracking-wide text-blue-300 ring-1 ring-blue-500/20 transition-colors hover:bg-blue-500/15"
|
||||
>Copy</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="cio-code-font flex text-[13px] leading-[1.6] text-slate-300">
|
||||
{#if showLineNumbers}
|
||||
<div class="py-5 pr-3 pl-5 text-right text-slate-600 tabular-nums select-none">
|
||||
{#each codeLines as _, n}
|
||||
<div>{n + 1}</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-1 overflow-x-auto whitespace-pre {showLineNumbers ? 'pr-5' : 'px-5'} py-5">
|
||||
{#each codeLines as line}
|
||||
<div>{@html line || ' '}</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if statusBar}
|
||||
<div class="cio-code-font border-t border-white/[0.06] bg-black/40 px-4 py-2 text-[10.5px] text-slate-500">
|
||||
<span class="text-blue-300">●</span>
|
||||
{statusBar}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* JetBrains Mono loaded via the Google Fonts link in src/app.html. System stack as fallback. */
|
||||
/* :global(...*) forces the font onto every descendant — including {@html}-rendered
|
||||
spans that don't carry Svelte's scope hash, and overrides any stray universal rules. */
|
||||
.cio-code-font,
|
||||
:global(.cio-code-font *) {
|
||||
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
|
||||
font-feature-settings:
|
||||
'liga' 0,
|
||||
'calt' 0;
|
||||
font-variant-ligatures: none;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,102 @@
|
||||
<script>
|
||||
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
|
||||
import Code2 from '@lucide/svelte/icons/code-2';
|
||||
import CodeBlock from './code-block.svelte';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Capability
|
||||
* @property {string} label
|
||||
* @property {string} sub
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {string} eyebrow Eyebrow label (e.g. "Embed anywhere")
|
||||
* @property {string} title Section heading
|
||||
* @property {string} description Body paragraph
|
||||
* @property {string} fileName Filename shown in code-editor chrome (e.g. "partner-portal.html")
|
||||
* @property {string[]} codeLines HTML-formatted lines for the code body (each may contain tag spans)
|
||||
* @property {string} statusBar Bottom status line text
|
||||
* @property {string} previewUrl URL shown in the preview browser bar
|
||||
* @property {string} previewImageSrc Static path for the preview image
|
||||
* @property {string} [previewAlt] Alt text for the preview image
|
||||
* @property {Capability[]} [capabilities] Optional 3 capability bullets shown below the pair
|
||||
* @property {string} [bgClass] Section background (default bg-white)
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let {
|
||||
eyebrow,
|
||||
title,
|
||||
description,
|
||||
fileName,
|
||||
codeLines,
|
||||
statusBar,
|
||||
previewUrl,
|
||||
previewImageSrc,
|
||||
previewAlt = '',
|
||||
capabilities = [],
|
||||
bgClass = 'bg-white'
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<section class="px-6 py-16 lg:px-12 lg:py-24 {bgClass}">
|
||||
<div class="mx-auto max-w-[1100px]">
|
||||
<!-- Intro -->
|
||||
<div class="mb-12 grid grid-cols-1 gap-10 lg:grid-cols-[0.85fr_1.15fr] lg:gap-20">
|
||||
<BlurFade delay={0} once>
|
||||
<div>
|
||||
<div class="mb-3 inline-flex items-center gap-2 text-xs font-medium tracking-widest text-blue-700 uppercase">
|
||||
<Code2 size={14} />
|
||||
{eyebrow}
|
||||
</div>
|
||||
<h2 class="text-[clamp(2rem,3vw,2.6rem)] leading-[1.15] font-medium tracking-tight">{title}</h2>
|
||||
</div>
|
||||
</BlurFade>
|
||||
<BlurFade delay={0.1} once>
|
||||
<p class="text-base leading-relaxed text-gray-500">{description}</p>
|
||||
</BlurFade>
|
||||
</div>
|
||||
|
||||
<!-- Code → Preview pair -->
|
||||
<div class="grid grid-cols-1 items-center gap-6 lg:grid-cols-[0.95fr_1.05fr]">
|
||||
<!-- LEFT: Code editor -->
|
||||
<BlurFade delay={0.05} once>
|
||||
<CodeBlock {fileName} {codeLines} lang="HTML" {statusBar} />
|
||||
</BlurFade>
|
||||
|
||||
<!-- RIGHT: Preview -->
|
||||
<BlurFade delay={0.15} once>
|
||||
<div class="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-2xl/5">
|
||||
<!-- Browser chrome -->
|
||||
<div class="flex items-center gap-2 border-b border-gray-200 bg-white px-4 py-3">
|
||||
<div class="h-2 w-2 rounded-full bg-red-400"></div>
|
||||
<div class="h-2 w-2 rounded-full bg-yellow-400"></div>
|
||||
<div class="h-2 w-2 rounded-full bg-green-500"></div>
|
||||
<div class="ml-3 flex flex-1 items-center gap-2 rounded-md bg-gray-50 px-3 py-1.5 text-xs text-gray-500">
|
||||
<span class="text-green-600">●</span>
|
||||
{previewUrl}
|
||||
</div>
|
||||
<span class="rounded-full bg-blue-50 px-2 py-0.5 text-[10px] font-medium text-blue-700">Renders as →</span>
|
||||
</div>
|
||||
|
||||
<img src={previewImageSrc} alt={previewAlt} class="block h-auto w-full" loading="lazy" decoding="async" />
|
||||
</div>
|
||||
</BlurFade>
|
||||
</div>
|
||||
|
||||
<!-- Capabilities -->
|
||||
{#if capabilities.length}
|
||||
<div class="mt-8 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each capabilities as item, i}
|
||||
<BlurFade delay={0.05 * i} once>
|
||||
<div class="rounded-xl border border-gray-200 bg-white px-4 py-3">
|
||||
<p class="text-sm font-medium text-gray-950">{item.label}</p>
|
||||
<p class="text-xs text-gray-500">{item.sub}</p>
|
||||
</div>
|
||||
</BlurFade>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,6 +1,5 @@
|
||||
<script>
|
||||
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
|
||||
import { BorderBeam } from '@cio/ui/custom/animation/border-beam';
|
||||
import { DotPattern } from '@cio/ui/custom/animation/dot-pattern';
|
||||
|
||||
const features = [
|
||||
@@ -146,8 +145,6 @@
|
||||
|
||||
<div class="relative">
|
||||
<div class="relative overflow-hidden rounded-2xl border border-white/[0.08] bg-white/[0.03] p-6">
|
||||
<BorderBeam colorFrom="#3b82f6" colorTo="#0233BD" size={200} duration={8} />
|
||||
|
||||
<div class="mb-5 flex items-center gap-2">
|
||||
<div class="h-3 w-3 rounded-full bg-white/[0.15]"></div>
|
||||
<div class="h-3 w-3 rounded-full bg-white/[0.1]"></div>
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<script>
|
||||
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
|
||||
import ArrowRight from '@lucide/svelte/icons/arrow-right';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Feature
|
||||
* @property {any} Icon
|
||||
* @property {string} title
|
||||
* @property {string} description
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {string} eyebrow
|
||||
* @property {string} title
|
||||
* @property {string} [description]
|
||||
* @property {string} [ctaLabel]
|
||||
* @property {string} [ctaHref]
|
||||
* @property {Feature[]} features
|
||||
* @property {2|3|4} [columns] Desktop columns (default 3)
|
||||
* @property {string} [bgClass]
|
||||
*/
|
||||
|
||||
/** @type {Props & { headerAside?: import('svelte').Snippet }} */
|
||||
let {
|
||||
eyebrow,
|
||||
title,
|
||||
description,
|
||||
ctaLabel,
|
||||
ctaHref,
|
||||
features,
|
||||
columns = 3,
|
||||
bgClass = 'bg-white',
|
||||
headerAside
|
||||
} = $props();
|
||||
|
||||
// Per-columns class string so Tailwind picks up the arbitrary nth-child variants statically.
|
||||
const gridClass = {
|
||||
2: 'sm:grid-cols-2 [&>*:nth-child(2n)]:sm:border-r-0 [&>*:nth-last-child(-n+2)]:sm:border-b-0',
|
||||
3: 'sm:grid-cols-2 lg:grid-cols-3 [&>*:nth-child(2n)]:sm:border-r-0 [&>*:nth-child(2n)]:lg:border-r [&>*:nth-child(3n)]:lg:border-r-0 [&>*:nth-last-child(-n+2)]:sm:border-b-0 [&>*:nth-last-child(-n+3)]:lg:border-b-0',
|
||||
4: 'sm:grid-cols-2 lg:grid-cols-4 [&>*:nth-child(2n)]:sm:border-r-0 [&>*:nth-child(2n)]:lg:border-r [&>*:nth-child(4n)]:lg:border-r-0 [&>*:nth-last-child(-n+2)]:sm:border-b-0 [&>*:nth-last-child(-n+4)]:lg:border-b-0'
|
||||
}[columns];
|
||||
</script>
|
||||
|
||||
<section class="px-6 py-12 lg:px-12 lg:py-16 {bgClass}">
|
||||
<div class="mx-auto max-w-[1100px]">
|
||||
<div class="mb-10 grid grid-cols-1 gap-10 lg:grid-cols-[1.05fr_1fr] lg:gap-20">
|
||||
<div>
|
||||
<div class="mb-3 text-xs font-medium tracking-widest text-blue-700 uppercase">{eyebrow}</div>
|
||||
<h2 class="text-[clamp(2rem,3.2vw,2.75rem)] leading-[1.05] font-medium tracking-tight text-gray-950">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{#if headerAside}
|
||||
<div class="flex flex-col justify-start lg:pt-2">
|
||||
{@render headerAside()}
|
||||
</div>
|
||||
{:else if description || ctaLabel}
|
||||
<div class="flex flex-col justify-start lg:pt-2">
|
||||
{#if description}
|
||||
<p class="text-base leading-relaxed text-gray-600">{description}</p>
|
||||
{/if}
|
||||
{#if ctaLabel && ctaHref}
|
||||
<a
|
||||
href={ctaHref}
|
||||
class="mt-6 inline-flex w-fit items-center gap-2 text-sm font-medium text-blue-700 underline decoration-blue-300 underline-offset-4 transition-colors hover:text-blue-800 hover:decoration-blue-500"
|
||||
>
|
||||
{ctaLabel}
|
||||
<ArrowRight size={16} />
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded-2xl border border-gray-200 bg-white">
|
||||
<div class="grid grid-cols-1 {gridClass}">
|
||||
{#each features as feature, i}
|
||||
<div class="group relative border-b border-gray-200 transition-colors hover:bg-gray-50/60 sm:border-r">
|
||||
<BlurFade delay={0.04 * i} once>
|
||||
<div class="h-full p-6">
|
||||
<p class="font-mono text-[11px] tracking-[0.15em] text-gray-400">
|
||||
{String(i + 1).padStart(2, '0')}
|
||||
</p>
|
||||
<div class="mt-4 flex items-center gap-2.5">
|
||||
<span class="text-blue-700"><feature.Icon size={18} /></span>
|
||||
<h3 class="text-sm font-semibold text-gray-950">{feature.title}</h3>
|
||||
</div>
|
||||
<p class="mt-3 text-sm leading-relaxed text-gray-500">{feature.description}</p>
|
||||
</div>
|
||||
</BlurFade>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,84 +1,207 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
|
||||
import Palette from '@lucide/svelte/icons/palette';
|
||||
import Globe from '@lucide/svelte/icons/globe';
|
||||
import Code2 from '@lucide/svelte/icons/code-2';
|
||||
import Settings2 from '@lucide/svelte/icons/settings-2';
|
||||
import Lock from '@lucide/svelte/icons/lock';
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
const features = [
|
||||
{
|
||||
Icon: Palette,
|
||||
iconBg: 'bg-rose-50',
|
||||
iconColor: 'text-rose-600',
|
||||
title: 'Full Theming',
|
||||
description: 'Pick the colors, fonts, and logo. Your brand shows up everywhere your learners look.',
|
||||
tags: ['Custom colors', 'Logo upload', 'Font control']
|
||||
},
|
||||
{
|
||||
Icon: Globe,
|
||||
iconBg: 'bg-blue-50',
|
||||
iconColor: 'text-blue-600',
|
||||
title: 'Custom Domain',
|
||||
description:
|
||||
'Host the LMS on learn.yourcompany.com with SSL included. No ClassroomIO branding anywhere on the page.',
|
||||
tags: ['SSL included', 'White label', 'DNS setup guide']
|
||||
},
|
||||
{
|
||||
Icon: Code2,
|
||||
iconBg: 'bg-violet-50',
|
||||
iconColor: 'text-violet-600',
|
||||
title: 'Embed courses on your site',
|
||||
description:
|
||||
'Add one script tag to any page. Your courses render right there and send learners back to your LMS to enroll.',
|
||||
tags: ['One-line script', 'Auto-styled', 'Links to your domain']
|
||||
},
|
||||
{
|
||||
Icon: Settings2,
|
||||
iconBg: 'bg-emerald-50',
|
||||
iconColor: 'text-emerald-600',
|
||||
title: 'Deep LMS Customization',
|
||||
description:
|
||||
'The codebase is modern and readable. Your developers can change anything without fighting a plugin system.',
|
||||
tags: ['Modern stack', 'Clean codebase', 'Extensible']
|
||||
}
|
||||
const ROTATION_INTERVAL_MS = 3000;
|
||||
let selectedTheme = $state('blue');
|
||||
let intervalId = $state<NodeJS.Timeout | null>(null);
|
||||
|
||||
const themes = [
|
||||
{ id: 'blue', color: '#1d4ee2', bgClass: 'bg-blue-700/20', activeBorder: 'border-[#1d4ee2]' },
|
||||
{ id: 'rose', color: '#be1241', bgClass: 'bg-red-600/20', activeBorder: 'border-[#be1241]' },
|
||||
{ id: 'green', color: '#0c891b', bgClass: 'bg-green-600/20', activeBorder: 'border-[#0c891b]' },
|
||||
{ id: 'orange', color: '#cc4902', bgClass: 'bg-orange-600/20', activeBorder: 'border-[#cc4902]' },
|
||||
{ id: 'purple', color: '#9333ea', bgClass: 'bg-purple-600/20', activeBorder: 'border-purple-600' }
|
||||
];
|
||||
|
||||
function startRotation() {
|
||||
if (intervalId) return;
|
||||
|
||||
intervalId = setInterval(() => {
|
||||
const currentIndex = themes.findIndex((t) => t.id === selectedTheme);
|
||||
const nextIndex = (currentIndex + 1) % themes.length;
|
||||
selectedTheme = themes[nextIndex].id;
|
||||
}, ROTATION_INTERVAL_MS);
|
||||
}
|
||||
|
||||
function stopRotation() {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function selectTheme(themeId: string) {
|
||||
selectedTheme = themeId;
|
||||
stopRotation();
|
||||
}
|
||||
|
||||
startRotation();
|
||||
|
||||
onDestroy(() => {
|
||||
stopRotation();
|
||||
});
|
||||
</script>
|
||||
|
||||
<section class="bg-gray-50 px-6 py-16 lg:px-12 lg:py-24">
|
||||
<section class="bg-[#f5f4f0] px-6 py-12 lg:px-12 lg:py-16">
|
||||
<div class="mx-auto max-w-[1100px]">
|
||||
<BlurFade once>
|
||||
<div class="mb-3.5 text-xs font-medium tracking-widest text-blue-700 uppercase">Make it yours</div>
|
||||
<h2 class="mb-4 text-[clamp(2rem,3vw,2.8rem)] leading-[1.15] font-medium tracking-tight">
|
||||
Your brand.<br />Your domain. Your rules.
|
||||
<h2
|
||||
class="max-w-[820px] text-[clamp(1.75rem,2.8vw,2.4rem)] leading-[1.1] font-medium tracking-tight text-gray-950"
|
||||
>
|
||||
Make it yours,<br />down to the favicon.
|
||||
</h2>
|
||||
<p class="mb-14 max-w-[520px] text-base leading-relaxed text-gray-500 lg:text-lg">
|
||||
ClassroomIO bends to fit your organization, not the other way around.
|
||||
<p class="mt-3 max-w-[620px] text-sm leading-relaxed text-gray-500">
|
||||
Your brand, your domain, your content. ClassroomIO bends to fit the academy you want to ship.
|
||||
</p>
|
||||
</BlurFade>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
{#each features as feature, i}
|
||||
<BlurFade delay={0.1 * i} once>
|
||||
<div class="rounded-2xl border border-gray-200 bg-white p-8 lg:p-9">
|
||||
<div
|
||||
class="mb-5 flex h-12 w-12 items-center justify-center rounded-xl {feature.iconBg} {feature.iconColor}"
|
||||
>
|
||||
<feature.Icon class="h-6 w-6" strokeWidth={1.75} />
|
||||
</div>
|
||||
<h3 class="mb-2.5 text-xl font-medium tracking-tight">{feature.title}</h3>
|
||||
<p class="mb-5 text-sm leading-relaxed text-gray-500">{feature.description}</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each feature.tags as tag}
|
||||
<span
|
||||
class="rounded-full border border-gray-200 bg-gray-50 px-2.5 py-1 text-xs font-medium text-gray-500"
|
||||
<div class="mt-8 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<!-- Theme + branding -->
|
||||
<BlurFade delay={0.05} once>
|
||||
<div
|
||||
class="flex h-full flex-col rounded-2xl border border-gray-200/80 p-6 transition-colors duration-500 lg:p-7 {themes.find(
|
||||
(t) => t.id === selectedTheme
|
||||
)?.bgClass}"
|
||||
>
|
||||
<h3 class="text-base font-medium tracking-tight text-gray-950 lg:text-lg">Theme everything</h3>
|
||||
<p class="mt-2 max-w-[420px] text-sm leading-relaxed text-gray-500">
|
||||
Pick a theme color or drop in your own hex. Every page picks it up.
|
||||
</p>
|
||||
|
||||
<!-- Visual: actual theme swatches from the app -->
|
||||
<div class="mt-8 flex flex-1 flex-col justify-end">
|
||||
<div class="flex flex-wrap items-center gap-2.5">
|
||||
{#each themes as theme (theme.id)}
|
||||
<button
|
||||
type="button"
|
||||
class="flex size-8 cursor-pointer items-center justify-center rounded-full border-2 bg-white transition-all {selectedTheme ===
|
||||
theme.id
|
||||
? theme.activeBorder
|
||||
: 'border-transparent'}"
|
||||
aria-label="{theme.id} theme"
|
||||
onclick={() => selectTheme(theme.id)}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
<span class="size-5 rounded-full" style="background-color: {theme.color}"></span>
|
||||
</button>
|
||||
{/each}
|
||||
<div
|
||||
class="flex h-9 w-9 items-center justify-center rounded-full border-2 border-dashed border-gray-400 text-gray-500"
|
||||
aria-label="Custom hex"
|
||||
>
|
||||
+
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BlurFade>
|
||||
{/each}
|
||||
</div>
|
||||
</BlurFade>
|
||||
|
||||
<!-- Custom domain -->
|
||||
<BlurFade delay={0.1} once>
|
||||
<div class="flex h-full flex-col rounded-2xl border border-gray-200/80 bg-[#ecebe6] p-6 lg:p-7">
|
||||
<h3 class="text-base font-medium tracking-tight text-gray-950 lg:text-lg">Your domain</h3>
|
||||
<p class="mt-2 max-w-[420px] text-sm leading-relaxed text-gray-500">
|
||||
Map a CNAME, click verify. SSL is handled. No ClassroomIO branding on the page.
|
||||
</p>
|
||||
|
||||
<!-- Visual: browser bar mock -->
|
||||
<div class="mt-8 flex flex-1 flex-col justify-end">
|
||||
<div class="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm">
|
||||
<div class="flex items-center gap-2 border-b border-gray-100 bg-white px-3 py-2.5">
|
||||
<div class="h-2 w-2 rounded-full bg-red-400"></div>
|
||||
<div class="h-2 w-2 rounded-full bg-yellow-400"></div>
|
||||
<div class="h-2 w-2 rounded-full bg-green-500"></div>
|
||||
<div
|
||||
class="ml-2 flex flex-1 items-center gap-1.5 rounded-md bg-gray-50 px-2.5 py-1 text-[11px] text-gray-600"
|
||||
>
|
||||
<Lock size={10} class="text-green-600" />
|
||||
<span class="font-mono">learn.acme.com</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-1.5 p-3">
|
||||
<div class="h-12 rounded-md bg-gradient-to-br from-blue-200 to-blue-400"></div>
|
||||
<div class="h-12 rounded-md bg-gradient-to-br from-indigo-200 to-blue-500"></div>
|
||||
<div class="h-12 rounded-md bg-gradient-to-br from-sky-200 to-cyan-400"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BlurFade>
|
||||
|
||||
<!-- Embed -->
|
||||
<BlurFade delay={0.15} once>
|
||||
<div class="flex h-full flex-col rounded-2xl border border-gray-200/80 bg-[#ecebe6] p-6 lg:p-7">
|
||||
<h3 class="text-base font-medium tracking-tight text-gray-950 lg:text-lg">Embed anywhere</h3>
|
||||
<p class="mt-2 max-w-[420px] text-sm leading-relaxed text-gray-500">
|
||||
One script tag. Drop courses into your help center, in-product, or marketing site.
|
||||
</p>
|
||||
|
||||
<!-- Visual: code snippet -->
|
||||
<div class="mt-8 flex flex-1 flex-col justify-end">
|
||||
<div
|
||||
class="overflow-hidden rounded-xl border border-gray-800/90 bg-gray-950 p-4 font-mono text-[11px] leading-relaxed text-gray-300 shadow-sm"
|
||||
>
|
||||
<p class="text-gray-500"><!-- in your help center --></p>
|
||||
<p class="mt-1.5">
|
||||
<span class="text-pink-400"><script</span>
|
||||
<span class="text-sky-300"> src</span>=<span class="text-emerald-300">"cdn.cio/widget.js"</span><span
|
||||
class="text-pink-400">></script></span
|
||||
>
|
||||
</p>
|
||||
<p class="mt-1.5">
|
||||
<span class="text-pink-400"><div</span>
|
||||
<span class="text-sky-300"> data-cio-widget</span>=<span class="text-emerald-300">"onboarding"</span
|
||||
><span class="text-pink-400">></div></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BlurFade>
|
||||
|
||||
<!-- Open source / deep customization -->
|
||||
<BlurFade delay={0.2} once>
|
||||
<div class="flex h-full flex-col rounded-2xl border border-gray-200/80 bg-[#ecebe6] p-6 lg:p-7">
|
||||
<h3 class="text-base font-medium tracking-tight text-gray-950 lg:text-lg">Fork the codebase</h3>
|
||||
<p class="mt-2 max-w-[420px] text-sm leading-relaxed text-gray-500">
|
||||
Modern stack, readable codebase, AGPL on GitHub. Change whatever you need.
|
||||
</p>
|
||||
|
||||
<!-- Visual: stylised file tree / stack -->
|
||||
<div class="mt-8 flex flex-1 flex-col justify-end">
|
||||
<div class="space-y-1.5">
|
||||
<div
|
||||
class="flex items-center gap-2 rounded-md bg-white/70 px-3 py-2 text-[11px] font-medium text-gray-700 ring-1 ring-gray-200"
|
||||
>
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-emerald-500"></span>
|
||||
<span class="font-mono">apps/dashboard</span>
|
||||
<span class="ml-auto text-gray-400">Svelte 5</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-2 rounded-md bg-white/70 px-3 py-2 text-[11px] font-medium text-gray-700 ring-1 ring-gray-200"
|
||||
>
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-blue-500"></span>
|
||||
<span class="font-mono">apps/api</span>
|
||||
<span class="ml-auto text-gray-400">Hono · Drizzle</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-2 rounded-md bg-white/70 px-3 py-2 text-[11px] font-medium text-gray-700 ring-1 ring-gray-200"
|
||||
>
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-fuchsia-500"></span>
|
||||
<span class="font-mono">packages/ai-assistant</span>
|
||||
<span class="ml-auto text-gray-400">Claude</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-2 rounded-md bg-white/70 px-3 py-2 text-[11px] font-medium text-gray-700 ring-1 ring-gray-200"
|
||||
>
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-amber-500"></span>
|
||||
<span class="font-mono">packages/mcp</span>
|
||||
<span class="ml-auto text-gray-400">MCP</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BlurFade>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,128 +1,210 @@
|
||||
<script>
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import Facebook from '@lucide/svelte/icons/facebook';
|
||||
import Youtube from '@lucide/svelte/icons/youtube';
|
||||
import Twitter from '@lucide/svelte/icons/twitter';
|
||||
</script>
|
||||
|
||||
<div class="border-t px-[2%] lg:px-[14%]">
|
||||
<div class="flex flex-col items-center justify-between py-[10%] pl-5 lg:flex-row lg:pl-0">
|
||||
<div class="w-full min-w-[200px] lg:w-3/12">
|
||||
<img width="32" height="32" loading="lazy" src="/logo-192.png" alt="classroomio logo" class="mb-5" />
|
||||
<p class="mr-2 mb-5 text-sm leading-7 tracking-wide text-gray-600 lg:text-base lg:leading-7">
|
||||
The Open Source Learning Management System for Companies
|
||||
<br />
|
||||
The LMS for compliance training and certification programs.
|
||||
<footer
|
||||
class="ui:border-border relative overflow-hidden border-t bg-[linear-gradient(to_top,#eef3fb_0%,#f8fafc_100%)] px-[5%] py-12 lg:px-[10%]"
|
||||
>
|
||||
<!-- Minor cloud — top-left, smaller than the hero -->
|
||||
<svg
|
||||
class="pointer-events-none absolute -top-4 left-3 z-[2] h-12 w-[160px] md:-top-5 md:left-4 md:h-16 md:w-[210px] lg:-top-6 lg:h-20 lg:w-[260px]"
|
||||
viewBox="0 0 240 110"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M 22 80
|
||||
C 22 60, 42 50, 62 56
|
||||
C 68 36, 100 30, 116 52
|
||||
C 130 32, 168 38, 174 60
|
||||
C 196 56, 216 68, 214 82
|
||||
C 214 96, 196 102, 174 96
|
||||
C 164 106, 130 106, 120 96
|
||||
C 110 106, 78 106, 66 94
|
||||
C 46 100, 22 96, 22 80 Z"
|
||||
fill="white"
|
||||
stroke="#94a3b8"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!-- Second smaller cloud, offset right -->
|
||||
<svg
|
||||
class="pointer-events-none absolute top-3 left-32 z-[2] h-9 w-[110px] md:top-4 md:left-44 md:h-12 md:w-[140px] lg:top-6 lg:left-[200px] lg:h-14 lg:w-[180px]"
|
||||
viewBox="0 0 200 90"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M 16 64
|
||||
C 16 48, 34 40, 52 46
|
||||
C 58 30, 86 26, 100 44
|
||||
C 114 28, 144 34, 150 52
|
||||
C 168 48, 184 58, 182 70
|
||||
C 182 82, 168 86, 150 82
|
||||
C 142 90, 114 90, 106 82
|
||||
C 98 90, 70 90, 60 80
|
||||
C 42 86, 16 82, 16 64 Z"
|
||||
fill="white"
|
||||
stroke="#94a3b8"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div class="relative z-10 flex flex-col gap-10 lg:flex-row lg:justify-between">
|
||||
<!-- Brand -->
|
||||
<div class="flex flex-col gap-4 lg:max-w-[220px]">
|
||||
<img width="32" height="32" loading="lazy" src="/logo-192.png" alt="ClassroomIO logo" />
|
||||
<p class="ui:text-muted-foreground text-sm leading-6">
|
||||
The Open Source LMS for compliance training and certification programs.
|
||||
</p>
|
||||
<!-- <a
|
||||
href="https://www.producthunt.com/posts/classroomio?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-classroomio"
|
||||
target="_blank"
|
||||
><img
|
||||
loading="lazy"
|
||||
src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=425236&theme=light"
|
||||
alt="ClassroomIO - The Open Source Platform for Tech Bootcamps | Product Hunt"
|
||||
style="width: 250px; height: 54px;"
|
||||
width="250"
|
||||
height="54"
|
||||
/></a
|
||||
> -->
|
||||
<div class="mt-5 flex items-center gap-3 lg:mt-10 lg:gap-7">
|
||||
<Button
|
||||
<div class="flex items-center gap-2">
|
||||
<a
|
||||
href="https://www.facebook.com/classroomiohq"
|
||||
target="_blank"
|
||||
variant="ghost"
|
||||
aria-label="Facebook Link"
|
||||
aria-label="Facebook"
|
||||
class="ui:text-muted-foreground ui:hover:text-foreground p-1 transition-colors"
|
||||
>
|
||||
<Facebook />
|
||||
</Button>
|
||||
<Button href="https://twitter.com/classroomio" target="_blank" variant="ghost" aria-label="Twitter Link">
|
||||
<img width="20" height="20" loading="lazy" src="/twitter.svg" alt="" />
|
||||
</Button>
|
||||
<Button href="https://www.youtube.com/@ClassroomIO" target="_blank" variant="ghost" aria-label="Youtube Link">
|
||||
<Youtube />
|
||||
</Button>
|
||||
<Facebook size={18} />
|
||||
</a>
|
||||
<a
|
||||
href="https://twitter.com/classroomio"
|
||||
target="_blank"
|
||||
aria-label="Twitter"
|
||||
class="ui:text-muted-foreground ui:hover:text-foreground p-1 transition-colors"
|
||||
>
|
||||
<Twitter size={18} />
|
||||
</a>
|
||||
<a
|
||||
href="https://www.youtube.com/@ClassroomIO"
|
||||
target="_blank"
|
||||
aria-label="YouTube"
|
||||
class="ui:text-muted-foreground ui:hover:text-foreground p-1 transition-colors"
|
||||
>
|
||||
<Youtube size={18} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav
|
||||
class="mt-16 flex w-full flex-col items-start justify-end gap-12 space-y-6 lg:mt-0 lg:w-3/4 lg:flex-row lg:space-y-0"
|
||||
>
|
||||
<div class="max-w-[120px]">
|
||||
<p class="mb-3 text-base leading-6 font-semibold text-gray-900 lg:mb-10 lg:text-base">Product</p>
|
||||
<ul class="space-y-2 lg:space-y-4">
|
||||
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
|
||||
<a href="/#features"> Certifications </a>
|
||||
<!-- Nav columns -->
|
||||
<nav class="grid grid-cols-2 gap-8 sm:grid-cols-4">
|
||||
<div>
|
||||
<p class="ui:text-foreground mb-4 text-sm font-semibold">Product</p>
|
||||
<ul class="space-y-3">
|
||||
<li>
|
||||
<a href="/mcp-recipes" class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors"
|
||||
>MCP Recipes</a
|
||||
>
|
||||
</li>
|
||||
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
|
||||
<a href="/#ai"> AI Content </a>
|
||||
<li>
|
||||
<a href="/automation" class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors"
|
||||
>API & MCP</a
|
||||
>
|
||||
</li>
|
||||
<!-- <li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
|
||||
<a href="/#deploy"> Deploy </a>
|
||||
</li> -->
|
||||
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
|
||||
<a href="/#testimonial"> Testimonials </a>
|
||||
<li>
|
||||
<a href="/pricing" class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors"
|
||||
>Pricing</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="max-w-[120px]">
|
||||
<p class="mb-3 text-base leading-6 font-semibold text-gray-900 lg:mb-10 lg:text-base">Solutions</p>
|
||||
<ul class="space-y-2 lg:space-y-4">
|
||||
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
|
||||
<a href="/employee-training">Employee Training</a>
|
||||
<div>
|
||||
<p class="ui:text-foreground mb-4 text-sm font-semibold">Solutions</p>
|
||||
<ul class="space-y-3">
|
||||
<li>
|
||||
<a
|
||||
href="/customer-education"
|
||||
class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors">Customer Education</a
|
||||
>
|
||||
</li>
|
||||
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
|
||||
<a href="/bootcamps">Bootcamps</a>
|
||||
<li>
|
||||
<a
|
||||
href="/compliance-training"
|
||||
class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors">Compliance Training</a
|
||||
>
|
||||
</li>
|
||||
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
|
||||
<a href="/customer-education">Customer Education</a>
|
||||
<li>
|
||||
<a
|
||||
href="/partner-training"
|
||||
class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors">Partner Training</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="max-w-[120px]">
|
||||
<p class="mb-3 text-base leading-6 font-semibold text-gray-900 lg:mb-10 lg:text-base">Resources</p>
|
||||
<ul class="space-y-2 lg:space-y-4">
|
||||
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
|
||||
<a href="/contact">Contact us</a>
|
||||
<div>
|
||||
<p class="ui:text-foreground mb-4 text-sm font-semibold">Resources</p>
|
||||
<ul class="space-y-3">
|
||||
<li>
|
||||
<a
|
||||
href="/docs-vs-company-academy"
|
||||
class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors"
|
||||
>Docs vs Company Academy</a
|
||||
>
|
||||
</li>
|
||||
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
|
||||
<a href="/teach">Teach with us</a>
|
||||
<li>
|
||||
<a href="/teach" class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors"
|
||||
>Teach with us</a
|
||||
>
|
||||
</li>
|
||||
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
|
||||
<a target="_blank" href="https://classroomio.com/docs/"> Help center </a>
|
||||
<li>
|
||||
<a
|
||||
href="https://classroomio.com/docs/"
|
||||
target="_blank"
|
||||
class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors">Help Center</a
|
||||
>
|
||||
</li>
|
||||
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
|
||||
<a target="_blank" href="https://classroomio.com/docs/quickstart/signup"> Getting Started </a>
|
||||
<li>
|
||||
<a
|
||||
href="https://classroomio.com/docs/quickstart/signup"
|
||||
target="_blank"
|
||||
class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors">Getting Started</a
|
||||
>
|
||||
</li>
|
||||
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
|
||||
<a href="/oss-friends"> OSS Friends </a>
|
||||
<li>
|
||||
<a href="/oss-friends" class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors"
|
||||
>OSS Friends</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="max-w-[120px]">
|
||||
<p class="mb-3 text-base leading-6 font-semibold text-gray-900 lg:mb-10 lg:text-base">Company</p>
|
||||
<ul class="space-y-2 lg:space-y-4">
|
||||
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
|
||||
<a href="/roadmap"> Roadmap </a>
|
||||
<div>
|
||||
<p class="ui:text-foreground mb-4 text-sm font-semibold">Company</p>
|
||||
<ul class="space-y-3">
|
||||
<li>
|
||||
<a href="/roadmap" class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors"
|
||||
>Roadmap</a
|
||||
>
|
||||
</li>
|
||||
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
|
||||
<a href="mailto:[email protected]" target="_blank"> Support </a>
|
||||
<li>
|
||||
<a
|
||||
href="mailto:[email protected]"
|
||||
target="_blank"
|
||||
class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors">Support</a
|
||||
>
|
||||
</li>
|
||||
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
|
||||
<a href="/privacy"> Privacy policy </a>
|
||||
<li>
|
||||
<a href="/privacy" class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors"
|
||||
>Privacy Policy</a
|
||||
>
|
||||
</li>
|
||||
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
|
||||
<a target="_blank" href="https://classroomio.com/docs/quickstart/signup"> Getting Started </a>
|
||||
</li>
|
||||
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
|
||||
<a href="/tos"> Terms of use </a>
|
||||
<li>
|
||||
<a href="/tos" class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors"
|
||||
>Terms of Use</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
<footer class="border-t py-3 text-center text-xs leading-5 font-light text-gray-500 lg:text-left lg:text-base">
|
||||
<p class="mx-auto w-fit">All rights reserved - ClassroomIO © {new Date().getFullYear()}</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="ui:border-border ui:text-muted-foreground relative z-10 mt-10 border-t pt-6 text-center text-xs lg:text-left"
|
||||
>
|
||||
All rights reserved — ClassroomIO © {new Date().getFullYear()}
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<script>
|
||||
/**
|
||||
* Standard hero subtitle paragraph used across marketing pages.
|
||||
* Defaults: `mt-6 max-w-[640px] text-lg leading-relaxed text-gray-600`.
|
||||
* Pass `class` to override or extend (e.g. a different max-width).
|
||||
*
|
||||
* @typedef {Object} Props
|
||||
* @property {string} [class]
|
||||
* @property {import('svelte').Snippet} [children]
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let { class: className = '', children } = $props();
|
||||
</script>
|
||||
|
||||
<p class="mt-6 max-w-[640px] text-lg leading-relaxed text-gray-600 {className}">
|
||||
{@render children?.()}
|
||||
</p>
|
||||
@@ -1,139 +1,176 @@
|
||||
<script>
|
||||
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
|
||||
import { BorderBeam } from '@cio/ui/custom/animation/border-beam';
|
||||
import { Badge } from '@cio/ui/base/badge';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import ParticleField from './particle-field.svelte';
|
||||
</script>
|
||||
|
||||
<section class="px-6 pt-32 pb-16 lg:px-12 lg:pt-40 lg:pb-24">
|
||||
<div class="mx-auto grid max-w-[1100px] grid-cols-1 items-center gap-12 lg:grid-cols-2 lg:gap-20">
|
||||
<div>
|
||||
<BlurFade delay={0} once>
|
||||
<a href="/blog/learn-from-the-best" class="mb-7 inline-block">
|
||||
<Badge variant="secondary" class="gap-2! px-3.5! py-1.5! text-xs! underline!">
|
||||
<span class="relative flex h-2 w-2">
|
||||
<span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"></span>
|
||||
<span class="relative inline-flex h-2 w-2 rounded-full bg-green-500"></span>
|
||||
</span>
|
||||
Introducing ClassroomIO Version 2 →
|
||||
</Badge>
|
||||
</a>
|
||||
</BlurFade>
|
||||
<section class="relative overflow-hidden px-6 pt-32 pb-16 lg:px-12 lg:pt-40 lg:pb-24">
|
||||
<!-- Soft off-white base with the faintest blue undertone -->
|
||||
<div
|
||||
class="pointer-events-none absolute inset-0 bg-[linear-gradient(135deg,#f7f9fc_0%,#eef2fa_60%,#e5ecf7_100%)]"
|
||||
></div>
|
||||
|
||||
<BlurFade delay={0.1} once>
|
||||
<h1 class="mb-6 text-[clamp(2.4rem,4vw,3.4rem)] leading-[1.12] font-medium tracking-tight text-gray-950">
|
||||
The LMS for
|
||||
<em class="text-blue-700 not-italic">compliance training</em>
|
||||
and
|
||||
<em class="text-blue-700 not-italic">certification programs.</em>
|
||||
</h1>
|
||||
</BlurFade>
|
||||
<!-- Bordered clouds, top-left — crisp SVG silhouettes -->
|
||||
<div class="pointer-events-none absolute inset-0 z-[1] overflow-hidden">
|
||||
<!-- Cloud 1 (largest) -->
|
||||
<svg
|
||||
class="cio-cloud-1 absolute top-6 left-3 h-24 w-[220px] md:top-8 md:left-5 md:h-36 md:w-[340px] lg:top-10 lg:left-6 lg:h-48 lg:w-[460px]"
|
||||
viewBox="0 0 240 110"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M 22 80
|
||||
C 22 60, 42 50, 62 56
|
||||
C 68 36, 100 30, 116 52
|
||||
C 130 32, 168 38, 174 60
|
||||
C 196 56, 216 68, 214 82
|
||||
C 214 96, 196 102, 174 96
|
||||
C 164 106, 130 106, 120 96
|
||||
C 110 106, 78 106, 66 94
|
||||
C 46 100, 22 96, 22 80 Z"
|
||||
fill="white"
|
||||
stroke="#cbd5e1"
|
||||
stroke-width="0.75"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<BlurFade delay={0.2} once>
|
||||
<p class="mb-9 max-w-[480px] text-base leading-relaxed text-gray-500 lg:text-lg">
|
||||
ClassroomIO is the open source LMS built for compliance-driven teams. Run your certification programs, track
|
||||
completions, and prove training happened. No Moodle headaches.
|
||||
</p>
|
||||
</BlurFade>
|
||||
<!-- Cloud 2 (medium) -->
|
||||
<svg
|
||||
class="cio-cloud-2 absolute top-24 left-40 h-16 w-[150px] md:top-32 md:left-56 md:h-24 md:w-[230px] lg:top-40 lg:left-72 lg:h-36 lg:w-[320px]"
|
||||
viewBox="0 0 200 90"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M 16 64
|
||||
C 16 48, 34 40, 52 46
|
||||
C 58 30, 86 26, 100 44
|
||||
C 114 28, 144 34, 150 52
|
||||
C 168 48, 184 58, 182 70
|
||||
C 182 82, 168 86, 150 82
|
||||
C 142 90, 114 90, 106 82
|
||||
C 98 90, 70 90, 60 80
|
||||
C 42 86, 16 82, 16 64 Z"
|
||||
fill="white"
|
||||
stroke="#cbd5e1"
|
||||
stroke-width="0.75"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<BlurFade delay={0.3} once>
|
||||
<div class="flex flex-wrap items-center gap-3.5">
|
||||
<a
|
||||
href="/signup"
|
||||
class="inline-flex items-center gap-2 rounded-lg bg-gray-950 px-7 py-3.5 text-sm font-medium text-white no-underline transition-all duration-200 hover:-translate-y-0.5 hover:bg-blue-700 lg:text-base"
|
||||
>
|
||||
Get Started Free →
|
||||
</a>
|
||||
<button
|
||||
data-cal-link="classroomio/demo"
|
||||
data-cal-config="'layout':'month_view'"
|
||||
class="cursor-pointer rounded-lg border-[1.5px] border-gray-200 bg-transparent px-7 py-3.5 text-sm font-medium text-gray-950 transition-all duration-200 hover:border-gray-950 lg:text-base"
|
||||
>
|
||||
Book a Demo
|
||||
</button>
|
||||
</div>
|
||||
</BlurFade>
|
||||
<!-- Cloud 3 (small, lower) -->
|
||||
<svg
|
||||
class="cio-cloud-3 absolute top-40 left-16 h-14 w-[120px] md:top-56 md:left-20 md:h-20 md:w-[180px] lg:top-72 lg:left-28 lg:h-28 lg:w-[240px]"
|
||||
viewBox="0 0 180 80"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M 14 56
|
||||
C 14 42, 30 36, 44 40
|
||||
C 50 26, 76 22, 88 38
|
||||
C 100 24, 128 30, 134 46
|
||||
C 150 42, 166 52, 164 62
|
||||
C 164 72, 150 76, 134 72
|
||||
C 128 80, 100 80, 92 72
|
||||
C 84 80, 60 80, 52 70
|
||||
C 36 76, 14 72, 14 56 Z"
|
||||
fill="white"
|
||||
stroke="#cbd5e1"
|
||||
stroke-width="0.75"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<BlurFade delay={0.4} once>
|
||||
<div class="mt-7 flex flex-wrap items-center gap-5 text-xs text-gray-500">
|
||||
<span class="flex items-center gap-1.5">
|
||||
<span class="font-medium text-blue-700">✓</span> Open source
|
||||
</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<span class="font-medium text-blue-700">✓</span> Self-hosted
|
||||
</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<span class="font-medium text-blue-700">✓</span> No per-seat fees
|
||||
</span>
|
||||
</div>
|
||||
</BlurFade>
|
||||
</div>
|
||||
<!-- Particle cloud — same component used in the footer at lower intensity -->
|
||||
<ParticleField class="absolute inset-0 z-[2] h-full w-full" />
|
||||
|
||||
<!-- Bottom fade to next section -->
|
||||
<div
|
||||
class="pointer-events-none absolute right-0 bottom-0 left-0 h-32 bg-gradient-to-b from-transparent to-white"
|
||||
></div>
|
||||
|
||||
<div class="relative z-10 mx-auto flex max-w-[1100px] flex-col items-center text-center">
|
||||
<BlurFade delay={0} once>
|
||||
<h1 class="max-w-[820px] text-[clamp(2rem,4vw,3.25rem)] leading-[1.1] font-medium tracking-tight text-gray-950">
|
||||
Teach your customers your product
|
||||
<em class="text-blue-700 not-italic">before support has to.</em>
|
||||
</h1>
|
||||
</BlurFade>
|
||||
|
||||
<BlurFade delay={0.1} once>
|
||||
<p class="mt-5 max-w-[580px] text-sm leading-relaxed lg:text-base">
|
||||
Launch a branded customer academy in days. Train customers, certify power users, and drive adoption on your own
|
||||
domain, with an AI tutor in every lesson.
|
||||
</p>
|
||||
</BlurFade>
|
||||
|
||||
<!-- Hero Visual -->
|
||||
<BlurFade delay={0.2} once>
|
||||
<div class="relative overflow-hidden rounded-2xl border border-gray-200 bg-gray-50 shadow-2xl/5">
|
||||
<BorderBeam size={250} duration={12} colorFrom="#0233BD" colorTo="#6b8fff" delay={1} />
|
||||
<div class="flex items-center gap-2 border-b border-gray-200 bg-white px-4 py-3.5">
|
||||
<div class="h-2.5 w-2.5 rounded-full bg-red-400"></div>
|
||||
<div class="h-2.5 w-2.5 rounded-full bg-yellow-400"></div>
|
||||
<div class="h-2.5 w-2.5 rounded-full bg-green-500"></div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="mb-3.5 text-xs font-medium tracking-wider text-gray-500 uppercase">Team Progress - This Week</div>
|
||||
<div class="mt-7 flex flex-wrap items-center justify-center gap-3">
|
||||
<Button data-cal-config="'layout':'month_view'" data-cal-link="classroomio/demo">Book a Demo</Button>
|
||||
<Button href="/signup" variant="outline">Start free</Button>
|
||||
</div>
|
||||
</BlurFade>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 flex items-center gap-3.5 rounded-xl border border-gray-200 bg-white p-4">
|
||||
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-blue-50 text-lg">📋</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="mb-1 text-sm font-medium">Annual Compliance Training</div>
|
||||
<div class="h-1 overflow-hidden rounded-full bg-gray-200">
|
||||
<div class="h-full rounded-full bg-blue-700" style="width: 85%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="shrink-0 rounded-full bg-blue-50 px-2 py-0.5 text-[0.7rem] font-medium whitespace-nowrap text-blue-700"
|
||||
>
|
||||
85%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 flex items-center gap-3.5 rounded-xl border border-gray-200 bg-white p-4">
|
||||
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-green-50 text-lg">🛡️</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="mb-1 text-sm font-medium">Security & Compliance</div>
|
||||
<div class="h-1 overflow-hidden rounded-full bg-gray-200">
|
||||
<div class="h-full rounded-full bg-green-500" style="width: 100%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="shrink-0 rounded-full bg-green-50 px-2 py-0.5 text-[0.7rem] font-medium whitespace-nowrap text-green-600"
|
||||
>
|
||||
✓ Done
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 flex items-center gap-3.5 rounded-xl border border-gray-200 bg-white p-4">
|
||||
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-orange-50 text-lg">🎯</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="mb-1 text-sm font-medium">Product Certification</div>
|
||||
<div class="h-1 overflow-hidden rounded-full bg-gray-200">
|
||||
<div class="h-full rounded-full bg-blue-700" style="width: 42%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="shrink-0 rounded-full bg-blue-50 px-2 py-0.5 text-[0.7rem] font-medium whitespace-nowrap text-blue-700"
|
||||
>
|
||||
42%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center gap-3 rounded-lg bg-blue-50 p-4">
|
||||
<span class="text-xl">🏆</span>
|
||||
<div>
|
||||
<div class="text-sm font-medium">3 certificates issued today</div>
|
||||
<div class="text-xs text-gray-500">Sarah, Marcus & Priya completed their certs</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative z-30 mx-auto mt-14 w-full max-w-[1100px]">
|
||||
<BlurFade delay={0.3} once>
|
||||
<div class="overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-gray-200">
|
||||
<img
|
||||
src="/hero-academy.jpeg"
|
||||
alt="Branded customer academy on a custom domain — catalog, programs, AI tutor"
|
||||
width="2560"
|
||||
height="1440"
|
||||
class="block h-auto w-full"
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
</BlurFade>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.cio-cloud-1 {
|
||||
animation: cio-cloud-drift-1 22s ease-in-out infinite;
|
||||
}
|
||||
.cio-cloud-2 {
|
||||
animation: cio-cloud-drift-2 28s ease-in-out infinite;
|
||||
}
|
||||
.cio-cloud-3 {
|
||||
animation: cio-cloud-drift-3 26s ease-in-out infinite;
|
||||
}
|
||||
@keyframes cio-cloud-drift-1 {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
opacity: 0.9;
|
||||
}
|
||||
50% {
|
||||
transform: translate(28px, 10px) scale(1.04);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes cio-cloud-drift-2 {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
opacity: 0.85;
|
||||
}
|
||||
50% {
|
||||
transform: translate(36px, -8px) scale(1.06);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes cio-cloud-drift-3 {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
50% {
|
||||
transform: translate(-12px, 14px) scale(1.05);
|
||||
opacity: 0.95;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<script>
|
||||
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
|
||||
import ImagePlaceholder from './image-placeholder.svelte';
|
||||
|
||||
/**
|
||||
* Heading row + full-width image preview. Renders a real image when `imageSrc` is set,
|
||||
* otherwise drops back to `ImagePlaceholder` so unstaged sections still surface the gap.
|
||||
*
|
||||
* @typedef {Object} Props
|
||||
* @property {string} eyebrow
|
||||
* @property {string} title
|
||||
* @property {string} [description]
|
||||
* @property {string} [imageSrc] Real image path (omit to render placeholder).
|
||||
* @property {string} [imageAlt]
|
||||
* @property {string} [suggestedFile] Required when `imageSrc` is omitted.
|
||||
* @property {string} [caption]
|
||||
* @property {string} [aspect] Tailwind aspect class (default "aspect-[16/9]")
|
||||
* @property {string} [bgClass] (default "bg-gray-50")
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let {
|
||||
eyebrow,
|
||||
title,
|
||||
description,
|
||||
imageSrc,
|
||||
imageAlt = '',
|
||||
suggestedFile,
|
||||
caption = '',
|
||||
aspect = 'aspect-[16/9]',
|
||||
bgClass = 'bg-gray-50'
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<section class="px-6 py-12 lg:px-12 lg:py-16 {bgClass}">
|
||||
<div class="mx-auto max-w-[1100px]">
|
||||
<div class="mb-10 max-w-[620px]">
|
||||
<div class="mb-2 text-xs font-medium tracking-widest text-blue-700 uppercase">{eyebrow}</div>
|
||||
<h2 class="text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight">{title}</h2>
|
||||
{#if description}
|
||||
<p class="mt-5 text-base leading-relaxed text-gray-500">{description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<BlurFade delay={0.1} once>
|
||||
<div class="mx-auto max-w-[1100px]">
|
||||
{#if imageSrc}
|
||||
<div class="overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-gray-200">
|
||||
<img src={imageSrc} alt={imageAlt} class="block h-auto w-full" loading="lazy" decoding="async" />
|
||||
</div>
|
||||
{:else}
|
||||
<ImagePlaceholder {suggestedFile} {caption} {aspect} />
|
||||
{/if}
|
||||
</div>
|
||||
</BlurFade>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script>
|
||||
import Image from '@lucide/svelte/icons/image';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {string} suggestedFile Suggested static path (e.g. "/static/foo.png")
|
||||
* @property {string} [caption] Short description of what should go here
|
||||
* @property {string} [aspect] Tailwind aspect class (default "aspect-[4/3]")
|
||||
* @property {string} [className] Extra classes
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let { suggestedFile, caption = '', aspect = 'aspect-[4/3]', className = '' } = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="relative flex w-full flex-col items-center justify-center gap-3 overflow-hidden rounded-2xl border-2 border-dashed border-blue-300 bg-gradient-to-br from-blue-50 via-white to-gray-100 p-6 text-center {aspect} {className}"
|
||||
>
|
||||
<div
|
||||
class="flex h-14 w-14 items-center justify-center rounded-2xl bg-white text-blue-700 shadow ring-1 ring-blue-100"
|
||||
>
|
||||
<Image size={26} strokeWidth={1.6} />
|
||||
</div>
|
||||
<p class="text-sm font-medium text-gray-700">Image placeholder</p>
|
||||
<p class="text-xs text-gray-500">Suggested: {suggestedFile}</p>
|
||||
{#if caption}
|
||||
<p class="max-w-[80%] text-[11px] text-gray-400">{caption}</p>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,30 +1,55 @@
|
||||
// UI Components - Named exports for easy importing
|
||||
export { default as AiBuildsSection } from './ai-builds-section.svelte';
|
||||
export { default as AiSection } from './ai-section.svelte';
|
||||
export { default as AiTutorSection } from './ai-tutor-section.svelte';
|
||||
export { default as AutomateSection } from './automate-section.svelte';
|
||||
export { default as AutomationBand } from './automation-band.svelte';
|
||||
export { default as BlogListItem } from './blog-list-item.svelte';
|
||||
export { default as BoxedGrid } from './boxed-grid.svelte';
|
||||
export { default as CertificateMock } from './certificate-mock.svelte';
|
||||
export { default as CertEditorSection } from './cert-editor-section.svelte';
|
||||
export { default as CertSection } from './cert-section.svelte';
|
||||
export { default as CodeBlock } from './code-block.svelte';
|
||||
export { default as CoreFeatureSection } from './core-feature-section.svelte';
|
||||
export { default as CoreFeatures } from './core-features.svelte';
|
||||
export { default as CustomImg } from './custom-img.svelte';
|
||||
export { default as DeploySection } from './deploy-section.svelte';
|
||||
export { default as EmbedCodePreview } from './embed-code-preview.svelte';
|
||||
export { default as EnterpriseSection } from './enterprise-section.svelte';
|
||||
export { default as Faq } from './faq.svelte';
|
||||
export { default as FeatureGrid } from './feature-grid.svelte';
|
||||
export { default as FeaturesSection } from './features-section.svelte';
|
||||
export { default as Footer } from './footer.svelte';
|
||||
export { default as FooterCta } from './footer-cta.svelte';
|
||||
export { default as Hero } from './hero.svelte';
|
||||
export { default as HeroSubtitle } from './hero-subtitle.svelte';
|
||||
export { default as ImageDemoSection } from './image-demo-section.svelte';
|
||||
export { default as ImagePlaceholder } from './image-placeholder.svelte';
|
||||
export { default as LanguageReel } from './language-reel.svelte';
|
||||
export { default as LearnerSection } from './learner-section.svelte';
|
||||
export { default as Modal } from './modal.svelte';
|
||||
export { default as MoreFeatures } from './more-features.svelte';
|
||||
export { default as McpServerSection } from './mcp-server-section.svelte';
|
||||
export { default as MultiAcademySection } from './multi-academy-section.svelte';
|
||||
export { default as Navigation } from './navigation.svelte';
|
||||
export { default as OssSection } from './oss-section.svelte';
|
||||
export { default as NotFound } from './not-found.svelte';
|
||||
export { default as PageHeader } from './page-header.svelte';
|
||||
export { default as PageSignupCTA } from './page-signup-cta.svelte';
|
||||
export { default as ParticleField } from './particle-field.svelte';
|
||||
export { default as PillList } from './pill-list.svelte';
|
||||
export { default as QuestionTypes } from './question-types.svelte';
|
||||
export { default as RoiBand } from './roi-band.svelte';
|
||||
export { default as SenjaEmbed } from './senja-embed.svelte';
|
||||
export { default as SplitFeatureSection } from './split-feature-section.svelte';
|
||||
export { default as StudentDashboard } from './student-dashboard.svelte';
|
||||
export { default as Success } from './success.svelte';
|
||||
export { default as Testimonial } from './testimonial.svelte';
|
||||
export { default as Timeline } from './timeline.svelte';
|
||||
export { default as ToolsHeader } from './tools-header.svelte';
|
||||
export { default as TrustBand } from './trust-band.svelte';
|
||||
export { default as UploadWidget } from './upload-widget.svelte';
|
||||
export { default as VideoDemoSection } from './video-demo-section.svelte';
|
||||
export { default as VideoPlaceholder } from './video-placeholder.svelte';
|
||||
export { default as VsHelpDocs } from './vs-help-docs.svelte';
|
||||
export { default as WorkflowSteps } from './workflow-steps.svelte';
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
<script>
|
||||
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
|
||||
import Check from '@lucide/svelte/icons/check';
|
||||
import Languages from '@lucide/svelte/icons/languages';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Lang
|
||||
* @property {string} code Two-letter language code (e.g. "en", "es")
|
||||
* @property {string} native Native script name (e.g. "Español", "हिन्दी")
|
||||
* @property {string} en English name (e.g. "Spanish", "Hindi")
|
||||
* @property {string} sample Translated sample lesson title in this language
|
||||
* @property {string} tone Tailwind gradient classes (e.g. "from-blue-100 to-blue-200")
|
||||
* @property {string} ring Tailwind ring color class (e.g. "ring-blue-200")
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {Lang[]} langs
|
||||
* @property {string} eyebrow Eyebrow label (e.g. "Global partners")
|
||||
* @property {string} headlineLead Headline lead text (e.g. "Train your network")
|
||||
* @property {string} headlineAccent Headline accent text shown in blue with a sweep (e.g. "in their language.")
|
||||
* @property {string} description Body paragraph
|
||||
* @property {string} moduleLabel Top of preview card (e.g. "Module 2 · Implementation")
|
||||
* @property {string} footerLabel Bottom badge in preview card (e.g. "8 lessons" or "6 lessons · 18 min")
|
||||
* @property {string} [bgClass] Optional section background (defaults to bg-gray-50)
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let {
|
||||
langs,
|
||||
eyebrow,
|
||||
headlineLead,
|
||||
headlineAccent,
|
||||
description,
|
||||
moduleLabel,
|
||||
footerLabel,
|
||||
bgClass = 'bg-gray-50'
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<section class="relative overflow-hidden px-6 py-16 lg:px-12 lg:py-24 {bgClass}">
|
||||
<div
|
||||
class="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_85%_40%,rgba(2,51,189,0.06),transparent_55%)]"
|
||||
></div>
|
||||
|
||||
<div class="relative mx-auto max-w-[1100px]">
|
||||
<div class="grid grid-cols-1 items-center gap-12 lg:grid-cols-2 lg:gap-20">
|
||||
<BlurFade delay={0} once>
|
||||
<div>
|
||||
<div class="mb-3 inline-flex items-center gap-2 text-xs font-medium tracking-widest text-blue-700 uppercase">
|
||||
<Languages size={14} />
|
||||
{eyebrow}
|
||||
</div>
|
||||
<h2 class="text-[clamp(2rem,3vw,2.6rem)] leading-[1.15] font-medium tracking-tight">
|
||||
{headlineLead}
|
||||
<span class="cio-lang-headline relative inline-block">
|
||||
<span class="text-blue-700">{headlineAccent}</span>
|
||||
</span>
|
||||
</h2>
|
||||
<p class="mt-5 text-base leading-relaxed text-gray-500">{description}</p>
|
||||
|
||||
<!-- Inline language switcher mock -->
|
||||
<div
|
||||
class="mt-7 inline-flex w-full max-w-[320px] flex-col rounded-2xl border border-gray-200 bg-white p-1.5 shadow-sm"
|
||||
>
|
||||
<div class="px-3 py-1.5 text-[10px] font-medium tracking-widest text-gray-500 uppercase">
|
||||
Lesson language
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
{#each langs.slice(0, 4) as lang, i}
|
||||
<div
|
||||
class="flex items-center justify-between rounded-lg px-3 py-2 transition-colors hover:bg-gray-50 {i ===
|
||||
1
|
||||
? 'bg-blue-50'
|
||||
: ''}"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<span
|
||||
class="flex h-6 w-6 items-center justify-center rounded-md bg-gradient-to-br {lang.tone} text-[10px] font-bold text-gray-800 ring-1 {lang.ring}"
|
||||
>
|
||||
{lang.code.toUpperCase()}
|
||||
</span>
|
||||
<span class="text-sm text-gray-800">{lang.native}</span>
|
||||
<span class="text-xs text-gray-400">{lang.en}</span>
|
||||
</div>
|
||||
{#if i === 1}
|
||||
<Check size={14} class="text-blue-700" />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
<div class="rounded-lg px-3 py-2 text-xs text-gray-400">
|
||||
+ {Math.max(0, langs.length - 4)} more languages
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BlurFade>
|
||||
|
||||
<!-- Animated polyglot lesson reel -->
|
||||
<BlurFade delay={0.15} once>
|
||||
<div class="relative">
|
||||
<div
|
||||
class="pointer-events-none absolute -inset-4 z-0 rounded-3xl bg-[conic-gradient(from_30deg,rgba(2,51,189,0.08),rgba(2,51,189,0.18),rgba(2,51,189,0.08))] blur-2xl"
|
||||
></div>
|
||||
|
||||
<div class="relative rounded-2xl border border-gray-200 bg-white shadow-2xl/5">
|
||||
<div class="flex items-center justify-between border-b border-gray-100 px-5 py-3.5">
|
||||
<div class="flex items-center gap-2 text-xs text-gray-500">
|
||||
<span class="relative flex h-2 w-2">
|
||||
<span class="absolute inline-flex h-2 w-2 animate-ping rounded-full bg-blue-500 opacity-60"></span>
|
||||
<span class="relative inline-flex h-2 w-2 rounded-full bg-blue-600"></span>
|
||||
</span>
|
||||
{moduleLabel}
|
||||
</div>
|
||||
<div class="text-[10px] font-medium tracking-widest text-blue-700 uppercase">Live preview</div>
|
||||
</div>
|
||||
|
||||
<!-- Cycling lesson card -->
|
||||
<div class="relative h-[260px] overflow-hidden">
|
||||
{#each langs as lang, i}
|
||||
<div
|
||||
class="cio-lang-cycle absolute inset-0 flex flex-col px-6 py-6"
|
||||
style:--delay="{i * 2}s"
|
||||
style:--total="{langs.length * 2}s"
|
||||
>
|
||||
<div class="mb-4 flex items-center gap-3">
|
||||
<span
|
||||
class="flex h-9 w-9 items-center justify-center rounded-xl bg-gradient-to-br {lang.tone} text-sm font-bold text-gray-800 ring-1 {lang.ring}"
|
||||
>
|
||||
{lang.code.toUpperCase()}
|
||||
</span>
|
||||
<div>
|
||||
<p class="text-[11px] tracking-wide text-gray-400 uppercase">Now rendering · {lang.en}</p>
|
||||
<p class="text-xs text-gray-600">{lang.native}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xl leading-snug font-medium text-gray-950" dir="auto">{lang.sample}</p>
|
||||
<div class="mt-5 space-y-2">
|
||||
<div class="h-1.5 w-[88%] rounded-full bg-gray-100"></div>
|
||||
<div class="h-1.5 w-[72%] rounded-full bg-gray-100"></div>
|
||||
<div class="h-1.5 w-[60%] rounded-full bg-gray-100"></div>
|
||||
</div>
|
||||
<div class="mt-auto flex items-center justify-between pt-4">
|
||||
<span class="rounded-full bg-blue-50 px-2.5 py-1 text-[10px] font-medium text-blue-700"
|
||||
>{footerLabel}</span
|
||||
>
|
||||
<span class="text-[10px] text-gray-400">auto · per-learner language</span>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Progress dots -->
|
||||
<div class="border-t border-gray-100 px-5 py-3">
|
||||
<div class="flex items-center justify-center gap-1.5">
|
||||
{#each langs as _, i}
|
||||
<span
|
||||
class="cio-lang-dot h-1.5 rounded-full bg-gray-200"
|
||||
style:--delay="{i * 2}s"
|
||||
style:--total="{langs.length * 2}s"
|
||||
></span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BlurFade>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
/* Each language gets a 2s window; total = langs.length × 2s, supplied via --total */
|
||||
.cio-lang-cycle {
|
||||
opacity: 0;
|
||||
animation: cio-lang-fade var(--total, 20s) ease-in-out infinite;
|
||||
animation-delay: var(--delay, 0s);
|
||||
}
|
||||
@keyframes cio-lang-fade {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
1% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
9% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
11% {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
}
|
||||
.cio-lang-dot {
|
||||
width: 6px;
|
||||
animation: cio-lang-dot var(--total, 20s) ease-in-out infinite;
|
||||
animation-delay: var(--delay, 0s);
|
||||
}
|
||||
@keyframes cio-lang-dot {
|
||||
0%,
|
||||
11%,
|
||||
100% {
|
||||
background-color: rgb(229 231 235);
|
||||
width: 6px;
|
||||
}
|
||||
1%,
|
||||
9% {
|
||||
background-color: rgb(29 78 216);
|
||||
width: 22px;
|
||||
}
|
||||
}
|
||||
.cio-lang-headline::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: auto 0 -2px 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, rgba(29, 78, 216, 0.35), transparent);
|
||||
animation: cio-lang-sweep 3.5s ease-in-out infinite;
|
||||
}
|
||||
@keyframes cio-lang-sweep {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
transform: translateX(100%);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,5 @@
|
||||
<script>
|
||||
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
|
||||
import { BorderBeam } from '@cio/ui/custom/animation/border-beam';
|
||||
|
||||
const learnerModes = [
|
||||
{
|
||||
@@ -77,7 +76,6 @@
|
||||
<!-- Lesson Mock UI -->
|
||||
<BlurFade delay={0.2} once>
|
||||
<div class="relative overflow-hidden rounded-2xl border border-gray-200 bg-gray-50 shadow-2xl/5">
|
||||
<BorderBeam size={200} duration={14} colorFrom="#0233BD" colorTo="#93c5fd" />
|
||||
<div class="flex items-center justify-between border-b border-gray-200 bg-white px-5 py-4">
|
||||
<div class="text-sm font-medium">Data Privacy & Compliance</div>
|
||||
<div class="rounded-full border border-gray-200 bg-gray-50 px-2.5 py-0.5 text-xs text-gray-500">
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<script>
|
||||
import CodeBlock from './code-block.svelte';
|
||||
|
||||
/**
|
||||
* Shared MCP server section — dark background, copy + mcp.json on left/right.
|
||||
* Used on /automation and /mcp-recipes.
|
||||
*
|
||||
* @typedef {Object} Props
|
||||
* @property {string} [eyebrow]
|
||||
* @property {string} [title]
|
||||
* @property {string} [description]
|
||||
* @property {boolean} [showTools] Render the list of MCP tool names (default true)
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let {
|
||||
eyebrow = 'MCP server',
|
||||
title = 'Course authoring from your coding agent.',
|
||||
description = 'Connect Cursor, Claude Code, Codex, or OpenCode to ClassroomIO and let your agent draft courses, reorganise sections, generate exercises, and publish them without leaving your terminal.',
|
||||
showTools = true
|
||||
} = $props();
|
||||
|
||||
const mcpJsonLines = [
|
||||
'<span class="text-slate-500">{</span>',
|
||||
' <span class="text-sky-300">"mcpServers"</span><span class="text-slate-500">: {</span>',
|
||||
' <span class="text-sky-300">"classroomio"</span><span class="text-slate-500">: {</span>',
|
||||
' <span class="text-sky-300">"command"</span><span class="text-slate-500">:</span> <span class="text-emerald-300">"npx"</span><span class="text-slate-500">,</span>',
|
||||
' <span class="text-sky-300">"args"</span><span class="text-slate-500">: [</span><span class="text-emerald-300">"-y"</span><span class="text-slate-500">,</span> <span class="text-emerald-300">"@classroomio/mcp"</span><span class="text-slate-500">],</span>',
|
||||
' <span class="text-sky-300">"env"</span><span class="text-slate-500">: { </span><span class="text-sky-300">"CLASSROOMIO_API_KEY"</span><span class="text-slate-500">: </span><span class="text-emerald-300">"<your-mcp-key>"</span><span class="text-slate-500"> }</span>',
|
||||
' <span class="text-slate-500">}</span>',
|
||||
' <span class="text-slate-500">}</span>',
|
||||
'<span class="text-slate-500">}</span>'
|
||||
];
|
||||
|
||||
const mcpTools = [
|
||||
'list_org_courses',
|
||||
'create_course_draft',
|
||||
'create_course_draft_from_course',
|
||||
'update_course_draft',
|
||||
'publish_course_draft',
|
||||
'publish_course_draft_to_existing_course',
|
||||
'update_course_landing_page',
|
||||
'create_course_exercise',
|
||||
'create_course_exercise_from_template',
|
||||
'update_course_exercise',
|
||||
'reorder_course_content',
|
||||
'tag_courses',
|
||||
'tag_course_draft',
|
||||
'get_course_structure',
|
||||
'get_course_draft',
|
||||
'get_course_exercise',
|
||||
'list_course_exercises'
|
||||
];
|
||||
</script>
|
||||
|
||||
<section class="relative overflow-hidden bg-gray-950 px-6 py-12 text-white lg:px-12 lg:py-16">
|
||||
<div
|
||||
class="pointer-events-none absolute -top-[200px] -right-[200px] h-[500px] w-[500px] bg-[radial-gradient(circle,rgba(2,51,189,0.18)_0%,transparent_70%)]"
|
||||
></div>
|
||||
|
||||
<div class="relative mx-auto grid max-w-[1100px] grid-cols-1 items-start gap-12 lg:grid-cols-[1fr_1fr] lg:gap-20">
|
||||
<div>
|
||||
<div class="mb-2 text-xs font-medium tracking-widest text-blue-400 uppercase">{eyebrow}</div>
|
||||
<h2 class="text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight text-white">
|
||||
{title}
|
||||
</h2>
|
||||
<p class="mt-5 max-w-[520px] text-sm leading-relaxed text-gray-400">
|
||||
{description}
|
||||
</p>
|
||||
|
||||
{#if showTools}
|
||||
<div class="mt-6">
|
||||
<p class="mb-3 text-[10px] tracking-wider text-gray-500 uppercase">Available tools</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each mcpTools as tool}
|
||||
<span
|
||||
class="rounded-md border border-white/10 bg-white/[0.05] px-2.5 py-1 font-mono text-[11px] text-blue-300"
|
||||
>
|
||||
{tool}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
<div class="relative overflow-hidden rounded-2xl border border-white/[0.08] bg-white/[0.03]">
|
||||
<CodeBlock
|
||||
fileName="mcp.json"
|
||||
lang="JSON"
|
||||
codeLines={mcpJsonLines}
|
||||
statusBar="paste into your agent's MCP config"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,133 @@
|
||||
<script>
|
||||
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
|
||||
import BoxedGrid from './boxed-grid.svelte';
|
||||
import Palette from '@lucide/svelte/icons/palette';
|
||||
import Globe from '@lucide/svelte/icons/globe';
|
||||
import Receipt from '@lucide/svelte/icons/receipt';
|
||||
|
||||
const academies = [
|
||||
{
|
||||
index: '01',
|
||||
name: 'Customer Academy',
|
||||
domain: 'learn.acme.com',
|
||||
audience: 'Customers · onboarding · adoption',
|
||||
barWidth: '72%',
|
||||
dot: 'bg-blue-500',
|
||||
bar: 'bg-blue-500',
|
||||
badge: 'bg-blue-50 text-blue-700'
|
||||
},
|
||||
{
|
||||
index: '02',
|
||||
name: 'Partner Academy',
|
||||
domain: 'partners.acme.com',
|
||||
audience: 'Resellers · agencies · integrators',
|
||||
barWidth: '54%',
|
||||
dot: 'bg-violet-500',
|
||||
bar: 'bg-violet-500',
|
||||
badge: 'bg-violet-50 text-violet-700'
|
||||
},
|
||||
{
|
||||
index: '03',
|
||||
name: 'Internal Academy',
|
||||
domain: 'team.acme.com',
|
||||
audience: 'Employees · onboarding · compliance',
|
||||
barWidth: '88%',
|
||||
dot: 'bg-emerald-500',
|
||||
bar: 'bg-emerald-500',
|
||||
badge: 'bg-emerald-50 text-emerald-700'
|
||||
}
|
||||
];
|
||||
|
||||
const benefits = [
|
||||
{
|
||||
Icon: Palette,
|
||||
title: 'Brand isolated',
|
||||
description: 'Own logo, colors, theme. Customers never see partner content.'
|
||||
},
|
||||
{
|
||||
Icon: Globe,
|
||||
title: 'Own domain',
|
||||
description: 'Point a CNAME, click verify. SSL handled.'
|
||||
},
|
||||
{
|
||||
Icon: Receipt,
|
||||
title: 'One invoice',
|
||||
description: 'Add a new academy when you launch a new audience.'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<section class="bg-white px-6 py-12 lg:px-12 lg:py-16">
|
||||
<div class="mx-auto max-w-[1100px]">
|
||||
<!-- Header: eyebrow on top, headline + supporting text in a split row -->
|
||||
<BlurFade once>
|
||||
<div class="text-xs font-medium tracking-widest text-blue-700 uppercase">Multi-academy</div>
|
||||
</BlurFade>
|
||||
|
||||
<div class="mt-6 grid grid-cols-1 gap-8 lg:grid-cols-[1.4fr_1fr] lg:items-end">
|
||||
<BlurFade delay={0.05} once>
|
||||
<h2 class="text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight text-gray-950">
|
||||
One platform,
|
||||
<em class="text-gray-700 not-italic">every audience.</em>
|
||||
</h2>
|
||||
</BlurFade>
|
||||
|
||||
<BlurFade delay={0.1} once>
|
||||
<p class="max-w-[360px] text-sm leading-relaxed text-gray-500 lg:text-right lg:text-base">
|
||||
Three workspaces, three brands, three domains. One login. One bill.
|
||||
</p>
|
||||
</BlurFade>
|
||||
</div>
|
||||
|
||||
<!-- Academies list -->
|
||||
<div class="mt-10 border-t border-gray-200">
|
||||
{#each academies as a, i}
|
||||
<BlurFade delay={0.05 * i + 0.15} once>
|
||||
<div
|
||||
class="grid grid-cols-[auto_1fr] gap-x-4 border-b border-gray-200 py-5 lg:grid-cols-[40px_1.6fr_1.6fr_1fr_auto] lg:items-center lg:gap-x-6"
|
||||
>
|
||||
<!-- Number -->
|
||||
<p class="font-mono text-xs text-gray-400">{a.index}</p>
|
||||
|
||||
<!-- Name + domain -->
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="mt-2 h-2 w-2 shrink-0 rounded-full {a.dot}"></span>
|
||||
<div>
|
||||
<p class="text-sm text-gray-950 lg:text-base">{a.name}</p>
|
||||
<p class="font-mono text-[11px] text-gray-500 lg:text-xs">{a.domain}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Audience -->
|
||||
<p class="col-span-2 mt-2 text-sm text-gray-600 lg:col-span-1 lg:mt-0">{a.audience}</p>
|
||||
|
||||
<!-- Progress bar -->
|
||||
<div class="col-span-2 mt-2 h-1 w-full overflow-hidden rounded-full bg-gray-100 lg:col-span-1 lg:mt-0">
|
||||
<div class="h-full rounded-full {a.bar}" style="width: {a.barWidth}"></div>
|
||||
</div>
|
||||
|
||||
<!-- Active badge -->
|
||||
<span
|
||||
class="col-span-2 mt-2 inline-block w-fit rounded-full px-2.5 py-1 text-xs font-medium {a.badge} lg:col-span-1 lg:mt-0"
|
||||
>
|
||||
Active
|
||||
</span>
|
||||
</div>
|
||||
</BlurFade>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Bottom feature row -->
|
||||
<div class="mt-10">
|
||||
<BoxedGrid items={benefits} cols={3} padding="p-5 lg:p-6">
|
||||
{#snippet cell(benefit)}
|
||||
<div class="text-gray-950">
|
||||
<benefit.Icon size={18} strokeWidth={1.6} />
|
||||
</div>
|
||||
<h3 class="mt-3 text-sm font-medium text-gray-950 lg:text-base">{benefit.title}</h3>
|
||||
<p class="mt-1.5 text-xs leading-relaxed text-gray-500 lg:text-sm">{benefit.description}</p>
|
||||
{/snippet}
|
||||
</BoxedGrid>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,13 +1,17 @@
|
||||
<script lang="ts">
|
||||
import ChevronDown from '@lucide/svelte/icons/chevron-down';
|
||||
import Gamepad from '@lucide/svelte/icons/gamepad';
|
||||
import Github from '@lucide/svelte/icons/github';
|
||||
import Hourglass from '@lucide/svelte/icons/hourglass';
|
||||
import LoaderCircle from '@lucide/svelte/icons/loader-circle';
|
||||
import LocateFixed from '@lucide/svelte/icons/locate-fixed';
|
||||
import Menu from '@lucide/svelte/icons/menu';
|
||||
import MessagesSquare from '@lucide/svelte/icons/messages-square';
|
||||
import MousePointerClick from '@lucide/svelte/icons/mouse-pointer-click';
|
||||
import Server from '@lucide/svelte/icons/server';
|
||||
import Sparkles from '@lucide/svelte/icons/sparkles';
|
||||
import Timer from '@lucide/svelte/icons/timer';
|
||||
import Webhook from '@lucide/svelte/icons/webhook';
|
||||
import X from '@lucide/svelte/icons/x';
|
||||
import { page } from '$app/state';
|
||||
import { fly } from 'svelte/transition';
|
||||
@@ -61,23 +65,50 @@
|
||||
}
|
||||
|
||||
const solutions: NavCollectionItem[] = [
|
||||
{
|
||||
key: 'employee-training',
|
||||
title: 'Employee Training',
|
||||
subtitle: 'Keep your team in sync.',
|
||||
href: '/employee-training'
|
||||
},
|
||||
{
|
||||
key: 'bootcamps',
|
||||
title: 'Bootcamps',
|
||||
subtitle: 'Drive student satisfaction.',
|
||||
href: '/bootcamps'
|
||||
},
|
||||
{
|
||||
key: 'customer-education',
|
||||
title: 'Customer Education',
|
||||
subtitle: 'Teach customers your product.',
|
||||
subtitle: 'Customer academies for SaaS.',
|
||||
href: '/customer-education'
|
||||
},
|
||||
{
|
||||
key: 'compliance-training',
|
||||
title: 'Compliance Training',
|
||||
subtitle: 'Turn policies and SOPs into proof.',
|
||||
href: '/compliance-training'
|
||||
},
|
||||
{
|
||||
key: 'partner-training',
|
||||
title: 'Partner Training',
|
||||
subtitle: 'A separate academy for your channel.',
|
||||
href: '/partner-training'
|
||||
}
|
||||
];
|
||||
|
||||
const developers: NavCollectionItem[] = [
|
||||
{
|
||||
key: 'automation',
|
||||
title: 'Automation',
|
||||
subtitle: 'API, Webhooks, and MCP server.',
|
||||
href: '/automation'
|
||||
},
|
||||
{
|
||||
key: 'self-hosting',
|
||||
title: 'Self-hosting & Docs',
|
||||
subtitle: 'Run ClassroomIO in your own VPC.',
|
||||
href: 'https://docs.classroomio.com'
|
||||
},
|
||||
{
|
||||
key: 'mcp-recipes',
|
||||
title: 'MCP Recipes',
|
||||
subtitle: 'Ready-to-run agent prompts.',
|
||||
href: '/mcp-recipes'
|
||||
},
|
||||
{
|
||||
key: 'github',
|
||||
title: 'GitHub',
|
||||
subtitle: 'Star, read the source, fork.',
|
||||
href: '/github'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -121,6 +152,12 @@
|
||||
href: '/',
|
||||
items: solutions
|
||||
},
|
||||
{
|
||||
key: 'developers',
|
||||
title: 'Developers',
|
||||
href: '/automation',
|
||||
items: developers
|
||||
},
|
||||
{
|
||||
key: 'free-tools',
|
||||
title: 'Free Tools',
|
||||
@@ -140,6 +177,7 @@
|
||||
];
|
||||
|
||||
let isSolutionsActive = $derived(!!solutions.some((s) => activeLink.includes(s.key)));
|
||||
let isDevelopersActive = $derived(!!developers.some((d) => activeLink.includes(d.key)));
|
||||
let isFreeToolsActive = $derived(activeLink.startsWith('/tools'));
|
||||
|
||||
function isNavItemActive(navItem: NavItem) {
|
||||
@@ -147,6 +185,10 @@
|
||||
return isSolutionsActive;
|
||||
}
|
||||
|
||||
if (navItem.key === 'developers') {
|
||||
return isDevelopersActive;
|
||||
}
|
||||
|
||||
if (navItem.key === 'free-tools') {
|
||||
return isFreeToolsActive;
|
||||
}
|
||||
@@ -165,12 +207,20 @@
|
||||
{...restProps}
|
||||
>
|
||||
<div class="shrink-0">
|
||||
{#if key === 'employee-training'}
|
||||
{#if key === 'compliance-training'}
|
||||
<LibraryBigIcon size={24} />
|
||||
{:else if key === 'bootcamps'}
|
||||
{:else if key === 'partner-training'}
|
||||
<LocateFixed size={24} />
|
||||
{:else if key === 'customer-education'}
|
||||
<MessagesSquare size={24} />
|
||||
{:else if key === 'automation'}
|
||||
<Webhook size={24} />
|
||||
{:else if key === 'self-hosting'}
|
||||
<Server size={24} />
|
||||
{:else if key === 'mcp-recipes'}
|
||||
<Sparkles size={24} />
|
||||
{:else if key === 'github'}
|
||||
<Github size={24} />
|
||||
{:else if key === 'progress'}
|
||||
<LoaderCircle size={24} />
|
||||
{:else if key === 'pomodoro'}
|
||||
@@ -206,7 +256,7 @@
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<section class="hidden lg:block">
|
||||
<section class="hidden! lg:block!">
|
||||
<NavigationMenu.Root class="p-1">
|
||||
<NavigationMenu.List class="flex w-full items-center justify-center gap-1">
|
||||
{#each navItems as navItem}
|
||||
@@ -253,7 +303,7 @@
|
||||
</NavigationMenu.Root>
|
||||
</section>
|
||||
|
||||
<div class="hidden flex-row items-center justify-between gap-3 lg:flex">
|
||||
<div class="hidden! flex-row items-center justify-between gap-3 lg:flex!">
|
||||
<a href="/discord" target="_blank" class="flex items-center transition-opacity duration-200 hover:opacity-80">
|
||||
<img loading="lazy" alt="discord logo" src="/discord-blue.png" class="h-5 w-6 cursor-pointer" />
|
||||
</a>
|
||||
@@ -284,7 +334,7 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<button type="button" aria-label="Hamburger Menu" class="shrink-0 lg:hidden" onclick={handleShowSolutions}>
|
||||
<button type="button" aria-label="Hamburger Menu" class="shrink-0 lg:hidden!" onclick={handleShowSolutions}>
|
||||
<Menu size={24} />
|
||||
</button>
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<script>
|
||||
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
|
||||
import Github from '@lucide/svelte/icons/github';
|
||||
import Server from '@lucide/svelte/icons/server';
|
||||
import Coins from '@lucide/svelte/icons/coins';
|
||||
|
||||
/** @type {{ stars?: number }} */
|
||||
let { stars = 0 } = $props();
|
||||
|
||||
const lanes = [
|
||||
{
|
||||
Icon: Github,
|
||||
title: 'AGPL on GitHub',
|
||||
description: 'Dashboard, API, AI assistant, MCP server. All of it.'
|
||||
},
|
||||
{
|
||||
Icon: Server,
|
||||
title: 'Self-host or hosted',
|
||||
description: 'Run it in your VPC, or let us run it. Same product.'
|
||||
},
|
||||
{
|
||||
Icon: Coins,
|
||||
title: 'No per-seat fees',
|
||||
description: 'Pricing scales with workspaces, not headcount.'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<section class="bg-white px-6 py-16 lg:px-12 lg:py-24">
|
||||
<div class="mx-auto max-w-[1100px]">
|
||||
<div class="mb-12 grid grid-cols-1 gap-12 lg:grid-cols-[1fr_1fr] lg:items-end lg:gap-20">
|
||||
<BlurFade once>
|
||||
<div>
|
||||
<div class="mb-3 text-xs font-medium tracking-widest text-blue-700 uppercase">Open source</div>
|
||||
<h2 class="text-[clamp(2rem,3vw,2.8rem)] leading-[1.15] font-medium tracking-tight">
|
||||
Read it. Fork it. Run it.
|
||||
</h2>
|
||||
</div>
|
||||
</BlurFade>
|
||||
|
||||
<BlurFade delay={0.1} once>
|
||||
<p class="text-base leading-relaxed text-gray-500 lg:text-lg">
|
||||
AGPL on GitHub. Self-host or use ours. Bring your own AI keys.
|
||||
</p>
|
||||
</BlurFade>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-5 md:grid-cols-3">
|
||||
{#each lanes as lane, i}
|
||||
<BlurFade delay={0.08 * i} once>
|
||||
<div class="h-full rounded-2xl border border-gray-200 bg-gray-50 p-6">
|
||||
<div
|
||||
class="mb-4 flex h-11 w-11 items-center justify-center rounded-xl bg-white text-blue-700 ring-1 ring-gray-200"
|
||||
>
|
||||
<lane.Icon size={20} strokeWidth={1.75} />
|
||||
</div>
|
||||
<h3 class="text-base font-medium text-gray-950">{lane.title}</h3>
|
||||
<p class="mt-2 text-sm leading-relaxed text-gray-500">{lane.description}</p>
|
||||
</div>
|
||||
</BlurFade>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex flex-wrap items-center gap-3">
|
||||
<a
|
||||
href="/github"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-2 rounded-lg bg-gray-950 px-5 py-2.5 text-sm font-medium text-white no-underline transition-all hover:-translate-y-0.5 hover:bg-blue-700"
|
||||
>
|
||||
<Github size={16} />
|
||||
Star on GitHub
|
||||
{#if stars}<span class="text-blue-300">{stars}</span>{/if}
|
||||
</a>
|
||||
<a
|
||||
href="https://docs.classroomio.com/self-hosting"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-5 py-2.5 text-sm font-medium text-gray-950 no-underline transition-all hover:border-gray-950"
|
||||
>
|
||||
Self-hosting guide →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -14,7 +14,7 @@
|
||||
<header class="relative w-full overflow-hidden border-b border-slate-100 pt-32 pb-16 lg:min-h-[20rem]">
|
||||
<DotField
|
||||
dotRadius={1.5}
|
||||
dotSpacing={14}
|
||||
dotSpacing={15}
|
||||
cursorRadius={420}
|
||||
bulgeOnly={true}
|
||||
bulgeStrength={56}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { Waves } from '@cio/ui/custom/animation/waves';
|
||||
|
||||
interface Props {
|
||||
header?: string;
|
||||
@@ -13,19 +14,40 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="floating grad relative mx-auto my-5 flex h-[61vh] w-full max-w-4xl flex-col items-center justify-center space-y-8 px-6 py-12 text-center shadow-2xl lg:h-[50vh] lg:w-[70%] lg:rounded-3xl"
|
||||
class="relative mx-auto my-5 flex h-[61vh] w-full max-w-4xl flex-col items-center justify-center overflow-hidden bg-gray-950 px-6 py-12 text-center shadow-2xl lg:h-[50vh] lg:w-[70%] lg:rounded-3xl"
|
||||
>
|
||||
<div>
|
||||
<h3 class="font-display mt-3 max-w-2xl text-xl tracking-tight text-white lg:mt-2 lg:text-4xl">
|
||||
<div
|
||||
class="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_50%_100%,rgba(2,51,189,0.92)_0%,rgba(4,15,45,0.98)_58%,rgb(3,7,18)_100%)]"
|
||||
></div>
|
||||
<Waves
|
||||
lineColor="rgba(255,255,255,0.18)"
|
||||
class="ui:pointer-events-none ui:absolute ui:inset-0 ui:h-full ui:w-full"
|
||||
xGap={14}
|
||||
yGap={34}
|
||||
waveAmpX={24}
|
||||
waveAmpY={12}
|
||||
maxCursorMove={72}
|
||||
/>
|
||||
<div
|
||||
class="pointer-events-none absolute inset-0 bg-linear-to-t from-gray-950/40 via-transparent to-white/[0.04]"
|
||||
></div>
|
||||
|
||||
<div class="relative z-10 flex w-full flex-col items-center text-center">
|
||||
<h3 class="font-display mx-auto mt-3 max-w-2xl text-center text-xl tracking-tight text-white lg:mt-2 lg:text-4xl">
|
||||
{header}
|
||||
</h3>
|
||||
<p class="text-md mt-6 text-gray-300">
|
||||
<p class="text-md mx-auto mt-6 max-w-2xl text-center text-gray-300">
|
||||
{subText}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if demo}
|
||||
<Button variant="ghost" data-cal-config="'layout':'month_view'" data-cal-link="classroomio/demo" class="bg-white!">
|
||||
<Button
|
||||
variant="ghost"
|
||||
data-cal-config="'layout':'month_view'"
|
||||
data-cal-link="classroomio/demo"
|
||||
class="relative z-10 mt-8 bg-white!"
|
||||
>
|
||||
{btnLabel}
|
||||
</Button>
|
||||
{:else}
|
||||
@@ -33,21 +55,10 @@
|
||||
href={link}
|
||||
variant="link"
|
||||
target="_blank"
|
||||
class="ui:bg-secondary ui:text-secondary-foreground"
|
||||
class="ui:bg-secondary ui:text-secondary-foreground relative z-10 mt-8"
|
||||
rel="noopener noreferrer nofollow"
|
||||
>
|
||||
{btnLabel}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.grad {
|
||||
/* background: rgb(2, 51, 189);
|
||||
background: radial-gradient(circle at 50% 100%, rgba(2, 51, 189, 1) 1%, rgb(4, 2, 56) 100%); */
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-image: url('https://brand.cdn.clsrio.com/cta-bg.png');
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
/**
|
||||
* Reusable animated particle cloud, same family as the hero.
|
||||
*
|
||||
* @typedef {Object} ClusterSpec
|
||||
* @property {number} x Normalized 0–1 x position
|
||||
* @property {number} y Normalized 0–1 y position
|
||||
* @property {number} rx
|
||||
* @property {number} ry
|
||||
* @property {number} density
|
||||
* @property {number} warmth 0 (cool) → 1 (warm/brand blue)
|
||||
*
|
||||
* @typedef {Object} Props
|
||||
* @property {ClusterSpec[]} [clusters] Density centers. Defaults to the hero's 4-cluster diagonal.
|
||||
* @property {number} [intensity] Multiplier for particle count (default 1)
|
||||
* @property {boolean} [highlights] Render glowing twinkle stars + constellation lines (default true)
|
||||
* @property {string} [class] Extra classes for the canvas element
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let {
|
||||
clusters: clusterSpec = [
|
||||
{ x: 0.3, y: 0.72, rx: 90, ry: 130, density: 0.55, warmth: 0.1 },
|
||||
{ x: 0.52, y: 0.55, rx: 150, ry: 100, density: 0.95, warmth: 0.38 },
|
||||
{ x: 0.74, y: 0.38, rx: 190, ry: 150, density: 1.25, warmth: 0.78 },
|
||||
{ x: 0.92, y: 0.22, rx: 120, ry: 85, density: 0.7, warmth: 1.0 }
|
||||
],
|
||||
intensity = 1,
|
||||
highlights: showHighlights = true,
|
||||
class: className = ''
|
||||
} = $props();
|
||||
|
||||
let canvas;
|
||||
|
||||
onMount(() => {
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||
let w = 0;
|
||||
let h = 0;
|
||||
let particles = [];
|
||||
let highlights = [];
|
||||
let clusters = [];
|
||||
let rafId;
|
||||
let time = 0;
|
||||
|
||||
function warmRGB(wm) {
|
||||
const r = Math.round(110 - wm * 80);
|
||||
const g = Math.round(130 - wm * 50);
|
||||
const b = Math.round(180 + wm * 50);
|
||||
return [r, g, b];
|
||||
}
|
||||
|
||||
function gaussian2d(rx, ry) {
|
||||
const u1 = Math.max(Math.random(), 1e-6);
|
||||
const u2 = Math.random();
|
||||
const r = Math.sqrt(-2 * Math.log(u1));
|
||||
const t = 2 * Math.PI * u2;
|
||||
return [r * Math.cos(t) * rx, r * Math.sin(t) * ry];
|
||||
}
|
||||
|
||||
function resize() {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
w = rect.width;
|
||||
h = rect.height;
|
||||
canvas.width = Math.floor(w * dpr);
|
||||
canvas.height = Math.floor(h * dpr);
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
ctx.scale(dpr, dpr);
|
||||
seed();
|
||||
}
|
||||
|
||||
function seed() {
|
||||
particles = [];
|
||||
highlights = [];
|
||||
|
||||
// Scale cluster sizes (and therefore particle counts) with viewport width.
|
||||
// Reference width is 1100px (the content max-width). Mobile shrinks to ~50%, never bigger than 1×.
|
||||
const scale = Math.max(0.5, Math.min(1, w / 1100));
|
||||
clusters = clusterSpec.map((c) => ({
|
||||
x: c.x * w,
|
||||
y: c.y * h,
|
||||
rx: c.rx * scale,
|
||||
ry: c.ry * scale,
|
||||
density: c.density,
|
||||
warmth: c.warmth
|
||||
}));
|
||||
|
||||
for (const c of clusters) {
|
||||
const count = Math.floor(((c.rx * c.ry) / 18) * c.density * intensity);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const [dx, dy] = gaussian2d(c.rx * 0.34, c.ry * 0.34);
|
||||
const z = Math.random();
|
||||
let r, a;
|
||||
if (z < 0.32) {
|
||||
r = 0.3 + Math.random() * 0.6;
|
||||
a = 0.1 + Math.random() * 0.18;
|
||||
} else if (z < 0.85) {
|
||||
r = 0.55 + Math.random() * 1.05;
|
||||
a = 0.28 + Math.random() * 0.38;
|
||||
} else {
|
||||
r = 1.0 + Math.random() * 1.6;
|
||||
a = 0.5 + Math.random() * 0.45;
|
||||
}
|
||||
particles.push({
|
||||
bx: c.x + dx,
|
||||
by: c.y + dy,
|
||||
r,
|
||||
a,
|
||||
warmth: Math.min(1, Math.max(0, c.warmth + (Math.random() - 0.5) * 0.18)),
|
||||
phase: Math.random() * Math.PI * 2,
|
||||
speed: 0.3 + Math.random() * 0.7,
|
||||
amp: 0.6 + Math.random() * 1.8
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const fringeCount = Math.floor(w * 0.18 * intensity);
|
||||
for (let i = 0; i < fringeCount; i++) {
|
||||
const c = clusters[1 + (i % Math.max(1, clusters.length - 1))] || clusters[0];
|
||||
const [dx, dy] = gaussian2d(c.rx * 1.4, c.ry * 1.4);
|
||||
particles.push({
|
||||
bx: c.x + dx,
|
||||
by: c.y + dy,
|
||||
r: 0.25 + Math.random() * 0.4,
|
||||
a: 0.06 + Math.random() * 0.15,
|
||||
warmth: c.warmth,
|
||||
phase: Math.random() * Math.PI * 2,
|
||||
speed: 0.2 + Math.random() * 0.4,
|
||||
amp: 0.8 + Math.random() * 1.6
|
||||
});
|
||||
}
|
||||
|
||||
if (showHighlights) {
|
||||
const starCount = 38;
|
||||
for (let i = 0; i < starCount; i++) {
|
||||
const c = clusters[Math.max(0, clusters.length - 2) + (i % 2)] || clusters[0];
|
||||
const [dx, dy] = gaussian2d(c.rx * 0.45, c.ry * 0.45);
|
||||
highlights.push({
|
||||
x: c.x + dx,
|
||||
y: c.y + dy,
|
||||
r: 1.2 + Math.random() * 1.6,
|
||||
phase: Math.random() * Math.PI * 2,
|
||||
speed: 0.8 + Math.random() * 0.7,
|
||||
warmth: c.warmth
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function draw() {
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
time += 0.0035;
|
||||
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
for (const c of clusters) {
|
||||
const radius = Math.max(c.rx, c.ry) * 1.7;
|
||||
const [r, g, b] = warmRGB(c.warmth);
|
||||
const grd = ctx.createRadialGradient(c.x, c.y, 0, c.x, c.y, radius);
|
||||
grd.addColorStop(0, `rgba(${r},${g},${b},${0.1 * c.density})`);
|
||||
grd.addColorStop(0.45, `rgba(${r},${g},${b},${0.04 * c.density})`);
|
||||
grd.addColorStop(1, 'rgba(0,0,0,0)');
|
||||
ctx.fillStyle = grd;
|
||||
ctx.fillRect(c.x - radius, c.y - radius, radius * 2, radius * 2);
|
||||
}
|
||||
|
||||
for (let i = 0; i < particles.length; i++) {
|
||||
const p = particles[i];
|
||||
const dx = Math.sin(time * p.speed + p.phase) * p.amp;
|
||||
const dy = Math.cos(time * p.speed * 0.65 + p.phase) * p.amp * 0.7;
|
||||
const x = p.bx + dx;
|
||||
const y = p.by + dy;
|
||||
const [r, g, b] = warmRGB(p.warmth);
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, p.r, 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgba(${r},${g},${b},${p.a})`;
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
if (showHighlights) {
|
||||
ctx.lineWidth = 0.55;
|
||||
for (let i = 0; i < highlights.length; i++) {
|
||||
const a = highlights[i];
|
||||
for (let j = i + 1; j < highlights.length; j++) {
|
||||
const b = highlights[j];
|
||||
const dx = a.x - b.x;
|
||||
const dy = a.y - b.y;
|
||||
const d2 = dx * dx + dy * dy;
|
||||
if (d2 < 12000) {
|
||||
const d = Math.sqrt(d2);
|
||||
const alpha = (1 - d / 110) * 0.22;
|
||||
if (alpha > 0) {
|
||||
const wm = (a.warmth + b.warmth) * 0.5;
|
||||
const [rr, gg, bb] = warmRGB(wm);
|
||||
ctx.strokeStyle = `rgba(${rr},${gg},${bb},${alpha})`;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(a.x, a.y);
|
||||
ctx.lineTo(b.x, b.y);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.globalCompositeOperation = 'screen';
|
||||
for (const hl of highlights) {
|
||||
const tw = 0.55 + 0.45 * Math.sin(time * hl.speed + hl.phase);
|
||||
const [r, g, b] = warmRGB(hl.warmth);
|
||||
const haloR = hl.r * 7.5;
|
||||
const grd = ctx.createRadialGradient(hl.x, hl.y, 0, hl.x, hl.y, haloR);
|
||||
grd.addColorStop(0, `rgba(${r},${g},${b},${0.55 * tw})`);
|
||||
grd.addColorStop(0.35, `rgba(${r},${g},${b},${0.13 * tw})`);
|
||||
grd.addColorStop(1, 'rgba(0,0,0,0)');
|
||||
ctx.fillStyle = grd;
|
||||
ctx.beginPath();
|
||||
ctx.arc(hl.x, hl.y, haloR, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(hl.x, hl.y, hl.r * (0.85 + 0.3 * tw), 0, Math.PI * 2);
|
||||
ctx.fillStyle = `rgba(225,236,255,${0.9 * tw})`;
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
}
|
||||
|
||||
rafId = requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
resize();
|
||||
draw();
|
||||
window.addEventListener('resize', resize);
|
||||
return () => {
|
||||
cancelAnimationFrame(rafId);
|
||||
window.removeEventListener('resize', resize);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<canvas bind:this={canvas} class="pointer-events-none {className}" aria-hidden="true"></canvas>
|
||||
@@ -0,0 +1,62 @@
|
||||
<script>
|
||||
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
|
||||
|
||||
/**
|
||||
* Wrap-flow list of pill-shaped bordered cards with an optional colored dot.
|
||||
* Used on solution pages for use-case lists, audience lists, etc.
|
||||
*
|
||||
* @typedef {Object} Props
|
||||
* @property {string[]} items
|
||||
* @property {boolean} [showDot] Show a small pastel dot at the start of each pill (default true)
|
||||
* @property {boolean} [animate] Wrap each pill in BlurFade (default true)
|
||||
* @property {string} [gap] Gap class between pills (default "gap-3")
|
||||
* @property {string} [size] Padding/text size variant: "default" | "compact" (default "default")
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let { items, showDot = true, animate = true, gap = 'gap-3', size = 'default' } = $props();
|
||||
|
||||
const dotColors = [
|
||||
'bg-blue-300',
|
||||
'bg-emerald-300',
|
||||
'bg-amber-300',
|
||||
'bg-violet-300',
|
||||
'bg-rose-300',
|
||||
'bg-cyan-300',
|
||||
'bg-orange-300',
|
||||
'bg-fuchsia-300'
|
||||
];
|
||||
|
||||
const sizes = {
|
||||
default: 'px-4 py-2 text-sm',
|
||||
compact: 'px-3 py-1.5 text-xs'
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="flex flex-wrap {gap}">
|
||||
{#each items as item, i}
|
||||
{#if animate}
|
||||
<BlurFade delay={0.03 * i} once>
|
||||
<span
|
||||
class="inline-flex items-center gap-2 rounded-full border border-gray-200 bg-white text-gray-800 {sizes[
|
||||
size
|
||||
]}"
|
||||
>
|
||||
{#if showDot}
|
||||
<span class="h-2 w-2 rounded-full {dotColors[i % dotColors.length]}"></span>
|
||||
{/if}
|
||||
{item}
|
||||
</span>
|
||||
</BlurFade>
|
||||
{:else}
|
||||
<span
|
||||
class="inline-flex items-center gap-2 rounded-full border border-gray-200 bg-white text-gray-800 {sizes[size]}"
|
||||
>
|
||||
{#if showDot}
|
||||
<span class="h-2 w-2 rounded-full {dotColors[i % dotColors.length]}"></span>
|
||||
{/if}
|
||||
{item}
|
||||
</span>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
@@ -0,0 +1,47 @@
|
||||
<script>
|
||||
import BoxedGrid from './boxed-grid.svelte';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {string} [bgClass]
|
||||
* @property {string} [eyebrow]
|
||||
* @property {string} [heading]
|
||||
* @property {string} [subhead]
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let {
|
||||
bgClass = 'bg-white',
|
||||
eyebrow = 'The case for an academy',
|
||||
heading = 'Customers who learn the product, stay.',
|
||||
subhead = 'Numbers from teams who built one:'
|
||||
} = $props();
|
||||
|
||||
const stats = [
|
||||
{ value: '+22%', label: 'Higher retention', source: 'Forrester · customer-education TEI' },
|
||||
{ value: '+38%', label: 'Product adoption', source: 'Intellum / Forrester benchmarks' },
|
||||
{ value: '−6.1%', label: 'Support costs', source: 'TSIA · education services impact' }
|
||||
];
|
||||
</script>
|
||||
|
||||
<section class="{bgClass} px-6 py-12 lg:px-12 lg:py-16">
|
||||
<div class="mx-auto max-w-[1100px]">
|
||||
<div class="mb-8 max-w-[640px]">
|
||||
<div class="mb-2 text-xs font-medium tracking-widest text-blue-700 uppercase">{eyebrow}</div>
|
||||
<h2 class="text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight">{heading}</h2>
|
||||
<p class="mt-3 text-sm leading-relaxed text-gray-500">{subhead}</p>
|
||||
</div>
|
||||
|
||||
<BoxedGrid items={stats} cols={3} padding="px-5 py-10 lg:py-12">
|
||||
{#snippet cell(stat)}
|
||||
<div class="flex h-full flex-col items-center justify-center text-center">
|
||||
<p class="text-[clamp(2rem,3.4vw,2.8rem)] font-medium tracking-tight text-gray-950">{stat.value}</p>
|
||||
<p class="mt-1.5 text-sm text-gray-600">{stat.label}</p>
|
||||
<p class="mt-3 text-[11px] text-gray-400">{stat.source}</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
</BoxedGrid>
|
||||
|
||||
<p class="mt-4 text-xs text-gray-400">Industry benchmarks. Your mileage will vary.</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script>
|
||||
/**
|
||||
* Two-column feature split — text + optional bullets on one side, image on the other.
|
||||
* Replaces bespoke per-page splits so we keep grid + spacing consistent across marketing pages.
|
||||
*
|
||||
* @typedef {Object} Props
|
||||
* @property {string} [eyebrow]
|
||||
* @property {string} title
|
||||
* @property {string} description
|
||||
* @property {string} imageSrc
|
||||
* @property {string} imageAlt
|
||||
* @property {string} [imageAspect] Tailwind aspect class (default "aspect-[4/3]")
|
||||
* @property {'left'|'right'} [imagePosition] (default "right")
|
||||
* @property {string} [bgClass] Section background (default "bg-white")
|
||||
* @property {import('svelte').Snippet} [bullets] Snippet rendering `<li>` children
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let {
|
||||
eyebrow,
|
||||
title,
|
||||
description,
|
||||
imageSrc,
|
||||
imageAlt,
|
||||
imageAspect = 'aspect-[4/3]',
|
||||
imagePosition = 'right',
|
||||
bgClass = 'bg-white',
|
||||
bullets
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<section class="px-6 py-12 lg:px-12 lg:py-16 {bgClass}">
|
||||
<div class="mx-auto grid max-w-[1100px] grid-cols-1 items-center gap-12 lg:grid-cols-2 lg:gap-20">
|
||||
<div class={imagePosition === 'left' ? 'lg:order-2' : ''}>
|
||||
{#if eyebrow}
|
||||
<div class="mb-2 text-xs font-medium tracking-widest text-blue-700 uppercase">{eyebrow}</div>
|
||||
{/if}
|
||||
<h2 class="text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight">{title}</h2>
|
||||
<p class="mt-5 text-base leading-relaxed text-gray-500">{description}</p>
|
||||
{#if bullets}
|
||||
<ul class="mt-6 space-y-3 text-sm text-gray-700">
|
||||
{@render bullets()}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-gray-200 {imageAspect}">
|
||||
<img src={imageSrc} alt={imageAlt} class="h-full w-full object-cover" loading="lazy" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -2,13 +2,13 @@
|
||||
import { SenjaEmbed } from '$lib/components';
|
||||
</script>
|
||||
|
||||
<section id="testimonial" class="bg-white px-6 py-16 lg:px-12 lg:py-24">
|
||||
<section id="testimonial" class="bg-white px-6 py-12 lg:px-12 lg:py-16">
|
||||
<div class="mx-auto max-w-[1100px] text-center">
|
||||
<div class="mb-3.5 text-xs font-bold tracking-widest text-blue-700 uppercase">Testimonials</div>
|
||||
<h2 class="mb-4 text-[clamp(2rem,3vw,2.8rem)] leading-[1.15] font-bold tracking-tight">
|
||||
<div class="mb-2 text-xs font-medium tracking-widest text-blue-700 uppercase">Testimonials</div>
|
||||
<h2 class="mb-3 text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight">
|
||||
Hear from happy ClassroomIO users
|
||||
</h2>
|
||||
<p class="mx-auto mb-10 max-w-[520px] text-base leading-relaxed text-gray-500 lg:text-lg">
|
||||
<p class="mx-auto mb-8 max-w-[520px] text-sm leading-relaxed text-gray-500">
|
||||
See what excites them about building and managing training programs with ClassroomIO.
|
||||
</p>
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<script>
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {string} caption Eyebrow caption (e.g. "For partner programs at")
|
||||
* @property {string[]} items Items shown pipe-separated
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let { caption, items } = $props();
|
||||
</script>
|
||||
|
||||
<section class="border-b border-gray-100 bg-white px-6 py-10 lg:px-12">
|
||||
<div class="mx-auto max-w-[1100px]">
|
||||
<p class="mb-6 text-center text-xs font-medium tracking-widest text-gray-500 uppercase">{caption}</p>
|
||||
<div class="flex flex-wrap items-center justify-center gap-x-10 gap-y-4 text-sm font-medium text-gray-500">
|
||||
{#each items as item, i}
|
||||
{#if i > 0}<span class="text-gray-300">·</span>{/if}
|
||||
<span>{item}</span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script>
|
||||
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
|
||||
import VideoPlaceholder from './video-placeholder.svelte';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {string} eyebrow Eyebrow label (e.g. "A live walkthrough")
|
||||
* @property {string} title Section heading
|
||||
* @property {string} [description] Lead paragraph below the heading
|
||||
* @property {string} suggestedFile Path passed through to VideoPlaceholder
|
||||
* @property {string} [bgClass] Optional section background (default bg-gray-50)
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let { eyebrow, title, description, suggestedFile, bgClass = 'bg-gray-50' } = $props();
|
||||
</script>
|
||||
|
||||
<section class="px-6 py-12 lg:px-12 lg:py-16 {bgClass}">
|
||||
<div class="mx-auto max-w-[1100px]">
|
||||
<div class="mb-10 max-w-[620px]">
|
||||
<div class="mb-2 text-xs font-medium tracking-widest text-blue-700 uppercase">{eyebrow}</div>
|
||||
<h2 class="text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight">{title}</h2>
|
||||
{#if description}
|
||||
<p class="mt-5 text-base leading-relaxed text-gray-500">{description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<BlurFade delay={0.1} once>
|
||||
<div class="relative mx-auto max-w-[920px]">
|
||||
<VideoPlaceholder {suggestedFile} />
|
||||
</div>
|
||||
</BlurFade>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script>
|
||||
import PlayCircle from '@lucide/svelte/icons/play-circle';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {string} suggestedFile Suggested static path (e.g. "/compliance-demo.mp4")
|
||||
* @property {string} [caption] Optional short description
|
||||
* @property {string} [className] Extra classes
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let { suggestedFile, caption = '', className = '' } = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="relative aspect-video w-full overflow-hidden rounded-2xl border-2 border-dashed border-blue-300 bg-gradient-to-br from-blue-50 via-white to-gray-100 {className}"
|
||||
>
|
||||
<div class="absolute inset-0 flex flex-col items-center justify-center gap-3 text-center">
|
||||
<div
|
||||
class="flex h-20 w-20 items-center justify-center rounded-full bg-white text-blue-700 shadow-lg ring-1 ring-blue-100"
|
||||
>
|
||||
<PlayCircle size={48} strokeWidth={1.4} />
|
||||
</div>
|
||||
<p class="text-sm font-medium text-gray-700">Video placeholder</p>
|
||||
<p class="text-xs text-gray-500">Replace with HeroVideoDialog · suggested file: {suggestedFile}</p>
|
||||
{#if caption}
|
||||
<p class="max-w-[80%] text-[11px] text-gray-400">{caption}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,146 @@
|
||||
<script>
|
||||
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
|
||||
import Check from '@lucide/svelte/icons/check';
|
||||
import X from '@lucide/svelte/icons/x';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {string} [bgClass]
|
||||
* @property {string} [eyebrow]
|
||||
* @property {string} [heading]
|
||||
* @property {string} [subhead]
|
||||
* @property {boolean} [showHeading]
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let {
|
||||
bgClass = 'bg-white',
|
||||
eyebrow = 'Help docs are not an academy',
|
||||
heading = 'Reading docs isn’t the same as learning.',
|
||||
subhead = 'Intercom Articles, Zendesk Guide, and Confluence are great at one thing — answering "how do I do X?" when the user already knows what they need. They were never built to teach.',
|
||||
showHeading = true
|
||||
} = $props();
|
||||
|
||||
const rows = [
|
||||
{
|
||||
capability: 'Sequenced learning path',
|
||||
docs: false,
|
||||
academy: true,
|
||||
note: 'A course goes from "what this product is" to "I can do this unsupervised." Docs are alphabetical, not sequential.'
|
||||
},
|
||||
{
|
||||
capability: 'Assessment & quiz grading',
|
||||
docs: false,
|
||||
academy: true,
|
||||
note: 'You can’t prove someone understood an article. You can prove they passed a 12-question final.'
|
||||
},
|
||||
{
|
||||
capability: 'Certificates & verifiable IDs',
|
||||
docs: false,
|
||||
academy: true,
|
||||
note: 'A shareable cert turns power-users into a public community. Help docs don’t.'
|
||||
},
|
||||
{
|
||||
capability: 'Programs / multi-course paths',
|
||||
docs: false,
|
||||
academy: true,
|
||||
note: 'Onboarding week, admin track, power-user path — three audiences, three sequenced journeys, one academy.'
|
||||
},
|
||||
{
|
||||
capability: 'AI tutor inside the lesson',
|
||||
docs: false,
|
||||
academy: true,
|
||||
note: 'Lesson-aware Claude tutor answers follow-ups before the learner bounces to a support ticket.'
|
||||
},
|
||||
{
|
||||
capability: 'Per-learner progress & analytics',
|
||||
docs: false,
|
||||
academy: true,
|
||||
note: 'See who finished, who stalled, where they got stuck. Page-view analytics don’t answer "did this customer learn?"'
|
||||
},
|
||||
{
|
||||
capability: 'Branded portal on your domain',
|
||||
docs: true,
|
||||
academy: true,
|
||||
note: 'Both can. Help-doc tools usually charge extra for it. ClassroomIO doesn’t.'
|
||||
},
|
||||
{
|
||||
capability: 'Search & quick lookup',
|
||||
docs: true,
|
||||
academy: false,
|
||||
note: 'When someone already knows what they need, docs win. Pair an academy with your docs — don’t replace them.'
|
||||
},
|
||||
{
|
||||
capability: 'In-product contextual answers',
|
||||
docs: true,
|
||||
academy: false,
|
||||
note: 'Help widgets in-app are still where short-form, "right now" answers belong.'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<section class="{bgClass} px-6 py-16 lg:px-12 lg:py-24">
|
||||
<div class="mx-auto max-w-[1100px]">
|
||||
{#if showHeading}
|
||||
<div class="mb-12 max-w-[680px]">
|
||||
<div class="mb-3 text-xs font-medium tracking-widest text-blue-700 uppercase">{eyebrow}</div>
|
||||
<h2 class="text-[clamp(2rem,3vw,2.8rem)] leading-[1.15] font-medium tracking-tight">{heading}</h2>
|
||||
<p class="mt-5 text-base leading-relaxed text-gray-500">{subhead}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="overflow-hidden rounded-2xl border border-gray-200 bg-white">
|
||||
<div
|
||||
class="grid grid-cols-[1.6fr_repeat(2,minmax(0,0.8fr))_1.4fr] gap-0 border-b border-gray-200 bg-gray-50 px-5 py-3.5 text-[11px] font-medium tracking-widest text-gray-500 uppercase"
|
||||
>
|
||||
<div>Capability</div>
|
||||
<div class="text-center">Help docs</div>
|
||||
<div class="text-center">Company academy</div>
|
||||
<div class="hidden lg:block">Why it matters</div>
|
||||
</div>
|
||||
|
||||
{#each rows as row, i}
|
||||
<BlurFade delay={0.03 * i} once>
|
||||
<div
|
||||
class="grid grid-cols-[1.6fr_repeat(2,minmax(0,0.8fr))_1.4fr] items-start gap-0 border-b border-gray-100 px-5 py-4 text-sm last:border-b-0 hover:bg-blue-50/30"
|
||||
>
|
||||
<div class="font-medium text-gray-950">{row.capability}</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
{#if row.docs}
|
||||
<span class="flex h-7 w-7 items-center justify-center rounded-full bg-emerald-50 text-emerald-600">
|
||||
<Check size={16} strokeWidth={2.5} />
|
||||
</span>
|
||||
{:else}
|
||||
<span class="flex h-7 w-7 items-center justify-center rounded-full bg-gray-100 text-gray-400">
|
||||
<X size={16} strokeWidth={2.5} />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
{#if row.academy}
|
||||
<span
|
||||
class="flex h-7 w-7 items-center justify-center rounded-full bg-blue-50 text-blue-700 ring-1 ring-blue-200"
|
||||
>
|
||||
<Check size={16} strokeWidth={2.5} />
|
||||
</span>
|
||||
{:else}
|
||||
<span class="flex h-7 w-7 items-center justify-center rounded-full bg-gray-100 text-gray-400">
|
||||
<X size={16} strokeWidth={2.5} />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="hidden text-xs leading-relaxed text-gray-500 lg:block">{row.note}</div>
|
||||
</div>
|
||||
</BlurFade>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<p class="mt-6 text-xs text-gray-500">
|
||||
Compared against typical capabilities of Intercom Articles, Zendesk Guide, and Confluence at time of writing. Run
|
||||
your own academy alongside your existing help center — most teams who launch a customer academy keep both.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,113 @@
|
||||
<script>
|
||||
/**
|
||||
* @typedef {Object} Step
|
||||
* @property {string} title
|
||||
* @property {string} description
|
||||
* @property {string} [label] Optional small uppercase label shown below the description (e.g. "STEP 01")
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @property {string} eyebrow Eyebrow label (e.g. "How it works")
|
||||
* @property {string} title Section heading
|
||||
* @property {string} [description] Optional lead paragraph below the heading
|
||||
* @property {Step[]} steps Workflow steps (typically 4)
|
||||
* @property {string} [bgClass] Optional section background (default bg-white)
|
||||
*/
|
||||
|
||||
/** @type {Props} */
|
||||
let { eyebrow, title, description, steps, bgClass = 'bg-white' } = $props();
|
||||
|
||||
let hoveredIndex = $state(-1);
|
||||
|
||||
function isActive(index) {
|
||||
if (hoveredIndex === -1) return index === 0;
|
||||
return index <= hoveredIndex;
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="px-6 py-12 lg:px-12 lg:py-16 {bgClass}">
|
||||
<div class="mx-auto max-w-[1100px]">
|
||||
<div class="max-w-[680px]">
|
||||
<div class="mb-2 text-xs font-medium tracking-widest text-blue-700 uppercase">{eyebrow}</div>
|
||||
<h2 class="text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight">{title}</h2>
|
||||
{#if description}
|
||||
<p class="mt-5 text-base leading-relaxed text-gray-500">{description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Desktop: horizontal timeline -->
|
||||
<div class="mt-12 hidden! md:block!">
|
||||
<div class="relative">
|
||||
<!-- connecting line -->
|
||||
<div class="absolute top-[7px] right-2 left-2 h-px bg-gray-200"></div>
|
||||
|
||||
<div
|
||||
class="relative grid gap-8"
|
||||
style:grid-template-columns="repeat({steps.length}, minmax(0, 1fr))"
|
||||
onmouseleave={() => (hoveredIndex = -1)}
|
||||
role="presentation"
|
||||
>
|
||||
{#each steps as step, i}
|
||||
<div class="relative" onmouseenter={() => (hoveredIndex = i)} role="presentation">
|
||||
<span
|
||||
class="relative z-10 flex h-3.5 w-3.5 items-center justify-center rounded-full transition-all duration-200 {isActive(
|
||||
i
|
||||
)
|
||||
? 'bg-blue-700 ring-4 ring-blue-100'
|
||||
: 'bg-gray-300'}"
|
||||
>
|
||||
{#if i === 0 && hoveredIndex === -1}
|
||||
<span class="absolute inline-flex h-3.5 w-3.5 animate-ping rounded-full bg-blue-500 opacity-60"
|
||||
></span>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<div class="mt-6">
|
||||
<h3 class="text-sm font-medium text-gray-950">{step.title}</h3>
|
||||
<p class="mt-2 text-sm leading-relaxed text-gray-500">{step.description}</p>
|
||||
<p
|
||||
class="mt-4 font-mono text-[10px] tracking-[0.15em] uppercase transition-colors duration-200 {isActive(
|
||||
i
|
||||
)
|
||||
? 'text-blue-700'
|
||||
: 'text-gray-400'}"
|
||||
>
|
||||
{step.label ?? `Step ${String(i + 1).padStart(2, '0')}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile: vertical timeline -->
|
||||
<div class="mt-10 md:hidden!">
|
||||
<div class="relative space-y-8 pl-6" onmouseleave={() => (hoveredIndex = -1)} role="presentation">
|
||||
<div class="absolute top-2 bottom-2 left-[6px] w-px bg-gray-200"></div>
|
||||
|
||||
{#each steps as step, i}
|
||||
<div class="relative" onmouseenter={() => (hoveredIndex = i)} role="presentation">
|
||||
<span
|
||||
class="absolute top-1 -left-6 flex h-3 w-3 items-center justify-center rounded-full transition-all duration-200 {isActive(
|
||||
i
|
||||
)
|
||||
? 'bg-blue-700 ring-4 ring-blue-100'
|
||||
: 'bg-gray-300'}"
|
||||
></span>
|
||||
<h3 class="text-sm font-medium text-gray-950">{step.title}</h3>
|
||||
<p class="mt-2 text-sm leading-relaxed text-gray-500">{step.description}</p>
|
||||
<p
|
||||
class="mt-3 font-mono text-[10px] tracking-[0.15em] uppercase transition-colors duration-200 {isActive(i)
|
||||
? 'text-blue-700'
|
||||
: 'text-gray-400'}"
|
||||
>
|
||||
{step.label ?? `Step ${String(i + 1).padStart(2, '0')}`}
|
||||
</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,81 +0,0 @@
|
||||
<script>
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import * as Field from '@cio/ui/base/field';
|
||||
import { Input } from '@cio/ui/base/input';
|
||||
import { Textarea } from '@cio/ui/base/textarea';
|
||||
import { isFormValid } from '$lib/utils/is-form-valid';
|
||||
import { submitForm } from '$lib/utils/submit-form';
|
||||
import { Success } from '$lib/components';
|
||||
|
||||
let name = $state('');
|
||||
let email = $state('');
|
||||
let title = $state('');
|
||||
let description = $state('');
|
||||
|
||||
let isLoading = $state(false);
|
||||
let errorMessage = $state('');
|
||||
let sent = $state(false);
|
||||
|
||||
const handleClick = async () => {
|
||||
const data = { name, email, title, description };
|
||||
|
||||
if (!isFormValid(data)) {
|
||||
errorMessage = 'Please fill out all required fields.';
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
|
||||
const formData = {
|
||||
title: `[FEEDBACK] ${title}`,
|
||||
description: `My name is ${JSON.stringify(name)}, email is ${JSON.stringify(
|
||||
email
|
||||
)}. \n\n${JSON.stringify(description)}.`,
|
||||
replyTo: email
|
||||
};
|
||||
|
||||
try {
|
||||
await submitForm(formData);
|
||||
sent = true;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if sent}
|
||||
<Success />
|
||||
{:else}
|
||||
<div class="my-10 w-full space-y-5">
|
||||
<Field.Group>
|
||||
<div class="flex flex-col gap-10 md:flex-row">
|
||||
<Field.Field class="w-full">
|
||||
<Field.Label for="feedback-name">Your Name</Field.Label>
|
||||
<Input id="feedback-name" bind:value={name} required />
|
||||
</Field.Field>
|
||||
<Field.Field class="w-full">
|
||||
<Field.Label for="feedback-email">Your Email</Field.Label>
|
||||
<Input id="feedback-email" type="email" bind:value={email} required />
|
||||
</Field.Field>
|
||||
</div>
|
||||
<Field.Field>
|
||||
<Field.Label for="feedback-subject">Subject</Field.Label>
|
||||
<Input id="feedback-subject" bind:value={title} required />
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label for="feedback-message">Message</Field.Label>
|
||||
<Textarea id="feedback-message" bind:value={description} required />
|
||||
</Field.Field>
|
||||
{#if !!errorMessage}
|
||||
<Field.Field>
|
||||
<Field.Error>{errorMessage}</Field.Error>
|
||||
</Field.Field>
|
||||
{/if}
|
||||
</Field.Group>
|
||||
<Button onclick={handleClick} disabled={isLoading} loading={isLoading}>Submit Feedback</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,81 +0,0 @@
|
||||
<script>
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import * as Field from '@cio/ui/base/field';
|
||||
import { Input } from '@cio/ui/base/input';
|
||||
import { Textarea } from '@cio/ui/base/textarea';
|
||||
import { isFormValid } from '$lib/utils/is-form-valid';
|
||||
import { submitForm } from '$lib/utils/submit-form';
|
||||
import { Success } from '$lib/components';
|
||||
|
||||
let name = $state('');
|
||||
let email = $state('');
|
||||
let title = $state('');
|
||||
let description = $state('');
|
||||
|
||||
let isLoading = $state(false);
|
||||
let errorMessage = $state('');
|
||||
let sent = $state(false);
|
||||
|
||||
async function handleClick() {
|
||||
const data = { name, email, title, description };
|
||||
|
||||
if (!isFormValid(data)) {
|
||||
errorMessage = 'Please fill out all required fields.';
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
|
||||
const formData = {
|
||||
title: `[HELP] ${title}`,
|
||||
description: `My name is ${JSON.stringify(name)}, email is ${JSON.stringify(
|
||||
email
|
||||
)}. \n\n${JSON.stringify(description)}.`,
|
||||
replyTo: email
|
||||
};
|
||||
|
||||
try {
|
||||
await submitForm(formData);
|
||||
sent = true;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if sent}
|
||||
<Success />
|
||||
{:else}
|
||||
<div class="my-10 w-full space-y-5">
|
||||
<Field.Group>
|
||||
<div class="flex flex-col gap-10 md:flex-row">
|
||||
<Field.Field class="w-full">
|
||||
<Field.Label for="help-name">Your Name</Field.Label>
|
||||
<Input id="help-name" bind:value={name} required />
|
||||
</Field.Field>
|
||||
<Field.Field class="w-full">
|
||||
<Field.Label for="help-email">Your Email</Field.Label>
|
||||
<Input id="help-email" type="email" bind:value={email} required />
|
||||
</Field.Field>
|
||||
</div>
|
||||
<Field.Field>
|
||||
<Field.Label for="help-subject">Subject</Field.Label>
|
||||
<Input id="help-subject" bind:value={title} required />
|
||||
</Field.Field>
|
||||
<Field.Field>
|
||||
<Field.Label for="help-message">Message</Field.Label>
|
||||
<Textarea id="help-message" bind:value={description} required />
|
||||
</Field.Field>
|
||||
{#if !!errorMessage}
|
||||
<Field.Field>
|
||||
<Field.Error>{errorMessage}</Field.Error>
|
||||
</Field.Field>
|
||||
{/if}
|
||||
</Field.Group>
|
||||
<Button onclick={handleClick} disabled={isLoading} loading={isLoading}>Submit Request</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,20 +0,0 @@
|
||||
export const submitForm = async (formData: any) => {
|
||||
try {
|
||||
const response = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to submit the form');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Response from API:', data);
|
||||
} catch (error) {
|
||||
console.error('Error submitting form:', error);
|
||||
}
|
||||
};
|
||||
@@ -24,12 +24,3 @@ export type OssFriend = {
|
||||
description: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
export type FORM_TYPE_KEY = keyof typeof FORM_TYPE;
|
||||
|
||||
export enum FORM_TYPE {
|
||||
BUG = 'BUG',
|
||||
HELP = 'HELP',
|
||||
FEATURE = 'FEATURE',
|
||||
FEEDBACK = 'FEEDBACK'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user