CLASSROOMIO-T-80: Replace sapper with sveltekit
This commit is contained in:
@@ -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="tutor@company.com, john@gmail.com"
|
||||
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="student@gmail.com, john@gmail.com"
|
||||
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>
|
||||
@@ -0,0 +1,10 @@
|
||||
export const statuses = [
|
||||
{
|
||||
chipClassName: 'bg-green-600 text-white',
|
||||
chipLabel: 'In progress',
|
||||
},
|
||||
{
|
||||
chipClassName: 'bg-red-600 text-white',
|
||||
chipLabel: 'Completed',
|
||||
},
|
||||
];
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 });
|
||||
Reference in New Issue
Block a user