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

This commit is contained in:
Rotimi Best
2026-05-18 03:47:22 +01:00
parent c31ad63955
commit ac3a2c14a7
644 changed files with 56050 additions and 5439 deletions
@@ -13,8 +13,10 @@
import { courseApi } from '$features/course/api';
import ContentCreateModal from '$features/course/components/content/content-create-modal.svelte';
import CourseCompletionModal from '$features/course/components/ceritficate/course-completion-modal.svelte';
import { AiCourseChat } from '$features/ai-assistant';
import { aiAssistantPanelDefinition } from '$features/ai-assistant';
import { initialChatPrompt, openAiAssistant } from '$features/ai-assistant/utils/store';
import { sidePanel, SidePanelRail } from '$features/side-panel';
import { transcriptPanelDefinition } from '$features/course/components/lesson/video/transcript-panel-definition';
import { get } from 'svelte/store';
import { profile } from '$lib/utils/store/user';
import { isOrgAdmin } from '$lib/utils/store/org';
@@ -27,6 +29,9 @@
COURSE_SIDEBAR_STORAGE_KEY
} from '$features/course/components/sidebar/constants';
sidePanel.register(aiAssistantPanelDefinition);
sidePanel.register(transcriptPanelDefinition);
interface Props {
children?: import('svelte').Snippet;
path: string;
@@ -186,5 +191,5 @@
{/if}
</Sidebar.Inset>
<AiCourseChat courseId={data.courseId} />
<SidePanelRail />
</Sidebar.Provider>
@@ -0,0 +1,7 @@
<script lang="ts">
import { CourseAiTutorSettingsPage } from '$features/ai-tutor-settings';
let { data } = $props();
</script>
<CourseAiTutorSettingsPage courseId={data.courseId} />
@@ -7,12 +7,13 @@
let { data } = $props();
</script>
<Page.Root class="mx-auto flex md:max-w-3xl lg:max-w-4xl">
<Page.Root class="w-full">
<Page.Header>
<Page.HeaderContent>
<Page.Title>
{$t('analytics.title')}
</Page.Title>
<p class="ui:text-muted-foreground text-sm">{$t('analytics.course_metrics.page_subtitle')}</p>
</Page.HeaderContent>
<Page.Action>
<RefreshPageData />
@@ -20,7 +21,7 @@
</Page.Header>
<Page.Body>
{#snippet child()}
<AnalyticsPage courseId={data.courseId} courseAnalytics={data.courseAnalytics} />
<AnalyticsPage courseAnalytics={data.courseAnalytics} />
{/snippet}
</Page.Body>
</Page.Root>
@@ -22,8 +22,6 @@
const certificateTheme = $derived(courseApi.course?.certificate?.theme ?? '');
const currentCertificateState = $derived.by(() => getCertificateStateSnapshot(courseApi.course));
const userRole = $derived.by(() => {
const user = courseApi.group.people.find((person) => person.profileId === $profile.id);
return user ? Number(user.roleId) : null;
@@ -40,7 +38,8 @@
description: course.description ?? '',
certificate: {
isDownloadable: course.certificate?.isDownloadable ?? false,
theme: parseCertificateThemeId(course.certificate?.theme) ?? 'professional',
theme: parseCertificateThemeId(course.certificate?.theme),
design: course.certificate?.design ?? null,
deadline: course.certificate?.deadline ?? null,
threshold: typeof course.certificate?.threshold === 'number' ? course.certificate.threshold : 100,
requiredExerciseId: course.certificate?.requiredExerciseId ?? null,
@@ -92,7 +91,8 @@
description: courseApi.course.description ?? '',
certificate: {
isDownloadable: courseApi.course.certificate?.isDownloadable ?? false,
theme: certificateTheme || 'professional',
theme: certificateTheme || 'classique',
design: courseApi.course.certificate?.design,
deadline: courseApi.course.certificate?.deadline ?? null,
threshold:
typeof courseApi.course.certificate?.threshold === 'number' ? courseApi.course.certificate.threshold : 100,
@@ -0,0 +1,5 @@
<script lang="ts">
let { children }: { children?: import('svelte').Snippet } = $props();
</script>
{@render children?.()}
@@ -0,0 +1,31 @@
<script lang="ts">
import { page } from '$app/state';
import { Spinner } from '@cio/ui/base/spinner';
import { t } from '$lib/utils/functions/translations';
import { courseApi } from '$features/course/api';
import { profile } from '$lib/utils/store/user';
import CertificateEditor from '$features/course/components/ceritficate/editor/certificate-editor.svelte';
const courseId = $derived(page.params.id ?? '');
$effect(() => {
if (!courseId || !$profile.id) return;
courseApi.ensureCourse(courseId, $profile.id);
});
const isReady = $derived(courseApi.course?.id === courseId);
</script>
<svelte:head>
<title>{courseApi.course?.title ?? $t('course.navItem.certificates.editor.title')}</title>
</svelte:head>
<div class="ui:bg-background min-h-screen w-full">
{#if isReady}
<CertificateEditor {courseId} />
{:else}
<div class="flex min-h-[60vh] items-center justify-center">
<Spinner class="size-8!" />
</div>
{/if}
</div>
@@ -32,7 +32,7 @@
in:fly={{ y: 500, duration: 500 }}
out:fly={{ y: 500, duration: 500 }}
>
<Sidebar.Provider bind:open={sidebarOpen} style="--sidebar-width: 280px">
<Sidebar.Provider bind:open={sidebarOpen} style="--sidebar-width: 360px; --sidebar-width-icon: 4rem">
<Sidebar.Root side="left" collapsible="icon" class="h-full">
<Editor {courseId} bind:course={courseApi.course} {syncCourseStore} onClose={handleClose} />
</Sidebar.Root>
@@ -35,7 +35,7 @@
});
</script>
<Page.Root class="mx-auto flex w-[90%] px-4 md:max-w-2xl lg:max-w-3xl">
<Page.Root class="mx-auto flex w-[90%] px-4 md:max-w-4xl lg:max-w-5xl">
{#key data.lessonId}
<LessonPage courseId={data.courseId} lessonId={data.lessonId} />
{/key}