feat: certificates, landing pages, ai tutor, website update, compliance
This commit is contained in:
@@ -1,44 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Component } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
value: string | number;
|
||||
subtitle?: string;
|
||||
icon: Component;
|
||||
iconColor?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
let { title, value, subtitle = '', icon, iconColor = 'blue', className = '' }: Props = $props();
|
||||
|
||||
let colorClasses = $derived({
|
||||
blue: 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400',
|
||||
green: 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400',
|
||||
purple: 'bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400',
|
||||
orange: 'bg-orange-50 dark:bg-orange-900/20 text-orange-600 dark:text-orange-400',
|
||||
red: 'bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400',
|
||||
yellow: 'bg-yellow-50 dark:bg-yellow-900/20 text-yellow-600 dark:text-yellow-400'
|
||||
});
|
||||
|
||||
const SvelteComponent = $derived(icon);
|
||||
</script>
|
||||
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-6 dark:border-neutral-700 dark:bg-neutral-800 {className}">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-lg {colorClasses[iconColor]}">
|
||||
<SvelteComponent className={colorClasses[iconColor]} />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">{title}</p>
|
||||
<p class="text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
{value}
|
||||
</p>
|
||||
{#if subtitle}
|
||||
<p class="text-xs text-gray-500 capitalize dark:text-gray-500">
|
||||
{subtitle}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
+56
-52
@@ -1,5 +1,9 @@
|
||||
<script lang="ts">
|
||||
import * as Chart from '@cio/ui/base/chart';
|
||||
import { Spinner } from '@cio/ui/base/spinner';
|
||||
import { Empty } from '@cio/ui/custom/empty';
|
||||
import BarChartIcon from '@lucide/svelte/icons/bar-chart-3';
|
||||
import { AnalyticsPanelCard } from '$features/analytics';
|
||||
import type { CourseAnalytics } from '$features/course/utils/types';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
@@ -29,10 +33,9 @@
|
||||
}
|
||||
} satisfies Chart.ChartConfig);
|
||||
|
||||
let hasStudentData = $derived(Boolean(courseAnalytics?.students?.length));
|
||||
const hasStudentData = $derived(Boolean(courseAnalytics?.students?.length));
|
||||
|
||||
// Transform data for progress distribution chart
|
||||
let progressChartData = $derived<DistributionChartData[]>(
|
||||
const progressChartData = $derived<DistributionChartData[]>(
|
||||
courseAnalytics?.students
|
||||
? [
|
||||
{
|
||||
@@ -55,8 +58,7 @@
|
||||
: []
|
||||
);
|
||||
|
||||
// Transform data for grade distribution chart
|
||||
let gradeChartData = $derived<DistributionChartData[]>(
|
||||
const gradeChartData = $derived<DistributionChartData[]>(
|
||||
courseAnalytics?.students
|
||||
? [
|
||||
{
|
||||
@@ -83,7 +85,7 @@
|
||||
: []
|
||||
);
|
||||
|
||||
let progressChartProps = $derived({
|
||||
const progressChartProps = $derived({
|
||||
xAxis: {
|
||||
label: $t('analytics.progress_range')
|
||||
},
|
||||
@@ -92,7 +94,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
let gradeChartProps = $derived({
|
||||
const gradeChartProps = $derived({
|
||||
xAxis: {
|
||||
label: $t('analytics.grade_range')
|
||||
},
|
||||
@@ -101,7 +103,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
let progressSeries = $derived([
|
||||
const progressSeries = $derived([
|
||||
{
|
||||
key: 'students',
|
||||
value: 'value',
|
||||
@@ -110,7 +112,7 @@
|
||||
}
|
||||
]);
|
||||
|
||||
let gradeSeries = $derived([
|
||||
const gradeSeries = $derived([
|
||||
{
|
||||
key: 'students',
|
||||
value: 'value',
|
||||
@@ -118,50 +120,52 @@
|
||||
color: 'var(--color-students)'
|
||||
}
|
||||
]);
|
||||
|
||||
const chartsLoading = $derived(!courseAnalytics);
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-1 gap-8 lg:grid-cols-2">
|
||||
<!-- Progress Distribution Chart -->
|
||||
<div class="">
|
||||
<h3 class="mb-6 text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{$t('analytics.student_progress_distribution')}
|
||||
</h3>
|
||||
{#if !courseAnalytics}
|
||||
<div class="flex h-[300px] items-center justify-center">
|
||||
<div class="animate-pulse text-gray-500 dark:text-gray-400">{$t('analytics.loading_chart')}</div>
|
||||
</div>
|
||||
{:else if hasStudentData}
|
||||
<Chart.ChartContainer class="h-[300px] w-full" config={progressChartConfig}>
|
||||
<Chart.BarChart
|
||||
data={progressChartData}
|
||||
x="group"
|
||||
axis="x"
|
||||
props={progressChartProps}
|
||||
series={progressSeries}
|
||||
/>
|
||||
</Chart.ChartContainer>
|
||||
{:else}
|
||||
<div class="flex h-[300px] items-center justify-center text-gray-500 dark:text-gray-400">
|
||||
{$t('analytics.no_progress_data')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<AnalyticsPanelCard
|
||||
title={$t('analytics.student_progress_distribution')}
|
||||
description={$t('analytics.course_metrics.progress_distribution_card_description')}
|
||||
>
|
||||
{#snippet children()}
|
||||
{#if chartsLoading}
|
||||
<div class="flex h-[280px] items-center justify-center">
|
||||
<Spinner class="ui:text-muted-foreground size-6" />
|
||||
</div>
|
||||
{:else if hasStudentData}
|
||||
<Chart.ChartContainer class="h-[280px] w-full" config={progressChartConfig}>
|
||||
<Chart.BarChart
|
||||
data={progressChartData}
|
||||
x="group"
|
||||
axis="x"
|
||||
props={progressChartProps}
|
||||
series={progressSeries}
|
||||
/>
|
||||
</Chart.ChartContainer>
|
||||
{:else}
|
||||
<Empty icon={BarChartIcon} title={$t('analytics.no_progress_data')} class="h-[280px]" />
|
||||
{/if}
|
||||
{/snippet}
|
||||
</AnalyticsPanelCard>
|
||||
|
||||
<!-- Grade Distribution Chart -->
|
||||
<div class="">
|
||||
<h3 class="mb-6 text-lg font-semibold text-gray-900 dark:text-white">{$t('analytics.grade_distribution')}</h3>
|
||||
{#if !courseAnalytics}
|
||||
<div class="flex h-[300px] items-center justify-center">
|
||||
<div class="animate-pulse text-gray-500 dark:text-gray-400">{$t('analytics.loading_chart')}</div>
|
||||
</div>
|
||||
{:else if hasStudentData}
|
||||
<Chart.ChartContainer class="h-[300px] w-full" config={gradeChartConfig}>
|
||||
<Chart.BarChart data={gradeChartData} x="group" axis="x" props={gradeChartProps} series={gradeSeries} />
|
||||
</Chart.ChartContainer>
|
||||
{:else}
|
||||
<div class="flex h-[300px] items-center justify-center text-gray-500 dark:text-gray-400">
|
||||
{$t('analytics.no_grade_data')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<AnalyticsPanelCard
|
||||
title={$t('analytics.grade_distribution')}
|
||||
description={$t('analytics.course_metrics.grade_distribution_card_description')}
|
||||
>
|
||||
{#snippet children()}
|
||||
{#if chartsLoading}
|
||||
<div class="flex h-[280px] items-center justify-center">
|
||||
<Spinner class="ui:text-muted-foreground size-6" />
|
||||
</div>
|
||||
{:else if hasStudentData}
|
||||
<Chart.ChartContainer class="h-[280px] w-full" config={gradeChartConfig}>
|
||||
<Chart.BarChart data={gradeChartData} x="group" axis="x" props={gradeChartProps} series={gradeSeries} />
|
||||
</Chart.ChartContainer>
|
||||
{:else}
|
||||
<Empty icon={BarChartIcon} title={$t('analytics.no_grade_data')} class="h-[280px]" />
|
||||
{/if}
|
||||
{/snippet}
|
||||
</AnalyticsPanelCard>
|
||||
</div>
|
||||
|
||||
+46
-53
@@ -2,61 +2,54 @@
|
||||
import { Skeleton } from '@cio/ui/base/skeleton';
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen">
|
||||
<div class="px-4 py-8">
|
||||
<!-- Course Overview Cards Skeleton -->
|
||||
<div class="mb-12">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
{#each Array(4) as _, i (i)}
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-6 dark:border-neutral-700 dark:bg-neutral-800">
|
||||
<div class="flex items-center space-x-4">
|
||||
<Skeleton class="h-[48px] w-[48px]" />
|
||||
<div class="flex-1">
|
||||
<Skeleton class="h-[48px] w-[48px]" />
|
||||
<Skeleton class="mb-2 h-[16px] w-[80px]" />
|
||||
<Skeleton class="mb-1 h-[24px] w-[40px]" />
|
||||
<Skeleton class="h-[12px] w-[60px]" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{#each Array(4) as _, i (i)}
|
||||
<div class="ui:bg-card ui:border-border overflow-hidden rounded-xl border shadow-sm">
|
||||
<div class="ui:border-border ui:space-y-2 border-b px-6 py-5">
|
||||
<Skeleton class="h-3 w-24" />
|
||||
</div>
|
||||
<div class="px-6 py-6">
|
||||
<Skeleton class="h-8 w-20" />
|
||||
<Skeleton class="mt-4 h-3 w-full max-w-[180px]" />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
{#each Array(2) as _, chartIndex (chartIndex)}
|
||||
<div class="ui:bg-card ui:border-border overflow-hidden rounded-xl border shadow-sm">
|
||||
<div class="ui:border-border ui:space-y-2 border-b px-6 py-5">
|
||||
<Skeleton class="h-5 w-48" />
|
||||
<Skeleton class="h-4 w-full max-w-md" />
|
||||
</div>
|
||||
<div class="px-6 py-6">
|
||||
<Skeleton class="h-[280px] w-full" />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="ui:bg-card ui:border-border overflow-hidden rounded-xl border shadow-sm">
|
||||
<div class="ui:border-border border-b px-6 py-5">
|
||||
<Skeleton class="h-5 w-56" />
|
||||
<Skeleton class="mt-2 h-4 max-w-xl" />
|
||||
</div>
|
||||
<div class="ui:divide-border divide-y px-6 py-2">
|
||||
{#each Array(5) as _, i (i)}
|
||||
<div class="flex flex-wrap items-center gap-4 py-4">
|
||||
<Skeleton class="size-8 rounded-full" />
|
||||
<div class="min-w-0 flex-1 space-y-2">
|
||||
<Skeleton class="h-4 w-[150px]" />
|
||||
<Skeleton class="h-3 w-[200px]" />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts Section Skeleton -->
|
||||
<div class="mb-12">
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-6 dark:border-neutral-700 dark:bg-neutral-800">
|
||||
<Skeleton class="mb-6 h-[24px] w-[200px]" />
|
||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<Skeleton class="h-[300px] w-full" />
|
||||
<Skeleton class="h-[300px] w-full" />
|
||||
<Skeleton class="h-4 w-[80px]" />
|
||||
<Skeleton class="h-4 w-[100px]" />
|
||||
<Skeleton class="h-6 w-[48px]" />
|
||||
<Skeleton class="h-8 w-[90px]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Students Table Skeleton -->
|
||||
<div class="rounded-lg border border-gray-200 bg-white dark:border-neutral-700 dark:bg-neutral-800">
|
||||
<div class="border-b border-gray-200 px-6 py-4 dark:border-neutral-700">
|
||||
<Skeleton class="mb-6 h-[24px] w-[200px]" />
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="space-y-4">
|
||||
{#each Array(5) as _, i (i)}
|
||||
<div class="flex items-center space-x-4">
|
||||
<Skeleton class="h-[32px] w-[32px]" />
|
||||
<div class="flex-1">
|
||||
<Skeleton class="mb-1 h-[16px] w-[150px]" />
|
||||
<Skeleton class="h-[12px] w-[200px]" />
|
||||
</div>
|
||||
<Skeleton class="h-[16px] w-[80px]" />
|
||||
<Skeleton class="h-[16px] w-[60px]" />
|
||||
<Skeleton class="h-[20px] w-[40px]" />
|
||||
<Skeleton class="h-[12px] w-[80px]" />
|
||||
<Skeleton class="h-[32px] w-[100px]" />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
import BookOpenIcon from '@lucide/svelte/icons/book-open';
|
||||
import ListChecksIcon from '@lucide/svelte/icons/list-checks';
|
||||
import PercentIcon from '@lucide/svelte/icons/percent';
|
||||
import UsersIcon from '@lucide/svelte/icons/users';
|
||||
import { KpiCard } from '$features/analytics';
|
||||
import type { CourseAnalytics } from '$features/course/utils/types';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
interface Props {
|
||||
data: CourseAnalytics;
|
||||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
const tutorsCount = $derived(data.totalTutors ?? 0);
|
||||
const exerciseTotal = $derived(data.totalExercises ?? 0);
|
||||
const avgGrade = $derived(data.averageGrade ?? 0);
|
||||
const lessonRate = $derived(data.lessonCompletionRate ?? 0);
|
||||
const exerciseRate = $derived(data.exerciseCompletionRate ?? 0);
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<KpiCard
|
||||
title={$t('analytics.students')}
|
||||
value={(data.totalStudents ?? 0).toLocaleString()}
|
||||
description={$t('analytics.course_metrics.kpi_team_tutors', { count: tutorsCount })}
|
||||
icon={UsersIcon}
|
||||
accent="primary"
|
||||
/>
|
||||
<KpiCard
|
||||
title={$t('analytics.lessons')}
|
||||
value={(data.totalLessons ?? 0).toLocaleString()}
|
||||
description={$t('analytics.course_metrics.kpi_lessons_exercises', { count: exerciseTotal })}
|
||||
icon={BookOpenIcon}
|
||||
accent="warning"
|
||||
/>
|
||||
<KpiCard
|
||||
title={$t('analytics.progress')}
|
||||
value={`${lessonRate}%`}
|
||||
description={$t('analytics.course_metrics.kpi_progress_grade', { grade: avgGrade })}
|
||||
icon={PercentIcon}
|
||||
accent="success"
|
||||
/>
|
||||
<KpiCard
|
||||
title={$t('analytics.exercises')}
|
||||
value={`${exerciseRate}%`}
|
||||
description={$t('analytics.completion_rate')}
|
||||
icon={ListChecksIcon}
|
||||
accent="primary"
|
||||
/>
|
||||
</div>
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
let { className = '' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
class="h-6 w-6 {className}"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
@@ -1,23 +0,0 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
let { className = '' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
class="h-6 w-6 {className}"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4 5a2 2 0 012-2h8a2 2 0 012 2v6a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm3 1a1 1 0 000 2h.01a1 1 0 100-2H7zm3 0a1 1 0 000 2h3a1 1 0 100-2h-3zm-3 4a1 1 0 100 2h.01a1 1 0 100-2H7zm3 0a1 1 0 100 2h3a1 1 0 100-2h-3z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
let { className = '' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
class="h-6 w-6 {className}"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M6.267 3.455a3.066 3.066 0 001.745-.723 3.066 3.066 0 013.976 0 3.066 3.066 0 001.745.723 3.066 3.066 0 012.812 2.812c.051.643.304 1.254.723 1.745a3.066 3.066 0 010 3.976 3.066 3.066 0 00-.723 1.745 3.066 3.066 0 01-2.812 2.812 3.066 3.066 0 00-1.745.723 3.066 3.066 0 01-3.976 0 3.066 3.066 0 00-1.745-.723 3.066 3.066 0 01-2.812-2.812 3.066 3.066 0 00-.723-1.745 3.066 3.066 0 010-3.976 3.066 3.066 0 00.723-1.745 3.066 3.066 0 012.812-2.812zm7.44 5.252a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
let { className = '' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
class="h-6 w-6 {className}"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3z" />
|
||||
</svg>
|
||||
@@ -59,11 +59,11 @@
|
||||
/>
|
||||
{:else}
|
||||
<!-- Responsive Table -->
|
||||
<div class="relative h-[300px] overflow-auto rounded-md border">
|
||||
<div class="ui:border-border relative max-h-[min(480px,70vh)] overflow-auto rounded-md border">
|
||||
<Table.Root class="min-w-[800px]">
|
||||
<Table.Header class="sticky top-0 z-10 bg-white dark:bg-neutral-800">
|
||||
<Table.Header class="ui:bg-muted/80 sticky top-0 z-10 backdrop-blur-sm">
|
||||
<Table.Row>
|
||||
<Table.Head class="sticky top-0 left-0 z-20 min-w-[200px] bg-white px-4 py-3 dark:bg-neutral-800">
|
||||
<Table.Head class="ui:bg-muted/95 sticky top-0 left-0 z-20 min-w-[200px] px-4 py-3 backdrop-blur-sm">
|
||||
{$t('analytics.student_name')}
|
||||
</Table.Head>
|
||||
<Table.Head class="min-w-[150px] px-4 py-3">{$t('analytics.lessons_completed')}</Table.Head>
|
||||
@@ -77,7 +77,7 @@
|
||||
{#each paginatedStudents as student (student.id)}
|
||||
<Table.Row class="group h-[100px]">
|
||||
<Table.Cell
|
||||
class="sticky left-0 z-10 min-w-[200px] bg-white px-4 py-3 transition-colors group-hover:bg-gray-50 dark:bg-neutral-800 dark:group-hover:bg-gray-700"
|
||||
class="ui:bg-card group-hover:ui:bg-muted/50 sticky left-0 z-10 min-w-[200px] px-4 py-3 transition-colors"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<UserAvatar
|
||||
@@ -86,10 +86,10 @@
|
||||
class="size-8"
|
||||
/>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate font-medium text-gray-900 dark:text-white">
|
||||
<p class="ui:text-foreground truncate font-medium">
|
||||
{student.profile.fullname}
|
||||
</p>
|
||||
<p class="truncate text-sm text-gray-500 dark:text-gray-400">
|
||||
<p class="ui:text-muted-foreground truncate text-sm">
|
||||
{student.profile.email}
|
||||
</p>
|
||||
</div>
|
||||
@@ -97,18 +97,18 @@
|
||||
</Table.Cell>
|
||||
<Table.Cell class="min-w-[150px] px-4 py-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
<span class="ui:text-foreground text-sm font-medium">
|
||||
{student.lessonsCompleted}/{student.totalLessons}
|
||||
</span>
|
||||
<div class="w-20 flex-shrink-0">
|
||||
<div class="h-2 rounded-full bg-gray-200 dark:bg-neutral-700">
|
||||
<div class="ui:bg-muted h-2 rounded-full">
|
||||
<div class="h-2 rounded-full bg-blue-500" style="width: {student.progressPercentage}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell class="min-w-[140px] px-4 py-3">
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
<span class="ui:text-foreground text-sm font-medium">
|
||||
{student.exercisesSubmitted} out of {student.totalExercises}
|
||||
</span>
|
||||
</Table.Cell>
|
||||
@@ -124,7 +124,7 @@
|
||||
</Badge>
|
||||
</Table.Cell>
|
||||
<Table.Cell class="min-w-[120px] px-4 py-3">
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">
|
||||
<span class="ui:text-muted-foreground text-sm">
|
||||
{student.lastSeen}
|
||||
</span>
|
||||
</Table.Cell>
|
||||
@@ -141,8 +141,8 @@
|
||||
|
||||
<!-- Pagination Controls -->
|
||||
{#if students.length > pageSize}
|
||||
<div class="mt-4 flex items-center justify-between border-t border-gray-200 px-4 py-3">
|
||||
<div class="text-sm text-gray-700 dark:text-gray-300">
|
||||
<div class="ui:border-border mt-4 flex items-center justify-between border-t px-4 py-3">
|
||||
<div class="ui:text-muted-foreground text-sm">
|
||||
{$t('analytics.showing_students', {
|
||||
start: startItem,
|
||||
end: endItem,
|
||||
|
||||
Reference in New Issue
Block a user