feat: certificates, landing pages, ai tutor, website update, compliance
This commit is contained in:
@@ -1,21 +1,17 @@
|
||||
<script lang="ts">
|
||||
import ChartBarIcon from '@lucide/svelte/icons/chart-bar';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { AnalyticsPanelCard } from '$features/analytics';
|
||||
import type { CourseAnalytics } from '$features/course/utils/types';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { fade } from 'svelte/transition';
|
||||
import StudentTable from '$features/course/components/analytics/student-table.svelte';
|
||||
import AnalyticsGraph from '$features/course/components/analytics/analytics-graph.svelte';
|
||||
import AnalyticsSkeleton from '$features/course/components/analytics/analytics-skeleton.svelte';
|
||||
import AnalyticsCard from '$features/course/components/analytics/analytics-card.svelte';
|
||||
import StudentsIcon from '$features/course/components/analytics/icons/students-icon.svelte';
|
||||
import LessonsIcon from '$features/course/components/analytics/icons/lessons-icon.svelte';
|
||||
import ProgressIcon from '$features/course/components/analytics/icons/progress-icon.svelte';
|
||||
import ExercisesIcon from '$features/course/components/analytics/icons/exercises-icon.svelte';
|
||||
import CourseAnalyticsKpis from '$features/course/components/analytics/course-analytics-kpis.svelte';
|
||||
import EmptyState from '$features/course/components/analytics/empty-state.svelte';
|
||||
import StudentTable from '$features/course/components/analytics/student-table.svelte';
|
||||
import { courseApi } from '../api/course.svelte';
|
||||
|
||||
interface Props {
|
||||
courseId: string;
|
||||
courseAnalytics: CourseAnalytics | null;
|
||||
}
|
||||
|
||||
@@ -35,70 +31,25 @@
|
||||
{#if isLoading}
|
||||
<AnalyticsSkeleton />
|
||||
{:else if courseAnalytics}
|
||||
<div class="min-h-screen" in:fade>
|
||||
<!-- Main Content Area -->
|
||||
<div class="px-4 py-8">
|
||||
<!-- Course Overview Cards -->
|
||||
<div class="mb-12">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<!-- Students Card -->
|
||||
<AnalyticsCard
|
||||
title={$t('analytics.students')}
|
||||
value={courseAnalytics?.totalStudents || 0}
|
||||
subtitle="{$t('analytics.tutors')}: {courseAnalytics?.totalTutors || 0}"
|
||||
icon={StudentsIcon}
|
||||
iconColor="blue"
|
||||
/>
|
||||
<div class="space-y-6" in:fade>
|
||||
<CourseAnalyticsKpis data={courseAnalytics} />
|
||||
|
||||
<!-- Lessons Card -->
|
||||
<AnalyticsCard
|
||||
title={$t('analytics.lessons')}
|
||||
value={courseAnalytics?.totalLessons || 0}
|
||||
subtitle="{$t('analytics.exercises')}: {courseAnalytics?.totalExercises || 0}"
|
||||
icon={LessonsIcon}
|
||||
iconColor="green"
|
||||
/>
|
||||
<AnalyticsGraph {courseAnalytics} />
|
||||
|
||||
<!-- Progress Card -->
|
||||
<AnalyticsCard
|
||||
title={$t('analytics.progress')}
|
||||
value="{courseAnalytics?.lessonCompletionRate || 0}%"
|
||||
subtitle="{$t('analytics.avg_grade')}: {courseAnalytics?.averageGrade || 0}%"
|
||||
icon={ProgressIcon}
|
||||
iconColor="purple"
|
||||
/>
|
||||
|
||||
<!-- Exercise Completion Card -->
|
||||
<AnalyticsCard
|
||||
title={$t('analytics.exercises')}
|
||||
value="{courseAnalytics?.exerciseCompletionRate || 0}%"
|
||||
subtitle={$t('analytics.completion_rate')}
|
||||
icon={ExercisesIcon}
|
||||
iconColor="orange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts Section -->
|
||||
<div class="mb-12">
|
||||
<AnalyticsGraph {courseAnalytics} />
|
||||
</div>
|
||||
|
||||
<!-- Students Overview Table -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{$t('analytics.student_progress_overview')}
|
||||
</h3>
|
||||
|
||||
<StudentTable students={courseAnalytics?.students || []} />
|
||||
</div>
|
||||
</div>
|
||||
<AnalyticsPanelCard
|
||||
title={$t('analytics.student_progress_overview')}
|
||||
description={$t('analytics.course_metrics.students_table_description')}
|
||||
>
|
||||
{#snippet children()}
|
||||
<StudentTable students={courseAnalytics?.students ?? []} />
|
||||
{/snippet}
|
||||
</AnalyticsPanelCard>
|
||||
</div>
|
||||
{:else}
|
||||
<EmptyState
|
||||
title={$t('analytics.no_analytics_data')}
|
||||
description={$t('analytics.no_analytics_description')}
|
||||
icon={ChartBarIcon}
|
||||
className="h-screen"
|
||||
className="min-h-[480px]"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { Badge } from '@cio/ui/base/badge';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { Checkbox } from '@cio/ui/base/checkbox';
|
||||
import * as Card from '@cio/ui/base/card';
|
||||
import * as Dialog from '@cio/ui/base/dialog';
|
||||
import { Empty } from '@cio/ui/custom/empty';
|
||||
import * as Field from '@cio/ui/base/field';
|
||||
@@ -470,14 +471,26 @@
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Content value="overview" class="mt-4">
|
||||
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
{#each summaryCards as card}
|
||||
<div class="ui:bg-muted/40 rounded-lg p-4">
|
||||
<p class="ui:text-muted-foreground text-xs tracking-wide uppercase">{$t(card.labelKey)}</p>
|
||||
<p class="mt-2 text-lg font-semibold">{card.value}</p>
|
||||
<Card.Root class="ui:bg-card">
|
||||
<Card.Header class="pb-2">
|
||||
<Card.Title class="text-base font-semibold"
|
||||
>{$t('course.navItem.compliance.overview_metrics.heading')}</Card.Title
|
||||
>
|
||||
<Card.Description>{$t('course.navItem.compliance.overview_metrics.description')}</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content class="pt-0">
|
||||
<div class="grid grid-cols-2 gap-x-4 gap-y-6 sm:grid-cols-3">
|
||||
{#each summaryCards as card}
|
||||
<div class="flex min-w-0 flex-col gap-1">
|
||||
<p class="ui:text-muted-foreground text-xs font-medium tracking-wide uppercase">
|
||||
{$t(card.labelKey)}
|
||||
</p>
|
||||
<p class="ui:text-foreground text-2xl font-semibold tabular-nums">{card.value}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="learners" class="mt-4 space-y-4">
|
||||
|
||||
@@ -375,7 +375,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Page.Header isSticky={true} class="z-100! min-h-[36px]">
|
||||
<Page.Header isSticky={true} class="top-12! z-100! min-h-[36px]">
|
||||
<Page.HeaderContent>
|
||||
<Page.Title class="flex flex-col gap-2">
|
||||
<span>{$questionnaire.title || 'Exercise'}</span>
|
||||
|
||||
@@ -416,7 +416,7 @@
|
||||
|
||||
<Page.Body>
|
||||
{#snippet child()}
|
||||
<div class="overflow-x-hidden lg:w-full xl:w-11/12">
|
||||
<div class={`overflow-x-hidden py-6 ${mode === MODES.edit ? 'lg:w-full xl:w-11/12' : 'mx-auto w-full max-w-3xl'}`}>
|
||||
{#if $isOrgStudent && lessonApi.lesson && !isLessonUnlocked}
|
||||
<Empty
|
||||
title={$t('course.navItem.lessons.content_locked_title')}
|
||||
|
||||
@@ -234,10 +234,11 @@
|
||||
const description = value.description.trim();
|
||||
const buttonLabel = value.buttonLabel.trim();
|
||||
const buttonUrl = value.buttonUrl.trim();
|
||||
const animation = value.animation ?? 'waves';
|
||||
|
||||
if (!title && !description && !buttonLabel && !buttonUrl) return null;
|
||||
|
||||
return { title, description, buttonLabel, buttonUrl };
|
||||
return { title, description, buttonLabel, buttonUrl, animation };
|
||||
}
|
||||
|
||||
function normalizeCallout(value: unknown): typeof $settings.callout {
|
||||
@@ -253,11 +254,17 @@
|
||||
return null;
|
||||
}
|
||||
|
||||
const animation =
|
||||
candidate.animation === 'dotted' || candidate.animation === 'none' || candidate.animation === 'waves'
|
||||
? candidate.animation
|
||||
: 'waves';
|
||||
|
||||
return {
|
||||
title: candidate.title,
|
||||
description: candidate.description,
|
||||
buttonLabel: candidate.buttonLabel,
|
||||
buttonUrl: candidate.buttonUrl
|
||||
buttonUrl: candidate.buttonUrl,
|
||||
animation
|
||||
};
|
||||
}
|
||||
|
||||
@@ -280,7 +287,7 @@
|
||||
if ($settings.type === 'PUBLIC' && $settings.callout === null) {
|
||||
settings.update((prev) => ({
|
||||
...prev,
|
||||
callout: { title: '', description: '', buttonLabel: '', buttonUrl: '' }
|
||||
callout: { title: '', description: '', buttonLabel: '', buttonUrl: '', animation: 'waves' }
|
||||
}));
|
||||
}
|
||||
});
|
||||
@@ -644,6 +651,44 @@
|
||||
type="url"
|
||||
/>
|
||||
</Field.Field>
|
||||
|
||||
<Field.Field>
|
||||
<Field.Label>{$t('course.navItem.settings.callout.animation_label')}</Field.Label>
|
||||
<Field.Description>
|
||||
{$t('course.navItem.settings.callout.animation_description')}
|
||||
</Field.Description>
|
||||
<RadioGroup.Root
|
||||
value={$settings.callout.animation ?? 'waves'}
|
||||
onValueChange={(value) => {
|
||||
if (!$settings.callout) return;
|
||||
const next = value === 'dotted' || value === 'none' ? value : 'waves';
|
||||
settings.update((prev) =>
|
||||
prev.callout ? { ...prev, callout: { ...prev.callout, animation: next } } : prev
|
||||
);
|
||||
hasUnsavedChanges = true;
|
||||
}}
|
||||
class="ui:mt-1 flex flex-col gap-2"
|
||||
>
|
||||
<Field.Field orientation="horizontal">
|
||||
<RadioGroup.Item value="waves" id="callout-animation-waves" />
|
||||
<Label for="callout-animation-waves">
|
||||
{$t('course.navItem.settings.callout.animation_waves')}
|
||||
</Label>
|
||||
</Field.Field>
|
||||
<Field.Field orientation="horizontal">
|
||||
<RadioGroup.Item value="dotted" id="callout-animation-dotted" />
|
||||
<Label for="callout-animation-dotted">
|
||||
{$t('course.navItem.settings.callout.animation_dotted')}
|
||||
</Label>
|
||||
</Field.Field>
|
||||
<Field.Field orientation="horizontal">
|
||||
<RadioGroup.Item value="none" id="callout-animation-none" />
|
||||
<Label for="callout-animation-none">
|
||||
{$t('course.navItem.settings.callout.animation_none')}
|
||||
</Label>
|
||||
</Field.Field>
|
||||
</RadioGroup.Root>
|
||||
</Field.Field>
|
||||
</Field.Group>
|
||||
|
||||
<Field.Field>
|
||||
|
||||
Reference in New Issue
Block a user