[DONE] clone a course
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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="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-2" label="Finish" type="submit" />
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
@@ -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>
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
<main>
|
||||
{#if $preloading && $delayedPreloading}
|
||||
<Backdrop>
|
||||
<Firework size="60" color="#fff" unit="px" duration="1s" />
|
||||
<Firework size="60" color="#1d4ed8" unit="px" duration="1s" />
|
||||
</Backdrop>
|
||||
{/if}
|
||||
{#if !['login', 'signup'].includes(path) && !isCoursePage(path)}
|
||||
|
||||
+11
-129
@@ -19,93 +19,25 @@
|
||||
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
// import { stores } from '@sapper/app';
|
||||
import PageNav from '../components/PageNav/index.svelte';
|
||||
import Courses from '../components/Courses/index.svelte';
|
||||
import NewCourseModal from '../components/Courses/components/NewCourseModal/index.svelte';
|
||||
import PrimaryButton from '../components/PrimaryButton/index.svelte';
|
||||
import Modal from '../components/Modal/index.svelte';
|
||||
import TextField from '../components/Form/TextField.svelte';
|
||||
import TextArea from '../components/Form/TextArea.svelte';
|
||||
import { courses, createCourseModal } from '../components/Courses/store';
|
||||
import { validateForm } from '../components/Courses/functions';
|
||||
import { ROLE } from '../utils/constants/roles';
|
||||
import { addGroupMember } from '../utils/services/courses';
|
||||
import { supabase } from '../utils/functions/supabase';
|
||||
import {
|
||||
courses,
|
||||
createCourseModal,
|
||||
courseMetaDeta,
|
||||
} from '../components/Courses/store';
|
||||
import { setProfileIdOfGroupMember } from '../utils/services/courses';
|
||||
|
||||
// const { page } = stores();
|
||||
|
||||
export let allCourses = [];
|
||||
export let cantFetch;
|
||||
|
||||
let errors = {
|
||||
emails: '',
|
||||
};
|
||||
|
||||
// let tab = '';
|
||||
|
||||
async function createCourse(e) {
|
||||
const { hasError, fieldErrors, students, tutors } =
|
||||
validateForm($createCourseModal);
|
||||
|
||||
errors = fieldErrors;
|
||||
if (hasError) return;
|
||||
|
||||
const { title, description } = $createCourseModal;
|
||||
// Create group from course_id
|
||||
const { data: newGroup } = await supabase
|
||||
.from('group')
|
||||
.insert({ name: title, description });
|
||||
|
||||
console.log(`newGroup`, newGroup);
|
||||
const { id: group_id } = newGroup[0];
|
||||
|
||||
// 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]]);
|
||||
|
||||
// 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;
|
||||
});
|
||||
}
|
||||
|
||||
async function getCourses(userId) {
|
||||
if (process.browser) {
|
||||
if (cantFetch && typeof cantFetch === 'boolean' && !allCourses.length) {
|
||||
$courseMetaDeta.isLoading = true;
|
||||
|
||||
const updatedProfile = await setProfileIdOfGroupMember(
|
||||
$profile.email,
|
||||
userId
|
||||
@@ -114,6 +46,7 @@
|
||||
const coursesResult = await fetchCourses(userId);
|
||||
console.log(`coursesResult`, coursesResult);
|
||||
|
||||
$courseMetaDeta.isLoading = false;
|
||||
if (!coursesResult) return;
|
||||
|
||||
// organizationId = coursesResult.organizationId;
|
||||
@@ -122,7 +55,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
onMount(() => {
|
||||
courses.set(allCourses);
|
||||
});
|
||||
|
||||
@@ -145,58 +78,7 @@
|
||||
</div>
|
||||
</PageNav>
|
||||
|
||||
<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-2" label="Finish" type="submit" />
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
<NewCourseModal />
|
||||
|
||||
<Courses />
|
||||
</section>
|
||||
|
||||
@@ -44,12 +44,14 @@
|
||||
let mode = MODES.view;
|
||||
let prevMode;
|
||||
let prevLessonId;
|
||||
let isFetching = false;
|
||||
|
||||
const { page } = stores();
|
||||
|
||||
async function fetchReqData(lessonId) {
|
||||
let lessonData;
|
||||
isFetching = true;
|
||||
|
||||
let lessonData;
|
||||
if (!$course.id) {
|
||||
const [_course, lesson] = await Promise.all([
|
||||
fetchCourse(courseId),
|
||||
@@ -69,6 +71,7 @@
|
||||
const totalExercises =
|
||||
lessonData.totalExercises[0] && lessonData.totalExercises[0].count;
|
||||
setLesson(lessonData, totalExercises);
|
||||
isFetching = false;
|
||||
}
|
||||
|
||||
function setLesson(lessonData, totalExercises) {
|
||||
@@ -92,7 +95,11 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<CourseContainer {path} isExercisePage={!isMaterialsTabActive && exerciseId}>
|
||||
<CourseContainer
|
||||
{isFetching}
|
||||
{path}
|
||||
isExercisePage={!isMaterialsTabActive && exerciseId}
|
||||
>
|
||||
<PageNav
|
||||
navItems={[
|
||||
{
|
||||
|
||||
@@ -18,7 +18,10 @@
|
||||
group,
|
||||
} from '../../../components/Course/store';
|
||||
import { lessons } from '../../../components/Course/components/Lesson/store/lessons';
|
||||
import { fetchCourse, fetchExercises } from '../../../utils/services/courses';
|
||||
import {
|
||||
fetchCourse,
|
||||
fetchExercisesByMarks,
|
||||
} from '../../../utils/services/courses';
|
||||
import { getLectureNo } from '../../../components/Course/function';
|
||||
import { fetchMarks } from '../../../utils/services/marks';
|
||||
import { profile } from '../../../utils/store/user';
|
||||
@@ -67,7 +70,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
const { data: exercises } = await fetchExercises(courseId);
|
||||
const { data: exercises } = await fetchExercisesByMarks(courseId);
|
||||
|
||||
// Map exercises to { lessonId: { exerciseId: exerciseTitle, ... }, ... }
|
||||
exercises.forEach((exercise) => {
|
||||
|
||||
+3
-13
@@ -64,7 +64,7 @@
|
||||
data-atf="1"
|
||||
/>
|
||||
-->
|
||||
<div>
|
||||
<div>
|
||||
<h3 class="text-4xl text-center">
|
||||
Classroom<span class="text-blue-700">IO</span>
|
||||
</h3>
|
||||
@@ -87,18 +87,8 @@
|
||||
isRequired={true}
|
||||
isDisabled={isAdding}
|
||||
/>
|
||||
<PrimaryButton
|
||||
type="submit"
|
||||
variant={isAdding ? VARIANTS.OUTLINED : VARIANTS.CONTAINED}
|
||||
className="py-2 w-full justify-center sm:w-auto {isAdding &&
|
||||
'py-4 border-blue-700'}"
|
||||
isDisabled={isAdding}
|
||||
>
|
||||
{#if isAdding}
|
||||
<BarLoader size="30" color="#1d4ed8" unit="px" duration="1s" />
|
||||
{:else}
|
||||
Join waiting list
|
||||
{/if}
|
||||
<PrimaryButton type="submit" isLoading={isAdding}>
|
||||
Join waiting list
|
||||
</PrimaryButton>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
<script context="module">
|
||||
export function preload({ params }) {
|
||||
console.log(`params`, params);
|
||||
return { profileId: params.id };
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import { Chasing } from 'svelte-loading-spinners';
|
||||
import Search32 from 'carbon-icons-svelte/lib/Search32';
|
||||
import UserAvatar16 from 'carbon-icons-svelte/lib/UserAvatar16';
|
||||
import Book16 from 'carbon-icons-svelte/lib/Book16';
|
||||
import TextField from '../../components/Form/TextField.svelte';
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
import { get } from 'svelte/store';
|
||||
import type { PostgrestError } from '@supabase/supabase-js';
|
||||
import { supabase } from '../../functions/supabase';
|
||||
// @ts-ignore
|
||||
import { addGroupMember } from './index';
|
||||
// @ts-ignore
|
||||
import { profile } from '../../store/user';
|
||||
import type { Course, Exercise, Lesson, Profile } from '../../types';
|
||||
// @ts-ignore
|
||||
import { ROLE } from '../../constants/roles';
|
||||
// @ts-ignore
|
||||
import { QUESTION_TYPE } from '../../../components/Question/constants';
|
||||
|
||||
type GetCourseResponse = {
|
||||
data: Course;
|
||||
error: PostgrestError | null;
|
||||
};
|
||||
|
||||
async function fetchCourseData(courseId: string): Promise<GetCourseResponse> {
|
||||
const { data, error } = await supabase
|
||||
.from('course')
|
||||
.select(
|
||||
`
|
||||
*,
|
||||
lessons:lesson(
|
||||
*,
|
||||
exercise:id(
|
||||
id, title, description, due_by,
|
||||
questions:question(
|
||||
*,
|
||||
options:option(*),
|
||||
question_type:question_type_id(id, label)
|
||||
)
|
||||
)
|
||||
)
|
||||
`
|
||||
)
|
||||
.match({ id: courseId })
|
||||
.single();
|
||||
|
||||
return { data, error };
|
||||
}
|
||||
|
||||
async function cloneGroupAndBasicCourse(
|
||||
course: Course,
|
||||
newTitle: string
|
||||
): Promise<{ newCourse: Course }> {
|
||||
const { description } = course;
|
||||
|
||||
const { data: newGroup } = await supabase
|
||||
.from('group')
|
||||
.insert({ name: newTitle, description });
|
||||
|
||||
console.log(`newGroup`, newGroup);
|
||||
if (!Array.isArray(newGroup)) {
|
||||
throw new Error('Group wasnt created successfully');
|
||||
}
|
||||
|
||||
const { id: group_id } = newGroup[0];
|
||||
|
||||
// 2. Create course with group_id
|
||||
const { data: newCourse } = await supabase.from('course').insert({
|
||||
...course,
|
||||
title: newTitle,
|
||||
description,
|
||||
group_id,
|
||||
lessons: undefined,
|
||||
id: undefined,
|
||||
updated_at: undefined,
|
||||
created_at: undefined,
|
||||
});
|
||||
|
||||
console.log(`newCourse`, newCourse);
|
||||
|
||||
// 3. Add group members
|
||||
|
||||
const profileStoreValue: Profile = get(profile);
|
||||
|
||||
// Get profile of author and add as member
|
||||
await addGroupMember({
|
||||
profile_id: profileStoreValue.id,
|
||||
email: profileStoreValue.email,
|
||||
group_id,
|
||||
role_id: ROLE.TUTOR,
|
||||
});
|
||||
|
||||
console.log('profileStoreValue', profileStoreValue);
|
||||
|
||||
return {
|
||||
newCourse: newCourse?.[0],
|
||||
};
|
||||
}
|
||||
|
||||
async function cloneLessons(lessons: Lesson[], courseId: Course['id']) {
|
||||
const clonedLessons = lessons
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
|
||||
)
|
||||
.map((lesson, index) => ({
|
||||
call_url: lesson.call_url,
|
||||
course_id: courseId,
|
||||
is_complete: lesson.is_complete,
|
||||
lesson_at: new Date(),
|
||||
note: lesson.note,
|
||||
order: lesson.order || index + 1,
|
||||
public: lesson.public,
|
||||
slide_url: lesson.slide_url,
|
||||
title: lesson.title,
|
||||
video_url: lesson.video_url,
|
||||
}));
|
||||
|
||||
const { data } = await supabase.from('lesson').insert(clonedLessons);
|
||||
|
||||
const newLessons = data?.map((dataItem, dataIndex) => {
|
||||
return {
|
||||
...dataItem,
|
||||
exercise: lessons[dataIndex].exercise,
|
||||
};
|
||||
});
|
||||
|
||||
console.log('newLessons', newLessons);
|
||||
return { newLessons };
|
||||
}
|
||||
|
||||
async function cloneExercises(lessons: Lesson[]) {
|
||||
console.log('lessons', lessons);
|
||||
|
||||
// Lessons
|
||||
for (const lesson of lessons) {
|
||||
const { id: lessonId, exercise = [] } = lesson;
|
||||
|
||||
// Exercises
|
||||
for (const exerciseItem of exercise) {
|
||||
const { title, description, questions = [] } = exerciseItem;
|
||||
// Insert
|
||||
const { data } = await supabase.from('exercise').insert({
|
||||
description,
|
||||
due_by: new Date(),
|
||||
lesson_id: lessonId,
|
||||
title,
|
||||
});
|
||||
const newExercise = Array.isArray(data) ? data[0] : data;
|
||||
console.log('newExercise', newExercise);
|
||||
|
||||
// Questions
|
||||
for (const question of questions) {
|
||||
const { options = [] } = question;
|
||||
|
||||
// Insert
|
||||
const { data } = await supabase.from('question').insert({
|
||||
name: question.name,
|
||||
title: question.title,
|
||||
points: question.points,
|
||||
order: question.order,
|
||||
question_type_id: question.question_type_id,
|
||||
exercise_id: newExercise.id,
|
||||
});
|
||||
const newQuestion = Array.isArray(data) ? data[0] : data;
|
||||
console.log('newQuestion', newQuestion);
|
||||
|
||||
// Options: Don't map options for 'Paragraph' questions
|
||||
if (QUESTION_TYPE.TEXTAREA !== newQuestion.question_type_id) {
|
||||
const newOptions = options.map((option) => ({
|
||||
value: option.value,
|
||||
label: option.label,
|
||||
is_correct: option.is_correct,
|
||||
question_id: newQuestion.id,
|
||||
}));
|
||||
|
||||
// Insert[Batch]
|
||||
await supabase.from('option').insert(newOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function cloneCourse(
|
||||
courseId: string,
|
||||
newTitle: string
|
||||
): Promise<Course> {
|
||||
const { data: course, error } = await fetchCourseData(courseId);
|
||||
console.log(`course`, course);
|
||||
|
||||
if (error) {
|
||||
console.error('Error when cloning', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Copy course table
|
||||
* 2. Copy lessons table
|
||||
* 3. Copy exercises table
|
||||
* 4. Copy questions table
|
||||
* 5. Copy options table
|
||||
*/
|
||||
|
||||
// tables: group, groupmember, course(group_id)
|
||||
const { newCourse } = await cloneGroupAndBasicCourse(course, newTitle);
|
||||
console.log('returned -> newCourse', newCourse);
|
||||
|
||||
const { newLessons } = await cloneLessons(course.lessons, newCourse.id);
|
||||
console.log('returned -> newLessons', newLessons);
|
||||
|
||||
const cloneExercisesRes = await cloneExercises(newLessons || []);
|
||||
console.log('returned -> cloneExercisesRes', cloneExercisesRes);
|
||||
|
||||
return newCourse;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
1. Click on 3 dots to clone course
|
||||
|
||||
2. Show a modal with a new name "Copy of previous course name"
|
||||
|
||||
-> Note the following information will be copied
|
||||
|
||||
3. Things to copy
|
||||
Overview
|
||||
Lessons
|
||||
Exercises - questions, options, everything basically.
|
||||
|
||||
Technical approach
|
||||
- Fetch all the data to the client
|
||||
- Manually call functions to create
|
||||
|
||||
- call a psql function with newname, course_id
|
||||
|
||||
- copy all in course table
|
||||
- copy all in group table by course_id
|
||||
- add only one group member as admin
|
||||
|
||||
- get all lessons in course
|
||||
- copy each one with unique ids
|
||||
|
||||
- get all exercises by course id
|
||||
- copy exercise
|
||||
|
||||
- get questions
|
||||
- copy questions
|
||||
|
||||
- get options
|
||||
- copy options
|
||||
*/
|
||||
@@ -114,8 +114,8 @@ export async function updateCourse(courseId, course) {
|
||||
return course.logo;
|
||||
}
|
||||
|
||||
export async function deleteCourse(courseId, course) {
|
||||
return await supabase.from('course').delete(course).match({ id: courseId });
|
||||
export async function deleteCourse(courseId) {
|
||||
return await supabase.from('course').delete().match({ id: courseId });
|
||||
}
|
||||
|
||||
export function addGroupMember(member) {
|
||||
@@ -165,7 +165,7 @@ export function createExercise(exercise) {
|
||||
return supabase.from('exercise').insert(exercise);
|
||||
}
|
||||
|
||||
export function fetchExercises(courseId) {
|
||||
export function fetchExercisesByMarks(courseId) {
|
||||
return supabase.rpc('get_exercises').eq('course_id', courseId);
|
||||
}
|
||||
|
||||
|
||||
+205
-1
@@ -1,3 +1,207 @@
|
||||
export * from './config';
|
||||
export * from './dashboard';
|
||||
export * from './user';
|
||||
|
||||
export interface CurrentSessionType {
|
||||
access_token: string;
|
||||
expires_at: number;
|
||||
expires_in: number;
|
||||
refresh_token: string;
|
||||
token_type: string;
|
||||
}
|
||||
|
||||
export interface UserType {
|
||||
openAuthModal: boolean;
|
||||
fetchingUser: boolean;
|
||||
isLoggedIn: boolean;
|
||||
currentSession: CurrentSessionType | null;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
export interface Organization {
|
||||
id: string /* primary key */;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Profile {
|
||||
id: string /* primary key */;
|
||||
fullname: string;
|
||||
username: string;
|
||||
avatar_url?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
email?: string;
|
||||
can_add_course?: boolean;
|
||||
}
|
||||
|
||||
export interface Question_type {
|
||||
id: number /* primary key */;
|
||||
label: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
typename?: string;
|
||||
}
|
||||
|
||||
export interface Submissionstatus {
|
||||
id: number /* primary key */;
|
||||
label: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
id: string /* primary key */;
|
||||
name: string;
|
||||
description?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
organization_id?: string /* foreign key to organization.id */;
|
||||
organization?: Organization;
|
||||
}
|
||||
|
||||
export interface Role {
|
||||
type: string;
|
||||
description?: string;
|
||||
id: number /* primary key */;
|
||||
updated_at?: string;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
export interface Waitinglist {
|
||||
id: number /* primary key */;
|
||||
email: string;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
export interface Course {
|
||||
created_at?: string;
|
||||
description: string;
|
||||
group_id?: string /* foreign key to group.id */;
|
||||
group?: Group;
|
||||
id: string /* primary key */;
|
||||
is_template?: boolean;
|
||||
lessons: Lesson[];
|
||||
logo?: string;
|
||||
organization_id?: string /* foreign key to organization.id */;
|
||||
organization?: Organization;
|
||||
overview?: string;
|
||||
title: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface Groupmember {
|
||||
id: string /* primary key */;
|
||||
group_id: string /* foreign key to group.id */;
|
||||
role_id: number /* foreign key to role.id */;
|
||||
profile_id?: string /* foreign key to profile.id */;
|
||||
email?: string;
|
||||
created_at?: string;
|
||||
assigned_student_id?: string;
|
||||
group?: Group;
|
||||
role?: Role;
|
||||
profile?: Profile;
|
||||
}
|
||||
|
||||
export interface Organizationmember {
|
||||
id: number /* primary key */;
|
||||
organization_id: string /* foreign key to organization.id */;
|
||||
role_id: number /* foreign key to role.id */;
|
||||
profile_id: string /* foreign key to profile.id */;
|
||||
organization?: Organization;
|
||||
role?: Role;
|
||||
profile?: Profile;
|
||||
}
|
||||
|
||||
export interface Group_attendance {
|
||||
id: number /* primary key */;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
course_id?: string /* foreign key to course.id */;
|
||||
student_id?: string /* foreign key to groupmember.id */;
|
||||
is_present?: boolean;
|
||||
lesson_id: string;
|
||||
course?: Course;
|
||||
groupmember?: Groupmember;
|
||||
}
|
||||
|
||||
export interface Lesson {
|
||||
call_url?: string;
|
||||
course_id: string /* foreign key to course.id */;
|
||||
created_at: string;
|
||||
exercise?: Exercise[];
|
||||
id: string /* primary key */;
|
||||
is_complete?: boolean;
|
||||
lesson_at?: string;
|
||||
note?: string;
|
||||
order: number;
|
||||
profile?: Profile;
|
||||
public?: boolean;
|
||||
slide_url?: string;
|
||||
teacher_id?: string /* foreign key to profile.id */;
|
||||
title: string;
|
||||
updated_at?: string;
|
||||
video_url?: string;
|
||||
}
|
||||
|
||||
export interface Exercise {
|
||||
created_at?: string;
|
||||
description?: string;
|
||||
due_by?: string;
|
||||
id: string /* primary key */;
|
||||
lesson_id: string /* foreign key */;
|
||||
questions?: Question[];
|
||||
title: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface Question {
|
||||
created_at?: string;
|
||||
exercise_id: string /* foreign key to exercise.id */;
|
||||
id: number /* primary key */;
|
||||
name?: string;
|
||||
options: Option[];
|
||||
order?: number;
|
||||
points?: string;
|
||||
question_type_id: number /* foreign key to question_type.id */;
|
||||
title: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface Submission {
|
||||
id: string /* primary key */;
|
||||
reviewer_id?: number;
|
||||
status_id?: number /* foreign key to submissionstatus.id */;
|
||||
total?: number;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
exercise_id: string /* foreign key to exercise.id */;
|
||||
submitted_by?: string /* foreign key to groupmember.id */;
|
||||
course_id?: string /* foreign key to course.id */;
|
||||
submissionstatus?: Submissionstatus;
|
||||
exercise?: Exercise;
|
||||
groupmember?: Groupmember;
|
||||
course?: Course;
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
created_at?: string;
|
||||
id?: number /* primary key */;
|
||||
is_correct: boolean;
|
||||
is_dirty?: boolean;
|
||||
label: string;
|
||||
question_id: number /* foreign key to question.id */;
|
||||
question?: Question;
|
||||
updated_at?: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export interface Question_answer {
|
||||
id: number /* primary key */;
|
||||
answers?: any[];
|
||||
question_id: number /* foreign key to question.id */;
|
||||
open_answer?: string;
|
||||
group_member_id: string /* foreign key to groupmember.id */;
|
||||
submission_id?: string /* foreign key to submission.id */;
|
||||
point?: number;
|
||||
question?: Question;
|
||||
groupmember?: Groupmember;
|
||||
submission?: Submission;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user