feat: certificates, landing pages, ai tutor, website update, compliance
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
"storybook:publish": "pnpm build-storybook && pnpm storybook:upload"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cio/certificates": "workspace:*",
|
||||
"@cio/ui": "workspace:*",
|
||||
"@cio/utils": "workspace:*",
|
||||
"svelte-inview": "^4.0.4",
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import { NumberBadge } from '@cio/ui/base/number-badge';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Atom/NumberBadge',
|
||||
component: NumberBadge,
|
||||
parameters: {
|
||||
layout: 'centered'
|
||||
},
|
||||
tags: ['autodocs']
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Inactive">
|
||||
{#snippet template()}
|
||||
<NumberBadge number={3} />
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Active">
|
||||
{#snippet template()}
|
||||
<NumberBadge number={3} active />
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Locked">
|
||||
{#snippet template()}
|
||||
<NumberBadge number={3} locked />
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="With group hover">
|
||||
{#snippet template()}
|
||||
<button
|
||||
type="button"
|
||||
class="group hover:border-border flex cursor-pointer items-center gap-2 rounded-md border border-transparent px-2 py-1.5"
|
||||
>
|
||||
<NumberBadge number={2} />
|
||||
<span class="text-sm">Hover the row to see badge emphasis</span>
|
||||
</button>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Sequence">
|
||||
{#snippet template()}
|
||||
<div class="flex items-center gap-2">
|
||||
<NumberBadge number={1} />
|
||||
<NumberBadge number={2} active />
|
||||
<NumberBadge number={3} locked />
|
||||
<NumberBadge number={4} />
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
@@ -16,7 +16,8 @@
|
||||
ShinnyText,
|
||||
Sparkle,
|
||||
SquareCardBody,
|
||||
SquareCardIcon
|
||||
SquareCardIcon,
|
||||
Waves
|
||||
} from '@cio/ui/custom/animation';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
@@ -72,6 +73,19 @@
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Waves">
|
||||
{#snippet template()}
|
||||
<div
|
||||
class="ui:relative ui:mx-auto ui:h-[500px] ui:w-full ui:max-w-3xl ui:overflow-hidden ui:rounded-xl ui:border ui:bg-background"
|
||||
>
|
||||
<Waves class="ui:absolute ui:inset-0 ui:h-full ui:w-full" />
|
||||
<div class="ui:relative ui:z-10 ui:flex ui:h-full ui:items-center ui:justify-center ui:text-sm">
|
||||
Move the cursor — animated waves use the theme primary stroke
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="CardContainer">
|
||||
{#snippet template()}
|
||||
<CardContainer className="ui:max-w-lg">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script module>
|
||||
<script module lang="ts">
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import { ModelPicker } from '@cio/ui/custom/model-picker';
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { DEFAULT_PICKER_MODEL_ID, type AgentModelId } from '@cio/utils/agent-models';
|
||||
|
||||
let selected = $state<AgentModelId>(DEFAULT_PICKER_MODEL_ID);
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
onPrev={() => console.log('prev')}
|
||||
onNext={() => console.log('next')}
|
||||
>
|
||||
<PublicCourse.PublicLessonView lesson={LESSON_FIXTURE} callout={CALLOUT_FIXTURE} />
|
||||
<PublicCourse.PublicLessonView lesson={LESSON_FIXTURE} videoCaptionsLabel="Captions" callout={CALLOUT_FIXTURE} />
|
||||
</PublicCourse.PublicCourseShell>
|
||||
{/snippet}
|
||||
</Story>
|
||||
@@ -68,7 +68,11 @@
|
||||
hasPrev
|
||||
hasNext={false}
|
||||
>
|
||||
<PublicCourse.PublicLessonView lesson={LESSON_LOCKED_FIXTURE} callout={CALLOUT_FIXTURE} />
|
||||
<PublicCourse.PublicLessonView
|
||||
lesson={LESSON_LOCKED_FIXTURE}
|
||||
videoCaptionsLabel="Captions"
|
||||
callout={CALLOUT_FIXTURE}
|
||||
/>
|
||||
</PublicCourse.PublicCourseShell>
|
||||
{/snippet}
|
||||
</Story>
|
||||
@@ -105,12 +109,26 @@
|
||||
{#snippet template()}
|
||||
<div class="ui:mx-auto ui:max-w-2xl ui:space-y-8 ui:p-8">
|
||||
<div>
|
||||
<h3 class="ui:mb-2 ui:text-sm ui:font-semibold ui:text-muted-foreground">Inline (at bottom of lessons)</h3>
|
||||
<PublicCourse.PublicCourseCallout callout={CALLOUT_FIXTURE} variant="inline" />
|
||||
<h3 class="ui:mb-2 ui:text-sm ui:font-semibold ui:text-muted-foreground">Inline · waves (default)</h3>
|
||||
<PublicCourse.PublicCourseCallout callout={CALLOUT_FIXTURE} variant="inline" animation="waves" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="ui:mb-2 ui:text-sm ui:font-semibold ui:text-muted-foreground">Full (locked item replacement)</h3>
|
||||
<PublicCourse.PublicCourseCallout callout={CALLOUT_FIXTURE} variant="full" />
|
||||
<h3 class="ui:mb-2 ui:text-sm ui:font-semibold ui:text-muted-foreground">Inline · dotted</h3>
|
||||
<PublicCourse.PublicCourseCallout callout={CALLOUT_FIXTURE} variant="inline" animation="dotted" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="ui:mb-2 ui:text-sm ui:font-semibold ui:text-muted-foreground">
|
||||
Full (locked item replacement) · waves
|
||||
</h3>
|
||||
<PublicCourse.PublicCourseCallout callout={CALLOUT_FIXTURE} variant="full" animation="waves" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="ui:mb-2 ui:text-sm ui:font-semibold ui:text-muted-foreground">Full · dotted</h3>
|
||||
<PublicCourse.PublicCourseCallout callout={CALLOUT_FIXTURE} variant="full" animation="dotted" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="ui:mb-2 ui:text-sm ui:font-semibold ui:text-muted-foreground">Inline · none (no motion)</h3>
|
||||
<PublicCourse.PublicCourseCallout callout={CALLOUT_FIXTURE} variant="inline" animation="none" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="ui:mb-2 ui:text-sm ui:font-semibold ui:text-muted-foreground">Full (no callout configured)</h3>
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
<script module lang="ts">
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import { Certificate } from '@cio/ui';
|
||||
import {
|
||||
CERTIFICATE_TEMPLATE_LIST,
|
||||
LONG_RENDER_DATA,
|
||||
MINIMAL_RENDER_DATA,
|
||||
SAMPLE_RENDER_DATA,
|
||||
STORY_ACCENT_VARIATIONS,
|
||||
buildDesign
|
||||
} from './fixtures';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
title: 'Templates/Certificate',
|
||||
component: Certificate.Preview,
|
||||
parameters: {
|
||||
layout: 'fullscreen'
|
||||
},
|
||||
tags: ['autodocs']
|
||||
});
|
||||
</script>
|
||||
|
||||
<Story name="Gallery · all templates">
|
||||
{#snippet template()}
|
||||
<div class="grid grid-cols-1 gap-6 bg-zinc-100 p-6 md:grid-cols-2 xl:grid-cols-3">
|
||||
{#each CERTIFICATE_TEMPLATE_LIST as template (template.id)}
|
||||
<figure class="overflow-hidden rounded-xl border border-zinc-200 bg-white shadow-sm">
|
||||
<div class="aspect-[1.4/1] w-full">
|
||||
<Certificate.Preview design={buildDesign(template.id)} data={SAMPLE_RENDER_DATA} />
|
||||
</div>
|
||||
<figcaption class="border-t border-zinc-200 px-4 py-3">
|
||||
<h3 class="text-sm font-medium text-zinc-900">{template.label}</h3>
|
||||
<p class="mt-0.5 text-xs text-zinc-500">{template.description}</p>
|
||||
</figcaption>
|
||||
</figure>
|
||||
{/each}
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Classique · full-size">
|
||||
{#snippet template()}
|
||||
<div class="flex h-screen items-center justify-center bg-zinc-100 p-8">
|
||||
<div class="aspect-[1.4/1] w-full max-w-5xl">
|
||||
<Certificate.Preview design={buildDesign('classique')} data={SAMPLE_RENDER_DATA} showControls />
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Brutalist · full-size">
|
||||
{#snippet template()}
|
||||
<div class="flex h-screen items-center justify-center bg-zinc-100 p-8">
|
||||
<div class="aspect-[1.4/1] w-full max-w-5xl">
|
||||
<Certificate.Preview
|
||||
design={buildDesign('brutalist', { accentColor: '#ff4500' })}
|
||||
data={SAMPLE_RENDER_DATA}
|
||||
showControls
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Noir · full-size">
|
||||
{#snippet template()}
|
||||
<div class="flex h-screen items-center justify-center bg-zinc-950 p-8">
|
||||
<div class="aspect-[1.4/1] w-full max-w-5xl">
|
||||
<Certificate.Preview
|
||||
design={buildDesign('noir', { accentColor: '#d4af37' })}
|
||||
data={SAMPLE_RENDER_DATA}
|
||||
showControls
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Poster · full-size">
|
||||
{#snippet template()}
|
||||
<div class="flex h-screen items-center justify-center bg-zinc-100 p-8">
|
||||
<div class="aspect-[1.4/1] w-full max-w-5xl">
|
||||
<Certificate.Preview
|
||||
design={buildDesign('poster', { accentColor: '#ff5722' })}
|
||||
data={SAMPLE_RENDER_DATA}
|
||||
showControls
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Minimal · full-size">
|
||||
{#snippet template()}
|
||||
<div class="flex h-screen items-center justify-center bg-zinc-100 p-8">
|
||||
<div class="aspect-[1.4/1] w-full max-w-5xl">
|
||||
<Certificate.Preview design={buildDesign('minimal')} data={SAMPLE_RENDER_DATA} showControls />
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Classique · accent variations">
|
||||
{#snippet template()}
|
||||
<div class="grid grid-cols-1 gap-5 bg-zinc-100 p-6 md:grid-cols-2 xl:grid-cols-3">
|
||||
{#each STORY_ACCENT_VARIATIONS as accent (accent)}
|
||||
<figure class="overflow-hidden rounded-xl border border-zinc-200 bg-white shadow-sm">
|
||||
<div class="aspect-[1.4/1] w-full">
|
||||
<Certificate.Preview design={buildDesign('classique', { accentColor: accent })} data={SAMPLE_RENDER_DATA} />
|
||||
</div>
|
||||
<figcaption class="border-t border-zinc-200 px-4 py-2 text-xs text-zinc-600">
|
||||
<code>{accent}</code>
|
||||
</figcaption>
|
||||
</figure>
|
||||
{/each}
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Edge cases · long names + short data">
|
||||
{#snippet template()}
|
||||
<div class="grid grid-cols-1 gap-5 bg-zinc-100 p-6 xl:grid-cols-2">
|
||||
<figure class="overflow-hidden rounded-xl border border-zinc-200 bg-white shadow-sm">
|
||||
<div class="aspect-[1.4/1] w-full">
|
||||
<Certificate.Preview design={buildDesign('poster')} data={LONG_RENDER_DATA} />
|
||||
</div>
|
||||
<figcaption class="border-t border-zinc-200 px-4 py-3 text-xs text-zinc-600">
|
||||
Long names · poster template
|
||||
</figcaption>
|
||||
</figure>
|
||||
<figure class="overflow-hidden rounded-xl border border-zinc-200 bg-white shadow-sm">
|
||||
<div class="aspect-[1.4/1] w-full">
|
||||
<Certificate.Preview design={buildDesign('minimal')} data={MINIMAL_RENDER_DATA} />
|
||||
</div>
|
||||
<figcaption class="border-t border-zinc-200 px-4 py-3 text-xs text-zinc-600">
|
||||
Minimal copy · minimal template
|
||||
</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
ACCENT_COLORS,
|
||||
CERTIFICATE_TEMPLATES,
|
||||
DEFAULT_CERTIFICATE_DESIGN,
|
||||
type CertificateDesign,
|
||||
type CertificateRenderData,
|
||||
type CertificateTemplateId
|
||||
} from '@cio/certificates';
|
||||
|
||||
export const CERTIFICATE_TEMPLATE_LIST = CERTIFICATE_TEMPLATES;
|
||||
|
||||
export const SAMPLE_RENDER_DATA: CertificateRenderData = {
|
||||
recipientName: 'Eleanor Vance',
|
||||
courseName: 'Certificate of Excellence',
|
||||
courseDescription:
|
||||
'In recognition of outstanding achievement and exemplary dedication to the craft of design and storytelling.',
|
||||
orgName: 'The Royal Academy of Arts',
|
||||
date: 'October 12, 2026',
|
||||
certificateId: 'N° 0247'
|
||||
};
|
||||
|
||||
export const LONG_RENDER_DATA: CertificateRenderData = {
|
||||
recipientName: 'Maximilian Alexandros Constantinopoulos',
|
||||
courseName: 'Advanced Editorial Layout & Typography for the Digital Era',
|
||||
courseDescription:
|
||||
'Awarded after sustained mastery of advanced editorial layout, typography, color theory, motion craft, and the careful coordination of a multi-stage production.',
|
||||
orgName: 'The International Atelier of Design & Storytelling',
|
||||
date: 'February 03, 2026',
|
||||
certificateId: 'N° 9991'
|
||||
};
|
||||
|
||||
export const MINIMAL_RENDER_DATA: CertificateRenderData = {
|
||||
recipientName: 'Jo',
|
||||
courseName: 'Intro',
|
||||
courseDescription: 'Successfully completed the introductory programme.',
|
||||
orgName: 'CIO',
|
||||
date: 'May 11, 2026',
|
||||
certificateId: 'N° 0001'
|
||||
};
|
||||
|
||||
export function buildDesign(
|
||||
templateId: CertificateTemplateId,
|
||||
overrides: Partial<CertificateDesign> = {}
|
||||
): CertificateDesign {
|
||||
return {
|
||||
templateId,
|
||||
accentColor: overrides.accentColor ?? DEFAULT_CERTIFICATE_DESIGN.accentColor,
|
||||
subtitle: overrides.subtitle ?? DEFAULT_CERTIFICATE_DESIGN.subtitle,
|
||||
descriptionOverride: overrides.descriptionOverride,
|
||||
signatories: overrides.signatories ?? [
|
||||
{ name: 'Margaret W. Holloway', role: 'Director of Studies' },
|
||||
{ name: 'James K. Reeves', role: 'Dean of Faculty' }
|
||||
],
|
||||
idFormat: overrides.idFormat ?? DEFAULT_CERTIFICATE_DESIGN.idFormat
|
||||
};
|
||||
}
|
||||
|
||||
export const STORY_ACCENT_VARIATIONS = ACCENT_COLORS;
|
||||
@@ -118,16 +118,71 @@ export const mockProps: OrgLandingPageProps = {
|
||||
href: '#contact'
|
||||
}
|
||||
},
|
||||
links: {
|
||||
heading: 'Resources',
|
||||
description: 'Everything you need beyond the catalog.',
|
||||
boldVisitLabel: 'Visit',
|
||||
classicLearnMoreLabel: 'Learn more',
|
||||
cards: [
|
||||
{
|
||||
icon: 'help-circle',
|
||||
title: 'Help Center',
|
||||
description: 'Guides, FAQs, and how-to articles for admins and learners.',
|
||||
href: 'https://example.com/help'
|
||||
},
|
||||
{
|
||||
icon: 'video',
|
||||
title: 'Webinars',
|
||||
description: 'Live sessions and recordings on compliance and product updates.',
|
||||
href: 'https://example.com/webinars'
|
||||
},
|
||||
{
|
||||
icon: 'users',
|
||||
title: 'Community',
|
||||
description: 'Connect with peers, share templates, and ask questions.',
|
||||
href: 'https://example.com/community'
|
||||
}
|
||||
]
|
||||
},
|
||||
callout: {
|
||||
heading: 'Ready to simplify compliance?',
|
||||
description:
|
||||
'Launch your first certification program in minutes. Automate tracking, reduce risk, and keep your team certified.',
|
||||
action: { label: 'Start Free', href: '#courses' }
|
||||
},
|
||||
footerLinks: [
|
||||
{ label: 'Terms of Service', href: '#terms' },
|
||||
{ label: 'Privacy Policy', href: '#privacy' },
|
||||
{ label: 'Support', href: '#support' }
|
||||
],
|
||||
footerText: '© 2026 Certifi Academy. All rights reserved.'
|
||||
footer: {
|
||||
brand: {
|
||||
socials: [
|
||||
{ platform: 'linkedin', href: 'https://linkedin.com/company/example' },
|
||||
{ platform: 'x', href: 'https://x.com/example' }
|
||||
]
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
id: 'col-company',
|
||||
heading: 'Company',
|
||||
links: [
|
||||
{ id: 'col-company-about', label: 'About', href: '#about' },
|
||||
{ id: 'col-company-careers', label: 'Careers', href: '#careers' }
|
||||
],
|
||||
cta: { label: 'Explore more', href: '#more' }
|
||||
},
|
||||
{
|
||||
id: 'col-resources',
|
||||
heading: 'Resources',
|
||||
links: [
|
||||
{ id: 'col-resources-help', label: 'Help Center', href: '#help' },
|
||||
{ id: 'col-resources-blog', label: 'Blog', href: '#blog' }
|
||||
]
|
||||
}
|
||||
],
|
||||
bottom: {
|
||||
text: '© 2026 Certifi Academy. All rights reserved.',
|
||||
links: [
|
||||
{ id: 'bottom-terms', label: 'Terms of Service', href: '#terms' },
|
||||
{ id: 'bottom-privacy', label: 'Privacy Policy', href: '#privacy' },
|
||||
{ id: 'bottom-support', label: 'Support', href: '#support' }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import { MinimalLandingPage, BoldLandingPage, ClassicLandingPage } from '@cio/ui/custom/org-landing-page';
|
||||
import {
|
||||
MinimalLandingPage,
|
||||
BoldLandingPage,
|
||||
ClassicLandingPage,
|
||||
SaasLandingPage,
|
||||
TechLandingPage,
|
||||
StudioLandingPage,
|
||||
CorporateLandingPage,
|
||||
TerminalLandingPage
|
||||
} from '@cio/ui/custom/org-landing-page';
|
||||
import { mockProps } from './fixtures';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
@@ -23,3 +32,23 @@
|
||||
<Story name="Classic">
|
||||
<ClassicLandingPage {...mockProps} />
|
||||
</Story>
|
||||
|
||||
<Story name="SaaS">
|
||||
<SaasLandingPage {...mockProps} />
|
||||
</Story>
|
||||
|
||||
<Story name="Tech">
|
||||
<TechLandingPage {...mockProps} />
|
||||
</Story>
|
||||
|
||||
<Story name="Studio">
|
||||
<StudioLandingPage {...mockProps} />
|
||||
</Story>
|
||||
|
||||
<Story name="Corporate">
|
||||
<CorporateLandingPage {...mockProps} />
|
||||
</Story>
|
||||
|
||||
<Story name="Terminal">
|
||||
<TerminalLandingPage {...mockProps} />
|
||||
</Story>
|
||||
|
||||
Reference in New Issue
Block a user