feat: 🚀 😃 convert repo into a monorepo 🚀 😃 (#51)

* Merge main into turbo-monrepo

* feat: rename apps

* feat: remove supbase videos and serve all from project

* feat: move supabase to root

* feat: update README

* docs: update link
This commit is contained in:
Rotimi Best
2023-11-25 21:56:02 +00:00
committed by GitHub
parent 03ca379094
commit 41e29014ef
806 changed files with 85853 additions and 5746 deletions
@@ -0,0 +1,54 @@
<script lang="ts">
import { onMount } from 'svelte';
import { sideBar } from '$lib/components/Org/store';
import { apps } from '$lib/components/Apps/store';
import APPS_CONSTANTS from '$lib/components/Apps/constants';
import hotkeys from 'hotkeys-js';
function handleClose() {
$apps.selectedApp = undefined;
if ($apps.isStudent) {
$apps.open = false;
}
}
function handleAppClick(appName?: string) {
if (appName === $apps.selectedApp) {
handleClose();
} else {
$apps.selectedApp = appName;
}
$apps.open = true;
$apps.dropdown = !$apps.dropdown;
}
function toggleSidebar() {
$sideBar.hidden = !$sideBar.hidden;
}
onMount(() => {
hotkeys('ctrl+b,command+b,ctrl+1,command+1,ctrl+2,command+2', function (event, handler) {
// Prevent default behavior
event.preventDefault();
console.log({ handlerKey: handler.key });
switch (handler.key) {
case 'ctrl+b':
case 'command+b':
toggleSidebar();
break;
case 'ctrl+1':
case 'command+1':
handleAppClick(APPS_CONSTANTS.APPS.COMMENTS);
break;
case 'ctrl+2':
case 'command+2':
handleAppClick(APPS_CONSTANTS.APPS.POLL);
break;
}
});
});
</script>
<slot />
@@ -0,0 +1,5 @@
export const load = ({ params = { id: '' } }) => {
return {
courseId: params.id
};
};
@@ -0,0 +1,10 @@
<script>
import Overview from '$lib/components/Course/index.svelte';
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
export let data;
</script>
<CourseContainer bind:courseId={data.courseId}>
<Overview />
</CourseContainer>
@@ -0,0 +1,5 @@
export const load = ({ params = { id: '' } }) => {
return {
courseId: params.id
};
};
@@ -0,0 +1,218 @@
<script lang="ts">
import { onMount } from 'svelte';
import {
Search,
Checkbox,
StructuredList,
StructuredListBody,
StructuredListCell,
StructuredListHead,
StructuredListRow,
Pagination
} from 'carbon-components-svelte';
import AudioConsoleIcon from 'carbon-icons-svelte/lib/AudioConsole.svelte';
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import PageNav from '$lib/components/PageNav/index.svelte';
import PageBody from '$lib/components/PageBody/index.svelte';
import Box from '$lib/components/Box/index.svelte';
import { course, group } from '$lib/components/Course/store';
import { getLectureNo } from '$lib/components/Course/function';
import { lessons } from '$lib/components/Course/components/Lesson/store/lessons';
import { ROLE } from '$lib/utils/constants/roles';
import { takeAttendance } from '$lib/utils/services/attendance';
import { snackbar } from '$lib/components/Snackbar/store';
import { attendance } from '$lib/utils/store/attendance.js';
import { profile } from '$lib/utils/store/user';
import type { GroupPerson, Lesson } from '$lib/utils/types/index';
import { browser } from '$app/environment';
export let data;
interface CourseData {
attendance: {
student_id: string;
lesson_id: string;
is_present: boolean;
id: number;
}[];
}
let students: GroupPerson[] = [];
let isStudent: boolean = false;
let searchValue = '';
function setAttendance(courseData: CourseData) {
for (const attendanceItem of courseData.attendance) {
const { student_id, lesson_id, is_present, id } = attendanceItem;
if (!$attendance[student_id]) {
$attendance[student_id] = {
[lesson_id]: {
id,
is_present
}
};
} else {
$attendance[student_id] = {
...$attendance[student_id],
[lesson_id]: {
id,
is_present
}
};
}
}
}
function handleAttendanceChange(e: any, student: GroupPerson, lesson: Lesson) {
if (isStudent) return;
const attendanceItem = $attendance[student.id]
? $attendance[student.id][lesson.id] || { id: undefined }
: { id: undefined };
/*
This validation is useless. We shouldn't assume we already have an attendance item id
because it might be a new attendance.
if (!attendanceItem.id) {
snackbar.error("Something isn't right. Please reload and take attendance again");
console.error(`Attendance Id Missing`, courseId, student.id, lesson.id);
return;
}
*/
const _data = {
id: attendanceItem.id,
student_id: student.id,
is_present: e.target.checked,
lesson_id: lesson.id,
course_id: data.courseId
};
takeAttendance(_data).then((res) => {
if (res.error) {
console.error(`res.error`, res.error);
snackbar.error("Something isn't right. Please reload and take attendance again");
} else {
// console.log('res', JSON.stringify(res));
const { id, is_present } = res.data[0];
if ($attendance[student.id]) {
$attendance[student.id][lesson.id] = {
id,
is_present
};
} else {
$attendance[student.id] = {
[lesson.id]: {
id,
is_present
}
};
}
}
});
}
// function for the searchbar
function searchStudents(query: string, _students: GroupPerson[]) {
const lowercaseQuery = query.toLowerCase();
return _students.filter((student) =>
student.profile.fullname.toLowerCase().includes(lowercaseQuery)
);
}
async function firstRender(courseId: string) {
if (courseId) {
if (!Object.keys($attendance).length) {
setAttendance($course);
}
return;
}
}
$: students = isStudent
? $group.people.filter((person) => !!person.profile && person.profile.id === $profile.id)
: $group.people.filter((person) => !!person.profile && person.role_id === ROLE.STUDENT);
$: browser && $course.id && firstRender($course.id);
</script>
<CourseContainer bind:isStudent bind:courseId={data.courseId}>
<PageNav title="Attendance" />
<PageBody width="w-full max-w-6xl md:w-11/12">
<section class="flex items-center mx-2 lg:mx-9 my-5">
<div
class="flex flex-col lg:flex-row items-start gap-2 lg:items-center justify-between w-full"
>
<div class="flex">
<p class="flex items-center mr-5">
<Checkbox checked disabled /> Present
</p>
<p class="flex items-center">
<Checkbox disabled /> Absent
</p>
</div>
<div>
<Search
class="dark:text-slate-950"
placeholder="Search students"
bind:value={searchValue}
/>
</div>
</div>
</section>
<section class="my-5 mx-2 lg:mx-9">
<StructuredList class="m-0 relative">
<!-- Moved the lesson headers outside the students loop -->
<StructuredListHead class="bg-primary-100">
<StructuredListRow head class="mx-7">
<StructuredListCell head class="text-primary-600 py-3">Student</StructuredListCell>
{#each $lessons as lesson, index}
<StructuredListCell head class="text-primary-600 py-3"
>Lesson 0{getLectureNo(index + 1)}</StructuredListCell
>
{/each}
</StructuredListRow>
</StructuredListHead>
{#each searchStudents(searchValue, students) as student}
<StructuredListBody>
<StructuredListRow>
<StructuredListCell>
<div class="w-1/4 flex items-center">
<p class="dark:text-white font-semibold">
{student.profile.fullname}
</p>
</div>
</StructuredListCell>
{#each $lessons as lesson}
<StructuredListCell class="">
<Checkbox
class={isStudent ? 'cursor-not-allowed' : ''}
disabled={isStudent}
checked={$attendance[student.id]
? $attendance[student.id][lesson.id]
? $attendance[student.id][lesson.id].is_present
: false
: false}
on:change={(e) => handleAttendanceChange(e, student, lesson)}
/>
</StructuredListCell>
{/each}
</StructuredListRow>
</StructuredListBody>
{/each}
</StructuredList>
{#if students.length === 0}
<Box className="h-[300px] w-full">
<AudioConsoleIcon size={32} class="carbon-icon w-80" />
<h3 class="text-3xl text-gray-500 dark:text-white text-center">No Student Added</h3>
</Box>
{/if}
{#if students.length !== 0}
<Pagination totalItems={10} pageSizes={[10, 15, 20]} />
{/if}
</section>
</PageBody>
</CourseContainer>
@@ -0,0 +1,5 @@
export const load = ({ params = { id: '' } }) => {
return {
courseId: params.id
};
};
@@ -0,0 +1,59 @@
<script lang="ts">
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import PageNav from '$lib/components/PageNav/index.svelte';
import PageBody from '$lib/components/PageBody/index.svelte';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import { setCourse, course } from '$lib/components/Course/store';
import IssueCertificateModal from '$lib/components/Course/components/Ceritficate/IssueCertificateModal.svelte';
// import DeleteConfirmation from '$lib/components/Course/components/People/DeleteConfirmation.svelte';
import Design from '$lib/components/Course/components/Ceritficate/Design.svelte';
// import Reports from '$lib/components/Course/components/Ceritficate/Reports.svelte';
// import { issueCertificateModal } from '$lib/components/Course/components/Ceritficate/store';
// import { Tabs, Tab } from 'carbon-components-svelte';
// import TabContent from 'carbon-components-svelte/src/Tabs/TabContent.svelte';
import StudentCertificate from '$lib/components/Course/components/Ceritficate/StudentCertificate/Index.svelte';
export let courseId: string = '';
let isStudent = false;
</script>
<IssueCertificateModal />
<CourseContainer bind:isStudent bind:courseId>
<PageNav title="Certificates" disableSticky={true}>
<slot:fragment slot="widget">
<RoleBasedSecurity allowedRoles={[1, 2]}>
<!-- Hide Functionality For V1 -->
<!-- <PrimaryButton
className="mr-2 rounded-md text-sm font-medium"
label="Issue Cert"
onClick={() => ($issueCertificateModal.open = true)}
/> -->
</RoleBasedSecurity>
</slot:fragment>
</PageNav>
<PageBody className="w-full mx-0" padding="px-3">
<RoleBasedSecurity allowedRoles={[1, 2]}>
<Design />
</RoleBasedSecurity>
<RoleBasedSecurity allowedRoles={[3]}>
<StudentCertificate />
</RoleBasedSecurity>
<!-- <Tabs autoWidth class="border-b border-gray-200">
<Tab label="Design" />
<Tab label="Reports" />
<svelte:fragment slot="content">
<TabContent>
<Design />
</TabContent>
Temp disable Reports tab for V2
<TabContent>
<Reports />
</TabContent>
</svelte:fragment>
</Tabs> -->
</PageBody>
</CourseContainer>
@@ -0,0 +1,5 @@
export const load = ({ params = { id: '' } }) => {
return {
courseId: params.id
};
};
@@ -0,0 +1,42 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import CourseLandingPage from '$lib/components/CourseLandingPage/index.svelte';
import Editor from '$lib/components/CourseLandingPage/components/Editor/index.svelte';
import { course } from '$lib/components/Course/store';
import { lessons } from '$lib/components/Course/components/Lesson/store/lessons';
import type { Course, Lesson } from '$lib/utils/types';
export let data;
const { courseId } = data;
let courseData: Course;
function setCourseData(course: Course, lessons: Lesson[]) {
courseData = { ...course, lessons };
}
$: setCourseData($course, $lessons);
$: console.log('courseData changed', courseData);
</script>
<div
class="absolute flex inset-0 z-50 bg-white"
in:fly={{ y: 500, duration: 500 }}
out:fly={{ y: 500, duration: 500 }}
>
<Editor {courseId} bind:course={courseData} />
<div class="rightBar">
<CourseLandingPage bind:courseData editMode={true} />
</div>
</div>
<style>
.rightBar {
flex-grow: 1;
width: 100%;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
}
</style>
@@ -0,0 +1,5 @@
export const load = ({ params = { id: '' } }) => {
return {
courseId: params.id
};
};
@@ -0,0 +1,344 @@
<script>
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { dndzone } from 'svelte-dnd-action';
import UnlockedIcon from 'carbon-icons-svelte/lib/Unlocked.svelte';
import LockedIcon from 'carbon-icons-svelte/lib/Locked.svelte';
import SaveIcon from 'carbon-icons-svelte/lib/Save.svelte';
import TrashCanIcon from 'carbon-icons-svelte/lib/TrashCan.svelte';
import Calendar from 'carbon-icons-svelte/lib/Calendar.svelte';
import Video from 'carbon-icons-svelte/lib/Video.svelte';
import WatsonHealthTextAnnotationToggle from 'carbon-icons-svelte/lib/WatsonHealthTextAnnotationToggle.svelte';
import { OverflowMenu, OverflowMenuItem } from 'carbon-components-svelte';
import PageNav from '$lib/components/PageNav/index.svelte';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import Box from '$lib/components/Box/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import TextField from '$lib/components/Form/TextField.svelte';
import IconButton from '$lib/components/IconButton/index.svelte';
import Select from '$lib/components/Form/Select.svelte';
import { updateLesson } from '$lib/utils/services/courses';
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import { profile } from '$lib/utils/store/user';
import {
lessons,
handleSaveLesson,
handleDelete
} from '$lib/components/Course/components/Lesson/store/lessons';
import { course, group } from '$lib/components/Course/store';
import PageBody from '$lib/components/PageBody/index.svelte';
import TextChip from '$lib/components/Chip/Text.svelte';
import Avatar from '$lib/components/Apps/components/Poll/components/Avatar.svelte';
import AddLessonModal from '$lib/components/Course/components/Lesson/AddLessonModal.svelte';
import DateField from '$lib/components/Form/Date.svelte';
export let data;
let lessonEditing;
let isStudent = true;
let openModal = false;
const flipDurationMs = 300;
function formatDate(date) {
const d = new Date(date);
const ye = new Intl.DateTimeFormat('en', { year: 'numeric' }).format(d);
const mo = new Intl.DateTimeFormat('en', { month: '2-digit' }).format(d);
const da = new Intl.DateTimeFormat('en', { day: '2-digit' }).format(d);
return `${ye}-${mo}-${da}`;
}
function addLesson() {
openModal = true;
}
function handleDndConsider(e) {
$lessons = e.detail.items;
}
function handleDndFinalize(e) {
const prevLessonsByOrder = $lessons.reduce((acc, l) => ({ ...acc, [l.id]: l.order }), {});
$lessons = e.detail.items;
// Only update the lesson order that changed
e.detail.items.forEach((item, index) => {
const order = index + 1;
if (order !== prevLessonsByOrder[item.id]) {
$lessons[index].order = order;
updateLesson({ order }, item.id).then((update) =>
console.log(`updated lesson order`, update)
);
}
});
}
function getLessonOrder(id) {
const index = $lessons.findIndex((lesson) => lesson.id === id);
if (index < 9) {
return '0' + (index + 1);
} else {
return index + 1;
}
}
$: {
const user = $group.people.find((person) => person.profile_id === $profile.id);
if (user) {
isStudent = user.role_id === 3;
}
}
</script>
<AddLessonModal {isStudent} bind:openModal />
<CourseContainer bind:isStudent bind:courseId={data.courseId}>
<PageNav title="Lessons">
<div slot="widget">
<RoleBasedSecurity allowedRoles={[1, 2]}>
<PrimaryButton label="Add" onClick={addLesson} isDisabled={!!lessonEditing} />
</RoleBasedSecurity>
</div>
</PageNav>
<PageBody width="max-w-6xl" padding="p-0">
{#if $lessons.length}
<section
class="w-full lg:w-11/12 p-3 lg:px-4 m-auto"
use:dndzone={{
items: $lessons,
flipDurationMs,
dragDisabled: isStudent,
dropTargetStyle: {
border: '2px #1d4ed8 solid',
'border-style': 'dashed'
}
}}
on:consider={handleDndConsider}
on:finalize={handleDndFinalize}
>
{#each $lessons as lesson (lesson.id)}
<div
class="sm:min-h-[245px] md:min-h-[100px] lg:min-h-[190px] relative m-auto rounded-md border-2 border-gray-200 py-3 px-5 mb-4 flex items-center dark:bg-neutral-800"
>
<div class="mr-5">
<TextChip
value={getLessonOrder(lesson.id)}
size="sm"
shape="rounded-full"
className="bg-primary-200 text-primary-600 text-xs"
/>
</div>
<div class="w-4/5">
{#if lessonEditing === lesson.id}
<TextField bind:value={lesson.title} autofocus={true} className="max-w-lg" />
{:else}
<h3 class="dark:text-white text-xl m-0 flex items-center">
<a
href={isStudent && !lesson.is_unlocked
? $page.url.pathname
: '/courses/' + $course.id + '/lessons/' + lesson.id}
class="dark:text-white no-underline hover:underline font-semibold text-black {isStudent &&
!lesson.is_unlocked
? 'cursor-not-allowed'
: ''}"
>
{lesson.title}
</a>
</h3>
{/if}
<!-- <div class="flex items-center mb-2">
<div class="flex items-center mr-2">
{resources.map((r) => `${r.value} ${r.label}`).join(', ')}
</div>
</div> -->
<div
class="flex items-start justify-between flex-col lg:flex-row lg:items-center mt-2 w-4/5"
>
<div class="lg:mb-0">
{#if lessonEditing === lesson.id}
<Select
bind:value={lesson.profile}
options={$group.tutors}
labelKey="fullname"
className="sm:my-1 w-[100%]"
/>
{:else if !lesson.profile}
<p class="dark:text-white ml-2 text-sm mb-3">No tutor added</p>
{:else}
<a href="." class="flex items-center hover:underline mb-2">
<Avatar
className="w-6 h-6"
src={lesson.profile.avatar_url}
name="Avatar placeholder"
/>
<p class="dark:text-white ml-2 text-sm">
{lesson.profile.fullname}
</p>
</a>
{/if}
</div>
<div class="flex items-center lg:mb-0">
{#if lessonEditing === lesson.id}
<DateField
value={formatDate(lesson.lesson_at)}
className="p-2 my-2 rounded-md sm:w-[179px] dark:bg-neutral-800 dark:text-white"
onChange={(e) => (lesson.lesson_at = e.target.value)}
/>
{:else}
<div class="flex mb-2">
<Calendar size={20} class="carbon-icon text-gray-400 dark:text-white" />
<p class="dark:text-white text-sm ml-2">
{new Date(lesson.lesson_at).toDateString()}
</p>
</div>
{/if}
</div>
<div class="flex items-center mb-3 lg:mb-0">
{#if lessonEditing === lesson.id}
<TextField
bind:value={lesson.call_url}
autofocus={true}
className="w-[179px]"
placeholder="https://meet.google.com/mga-dsjs-fmb"
/>
{:else}
<div class="flex">
<Video size={20} class="carbon-icon text-gray-400 dark:text-white ml-0.5" />
<a
class="text-sm ml-2 text-primary-600"
href={lesson.call_url || '#'}
target="_blank"
>
{lesson.call_url ? 'Join lesson' : 'No link'}
</a>
</div>
{/if}
</div>
</div>
<div class="flex flex-row absolute sm:bottom-20 bottom-10 right-10">
<div class="success hidden md:block">
<IconButton
disabled={isStudent}
onClick={() => {
lesson.is_unlocked = !lesson.is_unlocked;
handleSaveLesson(lesson, $course.id);
}}
toolTipProps={isStudent
? {}
: {
title: `Click to ${lesson.is_unlocked ? 'lock' : 'unlock'}`,
direction: 'right'
}}
>
{#if lesson.is_unlocked}
<UnlockedIcon size={24} class="carbon-icon dark:text-white" />
{:else}
<LockedIcon size={24} class="carbon-icon dark:text-white" />
{/if}
</IconButton>
</div>
<RoleBasedSecurity allowedRoles={[1, 2]}>
<div class="hidden md:block">
{#if lessonEditing === lesson.id}
<IconButton
onClick={() => {
lessonEditing = null;
handleSaveLesson(lesson, $course.id);
}}
>
<SaveIcon size={24} class="carbon-icon dark:text-white" />
</IconButton>
{:else}
<IconButton onClick={() => (lessonEditing = lesson.id)}>
<WatsonHealthTextAnnotationToggle
size={32}
class="carbon-icon dark:text-white"
/>
</IconButton>
{/if}
</div>
</RoleBasedSecurity>
<RoleBasedSecurity allowedRoles={[1, 2]}>
<div class="hidden md:block">
<IconButton onClick={handleDelete(lesson.id)}>
<TrashCanIcon size={24} class="carbon-icon dark:text-white" />
</IconButton>
</div>
</RoleBasedSecurity>
<RoleBasedSecurity allowedRoles={[1, 2]}>
<div class="overflowmenu">
<OverflowMenu size="xl" class="block absolute right-10 bottom-4 pl-4">
<OverflowMenuItem
disabled={isStudent}
text={lesson.is_unlocked ? 'Lock' : 'Unlock'}
on:click={() => {
lesson.is_unlocked = !lesson.is_unlocked;
handleSaveLesson(lesson, $course.id);
}}
/>
<OverflowMenuItem
text={lessonEditing === lesson.id ? 'Save' : 'Edit'}
on:click={() => {
if (lessonEditing === lesson.id) {
lessonEditing = null;
handleSaveLesson(lesson, $course.id);
} else {
lessonEditing = lesson.id;
}
}}
/>
<OverflowMenuItem danger text="Delete" on:click={handleDelete(lesson.id)} />
</OverflowMenu>
</div>
</RoleBasedSecurity>
</div>
</div>
</div>
{:else}
<Box>
<div class="flex justify-between flex-col items-center w-[90%] md:w-96">
<img src="/images/empty-lesson-icon.svg" alt="Lesson" class="my-2.5 mx-auto" />
<h2 class="text-xl my-1.5 font-normal">No lessons yet</h2>
<p class="text-sm text-center text-slate-500">
Share your knowledge with the world by creating engaging lessons. Start by clicking
on the Add button.
</p>
</div>
</Box>
{/each}
</section>
{:else}
<Box className="w-full lg:w-11/12 lg:px-4 m-auto">
<div class="flex justify-between flex-col items-center">
<img src="/images/empty-lesson-icon.svg" alt="Lesson" class="my-2.5 mx-auto" />
<h2 class="text-xl my-1.5 font-normal">No lessons yet</h2>
<p class="text-sm text-center text-slate-500">
Share your knowledge with the world by creating engaging lessons. Start by clicking on
the Add button.
</p>
</div>
</Box>
{/if}
</PageBody>
</CourseContainer>
<style>
@media (min-width: 760px) {
.overflowmenu {
display: none !important;
}
}
</style>
@@ -0,0 +1,14 @@
export const load = ({ params = { id: '' } }) => {
const { id: courseId, lessonParams = '' } = params;
const splitparams = lessonParams.split('/');
const [lessonId, exerciseRouteName, exerciseId] = splitparams;
return {
courseId,
lessonId,
exerciseRouteName,
exerciseId,
isMaterialsTabActive: !exerciseRouteName
};
};
@@ -0,0 +1,369 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import { PUBLIC_SERVER_URL } from '$env/static/public';
import { fetchLesson, updateLessonCompletion } from '$lib/utils/services/courses';
import CheckmarkOutlineIcon from 'carbon-icons-svelte/lib/CheckmarkOutline.svelte';
import CheckmarkFilledIcon from 'carbon-icons-svelte/lib/CheckmarkFilled.svelte';
import ListChecked from 'carbon-icons-svelte/lib/ListChecked.svelte';
import SendAlt from 'carbon-icons-svelte/lib/SendAlt.svelte';
import CourseIcon from '$lib/components/Icons/CourseIcon.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import { Loading } from 'carbon-components-svelte';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import PageNav from '$lib/components/PageNav/index.svelte';
import PageBody from '$lib/components/PageBody/index.svelte';
import Materials from '$lib/components/Course/components/Lesson/Materials/index.svelte';
import Exercises from '$lib/components/Course/components/Lesson/Exercises/index.svelte';
import MODES from '$lib/utils/constants/mode';
import { course } from '$lib/components/Course/store';
import Download from 'carbon-icons-svelte/lib/Download.svelte';
import OverflowMenuVertical from 'carbon-icons-svelte/lib/OverflowMenuVertical.svelte';
import { apps } from '$lib/components/Apps/store';
import APPS_CONSTANTS from '$lib/components/Apps/constants';
import IconButton from '$lib/components/IconButton/index.svelte';
import { lesson, lessons } from '$lib/components/Course/components/Lesson/store/lessons';
import { browser } from '$app/environment';
import { currentOrg } from '$lib/utils/store/org';
import { snackbar } from '$lib/components/Snackbar/store';
import type { LessonCompletion } from '$lib/utils/types';
import { profile } from '$lib/utils/store/user';
import { getIsLessonComplete } from '$lib/components/Course/components/Lesson/functions';
export let data;
let path = '';
let mode = MODES.view;
let prevMode = '';
let prevLessonId = '';
let isFetching = false;
let isMarkingComplete = false;
let isLoading = false;
let isSaving = false;
let isStudent = true;
let isLessonComplete = false;
function getLessonOrder(id: string) {
const index = $lessons.findIndex((lesson) => lesson.id === id);
if (index < 10) {
return '0' + (index + 1);
} else {
return index + 1;
}
}
async function fetchReqData(lessonId = '', isMaterialsTabActive: boolean) {
isFetching = true;
let lessonData;
if (isMaterialsTabActive) {
const lesson = await fetchLesson(lessonId);
lessonData = lesson.data;
}
console.log('lessonData', lessonData);
prevLessonId = lessonId;
const totalExercises = lessonData?.totalExercises?.[0] && lessonData.totalExercises[0].count;
const totalComments = lessonData?.totalComments?.[0] && lessonData.totalComments[0].count;
setLesson(lessonData, totalExercises || 0, totalComments || 0);
isFetching = false;
}
async function markLessonComplete(lessonId: string) {
let found = false;
isMarkingComplete = true;
let completion: LessonCompletion = {
is_complete: true,
profile_id: $profile.id || '',
lesson_id: lessonId,
created_at: new Date().toDateString(),
updated_at: new Date().toDateString()
};
const completions: LessonCompletion[] = $lesson.lesson_completion.map((_completion) => {
if (_completion.profile_id === $profile.id) {
found = true;
completion = {
...completion,
is_complete: _completion.is_complete
};
}
return _completion;
});
if (!found) {
completions.push(completion);
}
$lesson.lesson_completion = completions;
$lessons = $lessons.map((l) => {
if (l.id === $lesson.id) {
l.lesson_completion = completions;
}
return l;
});
await updateLessonCompletion(completion);
snackbar.success('Marked as complete');
isMarkingComplete = false;
}
const downloadLesson = async () => {
const currentLesson = $lessons.find((l) => l.id === $lesson?.id);
if (!currentLesson) {
return;
}
isLoading = true;
try {
const lessonVideo = $lesson.materials.videos.map((video) => video.link);
const lessonNumber = getLessonOrder(currentLesson.id);
const slideUrl = $lesson.materials.slide_url || '';
const response = await fetch(PUBLIC_SERVER_URL + '/downloadLesson', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: currentLesson.title,
number: lessonNumber,
orgName: $currentOrg.name,
note: $lesson.materials.note,
slideUrl: slideUrl,
video: lessonVideo,
courseTitle: $course.title
})
});
if (!response.ok) {
throw new Error(await response.text());
}
const data = await response.blob();
console.log(data);
const file = new Blob([data], { type: 'application/pdf' });
const fileURL = URL.createObjectURL(file);
let a = document.createElement('a');
document.body.append(a);
a.download = $course.title + ' - ' + 'Lesson ' + lessonNumber;
a.href = fileURL;
a.click();
a.remove();
snackbar.success('Download Complete');
} catch (error) {
console.log('error downloading lesson', error);
snackbar.error("Something's not right - Please try later");
}
isLoading = false;
};
function setLesson(lessonData, totalExercises: number, totalComments: number) {
if (!lessonData) return;
let lesson_completion: LessonCompletion[] = [];
if (Array.isArray(lessonData.lesson_completion)) {
lesson_completion = [...lessonData.lesson_completion];
}
lesson.update((l) => ({
...l,
id: data.lessonId,
totalExercises,
totalComments: totalComments,
materials: {
videos: lessonData.videos,
note: lessonData.note,
slide_url: lessonData.slide_url
},
lesson_completion,
exercises: []
}));
}
const toggleApps = () => {
$apps.open = !$apps.open;
};
function toggleMode() {
prevMode = mode;
mode = mode === MODES.edit ? MODES.view : MODES.edit;
}
function handleAppClick(appName: string) {
$apps.selectedApp = appName;
$apps.open = true;
}
$: path = $page.url?.pathname?.replace(/\/exercises[\/ 0-9 a-z -]*/, '');
$: if (data.courseId && browser) {
mode = MODES.view;
fetchReqData(data.lessonId, data.isMaterialsTabActive);
}
$: isLessonComplete = getIsLessonComplete($lesson.lesson_completion, $profile.id);
</script>
<CourseContainer
bind:isStudent
{path}
isExercisePage={!data.isMaterialsTabActive && data.exerciseId}
bind:courseId={data.courseId}
>
<PageNav
bind:hideOnMobile={isStudent}
navItems={[
{
label: 'Materials',
isActive: data.isMaterialsTabActive,
href: path
},
{
label: 'Exercises',
badgeValue: data.isMaterialsTabActive ? $lesson.totalExercises : $lesson.exercises.length,
isActive: !data.isMaterialsTabActive,
href: `${path}/exercises`
}
]}
>
<svelte:fragment slot="widget">
<RoleBasedSecurity allowedRoles={[1, 2]}>
{#if data.isMaterialsTabActive}
<div class="tab">
<IconButton onClick={toggleApps} buttonClassName="">
<OverflowMenuVertical size={24} />
</IconButton>
</div>
<div
class={`flex-row ${
$apps.dropdown && $apps.open
? 'absolute lg:relative top-[85px] lg:top-0 right-14 lg:right-0 z-40 dark:bg-neutral-800 p-3 lg:p-0'
: 'hidden'
} lg:flex items-center`}
>
{#if $course.metadata.lessonDownload && !!PUBLIC_SERVER_URL}
<PrimaryButton
className="mb-2 lg:mb-0 mr-2"
variant={VARIANTS.OUTLINED}
onClick={downloadLesson}
{isLoading}
>
<Download size={16} class="mr-2" />
Download PDF
</PrimaryButton>
{/if}
<PrimaryButton className="mr-2" variant={VARIANTS.OUTLINED} onClick={toggleMode}>
{#if isSaving}
<Loading withOverlay={false} small />
<span class="text-sm ml-2 italic">Autosaving...</span>
{:else}
{mode === MODES.edit ? 'Done' : 'Edit'}
{/if}
</PrimaryButton>
</div>
{/if}
</RoleBasedSecurity>
</svelte:fragment>
</PageNav>
{#if !data.isMaterialsTabActive}
<Exercises
bind:isStudent
lessonId={data.lessonId}
exerciseId={data.exerciseId}
path={`${path}/exercises`}
/>
{:else if !!data.lessonId}
<PageBody
bind:isPageNavHidden={isStudent}
onClick={() => {
$apps.open = false;
$apps.dropdown = false;
}}
width="lg:w-full xl:w-11/12"
className="overflow-x-hidden"
>
<Materials lessonId={data.lessonId} {mode} {prevMode} {toggleMode} bind:isSaving />
<div class="w-full hidden lg:flex flex-row-reverse mt-10">
<PrimaryButton
onClick={() => markLessonComplete(data.lessonId)}
isLoading={isMarkingComplete}
isDisabled={isMarkingComplete}
variant={VARIANTS.OUTLINED}
className="mt-10"
>
{#if isLessonComplete}
<CheckmarkFilledIcon size={24} class="carbon-icon text-primary-600 mr-2" />
{:else}
<CheckmarkOutlineIcon size={24} class="carbon-icon mr-2" />
{/if}
Mark as {isLessonComplete ? 'Incomplete' : 'Complete'}
</PrimaryButton>
</div>
</PageBody>
{/if}
<!-- Mobile Navigation -->
<div class="fixed bottom-5 w-[90%] flex items-center justify-center lg:hidden">
<div
class="flex items-center gap-2 w-fit rounded-full shadow-xl bg-gray-100 dark:bg-neutral-700 px-5 py-1"
>
{#if data.isMaterialsTabActive}
<button
class="px-2 my-2 pr-4 border-t-0 border-b-0 border-l-0 border border-gray-300 flex items-center"
on:click={() => goto(`${path}/exercises`)}
>
<ListChecked size={24} class="carbon-icon" />
<span class="ml-1">{$lesson.totalExercises}</span>
</button>
{:else}
<button
class="px-2 my-2 pr-4 border-t-0 border-b-0 border-l-0 border border-gray-300 flex items-center"
on:click={() => goto(path)}
>
<CourseIcon />
</button>
{/if}
<button
class="px-2 my-2 pr-4 border-t-0 border-b-0 border-l-0 border border-gray-300 flex items-center"
on:click={() => handleAppClick(APPS_CONSTANTS.APPS.COMMENTS)}
>
<SendAlt size={24} class="carbon-icon" />
<span class="ml-1">{$lesson.totalComments}</span>
</button>
<button
class="px-2 my-2"
on:click={() => markLessonComplete(data.lessonId)}
disabled={isMarkingComplete}
>
{#if isLessonComplete}
<CheckmarkFilledIcon size={24} class="carbon-icon text-primary-600" />
{:else}
<CheckmarkOutlineIcon size={24} class="carbon-icon" />
{/if}
</button>
</div>
</div>
</CourseContainer>
<style>
@media screen and (min-width: 1023px) {
.tab {
display: none;
}
}
</style>
@@ -0,0 +1,5 @@
export const load = ({ params = { id: '' } }) => {
return {
courseId: params.id
};
};
@@ -0,0 +1,189 @@
<script lang="ts">
import AudioConsoleIcon from 'carbon-icons-svelte/lib/AudioConsole.svelte';
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import PageNav from '$lib/components/PageNav/index.svelte';
import PageBody from '$lib/components/PageBody/index.svelte';
import Box from '$lib/components/Box/index.svelte';
import { ROLE } from '$lib/utils/constants/roles';
import { group } from '$lib/components/Course/store';
import { lessons } from '$lib/components/Course/components/Lesson/store/lessons';
import { fetchExercisesByMarks } from '$lib/utils/services/courses';
import { getLectureNo } from '$lib/components/Course/function';
import { fetchMarks } from '$lib/utils/services/marks';
import { profile } from '$lib/utils/store/user';
import { browser } from '$app/environment';
import { course } from '$lib/components/Course/store';
export let data;
const { courseId } = data;
let borderBottomGrey = 'border-r-0 border-t-0 border-b border-l-0 border-gray-300';
let borderleftGrey = 'border-r-0 border-t-0 border-b-0 border-l border-gray-300';
let students = [];
let isStudent = false;
let lessonMapping = {}; // { lessonId: { exerciseId: exerciseTitle, ... }, ... }
let studentMarksByExerciseId = {}; // { groupMemberId: { exerciseId: `total_gotten/points`, ... }, ... }
function calculateStudentTotal(studentExerciseData) {
if (!studentExerciseData) return 0;
return Object.values(studentExerciseData).reduce(
(total, point) => (total += parseInt(point)),
0
);
}
async function firstRender(courseId: string) {
const { data: marks } = await fetchMarks(courseId);
marks.forEach((mark) => {
const { groupmember_id, exercise_id, total_points_gotten } = mark;
if (studentMarksByExerciseId[groupmember_id]) {
studentMarksByExerciseId[groupmember_id] = {
...studentMarksByExerciseId[groupmember_id],
[exercise_id]: total_points_gotten
};
} else {
studentMarksByExerciseId[groupmember_id] = {
[exercise_id]: total_points_gotten
};
}
});
const { data: exercises } = await fetchExercisesByMarks(courseId);
// Map exercises to { lessonId: { exerciseId: exerciseTitle, ... }, ... }
exercises.forEach((exercise) => {
const { lesson_id, exercise_id, exercise_title, points } = exercise;
if (lessonMapping[lesson_id]) {
lessonMapping[lesson_id] = {
...lessonMapping[lesson_id],
[exercise_id]: { title: exercise_title, points }
};
} else {
lessonMapping[lesson_id] = {
[exercise_id]: { title: exercise_title, points }
};
}
});
}
$: students = isStudent
? $group.people.filter((person) => !!person.profile && person.profile.id === $profile.id)
: $group.people.filter((person) => !!person.profile && person.role_id === ROLE.STUDENT);
$: browser && $course.id && firstRender($course.id);
</script>
<CourseContainer bind:isStudent bind:courseId={data.courseId}>
<PageNav title="Marks" />
<PageBody width="w-full max-w-6xl md:w-11/12">
<div class="table rounded-md border border-gray-300 w-full">
<div class="flex items-center {borderBottomGrey}">
<div class="box flex items-center p-3">
<p class="dark:text-white w-40">Student</p>
</div>
{#each $lessons as lesson, index}
{#if lessonMapping[lesson.id]}
<div class="box flex flex-col items-center {borderleftGrey}">
<p class="dark:text-white col lesson-number" title={lesson.title}>
{getLectureNo(index + 1)}
</p>
<div
class="flex h-full items-center border-r-0 border-t border-b-0 border-l-0 border-gray-300"
>
{#each Object.keys(lessonMapping[lesson.id]) as exerciseId, index}
<p
class="col text-sm dark:text-white {index && borderleftGrey}"
title={lessonMapping[lesson.id][exerciseId].title}
>
{lessonMapping[lesson.id][exerciseId].title}
<span>({lessonMapping[lesson.id][exerciseId].points})</span>
</p>
{/each}
</div>
</div>
{/if}
{/each}
<div class="box flex items-center {borderleftGrey}">
<p class="dark:text-white w-20 text-center">Total</p>
</div>
</div>
{#each students as student}
<div class="flex relative items-center p-3 cursor-pointer {borderBottomGrey}">
<div class="w-40 flex items-center">
<img
alt="Student avatar"
src={student.profile.avatar_url}
class="w-8 h-8 rounded-full mr-2"
/>
<div class="text-sm">
<p class="dark:text-white font-semibold">
{student.profile.fullname}
</p>
<p class="dark:text-white">
{`${student.assigned_student_id ? '#' + student.assigned_student_id : '-'}`}
</p>
</div>
</div>
{#each $lessons as lesson}
{#if lessonMapping[lesson.id]}
<div class="flex items-center">
{#each Object.keys(lessonMapping[lesson.id]) as exerciseId}
<p class="dark:text-white col">
{studentMarksByExerciseId[student.id]
? studentMarksByExerciseId[student.id][exerciseId] || '-'
: '-'}
</p>
{/each}
</div>
{/if}
{/each}
<div class="w-20 flex items-center">
<div class="text-sm">
<p class="dark:text-white col">
{calculateStudentTotal(studentMarksByExerciseId[student.id])}
</p>
</div>
</div>
</div>
{:else}
<Box>
<AudioConsoleIcon size={32} class="carbon-icon w-80" />
<h3 class="text-3xl text-gray-500 dark:text-white">No Student Added</h3>
</Box>
{/each}
</div>
</PageBody>
</CourseContainer>
<style>
.col {
text-align: center;
padding: 5px;
width: 100px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
.br {
border-right: 1px solid rgba(209, 213, 219, var(--tw-border-opacity));
}
.box {
height: 95px;
}
.lesson-number {
height: 30px;
}
</style>
@@ -0,0 +1,5 @@
export const load = ({ params = { id: '' } }) => {
return {
courseId: params.id
};
};
@@ -0,0 +1,272 @@
<script lang="ts">
import TextChip from '$lib/components/Chip/Text.svelte';
import ComingSoon from '$lib/components/ComingSoon/index.svelte';
import copy from 'copy-to-clipboard';
import {
CopyButton,
Search,
StructuredList,
StructuredListBody,
StructuredListCell,
StructuredListHead,
StructuredListRow
} from 'carbon-components-svelte';
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import { deleteGroupMember, updatedGroupMember } from '$lib/utils/services/courses';
import TrashCanIcon from 'carbon-icons-svelte/lib/TrashCan.svelte';
import PageNav from '$lib/components/PageNav/index.svelte';
import PageBody from '$lib/components/PageBody/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import Select from '$lib/components/Form/Select.svelte';
import IconButton from '$lib/components/IconButton/index.svelte';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import { group } from '$lib/components/Course/store';
import InviationModal from '$lib/components/Course/components/People/InviationModal.svelte';
import DeleteConfirmation from '$lib/components/Course/components/People/DeleteConfirmation.svelte';
import {
invitationModal,
deleteMemberModal
} from '$lib/components/Course/components/People/store';
import type { Person, ProfileRole } from '$lib/components/Course/components/People/types';
import { ROLE_LABEL, ROLES } from '$lib/utils/constants/roles';
import { profile } from '$lib/utils/store/user';
import { snackbar } from '$lib/components/Snackbar/store';
import Avatar from '$lib/components/Avatar/index.svelte';
import type { GroupPerson } from '$lib/utils/types';
export let data;
let people: Array<Person> = [];
let member: { id?: string; email?: string; profile?: { email: string } } = {};
let shouldEditMemberId: string | null = null;
let filterBy: ProfileRole = ROLES[0];
let isStudent = false;
let searchValue = '';
// function setPeople(people: Array<Person>) {
// if (!Array.isArray(people)) return;
// people = (people || []).sort((a, b) => a.role_id - b.role_id);
// console.log(`people changed`, people);
// }
// function for the searchbar
function filterPeople(_query, people) {
const query = _query.toLowerCase();
return people.filter((person) => {
const { profile, email } = person;
return profile?.fullname?.toLowerCase()?.includes(query) || email?.includes(query);
});
}
async function deletePerson() {
$group.people = $group.people.filter((person: { id: string }) => person.id !== member.id);
$group.tutors = $group.tutors.filter((person: GroupPerson) => person.memberId !== member.id);
await deleteGroupMember(member.id);
}
async function editMemberId(person: Person) {
await updatedGroupMember(
{ assigned_student_id: person.assigned_student_id },
{ id: person.id }
);
shouldEditMemberId = null;
}
function sortAndFilterPeople(_people: Array<Person>, filterBy: ProfileRole) {
people = (_people || [])
.filter((person) => {
if (filterBy.value === 'all') return true;
return person.role_id === filterBy.value;
})
.sort((a: Person, b: Person) => new Date(a.created_at) - new Date(b.created_at))
.sort((a: Person, b: Person) => a.role_id - b.role_id);
}
function getEmail(person) {
const { profile, email } = person;
return profile ? profile.email : email;
}
function handleEditStudentIdMode(personId: string) {
shouldEditMemberId = personId;
}
function copyToClipboard(studentAssignedId: string | null) {
snackbar.success(`Copied to clipboard`);
if (studentAssignedId) {
copy(studentAssignedId);
}
}
$: sortAndFilterPeople($group.people, filterBy);
</script>
<InviationModal />
<DeleteConfirmation
email={member.email || (member.profile && member.profile.email)}
{deletePerson}
/>
<CourseContainer bind:isStudent bind:courseId={data.courseId}>
<PageNav title="People" disableSticky={true}>
<slot:fragment slot="widget">
<RoleBasedSecurity allowedRoles={[1, 2]}>
<PrimaryButton
className="mr-2"
label="Add"
onClick={() => ($invitationModal.open = true)}
/>
</RoleBasedSecurity>
</slot:fragment>
</PageNav>
<PageBody width="w-full max-w-6xl md:w-11/12">
<section class="my-5 mx-2 md:mx-9">
<div
class="flex flex-col md:flex-row flex-end gap-2 justify-end items-start md:items-center mb-7"
>
<div class="max-w-[320px]">
<Search
class="dark:text-slate-950 border-0 bg-zinc-100 w-full"
placeholder="Search people"
bind:value={searchValue}
/>
</div>
<div class="mb-3">
<Select
bind:value={filterBy}
options={ROLES}
className="dark:text-black mt-3 max-w-[80px]"
/>
<!-- <select bind:value={filterBy} class="mt-3">
{#each ROLES as option}
<option value={option.value}>{option.label}</option>
{/each}
</select> -->
</div>
<RoleBasedSecurity allowedRoles={[1, 2]}>
<p class="dark:text-white hidden lg:block text-lg w-20" />
</RoleBasedSecurity>
</div>
<StructuredList class="m-0">
<StructuredListHead
class="bg-slate-100 dark:bg-neutral-800 dark:border-2 dark:border-neutral-800"
>
<StructuredListRow head class="mx-7">
<StructuredListCell head class="text-primary-700 py-3 dark:text-white"
>Name</StructuredListCell
>
<StructuredListCell head class="text-primary-700 py-3 dark:text-white"
>Role</StructuredListCell
>
<StructuredListCell head class="text-primary-700 py-3 dark:text-white"
>Action</StructuredListCell
>
<RoleBasedSecurity allowedRoles={[1, 2]}>
<p class="dark:text-white hidden lg:block text-lg w-20" />
</RoleBasedSecurity>
</StructuredListRow>
</StructuredListHead>
{#each filterPeople(searchValue, people) as person}
<StructuredListBody>
<StructuredListRow class="relative">
<!-- first column -->
<StructuredListCell class="w-4/6 md:w-3/6">
{#if person.profile}
<div class="flex items-start lg:items-center">
<Avatar
src={person.profile.avatar_url}
name={person.profile.fullname}
width="w-8"
height="h-8"
className="mr-3"
/>
<div class="flex flex-col lg:flex-row items-start lg:items-center">
<div class="mr-2">
<p class="dark:text-white text-base font-normal">
{person.profile.fullname}
</p>
<p class="text-xs text-primary-600 line-clamp-1">
{getEmail(person)}
</p>
</div>
<div class="flex items-center">
<RoleBasedSecurity allowedRoles={[1, 2]}>
<CopyButton
text={getEmail(person)}
feedback="Copied Email to clipboard"
/>
</RoleBasedSecurity>
{#if person.profile_id == $profile.id}
<ComingSoon label="You" />
{/if}
</div>
</div>
</div>
{:else}
<div class="flex items-start lg:items-center w-2/4">
<TextChip
value={person.email.substring(0, 2).toUpperCase()}
className="bg-primary-200 text-black font-semibold text-xs mr-3"
shape="rounded-full"
/>
<a
href="mailto:{person.email}"
class="text-md text-primary-600 mr-2 dark:text-white"
>
{person.email}
</a>
<div class="flex items-center justify-between">
<RoleBasedSecurity allowedRoles={[1, 2]}>
<CopyButton text={getEmail(person)} feedback="Copied Email to clipboard" />
</RoleBasedSecurity>
<TextChip
value="Pending"
className="text-xs bg-yellow-200 text-yellow-700 h-fit"
size="sm"
/>
</div>
</div>
{/if}
</StructuredListCell>
<!-- second column -->
<StructuredListCell class="w-1/4 px-3">
<p class=" dark:text-white font-normal text-center text-base w-1/4">
{ROLE_LABEL[person.role_id]}
</p>
</StructuredListCell>
<!-- third column -->
<StructuredListCell class="p-0 w-1/4">
<RoleBasedSecurity allowedRoles={[1, 2]}>
<div class="hidden lg:flex lg:justify-between lg:items-center w-20">
{#if person.profile_id !== $profile.id}
<IconButton
onClick={() => {
member = person;
$deleteMemberModal.open = true;
}}
>
<TrashCanIcon size={16} class="carbon-icon dark:text-white" />
</IconButton>
{/if}
</div>
</RoleBasedSecurity>
</StructuredListCell>
</StructuredListRow>
</StructuredListBody>
{/each}
</StructuredList>
<!-- <Pagination totalItems={10} pageSizes={[10, 15, 20]} /> -->
</section>
</PageBody>
</CourseContainer>
@@ -0,0 +1,5 @@
export const load = ({ params = { id: '' } }) => {
return {
courseId: params.id
};
};
@@ -0,0 +1,16 @@
<script>
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import PageNav from '$lib/components/PageNav/index.svelte';
import PageBody from '$lib/components/PageBody/index.svelte';
import Settings from '$lib/components/Course/components/Settings/index.svelte';
export let data;
let isStudent = false;
</script>
<CourseContainer bind:isStudent bind:courseId={data.courseId}>
<PageNav title="Settings" />
<PageBody>
<Settings />
</PageBody>
</CourseContainer>
@@ -0,0 +1,5 @@
export const load = ({ params = { id: '' } }) => {
return {
courseId: params.id
};
};
@@ -0,0 +1,433 @@
<script lang="ts">
import { flip } from 'svelte/animate';
import { dndzone } from 'svelte-dnd-action';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import PageNav from '$lib/components/PageNav/index.svelte';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import MarkExerciseModal from '$lib/components/Course/components/Lesson/Exercise/MarkExerciseModal.svelte';
import Chip from '$lib/components/Chip/index.svelte';
import PageBody from '$lib/components/PageBody/index.svelte';
import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import { course } from '$lib/components/Course/store';
import {
fetchSubmissions,
updateSubmission,
updateQuestionAnswer
} from '$lib/utils/services/submissions/index';
import { formatAnswers } from '$lib/components/Course/function';
import { snackbar } from '$lib/components/Snackbar/store';
import isSubmissionEarly from '$lib/utils/functions/isSubmissionEarly';
import formatDate from '$lib/utils/functions/formatDate';
import {
triggerSendEmail,
NOTIFICATION_NAME
} from '$lib/utils/services/notification/notification';
import { currentOrg, currentOrgDomain } from '$lib/utils/store/org';
import { browser } from '$app/environment';
type items = {
id: number;
statusId: number;
isEarly: boolean;
submittedAt: string;
exercise: {
id: string;
title: string;
};
answers: string;
student: any;
lesson: {
id: string | any;
title: string | any;
};
};
type sectionType = {
id: number;
title: string;
value: number;
items: items[] | [];
};
export let data;
const { courseId } = data;
const flipDurationMs = 300;
let exerciseDetails: { id: string; title: string };
let lessonDetails: { id: string; title: string };
let totalMark = 0;
let maxMark = 0;
let submissionIdData: { [key: number]: any } = {};
let submissionId: string | number | null;
let openExercise = false;
const submissionStatus: { [key: number]: string } = {
1: 'Submitted',
2: 'In Progress',
3: 'Graded'
};
let sections: sectionType[] = [
{
id: 1,
title: 'Submitted',
value: 0,
items: []
},
{
id: 2,
title: 'In Progress',
value: 0,
items: []
},
{
id: 3,
title: 'Graded',
value: 10,
items: []
}
];
function getMaxPoints(questions) {
return (questions || []).reduce((acc, question) => acc + question.points, 0);
}
function calculateTotal(grades: string[]): number {
if (!grades) return 0;
return Object.values(grades).reduce((acc, grade) => acc + parseInt(grade), 0);
}
const sendEmail = (submissionData) => {
maxMark = getMaxPoints(submissionData?.questions);
totalMark = calculateTotal(submissionData?.questionAnswerByPoint);
const { fullname, email } = submissionData?.student;
const { title, status_id }: { title: string; status_id: number } = submissionData;
const exerciseLink = `${$currentOrgDomain}/courses/${courseId}/lessons/${lessonDetails.id}/exercises/${exerciseDetails.id}`;
const content = `
<p>Hello ${fullname},</p>
<p>The status of your submitted exercise on <strong>${title}</strong> has been updated to ${
submissionStatus[status_id]
}</p>
${
status_id == 3
? `<p>Your score was ${totalMark}/${maxMark}</p>
<a class="button" href="${exerciseLink}">View your Result</a>
`
: `<a class="button" href="${exerciseLink}">Open Exercise</a>`
}
<p>This exercise is for <strong>${
lessonDetails.title
}</strong> in a course you are taking titled <strong>${$course.title}</strong></p>
`;
triggerSendEmail(NOTIFICATION_NAME.SUBMISSION_UPDATE, {
to: email,
content,
orgName: $currentOrg?.name
});
};
function handleItemFinalize(
columnIdx: number,
newItems: { map: (arg0: (item: items) => items) => items[] }
) {
let itemToWithNewStatus: items | undefined;
const { id } = sections[columnIdx];
// Set column in the UI
sections[columnIdx].items = newItems.map((item) => {
if (item.statusId !== id) {
itemToWithNewStatus = item;
item.statusId = id;
}
return item;
});
// Update backend
if (itemToWithNewStatus) {
// Update key mapping for each submission also
submissionIdData[itemToWithNewStatus.id] = {
...submissionIdData[itemToWithNewStatus.id],
status_id: itemToWithNewStatus.statusId
};
sendEmail(submissionIdData[itemToWithNewStatus.id]);
updateSubmission({
id: itemToWithNewStatus.id,
status_id: itemToWithNewStatus.statusId
}).then((res) => console.log('Updated submission', res));
}
}
function handleDndConsiderCards(columnIdx: number) {
return function (e) {
sections[columnIdx].items = e.detail.items;
};
}
function handleDndFinalizeCards(columnIdx: number) {
return (e) => handleItemFinalize(columnIdx, e.detail.items);
}
function handleModalClose() {
goto($page.url.pathname);
}
// Via dialog
function updateStatus({
submissionId,
prevStatusId,
nextStatusId,
total
}: {
submissionId: number;
prevStatusId: number;
nextStatusId: number;
total: number;
}) {
let itemToWithNewStatus: items | undefined;
// Remove from current column
const { items } = sections[prevStatusId - 1];
sections[prevStatusId - 1].items = items?.filter((item) => {
if (item.id === submissionId) {
itemToWithNewStatus = Object.assign(item);
if (itemToWithNewStatus) {
itemToWithNewStatus.statusId = nextStatusId;
}
return false;
}
return true;
});
console.log(`itemToWithNewStatus`, itemToWithNewStatus);
// Move to right column
if (itemToWithNewStatus) {
sections[nextStatusId - 1].items = [...sections[nextStatusId - 1].items, itemToWithNewStatus];
}
// If something changed
if (itemToWithNewStatus) {
// Update key mapping for each submission also
submissionIdData[itemToWithNewStatus.id] = {
...submissionIdData[itemToWithNewStatus.id],
status_id: itemToWithNewStatus.statusId
};
sendEmail(submissionIdData[itemToWithNewStatus.id]);
// Update backend
updateSubmission({
id: itemToWithNewStatus.id,
status_id: itemToWithNewStatus.statusId,
total
}).then((res) => console.log('Updated submission', res));
}
}
async function handleSave(submission: { questionAnswerByPoint: any; questionAnswers: any }) {
const { questionAnswerByPoint, questionAnswers } = submission;
let totalPoints = 0;
const updates = Object.keys(questionAnswerByPoint).map((questionId) => {
const questionAnswer = questionAnswers.find(
(answer: { question_id: string }) => answer.question_id == questionId
);
const point = questionAnswerByPoint[questionId];
totalPoints += parseInt(point, 10);
return updateQuestionAnswer({ point }, { id: questionAnswer?.id });
});
updateSubmission({
id: submissionId,
total: totalPoints
}).then((res) => console.log('Updated submission', res));
snackbar.success('Grading saved and student notified');
await Promise.all(updates);
}
$: {
const query = new URLSearchParams($page.url.search);
submissionId = query.get('submissionId');
openExercise = !!submissionId && submissionIdData[submissionId];
}
async function firstRender(courseId: string) {
const { data: submissions } = await fetchSubmissions(courseId);
const sectionById: { [key: number]: sectionType[] } = {};
if (submissions) {
for (const submission of submissions) {
const { id, created_at, exercise, course, answers, groupmember, status_id } = submission;
const isEarly = isSubmissionEarly(created_at, exercise.due_by);
const submissionItem = {
id,
statusId: status_id,
isEarly,
submittedAt: formatDate(created_at),
exercise: {
id: exercise.id,
title: exercise.title
},
answers,
student: groupmember && groupmember.profile ? groupmember.profile : {},
lesson: {
id: exercise.lesson.id,
title: exercise.lesson.title
}
};
exerciseDetails = { id: submissionItem.exercise.id, title: submissionItem.exercise.title };
lessonDetails = {
id: submissionItem.lesson.id,
title: submissionItem.lesson.title
};
submissionIdData[id] = {
id,
status_id,
isEarly,
title: exercise.title,
student: submissionItem.student,
questions: exercise.questions,
answers: formatAnswers({ questions: exercise.questions, answers }),
questionAnswers: answers,
questionAnswerByPoint: answers.reduce((acc, answer) => {
acc[answer.question_id] = answer.point;
return acc;
}, {})
};
if (Array.isArray(sectionById[status_id])) {
sectionById[status_id].push(submissionItem);
} else {
sectionById[status_id] = [submissionItem];
}
}
}
sections = sections.map((section, index) => ({
...section,
value: Array.isArray(sectionById[index + 1]) ? sectionById[index + 1].length : 0,
items: Array.isArray(sectionById[index + 1]) ? sectionById[index + 1] : []
}));
}
$: browser && $course.id && firstRender($course.id);
</script>
<MarkExerciseModal
bind:open={openExercise}
onClose={handleModalClose}
data={submissionIdData[submissionId] || {}}
{handleSave}
{updateStatus}
{submissionId}
/>
<RoleBasedSecurity allowedRoles={[1, 2]}>
<CourseContainer bind:courseId={data.courseId}>
<PageNav title="Submitted Exercises" />
<PageBody width="w-full max-w-6xl md:w-11/12 overflow-x-auto">
<div class="flex items-center w-full">
{#each sections as { id, title, items }, idx (id)}
<div
class="section rounded-md bg-gray-100 dark:bg-black border border-gray-50 dark:border-neutral-600 p-3 h-80 mr-3 overflow-hidden"
animate:flip={{ duration: flipDurationMs }}
>
<div class="flex items-center mb-2">
<Chip value={items.length} className="bg-set dark:bg-neutral-800" />
<p class="dark:text-white ml-2 font-bold">{title}</p>
</div>
<div
class="content pr-2 overflow-y-auto mb-3"
use:dndzone={{
items,
flipDurationMs,
dropTargetStyle: { outline: 'blue' }
}}
on:consider={handleDndConsiderCards(idx)}
on:finalize={handleDndFinalizeCards(idx)}
>
{#each items as item (item.id)}
<div
class="{item.isEarly
? 'border-none'
: 'border border-red-700'} w-full my-2 mx-0 rounded-md bg-white dark:bg-neutral-800 py-3 px-3"
animate:flip={{ duration: flipDurationMs }}
>
<a
class="flex w-full items-center cursor-pointer text-black mb-2"
href={`${$page.url.pathname}?submissionId=${item.id}`}
>
<img
alt="Student avatar"
class="block rounded-full h-6 w-6"
src={item.student.avatar_url}
/>
<p class="dark:text-white ml-2 text-sm">
{item.student.username}
</p>
</a>
<a
class="text-primary-700 text-md font-bold"
href="{$page.url.pathname}?submissionId={item.id}"
>
{item.exercise.title}
</a>
<a
class="flex items-center no-underline hover:underline text-black my-2"
href="{$page.url?.pathname?.replace('submissions', 'lessons')}/{item.lesson
.id}/exercises/{item.exercise.id}"
>
<p class="dark:text-white text-grey text-sm">
#{item.lesson.title}
{`${item.tutor ? 'created by' + item.tutor.name : ''}`}
</p>
</a>
<p class="dark:text-white text-gray-500 text-xs">
{item.submittedAt}
</p>
<!-- <div class="badge rounded-md px-2 bg-green-500 text-white">
early
</div>
<div class="badge rounded-md px-2 bg-red-600 text-white">
late
</div> -->
</div>
{/each}
</div>
</div>
{/each}
</div>
</PageBody>
</CourseContainer>
</RoleBasedSecurity>
<style>
.section {
max-width: 355px;
min-width: 355px;
height: 75vh;
}
.content {
height: 95%;
}
@media screen and (max-width: 768px) {
.section {
min-width: 250px;
}
}
</style>