feat: course analytics (#574)
* feat: added anaytics for the course page * feat: Added pagination * feat: cleanup * feat: added translations * feat: added fixes * my improvements --------- Co-authored-by: Rotimi Best <best@classroomio.com>
This commit is contained in:
co-authored by
Rotimi Best
parent
f3de6a063e
commit
ec3875bac2
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
export let title: string;
|
||||
export let value: string | number;
|
||||
export let subtitle: string = '';
|
||||
export let icon: any;
|
||||
export let iconColor: string = 'blue';
|
||||
export let className: string = '';
|
||||
|
||||
$: colorClasses = {
|
||||
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'
|
||||
};
|
||||
</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]}">
|
||||
<svelte:component this={icon} 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 capitalize text-gray-500 dark:text-gray-500">
|
||||
{subtitle}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,175 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import type { CourseAnalytics } from '$lib/utils/types/analytics';
|
||||
import { ScaleTypes } from '@carbon/charts-svelte';
|
||||
import '@carbon/charts-svelte/styles.css';
|
||||
|
||||
export let courseAnalytics: CourseAnalytics | null = null;
|
||||
|
||||
let BarChartSimple: any;
|
||||
|
||||
// Transform data for progress distribution chart
|
||||
$: progressChartData = courseAnalytics?.students
|
||||
? [
|
||||
{
|
||||
group: '80%+ Progress',
|
||||
value: courseAnalytics.students.filter((s) => s.progressPercentage >= 80).length
|
||||
},
|
||||
{
|
||||
group: '60-79% Progress',
|
||||
value: courseAnalytics.students.filter(
|
||||
(s) => s.progressPercentage >= 60 && s.progressPercentage < 80
|
||||
).length
|
||||
},
|
||||
{
|
||||
group: 'Below 60% Progress',
|
||||
value: courseAnalytics.students.filter((s) => s.progressPercentage < 60).length
|
||||
}
|
||||
]
|
||||
: [];
|
||||
|
||||
// Transform data for grade distribution chart
|
||||
$: gradeChartData = courseAnalytics?.students
|
||||
? [
|
||||
{
|
||||
group: '90%+ Grade',
|
||||
value: courseAnalytics.students.filter((s) => s.averageGrade >= 90).length
|
||||
},
|
||||
{
|
||||
group: '80-89% Grade',
|
||||
value: courseAnalytics.students.filter((s) => s.averageGrade >= 80 && s.averageGrade < 90)
|
||||
.length
|
||||
},
|
||||
{
|
||||
group: '70-79% Grade',
|
||||
value: courseAnalytics.students.filter((s) => s.averageGrade >= 70 && s.averageGrade < 80)
|
||||
.length
|
||||
},
|
||||
{
|
||||
group: 'Below 70% Grade',
|
||||
value: courseAnalytics.students.filter((s) => s.averageGrade < 70).length
|
||||
}
|
||||
]
|
||||
: [];
|
||||
|
||||
onMount(async () => {
|
||||
if (browser) {
|
||||
const charts = await import('@carbon/charts-svelte');
|
||||
BarChartSimple = charts.BarChartSimple;
|
||||
}
|
||||
});
|
||||
|
||||
// Chart options for progress distribution
|
||||
$: progressChartOptions = {
|
||||
axes: {
|
||||
left: {
|
||||
mapsTo: 'value',
|
||||
title: 'Number of Students'
|
||||
},
|
||||
bottom: {
|
||||
mapsTo: 'group',
|
||||
scaleType: ScaleTypes.LABELS,
|
||||
title: 'Progress Range'
|
||||
}
|
||||
},
|
||||
height: '300px',
|
||||
data: {
|
||||
loading: !courseAnalytics
|
||||
},
|
||||
color: {
|
||||
scale: {
|
||||
'80%+ Progress': '#10b981', // green
|
||||
'60-79% Progress': '#f59e0b', // yellow
|
||||
'Below 60% Progress': '#ef4444' // red
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Chart options for grade distribution
|
||||
$: gradeChartOptions = {
|
||||
axes: {
|
||||
left: {
|
||||
mapsTo: 'value',
|
||||
title: 'Number of Students'
|
||||
},
|
||||
bottom: {
|
||||
mapsTo: 'group',
|
||||
scaleType: ScaleTypes.LABELS,
|
||||
title: 'Grade Range'
|
||||
}
|
||||
},
|
||||
height: '300px',
|
||||
data: {
|
||||
loading: !courseAnalytics
|
||||
},
|
||||
color: {
|
||||
scale: {
|
||||
'90%+ Grade': '#10b981', // green
|
||||
'80-89% Grade': '#3b82f6', // blue
|
||||
'70-79% Grade': '#f59e0b', // yellow
|
||||
'Below 70% Grade': '#ef4444' // red
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if browser && BarChartSimple}
|
||||
<div class="grid grid-cols-1 gap-8 lg:grid-cols-2">
|
||||
<!-- Progress Distribution Chart -->
|
||||
<div
|
||||
class="rounded-lg border border-gray-200 bg-white p-6 dark:border-neutral-700 dark:bg-neutral-800"
|
||||
>
|
||||
<h3 class="mb-6 text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Student Progress Distribution
|
||||
</h3>
|
||||
{#if courseAnalytics?.students && courseAnalytics.students.length > 0}
|
||||
<svelte:component
|
||||
this={BarChartSimple}
|
||||
data={progressChartData}
|
||||
options={progressChartOptions}
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex h-[300px] items-center justify-center text-gray-500 dark:text-gray-400">
|
||||
No progress data available
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Grade Distribution Chart -->
|
||||
<div
|
||||
class="rounded-lg border border-gray-200 bg-white p-6 dark:border-neutral-700 dark:bg-neutral-800"
|
||||
>
|
||||
<h3 class="mb-6 text-lg font-semibold text-gray-900 dark:text-white">Grade Distribution</h3>
|
||||
{#if courseAnalytics?.students && courseAnalytics.students.length > 0}
|
||||
<svelte:component this={BarChartSimple} data={gradeChartData} options={gradeChartOptions} />
|
||||
{:else}
|
||||
<div class="flex h-[300px] items-center justify-center text-gray-500 dark:text-gray-400">
|
||||
No grade data available
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Loading state -->
|
||||
<div class="grid grid-cols-1 gap-8 lg:grid-cols-2">
|
||||
<div
|
||||
class="rounded-lg border border-gray-200 bg-white p-6 dark:border-neutral-700 dark:bg-neutral-800"
|
||||
>
|
||||
<h3 class="mb-6 text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Student Progress Distribution
|
||||
</h3>
|
||||
<div class="flex h-[300px] items-center justify-center">
|
||||
<div class="animate-pulse text-gray-500 dark:text-gray-400">Loading chart...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-lg border border-gray-200 bg-white p-6 dark:border-neutral-700 dark:bg-neutral-800"
|
||||
>
|
||||
<h3 class="mb-6 text-lg font-semibold text-gray-900 dark:text-white">Grade Distribution</h3>
|
||||
<div class="flex h-[300px] items-center justify-center">
|
||||
<div class="animate-pulse text-gray-500 dark:text-gray-400">Loading chart...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
<script lang="ts">
|
||||
import { SkeletonPlaceholder } from 'carbon-components-svelte';
|
||||
</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 _}
|
||||
<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">
|
||||
<SkeletonPlaceholder style="width: 48px; height: 48px;" />
|
||||
<div class="flex-1">
|
||||
<SkeletonPlaceholder style="width: 80px; height: 16px; margin-bottom: 8px;" />
|
||||
<SkeletonPlaceholder style="width: 40px; height: 24px; margin-bottom: 4px;" />
|
||||
<SkeletonPlaceholder style="width: 60px; height: 12px;" />
|
||||
</div>
|
||||
</div>
|
||||
</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"
|
||||
>
|
||||
<SkeletonPlaceholder style="width: 200px; height: 24px; margin-bottom: 24px;" />
|
||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<SkeletonPlaceholder style="width: 100%; height: 300px;" />
|
||||
<SkeletonPlaceholder style="width: 100%; height: 300px;" />
|
||||
</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">
|
||||
<SkeletonPlaceholder style="width: 200px; height: 24px;" />
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="space-y-4">
|
||||
{#each Array(5) as _}
|
||||
<div class="flex items-center space-x-4">
|
||||
<SkeletonPlaceholder style="width: 32px; height: 32px;" />
|
||||
<div class="flex-1">
|
||||
<SkeletonPlaceholder style="width: 150px; height: 16px; margin-bottom: 4px;" />
|
||||
<SkeletonPlaceholder style="width: 200px; height: 12px;" />
|
||||
</div>
|
||||
<SkeletonPlaceholder style="width: 80px; height: 16px;" />
|
||||
<SkeletonPlaceholder style="width: 60px; height: 16px;" />
|
||||
<SkeletonPlaceholder style="width: 40px; height: 20px;" />
|
||||
<SkeletonPlaceholder style="width: 80px; height: 12px;" />
|
||||
<SkeletonPlaceholder style="width: 100px; height: 32px;" />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { ChartBar } from 'carbon-icons-svelte';
|
||||
|
||||
export let title: string = 'No data available';
|
||||
export let description: string = 'There is no data to display at this time.';
|
||||
export let icon: any = ChartBar;
|
||||
export let className: string = '';
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-center justify-center px-4 py-12 {className}">
|
||||
<div
|
||||
class="mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-gray-100 dark:bg-neutral-800"
|
||||
>
|
||||
<svelte:component this={icon} size="24" class="text-gray-400 dark:text-gray-500" />
|
||||
</div>
|
||||
<h3 class="mb-2 text-lg font-medium text-gray-900 dark:text-white">
|
||||
{title}
|
||||
</h3>
|
||||
<p class="max-w-sm text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
@@ -0,0 +1,183 @@
|
||||
<script lang="ts">
|
||||
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
import { Tag, Pagination } from 'carbon-components-svelte';
|
||||
import type { StudentOverview } from '$lib/utils/types/analytics';
|
||||
import EmptyState from './EmptyState.svelte';
|
||||
import { User } from 'carbon-icons-svelte';
|
||||
import { course } from '../../store';
|
||||
import { goto } from '$app/navigation';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
export let students: StudentOverview[] = [];
|
||||
|
||||
// Pagination state
|
||||
let currentPage = 1;
|
||||
const pageSize = 15;
|
||||
|
||||
// Computed values
|
||||
$: totalPages = Math.ceil(students.length / pageSize);
|
||||
$: startIndex = (currentPage - 1) * pageSize;
|
||||
$: endIndex = startIndex + pageSize;
|
||||
$: paginatedStudents = students.slice(startIndex, endIndex);
|
||||
$: startItem = startIndex + 1;
|
||||
$: endItem = Math.min(endIndex, students.length);
|
||||
|
||||
const GotoFullProfile = (student: StudentOverview) => {
|
||||
if (!student) return;
|
||||
goto(`/courses/${$course.id}/people/${student.id}?back=/courses/${$course.id}/analytics`);
|
||||
};
|
||||
|
||||
function handlePageChange(event: CustomEvent) {
|
||||
currentPage = event.detail.page;
|
||||
}
|
||||
|
||||
// Reset to first page when students array changes
|
||||
$: if (students.length > 0 && currentPage > totalPages) {
|
||||
currentPage = 1;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if students.length === 0}
|
||||
<EmptyState
|
||||
title={$t('analytics.no_students_found')}
|
||||
description={$t('analytics.no_students_description')}
|
||||
icon={User}
|
||||
/>
|
||||
{:else}
|
||||
<!-- Responsive Table -->
|
||||
<div class="relative h-[300px] overflow-hidden">
|
||||
<div class="h-full overflow-auto">
|
||||
<table class="w-full min-w-[800px]">
|
||||
<!-- Fixed Header -->
|
||||
<thead class="sticky top-0 z-10 bg-white dark:bg-neutral-800">
|
||||
<tr class="border-b border-gray-200 dark:border-neutral-600">
|
||||
<th
|
||||
class="sticky left-0 top-0 z-20 min-w-[200px] whitespace-nowrap bg-white px-4 py-3 text-left text-sm font-medium text-gray-500 dark:bg-neutral-800 dark:text-gray-300"
|
||||
>
|
||||
{$t('analytics.student_name')}
|
||||
</th>
|
||||
<th
|
||||
class="min-w-[150px] whitespace-nowrap px-4 py-3 text-left text-sm font-medium text-gray-500 dark:text-gray-300"
|
||||
>
|
||||
{$t('analytics.lessons_completed')}
|
||||
</th>
|
||||
<th
|
||||
class="min-w-[140px] whitespace-nowrap px-4 py-3 text-left text-sm font-medium text-gray-500 dark:text-gray-300"
|
||||
>
|
||||
{$t('analytics.exercises_submitted')}
|
||||
</th>
|
||||
<th
|
||||
class="min-w-[120px] whitespace-nowrap px-4 py-3 text-left text-sm font-medium text-gray-500 dark:text-gray-300"
|
||||
>
|
||||
{$t('analytics.average_grade')}
|
||||
</th>
|
||||
<th
|
||||
class="min-w-[120px] whitespace-nowrap px-4 py-3 text-left text-sm font-medium text-gray-500 dark:text-gray-300"
|
||||
>
|
||||
{$t('analytics.last_seen')}
|
||||
</th>
|
||||
<th
|
||||
class="min-w-[120px] whitespace-nowrap px-4 py-3 text-left text-sm font-medium text-gray-500 dark:text-gray-300"
|
||||
>
|
||||
{$t('analytics.actions')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-600">
|
||||
{#each paginatedStudents as student}
|
||||
<tr class="group h-[100px] border-b hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<!-- Fixed Name Column -->
|
||||
<td
|
||||
class="sticky left-0 z-10 min-w-[200px] whitespace-nowrap bg-white px-4 py-3 transition-colors group-hover:bg-gray-50 dark:bg-neutral-800 dark:group-hover:bg-gray-700"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<Avatar
|
||||
src={student.profile.avatar_url}
|
||||
name={student.profile.fullname}
|
||||
width="w-8"
|
||||
height="h-8"
|
||||
/>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate font-medium text-gray-900 dark:text-white">
|
||||
{student.profile.fullname}
|
||||
</p>
|
||||
<p class="truncate text-sm text-gray-500 dark:text-gray-400">
|
||||
{student.profile.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<!-- Scrollable Content -->
|
||||
<td class="min-w-[150px] whitespace-nowrap px-4 py-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{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="h-2 rounded-full bg-blue-500"
|
||||
style="width: {student.progressPercentage}%"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="min-w-[140px] whitespace-nowrap px-4 py-3">
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{student.exercisesSubmitted} out of {student.totalExercises}
|
||||
</span>
|
||||
</td>
|
||||
<td class="min-w-[120px] whitespace-nowrap px-4 py-3">
|
||||
<Tag
|
||||
type={student.averageGrade >= 80
|
||||
? 'green'
|
||||
: student.averageGrade >= 60
|
||||
? 'blue'
|
||||
: 'red'}
|
||||
size="sm"
|
||||
>
|
||||
{student.averageGrade}%
|
||||
</Tag>
|
||||
</td>
|
||||
<td class="min-w-[120px] whitespace-nowrap px-4 py-3">
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{student.lastSeen}
|
||||
</span>
|
||||
</td>
|
||||
<td class="min-w-[120px] whitespace-nowrap px-4 py-3">
|
||||
<button
|
||||
class="whitespace-nowrap rounded bg-blue-600 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-blue-700"
|
||||
on:click={() => GotoFullProfile(student)}
|
||||
>
|
||||
{$t('analytics.view_details')}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination Controls -->
|
||||
{#if students.length > pageSize}
|
||||
<div
|
||||
class="mt-4 flex items-center justify-between border-t border-gray-200 px-4 py-3 dark:border-neutral-600"
|
||||
>
|
||||
<div class="text-sm text-gray-700 dark:text-gray-300">
|
||||
{$t('analytics.showing_students', {
|
||||
start: startItem,
|
||||
end: endItem,
|
||||
total: students.length
|
||||
})}
|
||||
</div>
|
||||
<Pagination
|
||||
page={currentPage}
|
||||
totalItems={students.length}
|
||||
{pageSize}
|
||||
on:change={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
export let className: string = '';
|
||||
</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>
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
export let className: string = '';
|
||||
</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>
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
export let className: string = '';
|
||||
</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>
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
export let className: string = '';
|
||||
</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>
|
||||
@@ -9,6 +9,7 @@
|
||||
import IbmWatsonKnowledgeCatalog from 'carbon-icons-svelte/lib/IbmWatsonKnowledgeCatalog.svelte';
|
||||
import Settings from 'carbon-icons-svelte/lib/Settings.svelte';
|
||||
import Bullhorn from 'carbon-icons-svelte/lib/Bullhorn.svelte';
|
||||
import Analytics from 'carbon-icons-svelte/lib/Analytics.svelte';
|
||||
import { NAV_IDS } from './constants';
|
||||
|
||||
export let name = '';
|
||||
@@ -28,6 +29,8 @@
|
||||
<Result size={20} class="carbon-icon mr-2" />
|
||||
{:else if name === NAV_IDS.PEOPLE}
|
||||
<Group size={20} class="carbon-icon mr-2" />
|
||||
{:else if name === NAV_IDS.ANALYTICS}
|
||||
<Analytics size={20} class="carbon-icon mr-2" />
|
||||
{:else if name === NAV_IDS.LANDING_PAGE}
|
||||
<ApplicationWeb size={20} class="carbon-icon mr-2" />
|
||||
{:else if name === NAV_IDS.CERTIFICATES}
|
||||
|
||||
@@ -7,6 +7,7 @@ export const NAV_IDS = {
|
||||
CERTIFICATES: 'CERTIFICATES',
|
||||
LANDING_PAGE: 'LANDING_PAGE',
|
||||
PEOPLE: 'PEOPLE',
|
||||
ANALYTICS: 'ANALYTICS',
|
||||
SETTINGS: 'SETTINGS',
|
||||
SECTION: 'SECTION'
|
||||
};
|
||||
|
||||
@@ -158,6 +158,16 @@
|
||||
return isStudent ? $currentOrg.customization.course.newsfeed : true;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: NAV_IDS.ANALYTICS,
|
||||
label: $t('course.navItems.nav_analytics'),
|
||||
to: getNavItemRoute($course.id, 'analytics'),
|
||||
isPaidFeature: false,
|
||||
hideSortIcon: true,
|
||||
show() {
|
||||
return !isStudent;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: NAV_IDS.LESSONS,
|
||||
label: $t('course.navItems.nav_content'),
|
||||
@@ -262,14 +272,14 @@
|
||||
class={`
|
||||
${
|
||||
$sideBar.hidden
|
||||
? '-translate-x-[100%] absolute z-[40]'
|
||||
: 'translate-x-0 absolute md:relative z-[40]'
|
||||
? 'absolute z-[40] -translate-x-[100%]'
|
||||
: 'absolute z-[40] translate-x-0 md:relative'
|
||||
}
|
||||
transition w-[90vw] md:w-[300px] lg:w-[350px] bg-gray-100 dark:bg-black h-[calc(100vh-48px)]
|
||||
h-[calc(100vh-48px)] w-[90vw] bg-gray-100 transition md:w-[300px] lg:w-[350px] dark:bg-black
|
||||
|
||||
${
|
||||
resize ? 'border-r-8 border-r-blue-500' : 'dark:border-r-neutral-600'
|
||||
} overflow-y-auto border border-l-0 border-t-0 border-b-0 border-r-1`}
|
||||
} border-r-1 overflow-y-auto border border-b-0 border-l-0 border-t-0`}
|
||||
style={$sideBar.hidden === true ? 'width:0' : 'width:300px'}
|
||||
bind:this={sidebarRef}
|
||||
>
|
||||
@@ -297,7 +307,7 @@
|
||||
{#if $course.version === COURSE_VERSION.V1}
|
||||
{#each $lessons as item, index}
|
||||
<a
|
||||
class="pl-7 w-[95%] text-[0.80rem] mb-2 text-black dark:text-white {isStudent &&
|
||||
class="mb-2 w-[95%] pl-7 text-[0.80rem] text-black dark:text-white {isStudent &&
|
||||
!item.is_unlocked
|
||||
? 'cursor-not-allowed'
|
||||
: ''}"
|
||||
@@ -309,7 +319,7 @@
|
||||
title={item.title}
|
||||
>
|
||||
<div
|
||||
class="flex items-center py-2 px-4 {NavClasses.item} {(
|
||||
class="flex items-center px-4 py-2 {NavClasses.item} {(
|
||||
path || $page.url.pathname
|
||||
).includes(item.id) && NavClasses.active}"
|
||||
>
|
||||
@@ -319,7 +329,7 @@
|
||||
size="sm"
|
||||
shape="rounded-full"
|
||||
/>
|
||||
<span class="w-[85%] text-ellipsis line-clamp-2">{item.title}</span>
|
||||
<span class="line-clamp-2 w-[85%] text-ellipsis">{item.title}</span>
|
||||
<span class="grow" />
|
||||
{#if !item.is_unlocked}
|
||||
<span class="text-md ml-2" title="This lesson is locked.">
|
||||
@@ -348,7 +358,7 @@
|
||||
>
|
||||
{#each section.lessons as item}
|
||||
<a
|
||||
class="pl-7 w-[95%] text-[0.80rem] mb-2 text-black dark:text-white {isStudent &&
|
||||
class="mb-2 w-[95%] pl-7 text-[0.80rem] text-black dark:text-white {isStudent &&
|
||||
!item.is_unlocked
|
||||
? 'cursor-not-allowed'
|
||||
: ''}"
|
||||
@@ -360,11 +370,11 @@
|
||||
title={item.title}
|
||||
>
|
||||
<div
|
||||
class="flex items-center py-2 px-4 {NavClasses.item} {(
|
||||
class="flex items-center px-4 py-2 {NavClasses.item} {(
|
||||
path || $page.url.pathname
|
||||
).includes(item.id) && NavClasses.active}"
|
||||
>
|
||||
<span class="w-[85%] text-ellipsis line-clamp-2">{item.title}</span>
|
||||
<span class="line-clamp-2 w-[85%] text-ellipsis">{item.title}</span>
|
||||
<span class="grow" />
|
||||
{#if !item.is_unlocked}
|
||||
<span class="text-md ml-2" title="This lesson is locked.">
|
||||
|
||||
Reference in New Issue
Block a user