CLASSROOMIO-T-80: Replace sapper with sveltekit

This commit is contained in:
Rotimi Best
2023-08-02 19:35:27 +01:00
parent 8ffdf319b3
commit 7dfbb05451
320 changed files with 5207 additions and 18330 deletions
+18
View File
@@ -0,0 +1,18 @@
import { supabase } from '$lib/utils/functions/supabase';
export async function fetchCourses(profileId) {
if (!profileId) return;
const { data: allCourses } = await supabase
.rpc('get_courses')
.eq('profile_id', profileId);
console.log(`allCourses`, allCourses);
if (!Array.isArray(allCourses)) {
return {
allCourses: [],
};
}
return { allCourses };
}
@@ -0,0 +1,28 @@
<script lang="ts">
import { SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte';
</script>
<div
class="root w-72 border bg-gray-100 dark:bg-gray-700 mr-4 mb-4 rounded-md transition ease-in-out relative"
>
<SkeletonPlaceholder style="width: 100%; height: 10rem;" />
<div class="p-3 pb-0">
<!-- Title -->
<SkeletonText style="width: 50%;" />
<!-- Description -->
<SkeletonText style="width: 100%; margin-top: 1rem;" />
<SkeletonText style="width: 100%; margin-bottom: 1rem;" />
<div class="flex justify-between mb-3">
<SkeletonText style="width: 25%" />
<SkeletonText style="width: 25%" />
</div>
<SkeletonText style="width: 25%" />
</div>
</div>
<style>
.root {
min-height: 370px;
}
</style>
@@ -0,0 +1,132 @@
<script lang="ts">
import Dropdown from '$lib/components/Dropdown/index.svelte';
import { copyCourseModal } from '$lib/components/Courses/store';
import { ROLE } from '$lib/utils/constants/roles';
import getCurrencyFormatter from '$lib/utils/functions/getCurrencyFormatter';
import OverflowMenuHorizontalIcon from 'carbon-icons-svelte/lib/OverflowMenuHorizontal.svelte';
import ArrowRight from 'carbon-icons-svelte/lib/ArrowRight.svelte';
export let bannerImage: string | undefined;
export let id = '';
export let slug = '';
export let title = '';
export let description = '';
export let role_id = ROLE.STUDENT;
export let isPublished = false;
export let cost = 0;
export let totalLessons = 0;
export let currency = 'NGN';
export let isOnLandingPage = false;
let formatter = getCurrencyFormatter(currency);
function getOptions() {
return [
{
label: 'Make a copy',
// Show this option only if user has admin control over this course.
show: () => role_id === ROLE.ADMIN || role_id === ROLE.TUTOR,
onClick() {
// IIFI of copyCourseModal
(() => {
$copyCourseModal.id = id;
$copyCourseModal.open = true;
$copyCourseModal.title = `Copy of ${title}`;
})();
},
},
// {
// label: 'Delete',
// onClick() {
// console.log('Deleting');
// },
// },
].filter((option) => option.show());
}
</script>
<a
rel="prefetch"
href={isOnLandingPage ? `/course/${slug}` : `/courses/${id}`}
class=" text-black"
>
<div
class="root w-80 border bg-gray-100 dark:bg-gray-700 sm:mr-4 mb-4 rounded-md hover:shadow-2xl transition ease-in-out relative"
>
{#if !isOnLandingPage}
<Dropdown
options={getOptions()}
classNames="absolute top-4 right-4"
isIcon={true}
>
<div class="p-1 rounded-full bg-white dark:bg-gray-600">
<OverflowMenuHorizontalIcon size={20} class="carbon-icon" />
</div>
</Dropdown>
{/if}
<img
class="h-2/5 w-full rounded-tl rounded-tr"
src={bannerImage ?? '/images/classroomio-course-img-template.jpg'}
alt="Course Logo"
/>
<div class="px-3 py-5">
<h4 class="dark:text-white title text-md font-bold">
{title}
</h4>
<p
class="description text-left text-sm font-small flex flex-col text-gray-500 dark:text-white my-3"
title={description}
>
{description}
</p>
<div class="flex justify-between">
<p class="dark:text-white">{totalLessons} lessons</p>
{#if isOnLandingPage}
<ArrowRight size={24} class="carbon-class" />
{:else}
<div
class="rounded dark:text-white bg-{isPublished
? 'green'
: 'red'}-200 py-1 px-2"
>
{#if isPublished}
Published
{:else}
Unpublished
{/if}
</div>
{/if}
</div>
<p class="dark:text-white text-gray-500 text-xs mt-1">
{!cost ? 'free' : formatter.format(cost)}
</p>
</div>
</div>
</a>
<style>
.root {
min-height: 370px;
}
a,
a:hover {
text-decoration: none;
}
.title,
.description {
height: 40px;
line-height: 20px;
display: -webkit-box;
-webkit-box-orient: vertical;
-moz-box-orient: vertical;
-ms-box-orient: vertical;
box-orient: vertical;
-webkit-line-clamp: 2;
-moz-line-clamp: 2;
-ms-line-clamp: 2;
line-clamp: 2;
overflow: hidden;
}
</style>
@@ -0,0 +1,70 @@
<script>
import { goto } from '$app/navigation';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import Modal from '$lib/components/Modal/index.svelte';
import { snackbarStore } from '$lib/components/Snackbar/store';
import { SNACKBAR_SEVERITY } from '$lib/components/Snackbar/constants';
import TextField from '$lib/components/Form/TextField.svelte';
import {
copyCourseModalInitialState,
copyCourseModal,
} from '$lib/components/Courses/store';
import { cloneCourse } from '$lib/utils/services/courses/clone';
// clone, show spinner and redirect to new course
async function createCourse() {
console.log('Create course');
$copyCourseModal.isSaving = true;
try {
const newCourse = await cloneCourse(
$copyCourseModal.id,
$copyCourseModal.title
);
goto(`/courses/${newCourse.id}`);
copyCourseModal.set(copyCourseModalInitialState);
} catch (error) {
console.error(error);
// IIFI of snackbarStore to Show notification error
(() => {
$snackbarStore.message = "Something's not right - Please try later";
$snackbarStore.severity = SNACKBAR_SEVERITY.ERROR;
$snackbarStore.open = true;
})();
// Stop loader
$copyCourseModal.isSaving = false;
}
}
</script>
<Modal
onClose={() => ($copyCourseModal.open = false)}
bind:open={$copyCourseModal.open}
width="w-96"
modalHeading="Create a Course"
>
<form on:submit|preventDefault={createCourse}>
<TextField
label="Course name"
bind:value={$copyCourseModal.title}
autoFocus={true}
placeholder="Your course name"
className="mb-4"
isRequired={true}
autoComplete={false}
/>
<div class="mt-5 flex items-center flex-row-reverse">
<PrimaryButton
className="px-6 py-3"
label="Create"
type="submit"
isLoading={$copyCourseModal.isSaving}
/>
</div>
</form>
</Modal>
@@ -0,0 +1,139 @@
<script>
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import Modal from '$lib/components/Modal/index.svelte';
import TextField from '$lib/components/Form/TextField.svelte';
import TextArea from '$lib/components/Form/TextArea.svelte';
import { courses, createCourseModal } from '$lib/components/Courses/store';
import { validateForm } from '$lib/components/Courses/functions';
import { ROLE } from '$lib/utils/constants/roles';
import { addGroupMember } from '$lib/utils/services/courses';
import { supabase } from '$lib/utils/functions/supabase';
import { profile } from '$lib/utils/store/user';
import { currentOrg } from '$lib/utils/store/org';
let errors = {
emails: '',
};
async function createCourse(e) {
const { hasError, fieldErrors, students, tutors } =
validateForm($createCourseModal);
errors = fieldErrors;
if (hasError) return;
const { title, description } = $createCourseModal;
// 1. Create group
const { data: newGroup } = await supabase
.from('group')
.insert({ name: title, description, organization_id: $currentOrg.id })
.select();
console.log(`newGroup`, newGroup);
const { id: group_id } = newGroup[0];
// 2. Create course with group_id
const { data: newCourse } = await supabase
.from('course')
.insert({
title,
description,
group_id,
})
.select();
console.log(`newCourse`, newCourse);
courses.update((_courses) => [..._courses, newCourse[0]]);
// 3. Add group members
// Get profile of author and add as member
await addGroupMember({
profile_id: $profile.id,
email: $profile.email,
group_id,
role_id: ROLE.TUTOR,
});
let membersStack = [];
// Create groupmemebers from group_id and add teachers and students
for (const student of students) {
membersStack.push({
email: student,
group_id,
role_id: ROLE.STUDENT,
});
}
for (const tutor of tutors) {
membersStack.push({
email: tutor,
group_id,
role_id: ROLE.TUTOR,
});
}
addGroupMember(membersStack).then((membersAdded) => {
console.log(`membersAdded`, membersAdded);
$createCourseModal.open = false;
});
}
</script>
<svelte:head>
<title>Courses - ClassroomIO</title>
</svelte:head>
<Modal
onClose={() => ($createCourseModal.open = false)}
bind:open={$createCourseModal.open}
width="w-2/5"
modalHeading="Create a Course"
>
<form on:submit|preventDefault={createCourse}>
<TextField
label="Course name"
bind:value={$createCourseModal.title}
autofocus={true}
placeholder="Your course name"
className="mb-4"
isRequired={true}
errorMessage={errors.title}
autoComplete={false}
/>
<TextArea
label="Invite Tutors"
bind:value={$createCourseModal.tutors}
rows="2"
maxRows="3"
placeholder="[email protected], [email protected]"
helperMessage="To invite people, add their emails separated by a comma"
errorMessage={errors.tutors}
className="mb-4"
/>
<TextArea
label="Invite Students"
bind:value={$createCourseModal.students}
rows="2"
maxRows="3"
placeholder="[email protected], [email protected]"
helperMessage="To invite people, add their emails separated by a comma"
errorMessage={errors.students}
className="mb-4"
/>
<TextArea
label="Short description"
bind:value={$createCourseModal.description}
rows="4"
maxRows="4"
placeholder="A little description about this course"
className="mb-4"
isRequired={true}
/>
<div class="mt-5 flex items-center">
<PrimaryButton className="px-6 py-3" label="Finish" type="submit" />
</div>
</form>
</Modal>
+10
View File
@@ -0,0 +1,10 @@
export const statuses = [
{
chipClassName: 'bg-green-600 text-white',
chipLabel: 'In progress',
},
{
chipClassName: 'bg-red-600 text-white',
chipLabel: 'Completed',
},
];
+67
View File
@@ -0,0 +1,67 @@
import { validateEmail } from '$lib/utils/functions/validateEmail';
export function validateForm(fields) {
let hasError = false;
const { students = '', tutors = '', title, description } = fields;
const fieldErrors = {
tutors: '',
students: '',
title: '',
description: '',
};
const _tutors =
tutors.length < 5
? []
: tutors.split(',').map((_email) => {
const email = _email.trim();
if (!validateEmail(email)) {
hasError = true;
fieldErrors.tutors += !fieldErrors.tutors.length
? `Not valid email: ${email}`
: `, ${email}`;
}
return email;
});
const _students =
students.length < 5
? []
: students.split(',').map((_email) => {
const email = _email.trim();
if (!validateEmail(email)) {
hasError = true;
fieldErrors.students += !fieldErrors.students.length
? `Not valid email: ${email}`
: `, ${email}`;
}
return email;
});
if (!title) {
fieldErrors.title = 'Title is required';
return {
hasError: true,
fieldErrors,
};
}
if (!description) {
fieldErrors.description = 'Description is required';
return {
hasError: true,
fieldErrors,
};
}
return {
hasError,
fieldErrors,
tutors: _tutors,
students: _students,
};
}
+44
View File
@@ -0,0 +1,44 @@
<script>
import Box from '../Box/index.svelte';
import Card from './components/Card/index.svelte';
import CardLoader from './components/Card/Loader.svelte';
import CoursesEmptyIcon from '../Icons/CoursesEmptyIcon.svelte';
import CopyCourseModal from './components/CopyCourseModal/index.svelte';
import { courses, courseMetaDeta } from './store';
</script>
<CopyCourseModal />
<div
class="flex items-center justify-center lg:justify-start flex-wrap my-4 m-auto"
>
{#if $courseMetaDeta.isLoading}
<CardLoader />
<CardLoader />
<CardLoader />
{:else}
{#each $courses as courseData}
<Card
id={courseData.id}
logo={courseData.logo}
bannerImage={courseData.banner_image}
title={courseData.title}
description={courseData.description}
role_id={courseData.role_id}
isPublished={courseData.is_published}
cost={courseData.cost}
currency={courseData.currency}
totalLessons={courseData.total_lessons}
/>
{:else}
<Box>
<CoursesEmptyIcon />
<h3 class="dark:text-white text-2xl my-5">No Courses Created</h3>
<p class="dark:text-white w-1/3 text-center">
Share your knowledge with the world by creating engaging courses for
your students.
</p>
</Box>
{/each}
{/if}
</div>
+25
View File
@@ -0,0 +1,25 @@
import { writable } from 'svelte/store';
export const courses = writable([]);
export const courseMetaDeta = writable({
isLoading: false,
});
export const createCourseModal = writable({
open: false,
title: '',
description: '',
emails: '',
tutors: '',
students: '',
});
export const copyCourseModalInitialState = {
open: false,
courseIdToCopy: null,
id: '',
title: '',
isSaving: false,
error: null,
};
export const copyCourseModal = writable({ ...copyCourseModalInitialState });