From 773fc873c28c29d076d8ef3210f536a0b94961de Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Mon, 29 Jun 2026 11:49:27 +0100 Subject: [PATCH] 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 --------- --- .../features/course/components/card.svelte | 24 ++++++- .../features/lms/pages/certificates.svelte | 70 +++++++++++++++++++ .../src/lib/features/lms/pages/index.ts | 1 + .../features/ui/navigation/lms-navigation.ts | 9 +++ apps/dashboard/src/lib/utils/store/org.ts | 18 +++-- .../src/lib/utils/translations/da.json | 13 ++++ .../src/lib/utils/translations/de.json | 13 ++++ .../src/lib/utils/translations/en.json | 13 ++++ .../src/lib/utils/translations/es.json | 13 ++++ .../src/lib/utils/translations/fr.json | 13 ++++ .../src/lib/utils/translations/hi.json | 13 ++++ .../src/lib/utils/translations/pl.json | 13 ++++ .../src/lib/utils/translations/pt.json | 13 ++++ .../src/lib/utils/translations/ru.json | 13 ++++ .../src/lib/utils/translations/vi.json | 13 ++++ .../(app)/lms/certificates/+page.svelte | 23 ++++++ 16 files changed, 265 insertions(+), 10 deletions(-) create mode 100644 apps/dashboard/src/lib/features/lms/pages/certificates.svelte create mode 100644 apps/dashboard/src/routes/(app)/lms/certificates/+page.svelte diff --git a/apps/dashboard/src/lib/features/course/components/card.svelte b/apps/dashboard/src/lib/features/course/components/card.svelte index d4ab45e4..c77c9a6f 100644 --- a/apps/dashboard/src/lib/features/course/components/card.svelte +++ b/apps/dashboard/src/lib/features/course/components/card.svelte @@ -36,12 +36,13 @@ isOnLandingPage?: boolean; isLMS?: boolean; isExplore?: boolean; + isCertificateView?: boolean; href?: string; actions?: Snippet; onExploreClick?: () => void; } - let { course, isOnLandingPage, isLMS, isExplore, href, actions, onExploreClick }: Props = $props(); + let { course, isOnLandingPage, isLMS, isExplore, isCertificateView, href, actions, onExploreClick }: Props = $props(); let { bannerImage, @@ -150,6 +151,10 @@ return href; } + if (isCertificateView) { + return `/courses/${id}/certificates`; + } + if (isOnLandingPage || isExplore) { if (!slug) { return undefined; @@ -198,6 +203,9 @@ ? getStudentCourseComplianceDate(course as UserEnrolledCourses[number]) : null ); + const certificateEarnedAt = $derived( + isCertificateView && 'certificateEarnedAt' in course ? course.certificateEarnedAt : null + ); function formatDate(value: string | null | undefined) { if (!value) { @@ -275,7 +283,13 @@ {/if} {:else if isLMS} - {#if !isExplore} + {#if isCertificateView} + {#if certificateEarnedAt} +

+ {$t('certificates.earned_on')}: {formatDate(certificateEarnedAt)} +

+ {/if} + {:else if !isExplore}

{progressRate}%

@@ -305,7 +319,11 @@ {#if isLMS} diff --git a/apps/dashboard/src/lib/features/lms/pages/certificates.svelte b/apps/dashboard/src/lib/features/lms/pages/certificates.svelte new file mode 100644 index 00000000..e73bba91 --- /dev/null +++ b/apps/dashboard/src/lib/features/lms/pages/certificates.svelte @@ -0,0 +1,70 @@ + + +
+ + + {#if coursesApi.isLoading} +
+ + + +
+ {:else if filteredCertificates.length === 0} + + {#if !searchValue.trim()} + + {/if} + + {:else} + + {/if} +
diff --git a/apps/dashboard/src/lib/features/lms/pages/index.ts b/apps/dashboard/src/lib/features/lms/pages/index.ts index 61fdb684..ce2016c6 100644 --- a/apps/dashboard/src/lib/features/lms/pages/index.ts +++ b/apps/dashboard/src/lib/features/lms/pages/index.ts @@ -1,4 +1,5 @@ export { default as DashboardPage } from './dashboard.svelte'; export { default as ExplorePage } from './explore.svelte'; export { default as MyLearningPage } from './mylearning.svelte'; +export { default as CertificatesPage } from './certificates.svelte'; export { default as ExercisesPage } from './exercises.svelte'; diff --git a/apps/dashboard/src/lib/features/ui/navigation/lms-navigation.ts b/apps/dashboard/src/lib/features/ui/navigation/lms-navigation.ts index d66b36d4..6e61b877 100644 --- a/apps/dashboard/src/lib/features/ui/navigation/lms-navigation.ts +++ b/apps/dashboard/src/lib/features/ui/navigation/lms-navigation.ts @@ -1,4 +1,5 @@ import { + CertificateIcon, CommunityIcon, CourseIcon, ExerciseIcon, @@ -11,6 +12,7 @@ import { import type { AccountOrg } from '$features/app/types'; import type { Component } from 'svelte'; import { isActive } from '$lib/utils/functions/app'; +import { isOrgOnFreePlan } from '$lib/utils/store/org'; export interface NavItem { title: string; @@ -57,6 +59,13 @@ export const baseNavConfig: NavItemConfig[] = [ icon: CourseIcon, matchPattern: '^/lms/mylearning(/.*)?$' }, + { + titleKey: 'lms_navigation.certificates', + path: '/certificates', + icon: CertificateIcon, + matchPattern: '^/lms/certificates(/.*)?$', + show: (currentOrg) => !isOrgOnFreePlan(currentOrg) + }, { titleKey: 'lms_navigation.explore', path: '/explore', diff --git a/apps/dashboard/src/lib/utils/store/org.ts b/apps/dashboard/src/lib/utils/store/org.ts index d26ad994..992ff0ef 100644 --- a/apps/dashboard/src/lib/utils/store/org.ts +++ b/apps/dashboard/src/lib/utils/store/org.ts @@ -71,6 +71,16 @@ const getActivePlan = (org: AccountOrg) => { return org.plans.find((p) => p.isActive); }; +export function isOrgOnFreePlan(org: AccountOrg | null | undefined) { + if (!org?.id || PUBLIC_IS_SELFHOSTED === 'true') { + return false; + } + + const plan = getActivePlan(org); + + return !plan || plan.planName === PLAN.BASIC; +} + export const currentOrgPlan = derived(currentOrg, ($currentOrg) => getActivePlan($currentOrg)); export const currentOrgPath = derived(currentOrg, ($currentOrg) => @@ -126,13 +136,7 @@ export function getOrgPublicUrl(org: OrgPublicOrigin, pathname = '/'): string { export const currentOrgDomain = derived(currentOrg, ($currentOrg) => getOrgPublicOrigin($currentOrg)); -export const isFreePlan = derived(currentOrg, ($currentOrg) => { - if (!$currentOrg.id || PUBLIC_IS_SELFHOSTED === 'true') return false; - - const plan = getActivePlan($currentOrg); - - return !plan || plan.planName === PLAN.BASIC; -}); +export const isFreePlan = derived(currentOrg, ($currentOrg) => isOrgOnFreePlan($currentOrg)); export const isEnterprisePlan = derived(currentOrg, ($currentOrg) => { if (PUBLIC_IS_SELFHOSTED === 'true') return true; diff --git a/apps/dashboard/src/lib/utils/translations/da.json b/apps/dashboard/src/lib/utils/translations/da.json index 33118de2..0c6c531c 100644 --- a/apps/dashboard/src/lib/utils/translations/da.json +++ b/apps/dashboard/src/lib/utils/translations/da.json @@ -471,9 +471,22 @@ "any_course": "Ethvert kursus, du gennemfører, vil blive vist her", "find_courses": "Find kurser" }, + "certificates": { + "heading": "Certifikater", + "description": "Se og download certifikater, du har opnået ved at gennemføre kurser.", + "search": "Søg certifikater", + "earned_on": "Opnået den", + "view_certificate": "Se certifikat", + "empty_title": "Ingen certifikater endnu", + "empty_description": "Gennemfør et kursus for at opnå dit første certifikat. Dine opnåede certifikater vises her.", + "no_results_title": "Ingen certifikater fundet", + "no_results_description": "Prøv et andet søgeord.", + "go_to_my_learning": "Gå til Min Læring" + }, "lms_navigation": { "home": "Hjem", "my_learning": "Min Læring", + "certificates": "Certifikater", "exercise": "Motion", "community": "Fællesskab", "explore": "Udforsk", diff --git a/apps/dashboard/src/lib/utils/translations/de.json b/apps/dashboard/src/lib/utils/translations/de.json index ee467145..f9a4ff3b 100644 --- a/apps/dashboard/src/lib/utils/translations/de.json +++ b/apps/dashboard/src/lib/utils/translations/de.json @@ -471,9 +471,22 @@ "any_course": "Alle von Ihnen abgeschlossenen Kurse werden hier angezeigt", "find_courses": "Kurse finden" }, + "certificates": { + "heading": "Zertifikate", + "description": "Sieh dir Zertifikate an und lade sie herunter, die du durch den Abschluss von Kursen erworben hast.", + "search": "Zertifikate suchen", + "earned_on": "Erworben am", + "view_certificate": "Zertifikat anzeigen", + "empty_title": "Noch keine Zertifikate", + "empty_description": "Schließe einen Kurs ab, um dein erstes Zertifikat zu verdienen. Deine erworbenen Zertifikate werden hier angezeigt.", + "no_results_title": "Keine Zertifikate gefunden", + "no_results_description": "Versuche einen anderen Suchbegriff.", + "go_to_my_learning": "Zu Mein Lernen" + }, "lms_navigation": { "home": "Zuhause", "my_learning": "Mein Lernen", + "certificates": "Zertifikate", "exercise": "Übung", "community": "Gemeinschaft", "explore": "Entdecken", diff --git a/apps/dashboard/src/lib/utils/translations/en.json b/apps/dashboard/src/lib/utils/translations/en.json index 6d596402..aae52c0b 100644 --- a/apps/dashboard/src/lib/utils/translations/en.json +++ b/apps/dashboard/src/lib/utils/translations/en.json @@ -4059,6 +4059,7 @@ "lms_navigation": { "home": "Home", "my_learning": "My Learning", + "certificates": "Certificates", "programs": "Programs", "exercise": "Exercise", "community": "Community", @@ -4077,6 +4078,18 @@ "any_course": "Any course that you complete will be displayed here", "find_courses": "Find courses" }, + "certificates": { + "heading": "Certificates", + "description": "View and download certificates you've earned by completing courses.", + "search": "Search certificates", + "earned_on": "Earned on", + "view_certificate": "View Certificate", + "empty_title": "No certificates yet", + "empty_description": "Complete a course to earn your first certificate. Your earned certificates will appear here.", + "no_results_title": "No certificates found", + "no_results_description": "Try a different search term.", + "go_to_my_learning": "Go to My Learning" + }, "exercises": { "heading": "Exercises", "not_submitted": "Not Submitted", diff --git a/apps/dashboard/src/lib/utils/translations/es.json b/apps/dashboard/src/lib/utils/translations/es.json index 2c83ca4e..0ace87e4 100644 --- a/apps/dashboard/src/lib/utils/translations/es.json +++ b/apps/dashboard/src/lib/utils/translations/es.json @@ -471,9 +471,22 @@ "any_course": "Cualquier curso que completes se mostrará aquí.", "find_courses": "encontrar cursos" }, + "certificates": { + "heading": "Certificados", + "description": "Ve y descarga los certificados que has obtenido al completar cursos.", + "search": "Buscar certificados", + "earned_on": "Obtenido el", + "view_certificate": "Ver certificado", + "empty_title": "Aún no hay certificados", + "empty_description": "Completa un curso para obtener tu primer certificado. Tus certificados obtenidos aparecerán aquí.", + "no_results_title": "No se encontraron certificados", + "no_results_description": "Prueba con otro término de búsqueda.", + "go_to_my_learning": "Ir a Mi aprendizaje" + }, "lms_navigation": { "home": "Inicio", "my_learning": "Mi aprendizaje", + "certificates": "Certificados", "exercise": "Ejercicio", "community": "Comunidad", "explore": "Explorar", diff --git a/apps/dashboard/src/lib/utils/translations/fr.json b/apps/dashboard/src/lib/utils/translations/fr.json index 652edc82..0504da8e 100644 --- a/apps/dashboard/src/lib/utils/translations/fr.json +++ b/apps/dashboard/src/lib/utils/translations/fr.json @@ -471,9 +471,22 @@ "any_course": "Tout cours que vous terminez sera affiché ici", "find_courses": "Trouver des cours" }, + "certificates": { + "heading": "Certificats", + "description": "Consultez et téléchargez les certificats que vous avez obtenus en terminant des cours.", + "search": "Rechercher des certificats", + "earned_on": "Obtenu le", + "view_certificate": "Voir le certificat", + "empty_title": "Aucun certificat pour le moment", + "empty_description": "Terminez un cours pour obtenir votre premier certificat. Vos certificats obtenus apparaîtront ici.", + "no_results_title": "Aucun certificat trouvé", + "no_results_description": "Essayez un autre terme de recherche.", + "go_to_my_learning": "Aller à Mon apprentissage" + }, "lms_navigation": { "home": "Accueil", "my_learning": "Mon apprentissage", + "certificates": "Certificats", "exercise": "Exercice", "community": "Communauté", "explore": "Explorer", diff --git a/apps/dashboard/src/lib/utils/translations/hi.json b/apps/dashboard/src/lib/utils/translations/hi.json index 75b3675e..f3f35dad 100644 --- a/apps/dashboard/src/lib/utils/translations/hi.json +++ b/apps/dashboard/src/lib/utils/translations/hi.json @@ -471,9 +471,22 @@ "any_course": "आपके द्वारा पूरा किया गया कोई भी पाठ्यक्रम यहां प्रदर्शित किया जाएगा", "find_courses": "पाठ्यक्रम खोजें" }, + "certificates": { + "heading": "प्रमाणपत्र", + "description": "पाठ्यक्रम पूरे करने पर आपको मिले प्रमाणपत्र देखें और डाउनलोड करें।", + "search": "प्रमाणपत्र खोजें", + "earned_on": "प्राप्त तिथि", + "view_certificate": "प्रमाणपत्र देखें", + "empty_title": "अभी तक कोई प्रमाणपत्र नहीं", + "empty_description": "अपना पहला प्रमाणपत्र अर्जित करने के लिए एक पाठ्यक्रम पूरा करें। आपके अर्जित प्रमाणपत्र यहाँ दिखाई देंगे।", + "no_results_title": "कोई प्रमाणपत्र नहीं मिला", + "no_results_description": "कोई अलग खोज शब्द आज़माएँ।", + "go_to_my_learning": "मेरी सीख पर जाएँ" + }, "lms_navigation": { "home": "घर", "my_learning": "मेरी सीख", + "certificates": "प्रमाणपत्र", "exercise": "व्यायाम", "community": "समुदाय", "explore": "अन्वेषण करें", diff --git a/apps/dashboard/src/lib/utils/translations/pl.json b/apps/dashboard/src/lib/utils/translations/pl.json index 6e407f72..78a2e24e 100644 --- a/apps/dashboard/src/lib/utils/translations/pl.json +++ b/apps/dashboard/src/lib/utils/translations/pl.json @@ -471,9 +471,22 @@ "any_course": "Tutaj wyświetli się każdy ukończony kurs", "find_courses": "Znajdź kursy" }, + "certificates": { + "heading": "Certyfikaty", + "description": "Przeglądaj i pobieraj certyfikaty uzyskane po ukończeniu kursów.", + "search": "Szukaj certyfikatów", + "earned_on": "Uzyskano", + "view_certificate": "Zobacz certyfikat", + "empty_title": "Brak certyfikatów", + "empty_description": "Ukończ kurs, aby zdobyć swój pierwszy certyfikat. Uzyskane certyfikaty pojawią się tutaj.", + "no_results_title": "Nie znaleziono certyfikatów", + "no_results_description": "Spróbuj innego wyszukiwania.", + "go_to_my_learning": "Przejdź do Moja nauka" + }, "lms_navigation": { "home": "Dom", "my_learning": "Moja nauka", + "certificates": "Certyfikaty", "exercise": "Ćwicz", "community": "Społeczność", "explore": "Eksploruj", diff --git a/apps/dashboard/src/lib/utils/translations/pt.json b/apps/dashboard/src/lib/utils/translations/pt.json index 4a2e3d8e..9b9be200 100644 --- a/apps/dashboard/src/lib/utils/translations/pt.json +++ b/apps/dashboard/src/lib/utils/translations/pt.json @@ -471,9 +471,22 @@ "any_course": "Qualquer curso que você concluir será exibido aqui", "find_courses": "Encontre cursos" }, + "certificates": { + "heading": "Certificados", + "description": "Veja e baixe os certificados que você obteve ao concluir cursos.", + "search": "Pesquisar certificados", + "earned_on": "Obtido em", + "view_certificate": "Ver certificado", + "empty_title": "Nenhum certificado ainda", + "empty_description": "Conclua um curso para obter seu primeiro certificado. Seus certificados obtidos aparecerão aqui.", + "no_results_title": "Nenhum certificado encontrado", + "no_results_description": "Tente outro termo de pesquisa.", + "go_to_my_learning": "Ir para Meu aprendizado" + }, "lms_navigation": { "home": "Página inicial", "my_learning": "Meu aprendizado", + "certificates": "Certificados", "exercise": "Exercício", "community": "Comunidade", "explore": "Explorar", diff --git a/apps/dashboard/src/lib/utils/translations/ru.json b/apps/dashboard/src/lib/utils/translations/ru.json index 0555f630..0513234e 100644 --- a/apps/dashboard/src/lib/utils/translations/ru.json +++ b/apps/dashboard/src/lib/utils/translations/ru.json @@ -471,9 +471,22 @@ "any_course": "Здесь будут отображаться все пройденные вами курсы.", "find_courses": "Найти курсы" }, + "certificates": { + "heading": "Сертификаты", + "description": "Просматривайте и скачивайте сертификаты, полученные за завершение курсов.", + "search": "Поиск сертификатов", + "earned_on": "Получен", + "view_certificate": "Посмотреть сертификат", + "empty_title": "Пока нет сертификатов", + "empty_description": "Завершите курс, чтобы получить свой первый сертификат. Полученные сертификаты появятся здесь.", + "no_results_title": "Сертификаты не найдены", + "no_results_description": "Попробуйте другой поисковый запрос.", + "go_to_my_learning": "Перейти к Моему обучению" + }, "lms_navigation": { "home": "Главная", "my_learning": "Мое обучение", + "certificates": "Сертификаты", "exercise": "Упражнение", "community": "Сообщество", "explore": "Исследуйте", diff --git a/apps/dashboard/src/lib/utils/translations/vi.json b/apps/dashboard/src/lib/utils/translations/vi.json index 0f873a22..080e5178 100644 --- a/apps/dashboard/src/lib/utils/translations/vi.json +++ b/apps/dashboard/src/lib/utils/translations/vi.json @@ -471,9 +471,22 @@ "any_course": "Bất kỳ khóa học nào bạn hoàn thành sẽ được hiển thị ở đây", "find_courses": "Tìm khóa học" }, + "certificates": { + "heading": "Chứng chỉ", + "description": "Xem và tải xuống các chứng chỉ bạn đã đạt được khi hoàn thành khóa học.", + "search": "Tìm kiếm chứng chỉ", + "earned_on": "Đạt được vào", + "view_certificate": "Xem chứng chỉ", + "empty_title": "Chưa có chứng chỉ nào", + "empty_description": "Hoàn thành một khóa học để nhận chứng chỉ đầu tiên của bạn. Các chứng chỉ đã đạt được sẽ hiển thị tại đây.", + "no_results_title": "Không tìm thấy chứng chỉ", + "no_results_description": "Hãy thử một từ khóa tìm kiếm khác.", + "go_to_my_learning": "Đi tới Học tập của tôi" + }, "lms_navigation": { "home": "Trang chủ", "my_learning": "Học tập của tôi", + "certificates": "Chứng chỉ", "exercise": "tập thể dục", "community": "cộng đồng", "explore": "Khám phá", diff --git a/apps/dashboard/src/routes/(app)/lms/certificates/+page.svelte b/apps/dashboard/src/routes/(app)/lms/certificates/+page.svelte new file mode 100644 index 00000000..35bd315b --- /dev/null +++ b/apps/dashboard/src/routes/(app)/lms/certificates/+page.svelte @@ -0,0 +1,23 @@ + + + + Certificates - ClassroomIO + + + + + + {$t('certificates.heading')} + {$t('certificates.description')} + + + + {#snippet child()} + + {/snippet} + +