Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Pathway/backend integration #446

Open
wants to merge 26 commits into
base: feat/pathways
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7296fda
feat: Pathways News Feed
tunny17 Aug 12, 2024
8d0d32e
feat: completion of newsfeed integration
tunny17 Aug 14, 2024
dae1a2f
feat: courses page
tunny17 Aug 20, 2024
df73b4d
Merge branch 'feat/pathways' into pathway/backendIntegration
tunny17 Aug 20, 2024
44f9b3f
merge fixes
tunny17 Aug 20, 2024
72e0f4f
checkpoint: people's page completion
tunny17 Aug 20, 2024
ef83c9b
feat: course landing page integration
tunny17 Aug 21, 2024
2a942bf
working on the LMS side
Chifez Aug 21, 2024
04c4715
feat: bug
tunny17 Aug 21, 2024
361b810
code clean up
tunny17 Aug 22, 2024
4134660
Merge pull request #447 from rotimi-best/pathway/settings_backend_issue
tunny17 Aug 22, 2024
5c4eced
Lms pathway fully done
Chifez Aug 24, 2024
3aa03c3
fix; navigation id for courses
tunny17 Aug 24, 2024
340e3de
fix: unlocking courses bug
tunny17 Aug 24, 2024
1bb1350
wrapping things up
Chifez Aug 25, 2024
0e92293
still cleaning things up
Chifez Aug 25, 2024
8eadfb8
Merge branch 'pathway/settings_backend_issue' into pathway/backendInt…
tunny17 Aug 25, 2024
60a58e5
merge fix
tunny17 Aug 25, 2024
28a167f
feat: certificate display
tunny17 Aug 26, 2024
5e72e62
feat: db migration
tunny17 Aug 27, 2024
ab87a0a
update seed
rotimi-best Aug 27, 2024
39bfe64
Fixed the setting update issue
Chifez Aug 28, 2024
959a4c3
finished the unlocking of pathway
Chifez Aug 28, 2024
2d78d62
Merge pull request #451 from rotimi-best/pathway/pathway_progress
tunny17 Aug 28, 2024
6bb0957
checkpoint: certificates update
tunny17 Aug 29, 2024
2899617
feat: bug fixes
tunny17 Sep 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions apps/backend/src/routes/downloadPathwayCertificate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const express = require('express');
const zod = require('zod');
const { generateCertificate } = require('../utils/certificate');

const router = express.Router();

router.post('/', async (req, res) => {
try {
// Validate
const mySchema = zod.object({
theme: zod.string(),
studentName: zod.string(),
courseName: zod.string(),
courseDescription: zod.string(),
orgLogoUrl: zod.string(),
orgName: zod.string(),
});
console.log('req.body', req.body);

mySchema.parse(req.body);

const pdfBuffer = await generateCertificate(req.body);

res.contentType('application/pdf');
res.send(pdfBuffer);
} catch (error) {
console.error('Error', error);
res.json({
success: false,
error,
});
}
});

module.exports = router;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script>
import { getLectureNo } from '$lib/components/Course/function';
import Chip from '$lib/components/Chip/index.svelte';

export let index;
export let title;
</script>

<div class="px-2 py-1 m-2 border rounded">
<Chip value={getLectureNo(index + 1, '0')} className="bg-primary-100 text-primary-700 inline" />
<p class="ml-2 text-xs font-light dark:text-white inline">
{title}
</p>
</div>
81 changes: 17 additions & 64 deletions apps/dashboard/src/lib/components/CourseLandingPage/index.svelte
Original file line number Diff line number Diff line change
@@ -1,32 +1,30 @@
<script lang="ts">
import get from 'lodash/get';
import { onMount, onDestroy } from 'svelte';
import PoweredBy from '$lib/components/Upgrade/PoweredBy.svelte';
import pluralize from 'pluralize';
import { page } from '$app/stores';
import Notebook from 'carbon-icons-svelte/lib/Notebook.svelte'; //note
import PresentationFile from 'carbon-icons-svelte/lib/PresentationFile.svelte'; // exercise
import Video from 'carbon-icons-svelte/lib/Video.svelte'; //video
import PageNumber from 'carbon-icons-svelte/lib/PageNumber.svelte';
import PlayFilled from 'carbon-icons-svelte/lib/PlayFilled.svelte';
import PricingSection from './components/PricingSection.svelte';
import { observeIntersection } from './components/IntersectionObserver';
import { getLectureNo } from '../Course/function';

import { NAV_ITEMS } from './constants';
import Modal from '../Modal/index.svelte';
import { currentOrg } from '$lib/utils/store/org';
import { course } from '$lib/components/Course/store';
import { t } from '$lib/utils/functions/translations';
import { calDateDiff } from '$lib/utils/functions/date';
import { handleOpenWidget, reviewsModalStore } from './store';
import Chip from '../Chip/index.svelte';
import Avatar from '$lib/components/Avatar/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import { getEmbedId } from '$lib/utils/functions/formatYoutubeVideo';

