[DONE] clone a course

This commit is contained in:
Rotimi Best
2021-12-30 22:30:30 +02:00
parent a1f6c5728b
commit 23c68c9d78
24 changed files with 834 additions and 195 deletions
+1 -1
View File
@@ -12,7 +12,7 @@
position: fixed;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.5);
background-color: rgba(0, 0, 0, 0.3);
-webkit-tap-highlight-color: transparent;
color: #fff;
z-index: 1201;
+1 -1
View File
@@ -56,7 +56,7 @@
<style>
canvas {
position: absolute;
width: 70%;
width: 60%;
height: 70%;
}
</style>
@@ -3,6 +3,8 @@
import { onMount } from 'svelte';
import AudioConsole32 from 'carbon-icons-svelte/lib/AudioConsole32';
import Box from '../../../../Box/index.svelte';
import { Firework } from 'svelte-loading-spinners';
import Backdrop from '../../../../Backdrop/index.svelte';
import PrimaryButton from '../../../../PrimaryButton/index.svelte';
import RoleBasedSecurity from '../../../../RoleBasedSecurity/index.svelte';
import PageBody from '../../../../PageBody/index.svelte';
@@ -20,6 +22,7 @@
export let lessonId;
let open = false;
let isFetching = false;
let newExercise = {
id: 1,
title: '',
@@ -52,17 +55,19 @@
async function getExercises() {
if (!lessonId) return;
isFetching = true;
const exercisesData = await supabase
.from('exercise')
.select(`id, title, created_at`)
.match({ lesson_id: lessonId });
$lesson.exercises = exercisesData.data;
isFetching = false;
}
async function getExercise(exerciseId) {
if (!exerciseId) return;
isFetching = false;
isQuestionnaireFetching.update(() => true);
const { data } = await supabase
@@ -101,6 +106,7 @@
console.log('getExercise', exerciseId, $questionnaire);
isQuestionnaireFetching.update(() => false);
isFetching = true;
}
function goBack() {
@@ -114,6 +120,12 @@
$: getExercise(exerciseId);
</script>
{#if isFetching}
<Backdrop>
<Firework size="60" color="#1d4ed8" unit="px" duration="1s" />
</Backdrop>
{/if}
{#if exerciseId}
<Exercise
{exerciseId}
@@ -1,4 +1,5 @@
<script>
import { goto } from '@sapper/app';
import Modal from '../../../Modal/index.svelte';
import TextField from '../../../Form/TextField.svelte';
import TextArea from '../../../Form/TextArea.svelte';
@@ -44,6 +45,7 @@
async function handleDelete() {
$settingsDialog.open = false;
await deleteCourse($course.id);
goto('/courses');
}
// function setAvatar(logo) {
@@ -1,5 +1,7 @@
<script>
import { Firework } from 'svelte-loading-spinners';
import Navigation from '../Course/components/Navigation/index.svelte';
import Backdrop from '../../components/Backdrop/index.svelte';
// import { lessons, lesson } from '../Course/components/Lesson/store/lessons';
// import { questionnaire } from '../Course/components/Lesson/Exercise/store/exercise';
// import { questionnaireMetaData } from '../Course/components/Lesson/store/answers';
@@ -11,6 +13,7 @@
export let path = '';
export let isExercisePage = false;
export let isStudent = false;
export let isFetching = false;
$: {
const user = $group.people.find(
@@ -27,6 +30,12 @@
<title>{$course.title || 'ClassroomIO Course'}</title>
</svelte:head>
{#if isFetching}
<Backdrop>
<Firework size="60" color="#1d4ed8" unit="px" duration="1s" />
</Backdrop>
{/if}
<div class="root">
<Navigation {path} {isStudent} />
<div class="rightBar" class:isMobile={$isMobile}>
@@ -1,22 +1,56 @@
<script>
import Dropdown from '../../../Dropdown/index.svelte';
import { copyCourseModal } from '../../store';
import { ROLE } from '../../../../utils/constants/roles';
import OverflowMenuVertical24 from 'carbon-icons-svelte/lib/OverflowMenuVertical24';
export let logo = '';
export let id = '';
export let title = '';
export let description = '';
export let role_id = ROLE.STUDENT;
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"
class="root w-72 border bg-white px-2 py-2 mr-4 mb-4 rounded-md hover:shadow-2xl transition ease-in-out"
href="/courses/{id}"
<div
class="root w-72 border bg-white px-2 py-2 mr-4 mb-4 rounded-md hover:shadow-2xl transition ease-in-out relative"
>
<Dropdown
options={getOptions()}
classNames="absolute top-4 right-0"
isIcon={true}
>
<OverflowMenuVertical24 class="carbon-icon" />
</Dropdown>
<img
class="w-24 h-24 rounded-full border mx-auto"
src={logo}
alt="Course Logo"
/>
<div class="pt-3 text-center space-y-4">
<h3 class="text-lg font-semibold">
<a rel="prefetch" href="/courses/{id}" class="pt-3 text-center space-y-4">
<h3 class="text-lg font-semibold hover:underline">
{title}
</h3>
<div
@@ -25,15 +59,17 @@
>
{description}
</div>
</div>
</a>
</a>
</div>
<style>
.root {
min-height: 300px;
text-decoration: none;
}
a {
text-decoration: none;
}
.description {
line-height: 20px;
display: -webkit-box;
@@ -0,0 +1,67 @@
<script>
import { goto } from '@sapper/app';
import PrimaryButton from '../../../PrimaryButton/index.svelte';
import Modal from '../../../Modal/index.svelte';
import { snackbarStore } from '../../../Snackbar/store';
import { SNACKBAR_SEVERITY } from '../../../Snackbar/constants';
import TextField from '../../../Form/TextField.svelte';
import { copyCourseModalInitialState, copyCourseModal } from '../../store';
import { cloneCourse } from '../../../../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-2"
label="Create"
type="submit"
isLoading={$copyCourseModal.isSaving}
/>
</div>
</form>
</Modal>
@@ -0,0 +1,134 @@
<script>
import PrimaryButton from '../../../PrimaryButton/index.svelte';
import Modal from '../../../Modal/index.svelte';
import TextField from '../../../Form/TextField.svelte';
import TextArea from '../../../Form/TextArea.svelte';
import { courses, createCourseModal } from '../../../Courses/store';
import { validateForm } from '../../../Courses/functions';
import { ROLE } from '../../../../utils/constants/roles';
import { addGroupMember } from '../../../../utils/services/courses';
import { supabase } from '../../../../utils/functions/supabase';
import { profile } from '../../../../utils/store/user';
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 });
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,
});
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-2" label="Finish" type="submit" />
</div>
</form>
</Modal>
+22 -11
View File
@@ -1,22 +1,33 @@
<script>
import Box from '../Box/index.svelte';
import { Firework } from 'svelte-loading-spinners';
import Card from './components/Card/index.svelte';
import { courses } from './store';
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"
>
{#each $courses as courseData}
<Card
id={courseData.id}
logo={courseData.logo}
title={courseData.title}
description={courseData.description}
/>
{:else}
{#if $courseMetaDeta.isLoading}
<Box>
<h3 class="text-2xl text-gray-500">No Course Assigned</h3>
<Firework size="60" color="#1d4ed8" unit="px" duration="1s" />
</Box>
{/each}
{:else}
{#each $courses as courseData}
<Card
id={courseData.id}
logo={courseData.logo}
title={courseData.title}
description={courseData.description}
role_id={courseData.role_id}
/>
{:else}
<Box>
<h3 class="text-2xl text-gray-500">No Course Assigned</h3>
</Box>
{/each}
{/if}
</div>
+13
View File
@@ -1,6 +1,9 @@
import { writable } from 'svelte/store';
export const courses = writable([]);
export const courseMetaDeta = writable({
isLoading: false,
});
export const createCourseModal = writable({
open: false,
@@ -10,3 +13,13 @@ export const createCourseModal = writable({
tutors: '',
students: '',
});
export const copyCourseModalInitialState = {
open: false,
courseIdToCopy: null,
id: '',
title: '',
isSaving: false,
error: null,
};
export const copyCourseModal = writable({ ...copyCourseModalInitialState });
+23 -10
View File
@@ -2,14 +2,24 @@
// import ChevronDown24 from "carbon-icons-svelte/lib/ChevronDown24";
export let options = [];
export let handleOptionClick;
export let classNames = 'relative';
export let isIcon = false;
let open = false;
function onClick(optionIndex) {
const notIconClass =
'border rounded-lg border-grey py-2 px-3 focus:outline-none focus:border-gray-400 focus:bg-gray-200';
function isOptionLast(index) {
return index === options.length - 1;
}
function onClick(option) {
return () => {
open = !open;
handleOptionClick(optionIndex);
if (option && option.onClick) {
option.onClick();
}
};
}
@@ -19,10 +29,10 @@
}
</script>
<div class="relative">
<div class="{classNames} z-10d {!options.length && 'hidden'}">
<button
class="flex flex-row items-center border rounded-lg border-grey py-2 px-3 focus:outline-none focus:border-gray-400 focus:bg-gray-200"
on:click={() => (open = !open)}
class="flex flex-row items-center {!isIcon && notIconClass}"
on:click={(e) => (open = !open)}
on:blur={onBlur}
>
<slot />
@@ -31,14 +41,14 @@
{#if open}
<div
class="dropdown w-48 bg-white border border-grey rounded-lg mt-2 py-2 absolute -top-0.5"
class="dropdown w-48 bg-white border border-grey rounded-lg mt-2 py-2 absolute"
>
{#each options as option, index}
<button
class="block px-4 py-2 border-b w-full"
on:click={onClick(index)}
class="block px-4 py-2 {!isOptionLast(index) && 'border-b'} w-full"
on:click={onClick(option)}
>
{option}
{option.label}
</button>
{/each}
</div>
@@ -49,5 +59,8 @@
.dropdown {
box-shadow: 0px 5px 5px -3px rgb(0 0 0 / 20%),
0px 8px 10px 1px rgb(0 0 0 / 14%), 0px 3px 14px 2px rgb(0 0 0 / 12%);
right: 0px;
top: 20px;
left: unset;
}
</style>
+2
View File
@@ -21,11 +21,13 @@
{#if open}
<div
class="dialog fixed overflow-x-hidden overflow-y-auto inset-0 flex justify-center items-center z-50 bg-gray-100 bg-opacity-50"
on:click={onClose}
>
<div aria-hidden="true" class="backdrop" />
<div
class="{maxWidth ||
'container'} bg-white mx-auto {width} shadow-lg p-5 pt-2 rounded-md"
on:click={(e) => e.stopPropagation()}
>
<div class="flex items-center justify-between mb-4">
<h3 class="text-xl m-0">
+11 -2
View File
@@ -1,5 +1,7 @@
<script>
import { BarLoader } from 'svelte-loading-spinners';
import { VARIANTS, VARIANTS_CLASS } from './constants';
export let label = '';
export let className = '';
export let onClick = () => {};
@@ -8,6 +10,10 @@
export let variant = VARIANTS.CONTAINED;
export let disablePadding = false;
export let isDisabled = false;
export let isLoading = false;
$: variant = isLoading ? VARIANTS.OUTLINED : variant;
$: isDisabled = isLoading ? true : isDisabled;
</script>
<button
@@ -15,13 +21,16 @@
'bg-opacity-25 hover:bg-opacity-25 cursor-not-allowed'} flex items-center h-auto {VARIANTS_CLASS[
variant
]} {!disablePadding &&
'py-1 px-5'} rounded {className} transition delay-150 duration-300 ease-in-out "
'py-1 px-5'} rounded {className} py-2 w-full justify-center sm:w-auto {isLoading &&
'py-4 border-blue-700'} transition delay-150 duration-300 ease-in-out"
on:click={onClick}
{name}
{type}
disabled={isDisabled}
>
{#if !!label}
{#if isLoading}
<BarLoader size="30" color="#1d4ed8" unit="px" duration="1s" />
{:else if !!label}
{label}
{:else}
<slot />
+2 -2
View File
@@ -6,7 +6,7 @@
import CloseButton from '../Buttons/Close/index.svelte';
import { SNACKBAR_SEVERITY, SNACKBAR_SEVERITY_COLOR } from './constants';
import { snackbarStore } from './store';
import { snackbarStore, snackbarStoreInitialState } from './store';
let severityColor;
@@ -15,7 +15,7 @@
$snackbarStore.handleClose();
}
$snackbarStore.open = false;
snackbarStore.set(snackbarStoreInitialState);
}
// When open is true trigger autoHideDuration
+4 -2
View File
@@ -1,10 +1,12 @@
import { writable } from 'svelte/store';
import { SNACKBAR_SEVERITY } from './constants';
export const snackbarStore = writable({
export const snackbarStoreInitialState = {
open: false,
message: '',
severity: SNACKBAR_SEVERITY.DEFAULT,
handleClose: () => {},
autoHideDuration: 6000,
});
};
export const snackbarStore = writable({ ...snackbarStoreInitialState });