Files
classroomio/apps/dashboard/src/lib/features/lms/pages/certificates.svelte
T
Rotimi Best 773fc873c2 feat(lms): add student certificates page and sidebar nav (#696)
* feat(lms): add student certificates page and sidebar nav

Add a Certificates item to the student LMS sidebar that lists courses
where the student has earned a certificate. Each course card includes a
View Certificate action linking to the existing per-course certificate
page.

* refactor: reuse isOrgOnFreePlan helper from org store

* chore(i18n): add certificates translations for all locales

---------
2026-06-29 11:49:27 +01:00

71 lines
2.4 KiB
Svelte

<script lang="ts">
import { Button } from '@cio/ui/base/button';
import { Empty } from '@cio/ui/custom/empty';
import { Search } from '@cio/ui/custom/search';
import { CourseCardList, CourseCardLoader } from '$features/course/components';
import { coursesApi } from '$features/course/api';
import { profile } from '$lib/utils/store/user';
import { currentOrg } from '$lib/utils/store/org';
import { t } from '$lib/utils/functions/translations';
import type { UserEnrolledCourses } from '$features/course/types';
let searchValue = $state('');
const earnedCertificates = $derived.by(() => {
const courses = coursesApi.enrolledCourses.filter((course) => course.certificateEarnedAt != null);
return [...courses].sort((leftCourse, rightCourse) => {
const leftDate = leftCourse.certificateEarnedAt ? new Date(leftCourse.certificateEarnedAt).getTime() : 0;
const rightDate = rightCourse.certificateEarnedAt ? new Date(rightCourse.certificateEarnedAt).getTime() : 0;
return rightDate - leftDate;
});
});
const filteredCertificates = $derived.by(() => {
const query = searchValue.trim().toLowerCase();
if (!query) {
return earnedCertificates;
}
return earnedCertificates.filter((course) => {
const title = course.title?.toLowerCase() ?? '';
const description = course.description?.toLowerCase() ?? '';
return title.includes(query) || description.includes(query);
});
});
$effect(() => {
if (!$profile.id || !$currentOrg.id) return;
coursesApi.getEnrolledCourses();
});
</script>
<div class="space-y-4">
<Search bind:value={searchValue} placeholder={$t('certificates.search')} />
{#if coursesApi.isLoading}
<section class="cards-container">
<CourseCardLoader />
<CourseCardLoader />
<CourseCardLoader />
</section>
{:else if filteredCertificates.length === 0}
<Empty
title={searchValue.trim() ? $t('certificates.no_results_title') : $t('certificates.empty_title')}
description={searchValue.trim()
? $t('certificates.no_results_description')
: $t('certificates.empty_description')}
>
{#if !searchValue.trim()}
<Button href="/lms/mylearning">{$t('certificates.go_to_my_learning')}</Button>
{/if}
</Empty>
{:else}
<CourseCardList courses={filteredCertificates as UserEnrolledCourses} isLMS={true} isCertificateView={true} />
{/if}
</div>