import type { Course, Lesson, Review } from '$lib/utils/types';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';

import Modal from '../Modal/index.svelte';
import LessonCard from './components/LessonCard.svelte';
import Avatar from '$lib/components/Avatar/index.svelte';
import PricingSection from './components/PricingSection.svelte';
import PoweredBy from '$lib/components/Upgrade/PoweredBy.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { observeIntersection } from './components/IntersectionObserver';
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
import { calDateDiff } from '$lib/utils/functions/date';
import { currentOrg } from '$lib/utils/store/org';
import UploadWidget from '$lib/components/UploadWidget/index.svelte';
import { course } from '$lib/components/Course/store';
import { t } from '$lib/utils/functions/translations';

export let editMode = false;
export let courseData: Course;
Expand Down Expand Up @@ -269,53 +267,8 @@

<div class="flex flex-wrap">
{#each lessons as lesson, index}
<div class="px-2 py-1 m-2 border rounded">
<Chip
value={getLectureNo(index + 1, '0')}
className="bg-primary-100 text-primary-700 inline "
/>
<p class="ml-2 text-xs font-light dark:text-white inline">
{lesson.title}
</p>


<!-- <div class="flex items-center">
{#if lesson.slide_url}
<span class="text-sm font-light flex w-2/4"
><PresentationFile size={16} class="mr-1" />{$t(
'course.navItem.landing_page.slide'
)}</span
>
{/if}
{#if lesson.note}
<span class="text-sm font-light flex w-2/4"
><Notebook size={16} class="mr-1" />{$t(
'course.navItem.landing_page.note'
)}</span
>
{/if}
</div> -->


<!-- <div class="flex items-center">
{#if lesson.videos}
<span class="text-sm font-light flex w-2/4"
><Video size={16} class="mr-1" />{lesson.videos.length}
{$t('course.navItem.landing_page.video')}{lesson.videos.length > 1 ? 's' : ''}
</span>
{/if}
{#if get(lesson, 'totalExercises[0].count')}
<span class="flex w-2/4 text-sm font-light"
><PageNumber size={16} class="mr-1" />{pluralize(
'exercise',
get(lesson, 'totalExercises[0].count', 0),
true
)}</span
>
{/if}
</div> -->
</div>
{/each}
<LessonCard {index} title={lesson.title} />
{/each}
</div>
</section>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
export let isLMS = false;
export let isLearningPath = false;
export let totalCourse = 0;
export let completedCourse = 0;
export let pathwaycompletedCourses = 0;
export let isExplore = false;
export let progressRate = 45;
export let type: COURSE_TYPE;
Expand Down Expand Up @@ -63,6 +63,10 @@
}

function getCourseUrl() {
if (isLMS && isLearningPath) {
return `/pathways/${id}`;
}

return isOnLandingPage || isExplore
? `/course/${slug}`
: `/courses/${id}${isLMS ? '/lessons?next=true' : ''}`;
Expand Down Expand Up @@ -145,7 +149,7 @@
<svelte:fragment slot="error">{$t('courses.course_card.error_message')}</svelte:fragment>
</ImageLoader>

{#if isLearningPath && isLMS}
{#if isLMS && isLearningPath}
<span
class="absolute top-2 left-2 z-10 text-xs font-bold uppercase bg-white text-primary-600 rounded-sm p-1"
>
Expand Down Expand Up @@ -175,7 +179,7 @@
<div>
<p class="text-xs {!isLMS && 'pl-2'} font-normal dark:text-white">
{#if isLearningPath && isLMS}
{completedCourse} / {totalCourse} {$t('lms_pathway.course')}
{pathwaycompletedCourses} / {totalCourse} {$t('lms_pathway.course')}
{:else}
{totalLessons}
{$t('courses.course_card.lessons_number')}
Expand Down
41 changes: 26 additions & 15 deletions apps/dashboard/src/lib/components/Courses/index.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import CardLoader from '$lib/components/Courses/components/Card/Loader.svelte';
import CoursesEmptyIcon from '$lib/components/Icons/CoursesEmptyIcon.svelte';
import { courseMetaDeta } from '$lib/components/Courses/store';
import type { Pathway } from '$lib/utils/types';
import type { Course, Pathway } from '$lib/utils/types';
import { globalStore } from '$lib/utils/store/app';
import {
StructuredList,
Expand All @@ -16,9 +16,10 @@
} from 'carbon-components-svelte';
import { t } from '$lib/utils/functions/translations';
import { isMobile } from '$lib/utils/store/useMobile';
import type { LmsCourse } from '$lib/components/LMS/store';
import type { LMSCourse } from '$lib/components/LMS/store';
import { getPathwayCompletedCoursesLength } from '$lib/utils/functions/pathway';

export let courses: LmsCourse[] = [];
export let courses: LMSCourse[] = [];
export let emptyTitle = $t('courses.course_card.empty_title');
export let emptyDescription = $t('courses.course_card.empty_description');
export let isExplore = false;
Expand All @@ -28,16 +29,29 @@
if (!progressRate || !totalItem) {
return 0;
}

return Math.round((progressRate / totalItem) * 100);
}

const getCompletedCourses = (course: Pathway) => {
if (!course.isPathway) return;
const completedCourse =
course.courses?.filter((course) => course.progress_rate === 100).length || 0;
return completedCourse;
};
function calculatePathwayProgress(pathway: Pathway): number {
if (!pathway.isPathway) return 0;

const totalCourses = pathway.total_course;
if (totalCourses === 0) return 0;

// Number of courses completed within the pathway
const completedCourses = getPathwayCompletedCoursesLength(pathway);

return Math.round((completedCourses / totalCourses) * 100);
}

function calculateCourseAndPathwayProgress(course: LMSCourse): number {
if (course.isPathway) {
return calculatePathwayProgress(course as Pathway);
} else {
// Individual course progress calculation
return calcProgressRate(course.progress_rate, course.total_lessons);
}
}
</script>

<!-- <CopyCourseModal /> -->
Expand Down Expand Up @@ -105,16 +119,13 @@
type={courseData.type}
isLearningPath={courseData.isPathway}
totalCourse={courseData.total_course}
completedCourse={getCompletedCourses(courseData)}
pathwaycompletedCourses={getPathwayCompletedCoursesLength(courseData)}
currency={courseData.currency}
totalLessons={courseData.total_lessons}
totalStudents={courseData.total_students}
isLMS={$globalStore.isOrgSite}
{isExplore}
progressRate={calcProgressRate(
courseData.progress_rate,
courseData.isPathway ? courseData.total_course : courseData.total_lessons
)}
progressRate={calculateCourseAndPathwayProgress(courseData)}
/>
{/key}
{/each}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { t } from '$lib/utils/functions/translations';
import { goto } from '$app/navigation';
import type { Pathway } from '$lib/utils/types';
import { courseProgress } from '$lib/utils/functions/pathway';

export let open = false;
export let pathway: Pathway | any;
Expand All @@ -24,42 +25,52 @@
buttonClass="flex justify-end"
bind:open
>
<div class="h-full">
{#each pathway.courses as course}
<a href={course.is_unlocked ? `/course/${course.id}` : null} class="hover:no-underline">
<div class="p-2 cursor-pointer space-y-2 {NavClasses.item}">
<p class="text-sm font-normal w-[60%] truncate">{course.title}</p>
{#if !course.is_unlocked}
<p class="text-sm font-normal text-gray-500 bg-gray-300 w-fit px-1 rounded-sm">
{$t('lms_pathway.locked')}
</p>
{:else}
<div class="flex items-center gap-2">
<div class="relative bg-[#EAEAEA] w-[40%] h-2">
<div
style="width: {course.progress_rate}%"
class="absolute top-0 left-0 bg-primary-900 h-full"
/>
</div>
<p class="text-xs font-medium">
{course.progress_rate === 100
? `${$t('lms_pathway.completed')}`
: `${course.progress_rate}%`}
{#if pathway.pathway_course?.length > 0}
<div class="h-full">
{#each pathway.pathway_course as pathway_course}
<a
href={pathway_course.is_unlocked && pathway_course.course.is_published
? `/courses/${pathway_course.course.id}`
: null}
class="hover:no-underline"
>
<div class="p-2 cursor-pointer space-y-2 {NavClasses.item}">
<p class="text-sm font-normal w-[60%] truncate">{pathway_course.course.title}</p>
{#if pathway_course.is_unlocked == false || pathway_course.course.is_published == false}
<p class="text-sm font-normal text-gray-500 bg-gray-300 w-fit px-1 rounded-sm">
{$t('lms_pathway.locked')}
</p>
</div>
{/if}
</div>
</a>
{/each}
</div>

{:else}
<div class="flex items-center gap-2">
<div class="relative bg-[#EAEAEA] w-[40%] h-2">
<div
style="width: {courseProgress(pathway_course.course.lesson) || 0}%"
class="absolute top-0 left-0 bg-primary-900 h-full"
/>
</div>
<p class="text-xs font-medium">
{courseProgress(pathway_course.course.lesson) === 100
? `${$t('lms_pathway.completed')}`
: `${courseProgress(pathway_course.course.lesson) || 0}%`}
</p>
</div>
{/if}
</div>
</a>
{/each}
</div>
{:else}
<h3 class="dark:text-white text-2xl my-5">{$t('search.no_course')}</h3>
{/if}
<!-- Button -->
<div slot="buttons">
<PrimaryButton
label={$t('lms_pathway.goto_pathway')}
variant={VARIANTS.TEXT}
className="text-primary-800 font-semibold"
onClick={gotoPathway}
/>
{#if pathway.pathway_course?.length > 0}
<PrimaryButton
label={$t('lms_pathway.goto_pathway')}
variant={VARIANTS.TEXT}
className="text-primary-800 font-semibold"
onClick={gotoPathway}
/>
{/if}
</div>
</Modal>
Loading