Sync with Server

This commit is contained in:
rotimi-best
2021-08-18 19:47:02 +03:00
parent e8c74de9ce
commit 60f06d6ab1
77 changed files with 2989 additions and 7610 deletions
+144 -6512
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -13,6 +13,7 @@
"@supabase/supabase-js": "^1.20.0",
"@tailwindcss/forms": "^0.3.2",
"body-parser": "^1.19.0",
"canvas-confetti": "^1.4.0",
"compression": "^1.7.1",
"cookie-parser": "^1.4.5",
"express": "^4.17.1",
+1 -1
View File
@@ -104,7 +104,7 @@
<style lang="scss">
.root {
height: 90vh;
height: 93vh;
position: sticky;
top: 0;
border-left: 1px solid var(--border-color);
+21 -21
View File
@@ -1,34 +1,34 @@
<script context="module">
import ChevronDown24 from "carbon-icons-svelte/lib/ChevronDown24";
import TextEditor from "../TextEditor/index.svelte";
import ChevronDown24 from 'carbon-icons-svelte/lib/ChevronDown24';
import TextEditor from '../TextEditor/index.svelte';
let questionTitle = "";
let questionTitle = '';
let questionTags = [];
let questionDescription = "";
let questionDescription = '';
let facultyFilterOpened = true;
const faculties = [
"ИЭКСУ",
"ИМБТ",
"ИМИ",
"ИПТДМ",
"ИИИР",
"ГФ",
"ХТФ",
"ИДЗО",
"УИИ",
"УНИ",
'ИЭКСУ',
'ИМБТ',
'ИМИ',
'ИПТДМ',
'ИИИР',
'ГФ',
'ХТФ',
'ИДЗО',
'УИИ',
'УНИ',
];
function handleFacultyFilters() {
facultyFilterOpened = facultyFilterOpened ? false : true;
}
let things = [
{ id: 1, color: "darkblue" },
{ id: 2, color: "indigo" },
{ id: 3, color: "deeppink" },
{ id: 4, color: "salmon" },
{ id: 5, color: "gold" },
{ id: 1, color: 'darkblue' },
{ id: 2, color: 'indigo' },
{ id: 3, color: 'deeppink' },
{ id: 4, color: 'salmon' },
{ id: 5, color: 'gold' },
];
function handleClick() {
@@ -37,11 +37,11 @@
</script>
<svelte:head>
<title>Спросить собшество вопрос</title>
<title>Ask a question.</title>
</svelte:head>
<div class="lg:w-3/5 mx-auto md:mx-10 lg:mb-20">
<h2 class="pb-2">Спросить собшество вопрос</h2>
<h2 class="pb-2">Ask a question.</h2>
<div class="my-1 px-1">
<div class="mb-5">
<button on:click={handleClick}> Remove first thing </button>
+3
View File
@@ -0,0 +1,3 @@
<div class="w-full h-80 flex items-center justify-center flex-col">
<slot />
</div>
+3 -3
View File
@@ -1,11 +1,11 @@
<script>
import Close24 from "carbon-icons-svelte/lib/Close24";
import IconButton from "../../IconButton/index.svelte";
import Close24 from 'carbon-icons-svelte/lib/Close24';
import IconButton from '../../IconButton/index.svelte';
export let onClick;
export let contained;
</script>
<IconButton {onClick} {contained} title="Close">
<IconButton {onClick} {contained}>
<Close24 class="carbon-icon" title="Close" />
</IconButton>
+62
View File
@@ -0,0 +1,62 @@
<script>
import { onMount } from 'svelte';
import confetti from 'canvas-confetti';
import { confettiStore } from './store';
let myCanvas;
let myConfetti;
function showConfetti(show) {
if (!show) return;
const duration = 5 * 1000;
const animationEnd = Date.now() + duration;
const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 };
function randomInRange(min, max) {
return Math.random() * (max - min) + min;
}
const interval = setInterval(function () {
const timeLeft = animationEnd - Date.now();
if (timeLeft <= 0) {
return clearInterval(interval);
}
const particleCount = 50 * (timeLeft / duration);
// since particles fall down, start a bit higher than random
myConfetti(
Object.assign({}, defaults, {
particleCount,
origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 },
})
);
myConfetti(
Object.assign({}, defaults, {
particleCount,
origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 },
})
);
}, 250);
}
onMount(() => {
myConfetti = confetti.create(myCanvas, {
resize: true,
useWorker: true,
});
});
$: showConfetti($confettiStore.show);
</script>
<canvas bind:this={myCanvas} />
<style>
canvas {
position: absolute;
width: 70%;
height: 70%;
}
</style>
+12
View File
@@ -0,0 +1,12 @@
import { writable } from 'svelte/store';
export const confettiStore = writable({
show: false,
});
export function toggleConfetti() {
confettiStore.update((confetti) => ({
...confetti,
show: !confetti.show,
}));
}
@@ -1,39 +1,45 @@
<script>
import {
questionnaire,
questionnaireValidation,
handleAddOption,
handleRemoveOption,
handleRemoveQuestion,
handleCode,
handleAnswerSelect,
} from "./store/index";
import Delete24 from "carbon-icons-svelte/lib/Delete24";
import AddFilled24 from "carbon-icons-svelte/lib/AddFilled24";
import CheckmarkFilled24 from "carbon-icons-svelte/lib/CheckmarkFilled24";
import CheckmarkOutline24 from "carbon-icons-svelte/lib/CheckmarkOutline24";
addDynamicValue,
} from '../store/exercise';
import Delete24 from 'carbon-icons-svelte/lib/Delete24';
import AddFilled24 from 'carbon-icons-svelte/lib/AddFilled24';
import CheckmarkFilled24 from 'carbon-icons-svelte/lib/CheckmarkFilled24';
import CheckmarkOutline24 from 'carbon-icons-svelte/lib/CheckmarkOutline24';
// import Subtract24 from "carbon-icons-svelte/lib/Subtract24";
import TextField from "../../../../Form/TextField.svelte";
import TextArea from "../../../../Form/TextArea.svelte";
import Checkbox from "../../../../Form/Checkbox.svelte";
import RadioItem from "../../../../Form/RadioItem.svelte";
import IconButton from "../../../../IconButton/index.svelte";
import PrimaryButton from "../../../../PrimaryButton/index.svelte";
import Dropdown from "../../../../Dropdown/index.svelte";
import { VARIANTS } from "../../../../PrimaryButton/constants";
import Select from "../../../../Form/Select.svelte";
import QuestionContainer from "../../../../QuestionContainer/index.svelte";
import TextField from '../../../../Form/TextField.svelte';
import TextArea from '../../../../Form/TextArea.svelte';
import Checkbox from '../../../../Form/Checkbox.svelte';
import RadioItem from '../../../../Form/RadioItem.svelte';
import IconButton from '../../../../IconButton/index.svelte';
import ErrorMessage from '../../../../ErrorMessage/index.svelte';
import PrimaryButton from '../../../../PrimaryButton/index.svelte';
import Dropdown from '../../../../Dropdown/index.svelte';
import { VARIANTS } from '../../../../PrimaryButton/constants';
import Select from '../../../../Form/Select.svelte';
import QuestionContainer from '../../../../QuestionContainer/index.svelte';
import {
QUESTION_TYPE,
QUESTION_TYPES,
} from "../../../../Question/constants";
} from '../../../../Question/constants';
import { filterOutDeleted } from './functions';
// import EditContent from "../../../../EditContent/index.svelte";
// import readme from "../../readme.js";
// let value = readme;
const extraActions = ["Code Snippets", "Upload image"];
const extraActions = ['Code Snippets', 'Upload image'];
const initialQuestionsLength = $questionnaire.questions.length;
let errors = {};
function shouldScrollToLast(index, questions) {
const currentQuestionsLength = questions.length;
const isLast = index + 1 === currentQuestionsLength;
@@ -51,47 +57,85 @@
// uploadImage(questionIndex);
break;
default:
console.log("No option");
console.log('No option');
}
};
}
function getQuestionErrorMsg(errors, question, errorKey) {
return errors[question.id] ? errors[question.id][errorKey] : null;
}
$: errors = $questionnaireValidation;
</script>
<div class="w-auto m-auto">
<div class="w-11/12 m-auto">
<QuestionContainer isTitle={true}>
<TextField
placeholder="Title"
bind:value={$questionnaire.title}
className="mb-2"
onChange={() => {
console.log('title dirty');
$questionnaire.is_title_dirty = true;
}}
/>
<TextArea
placeholder="Description and Rules"
bind:value={$questionnaire.description}
onChange={() => {
$questionnaire.is_description_dirty = true;
}}
/>
</QuestionContainer>
{#each $questionnaire.questions as question, index}
{#if Object.values(errors).length}
<div class="w-full flex justify-center mb-4">
<ErrorMessage message="You have some errors" />
</div>
{/if}
{#each filterOutDeleted($questionnaire.questions) as question, index}
<QuestionContainer
onClose={handleRemoveQuestion(question.id)}
scrollToQuestion={shouldScrollToLast(index, $questionnaire.questions)}
bind:points={question.points}
hasError={!!errors[question.id]}
onPointsChange={() => {
question.is_dirty = true;
}}
>
<div class="flex justify-between items-center">
<div class="mr-2 w-3/5">
<TextField placeholder="Question" bind:value={question.title} />
<TextField
placeholder="Question"
bind:value={question.title}
isRequired={true}
onChange={() => {
console.log('title changed', question.title);
question.is_dirty = true;
}}
/>
</div>
<Dropdown
<!-- <Dropdown
options={extraActions}
handleOptionClick={handleOptionClick(question.id)}
>
<AddFilled24 class="carbon-icon" />
<p class="ml-2 text-gray-600">Add</p>
</Dropdown>
</Dropdown> -->
<Select bind:value={question.type} options={QUESTION_TYPES} />
<Select
bind:value={question.question_type}
options={QUESTION_TYPES}
onChange={() => {
question.is_dirty = true;
}}
/>
</div>
{#if typeof question.code === "string"}
{#if typeof question.code === 'string'}
<div class="flex justify-between items-center my-3 w-3/5">
<TextArea
bind:value={question.code}
@@ -108,12 +152,13 @@
{/if}
<div class="flex flex-col mt-2">
{#if QUESTION_TYPE.RADIO === question.type}
{#each question.options as option}
{#if QUESTION_TYPE.RADIO === question.question_type.id}
{#each filterOutDeleted(question.options) as option}
<RadioItem
isEditable={true}
name={question.title || "radio-name"}
bind:label={option.value}
name={question.title || 'radio-name'}
bind:label={option.label}
onChange={addDynamicValue(question.id, option.id)}
>
<div slot="iconbutton" class="flex items-center">
<IconButton
@@ -125,10 +170,9 @@
<IconButton
value={option.id}
onClick={handleAnswerSelect(question.id, option.id)}
buttonClassName={question.answers.includes(option.id) &&
"success"}
buttonClassName={option.is_correct && 'success'}
>
{#if question.answers.includes(option.id)}
{#if option.is_correct}
<CheckmarkFilled24 class="carbon-icon" />
{:else}
<CheckmarkOutline24 class="carbon-icon" />
@@ -139,12 +183,13 @@
{/each}
{/if}
{#if QUESTION_TYPE.CHECKBOX === question.type}
{#each question.options as option}
{#if QUESTION_TYPE.CHECKBOX === question.question_type.id}
{#each filterOutDeleted(question.options) as option}
<Checkbox
isEditable={true}
name={question || "checkbox-name"}
bind:label={option.value}
name={question || 'checkbox-name'}
bind:label={option.label}
onChange={addDynamicValue(question.id, option.id)}
>
<div slot="iconbutton" class="flex items-center">
<IconButton
@@ -156,10 +201,9 @@
<IconButton
value={option.id}
onClick={handleAnswerSelect(question.id, option.id)}
buttonClassName={question.answers.includes(option.id) &&
"success"}
buttonClassName={option.is_correct && 'success'}
>
{#if question.answers.includes(option.id)}
{#if option.is_correct}
<CheckmarkFilled24 class="carbon-icon" />
{:else}
<CheckmarkOutline24 class="carbon-icon" />
@@ -170,12 +214,18 @@
{/each}
{/if}
{#if QUESTION_TYPE.TEXTAREA === question.type}
{#if QUESTION_TYPE.TEXTAREA === question.question_type.id}
<TextArea bind:value={question.value} />
{/if}
{#if getQuestionErrorMsg(errors, question, 'option')}
<ErrorMessage
message={getQuestionErrorMsg(errors, question, 'option')}
/>
{/if}
</div>
{#if QUESTION_TYPE.TEXTAREA !== question.type}
{#if QUESTION_TYPE.TEXTAREA !== question.question_type.id}
<div class="flex items-center mt-3">
<PrimaryButton
disablePadding={true}
@@ -1,25 +1,80 @@
<script>
import Modal from "../../../../Modal/index.svelte";
import Preview from "./Preview.svelte";
import { questionnaire } from "./store";
import { questionnaireMetaData } from "./store/answers";
import Modal from '../../../../Modal/index.svelte';
import Preview from './Preview.svelte';
export let open;
export let onClose;
// export let submissionId;
// get submission data by submissionId;
export let submissionId;
export let data = {};
$: console.log("questionnaireMetaData", $questionnaireMetaData);
let grades = {};
let total = 0;
let gradeTotal = 0;
function setGrades(data) {
grades = (data.questions || []).reduce(
(acc, question) => ({ ...acc, [question.name]: question.points }),
{}
);
gradeTotal = calculateTotal(grades);
}
function calculateTotal(grades) {
return Object.values(grades).reduce(
(acc, grade) => acc + parseInt(grade),
0
);
}
$: setGrades(data);
$: total = calculateTotal(grades);
</script>
<Modal
modalHeading="Lesson 1: How to use props in React.js"
bind:open
{onClose}
width="w-3/5"
width="w-4/5"
containerClass="flex items-start"
>
<Preview
questions={$questionnaire.questions}
questionnaireMetaData={$questionnaireMetaData}
/>
<div class="w-full">
<Preview
questions={data.questions || []}
questionnaireMetaData={{
answers: data.answers || {},
isFinished: true,
}}
bind:grades
/>
</div>
<div class="ml-10 w-1/3">
<div class="border border-gray-300 rounded-md">
<div
class="hover:bg-gray-100 border-b border-t-0 border-l-0 border-r-0 border-gray-300 p-3"
>
<p class="font-bold text-lg">Details</p>
</div>
<div class="flex items-center text-sm p-3">
<p class="font-bold w-1/2">Total grade</p>
<p>{total} / {gradeTotal}</p>
</div>
<div class="flex items-center text-sm p-3">
<p class="font-bold w-1/2">Status</p>
<div class="flex items-center">
<p class="rounded-full w-5 h-5 bg-yellow-300 mr-2" />
<p>Grading</p>
</div>
</div>
<div class="flex items-center text-sm p-3">
<p class="font-bold w-1/2">Student</p>
<p>Jeremy Matt</p>
</div>
<div class="flex items-center text-sm p-3">
<p class="font-bold w-1/2">Teacher</p>
<p>rotimi-best</p>
</div>
</div>
</div>
</Modal>
@@ -5,14 +5,16 @@
import { QUESTION_TYPE } from '../../../../Question/constants';
import { getPropsForQuestion } from './functions';
export let questions;
export let questionnaireMetaData;
export let questions = [];
export let questionnaireMetaData = {};
export let grades = {};
export let onSubmit = () => {};
export let onPrevious = () => {};
export let handleGrade = () => {};
</script>
{#each questions as currentQuestion, currentQuestionIndex}
{#if QUESTION_TYPE.RADIO === currentQuestion.type}
{#if QUESTION_TYPE.RADIO === currentQuestion.question_type.id}
<RadioQuestion
{...getPropsForQuestion(
questions,
@@ -23,8 +25,11 @@
onPrevious,
true
)}
bind:grade={grades[currentQuestion.name]}
gradeMax={currentQuestion.points}
handleGrade={handleGrade(currentQuestion.name)}
/>
{:else if QUESTION_TYPE.CHECKBOX === currentQuestion.type}
{:else if QUESTION_TYPE.CHECKBOX === currentQuestion.question_type.id}
<CheckboxQuestion
{...getPropsForQuestion(
questions,
@@ -35,8 +40,11 @@
onPrevious,
true
)}
bind:grade={grades[currentQuestion.name]}
gradeMax={currentQuestion.points}
handleGrade={handleGrade(currentQuestion.name)}
/>
{:else if QUESTION_TYPE.TEXTAREA === currentQuestion.type}
{:else if QUESTION_TYPE.TEXTAREA === currentQuestion.question_type.id}
<TextareaQuestion
{...getPropsForQuestion(
questions,
@@ -47,6 +55,9 @@
onPrevious,
true
)}
bind:grade={grades[currentQuestion.name]}
gradeMax={currentQuestion.points}
handleGrade={handleGrade(currentQuestion.name)}
/>
{/if}
{/each}
@@ -1,6 +1,8 @@
<script>
import { questionnaire } from './store/index';
import { questionnaireMetaData } from './store/answers';
import { fade, fly, slide } from 'svelte/transition';
import { group, course } from '../../../store';
import { questionnaire } from '../store/exercise';
import { questionnaireMetaData } from '../store/answers';
import Preview from './Preview.svelte';
import RadioQuestion from '../../../../Question/RadioQuestion/index.svelte';
import CheckboxQuestion from '../../../../Question/CheckboxQuestion/index.svelte';
@@ -10,10 +12,13 @@
import { removeDuplicate } from '../../../../../utils/functions/removeDuplicate';
import { QUESTION_TYPE } from '../../../../Question/constants';
import { STATUS } from './constants';
import { wasCorrectAnswerSelected, getPropsForQuestion } from './functions';
import { getPropsForQuestion, filterOutDeleted } from './functions';
import { submitExercise } from '../../../../../utils/services/courses';
import { profile } from '../../../../../utils/store/user';
export let preview;
export let exerciseId;
let currentQuestion = {};
let renderProps = {};
@@ -21,22 +26,44 @@
$questionnaireMetaData.currentQuestionIndex += 1;
}
function onSubmit(id, value) {
const prevAnswer = $questionnaireMetaData.answers[id] || [];
function onSubmit(id, value, moveToNextQuestion = false) {
const { answers } = $questionnaireMetaData;
const { questions } = $questionnaire;
const prevAnswer = answers[id] || [];
const formattedAnswer =
typeof value === 'string'
? value
: removeDuplicate([...prevAnswer, ...(value || [])]);
$questionnaireMetaData.answers = {
...$questionnaireMetaData.answers,
...answers,
[id]: formattedAnswer,
};
if (
wasCorrectAnswerSelected(currentQuestion, $questionnaireMetaData.answers)
) {
if (moveToNextQuestion) {
$questionnaireMetaData.currentQuestionIndex += 1;
}
const isFinished =
!questions[$questionnaireMetaData.currentQuestionIndex - 1];
console.log(`isFinished`, isFinished);
console.log(
`$questionnaireMetaData.currentQuestionIndex`,
$questionnaireMetaData.currentQuestionIndex
);
// If last question send to server
if (isFinished) {
submitExercise(
$questionnaireMetaData.answers,
questions,
exerciseId,
$course.id,
$group,
$profile
);
}
}
function onPrevious() {
@@ -95,7 +122,7 @@
{#if preview}
<Preview
questions={$questionnaire.questions}
questions={filterOutDeleted($questionnaire.questions)}
questionnaireMetaData={$questionnaireMetaData}
/>
{:else if !$questionnaire.questions.length}
@@ -151,10 +178,42 @@
questions={$questionnaire.questions}
questionnaireMetaData={$questionnaireMetaData}
/>
{:else if QUESTION_TYPE.RADIO === currentQuestion.type}
<RadioQuestion {...renderProps} />
{:else if QUESTION_TYPE.CHECKBOX === currentQuestion.type}
<CheckboxQuestion {...renderProps} />
{:else if QUESTION_TYPE.TEXTAREA === currentQuestion.type}
<TextareaQuestion {...renderProps} />
{:else if currentQuestion}
{#key currentQuestion.id}
<!-- <div transition:fade id="question"> -->
<div in:fly={{ x: 500, duration: 1000 }} id="question">
{#if QUESTION_TYPE.RADIO === currentQuestion.question_type.id}
<RadioQuestion {...renderProps} key={currentQuestion.id} />
{:else if QUESTION_TYPE.CHECKBOX === currentQuestion.question_type.id}
<CheckboxQuestion {...renderProps} key={currentQuestion.id} />
{:else if QUESTION_TYPE.TEXTAREA === currentQuestion.question_type.id}
<TextareaQuestion {...renderProps} key={currentQuestion.id} />
{/if}
</div>
{/key}
{/if}
<style>
:global(.shake) {
animation: shake 0.85s;
}
@keyframes shake {
10%,
90% {
transform: translate3d(-15px, 0, 0);
}
20%,
80% {
transform: translate3d(15px, 0, 0);
}
30%,
50%,
70% {
transform: translate3d(-15px, 0, 0);
}
40%,
60% {
transform: translate3d(15px, 0, 0);
}
}
</style>
@@ -1,26 +1,35 @@
import { QUESTION_TYPE } from '../../../../Question/constants';
import { toggleConfetti } from '../../../../Confetti/store';
export function wasCorrectAnswerSelected(currentQuestion, answers) {
if (currentQuestion.type === QUESTION_TYPE.TEXTAREA) {
export const isAnswerCorrect = (options, answer) => {
return options.some((option) => option.is_correct && option.value === answer);
};
export function wasCorrectAnswerSelected(currentQuestion, answers, isFinished) {
if (currentQuestion.question_type.id === QUESTION_TYPE.TEXTAREA) {
return true;
}
const answer = answers[currentQuestion.id];
const answer = answers[currentQuestion.name];
const formattedAnswers = typeof answer === 'string' ? [answer] : answer;
return (
const isCorrect =
formattedAnswers &&
formattedAnswers.some((answer) => {
if (
!currentQuestion.answers ||
!currentQuestion.answers.length ||
currentQuestion.answers.includes(answer)
) {
if (isAnswerCorrect(currentQuestion.options, answer)) {
return true;
}
return false;
})
);
});
if (isCorrect && !isFinished) {
toggleConfetti();
setTimeout(toggleConfetti, 1500);
}
return isCorrect;
}
export function getPropsForQuestion(
@@ -33,32 +42,42 @@ export function getPropsForQuestion(
preview
) {
const { answers, isFinished } = questionnaireMetaData;
const isLast = questionIndex === questions.length;
const isOpenQuesiton = question.type === QUESTION_TYPE.TEXTAREA;
const isOpenQuesiton = question.question_type.id === QUESTION_TYPE.TEXTAREA;
const isCorrect = wasCorrectAnswerSelected(question, answers, isFinished);
if (!isCorrect && document && document.getElementById('question')) {
const questionElement = document.getElementById('question');
// Shake if wrong answer was selected
questionElement.classList.toggle('shake');
setTimeout(() => {
// Remove so we can trigger animation again
questionElement.classList.toggle('shake');
}, 200);
}
return {
title: questionIndex + '. ' + question.title,
name: `${question.id}`,
name: `${question.name}`,
options: question.options,
answers: question.answers,
code: question.code,
defaultValue: isOpenQuesiton
? answers[question.id] || ''
: answers[question.id] || [],
? answers[question.name] || ''
: answers[question.name] || [],
onSubmit,
onPrevious,
disablePreviousButton: questionIndex === 1,
isLast,
isPreview: preview || isFinished,
disabled:
(isFinished && wasCorrectAnswerSelected(question, answers)) || isFinished,
isCorrect,
disabled: (isFinished && isCorrect) || isFinished,
nextButtonProps: isOpenQuesiton
? {
label: 'Next',
isActive: true,
}
: wasCorrectAnswerSelected(question, answers)
: isCorrect
? {
label: isLast ? 'Finish' : 'Next',
isActive: true,
@@ -70,3 +89,7 @@ export function getPropsForQuestion(
},
};
}
export function filterOutDeleted(array) {
return array.filter((item) => !item.deleted_at);
}
@@ -1,31 +1,53 @@
<script>
import { handleAddQuestion } from './store/index';
import { questionnaireMetaData } from './store/answers';
import { onDestroy } from 'svelte';
import AddAlt16 from 'carbon-icons-svelte/lib/AddAlt16';
import {
handleAddQuestion,
questionnaire,
validateQuestionnaire,
reset,
} from '../store/exercise';
import PrimaryButton from '../../../../PrimaryButton/index.svelte';
import PageBody from '../../../../PageBody/index.svelte';
import ViewMode from './ViewMode.svelte';
import EditMode from './EditMode.svelte';
import MODES from '../../../../../utils/constants/mode.js';
import { VARIANTS } from '../../../../PrimaryButton/constants';
import { upsertExercise } from '../../../../../utils/services/courses';
// export let exerciseId;
export let exerciseId;
let mode = MODES.view;
let preview = false;
function handleMode() {
mode = mode === MODES.edit ? MODES.view : MODES.edit;
preview = false;
async function handleMode() {
if (mode === MODES.edit) {
const errors = validateQuestionnaire($questionnaire.questions);
if (Object.values(errors).length > 0) {
return;
}
if (mode === MODES.view) {
questionnaireMetaData.update((metaData) => {
metaData.answers = {};
metaData.currentQuestionIndex = 0;
metaData.isFinished = false;
return metaData;
});
mode = MODES.view;
reset();
const updatedQuestions = await upsertExercise($questionnaire, exerciseId);
questionnaire.update((q) => ({
...q,
is_title_dirty: false,
is_description_dirty: false,
questions: updatedQuestions,
}));
} else {
mode = MODES.edit;
preview = false;
}
}
onDestroy(() => {
reset();
});
</script>
<PageBody>
@@ -48,11 +70,9 @@
{#if mode === MODES.edit}
<div class="flex items-center">
<PrimaryButton
variant={VARIANTS.OUTLINED}
onClick={handleAddQuestion}
label="Add Question"
/>
<PrimaryButton variant={VARIANTS.OUTLINED} onClick={handleAddQuestion}>
Add Question <AddAlt16 class="carbon-icon" />
</PrimaryButton>
</div>
{/if}
</svelte:fragment>
@@ -60,6 +80,6 @@
{#if mode === MODES.edit && !preview}
<EditMode />
{:else}
<ViewMode {preview} />
<ViewMode {preview} {exerciseId} />
{/if}
</PageBody>
@@ -1,158 +0,0 @@
import { writable } from 'svelte/store';
import {
QUESTION_TEMPLATE,
QUESTION_TYPE,
} from '../../../../../Question/constants';
export const questionnaire = writable({
title: '',
description: '',
questions: [],
});
export function handleAddQuestion() {
questionnaire.update((q) => {
const { questions } = q;
return {
...q,
questions: [
...questions,
{
...QUESTION_TEMPLATE,
id: `${questions.length + 1}-form`,
value: '',
answers: [],
options: [
{
id: '1',
value: null,
},
],
},
],
};
});
}
export function handleAddOption(questionId) {
return () => {
questionnaire.update((_questionnaire) => {
const { questions } = _questionnaire;
return {
..._questionnaire,
questions: questions.map((question) => {
if (question.id === questionId) {
return {
...question,
options: [
...question.options,
{
id: question.options[question.options.length - 1].id + 1,
value: '',
},
],
};
}
return question;
}),
};
});
};
}
export function handleRemoveOption(questionId, optionId) {
return () => {
questionnaire.update((q) => {
const { questions } = q;
return {
...q,
questions: questions.map((question) => {
if (question.id === questionId) {
return {
...question,
options: [
...question.options.filter((option) => option.id !== optionId),
],
};
}
return question;
}),
};
});
};
}
export function handleRemoveQuestion(questionId) {
return () => {
questionnaire.update((q) => {
const { questions } = q;
return {
...q,
questions: questions.filter((q) => q.id !== questionId),
};
});
};
}
export function handleCode(questionId, shouldAdd = true) {
questionnaire.update((q) => {
const { questions } = q;
return {
...q,
questions: questions.map((question) => {
if (question.id === questionId) {
return {
...question,
code: shouldAdd ? question.code || '' : undefined,
};
}
return question;
}),
};
});
}
export function handleAnswerSelect(questionId, optionId) {
return () => {
questionnaire.update((q) => {
const { questions } = q;
return {
...q,
questions: questions.map((question) => {
if (question.id === questionId) {
const selectOption = question.options.find(
(option) => option.id === optionId
);
const newAnswers = [];
if (question.type === QUESTION_TYPE.RADIO) {
newAnswers.push(optionId);
} else if (question.answers.includes(selectOption.id)) {
newAnswers.push(
...question.answers.filter(
(answer) => answer !== selectOption.id
)
);
} else {
newAnswers.push(...question.answers, selectOption.id);
}
return {
...question,
answers: newAnswers,
};
}
return question;
}),
};
});
};
}
@@ -1,32 +1,37 @@
<script>
import { onMount } from 'svelte';
import PrimaryButton from '../../../../PrimaryButton/index.svelte';
import PageBody from '../../../../PageBody/index.svelte';
import Modal from '../../../../Modal/index.svelte';
import TextField from '../../../../Form/TextField.svelte';
import Exercise from '../Exercise/index.svelte';
import { supabase } from '../../../../../utils/functions/supabase';
import { createExercise } from '../../../../../utils/services/courses';
import { QUESTION_TYPES } from '../../../../Question/constants';
import { lesson } from '../store/lessons';
import { questionnaire, isQuestionnaireFetching } from '../store/exercise';
export let path;
export let exerciseId;
export let lessonId;
let open = false;
let newExercise = {
id: 1,
title: '',
link: '',
description: '',
};
function handleAddExercise() {
async function handleAddExercise() {
const { data, error } = await createExercise({
title: newExercise.title,
lesson_id: lessonId,
});
console.log(`data, error `, data, error);
lesson.update((_lesson) => ({
..._lesson,
exercises: [
..._lesson.exercises,
{
...newExercise,
id: _lesson.exercises.length + 1,
},
],
exercises: [..._lesson.exercises, data[0]],
}));
handleCancelAddExercise();
@@ -37,9 +42,63 @@
newExercise = {
id: $lesson.exercises.length + 1,
title: '',
link: '',
description: '',
};
}
async function getExercises() {
const exercisesData = await supabase
.from('exercise')
.select(`id, title, created_at`)
.match({ lesson_id: lessonId });
$lesson.exercises = exercisesData.data;
}
async function getExercise(exerciseId) {
console.log('getExercise', exerciseId);
if (!exerciseId) return;
isQuestionnaireFetching.update(() => true);
const { data } = await supabase
.from('exercise')
.select(
`
id, title, description,
questions:question(
*,
options:option(*),
question_type:question_type_id(id, label)
)
`
)
.eq('id', exerciseId)
.single();
// Need to set the question type inorder for the select in the questionnaire builder to match
data.questions.forEach((question) => {
question.question_type = QUESTION_TYPES.find(
(type) => type.id === question.question_type.id
);
return question;
});
data.is_title_dirty = false;
data.is_description_dirty = false;
questionnaire.set(data);
console.log('getExercise', exerciseId, $questionnaire);
isQuestionnaireFetching.update(() => false);
}
onMount(() => {
getExercises();
});
$: getExercise(exerciseId);
</script>
{#if exerciseId}
@@ -49,6 +108,7 @@
onClose={handleCancelAddExercise}
bind:open
modalHeading="Create an Exercise"
width="w-80"
>
<TextField
bind:value={newExercise.title}
@@ -1,9 +1,14 @@
<script>
import { onMount, onDestroy } from 'svelte';
import Tabs from '../../../../Tabs/index.svelte';
import TabContent from '../../../../TabContent/index.svelte';
import EditContent from '../../../../EditContent/index.svelte';
import TextField from '../../../../Form/TextField.svelte';
import { lesson } from '../store/lessons';
import {
lesson,
handleUpdateLessonMaterials,
isLessonDirty,
} from '../store/lessons';
const tabs = [
{
@@ -26,6 +31,19 @@
currentTab = tabValue;
};
}
onMount(() => {
isLessonDirty.set(false);
});
onDestroy(() => {
if (!$isLessonDirty) {
return;
}
handleUpdateLessonMaterials($lesson);
});
$: console.log(`isLessonDirty`, $isLessonDirty);
</script>
<Tabs {tabs} {currentTab} {onChange}>
@@ -35,13 +53,22 @@
writeLabel="Note"
bind:value={$lesson.materials.note}
placeholder="Start typing your lesson"
onInputChange={() => ($isLessonDirty = true)}
/>
</TabContent>
<TabContent value={tabs[1].value} index={currentTab}>
<TextField label="Slide link" bind:value={$lesson.materials.slide} />
<TextField
label="Slide link"
bind:value={$lesson.materials.slide_url}
onChange={() => ($isLessonDirty = true)}
/>
</TabContent>
<TabContent value={tabs[2].value} index={currentTab}>
<TextField label="Youtube link" bind:value={$lesson.materials.video} />
<TextField
label="Youtube link"
bind:value={$lesson.materials.video_url}
onChange={() => ($isLessonDirty = true)}
/>
</TabContent>
</slot:fragment>
</Tabs>
@@ -2,6 +2,7 @@
import Tabs from '../../../../Tabs/index.svelte';
import TabContent from '../../../../TabContent/index.svelte';
import MarkdownRender from '../../../../MarkdownRender/index.svelte';
import Box from '../../../../Box/index.svelte';
import { lesson } from '../store/lessons';
const tabs = [
@@ -30,30 +31,51 @@
<Tabs {tabs} {currentTab} {onChange}>
<slot:fragment slot="content">
<TabContent value={tabs[0].value} index={currentTab}>
<MarkdownRender content={$lesson.materials.note} />
{#if $lesson.materials.note}
<MarkdownRender content={$lesson.materials.note} />
{:else}
<Box>
<h3 class="text-3xl text-gray-500">No note added</h3>
<img alt="Video not found" src="/notfound.webp" class="w-80" />
</Box>
{/if}
</TabContent>
<TabContent value={tabs[1].value} index={currentTab}>
<iframe
title="Embeded Slides"
src={$lesson.materials.slide}
frameborder="0"
width="100%"
height="569"
allowfullscreen="true"
mozallowfullscreen="true"
webkitallowfullscreen="true"
/>
{#if $lesson.materials.slide_url}
<iframe
title="Embeded Slides"
src={$lesson.materials.slide_url}
frameborder="0"
width="100%"
height="569"
allowfullscreen="true"
mozallowfullscreen="true"
webkitallowfullscreen="true"
/>
{:else}
<Box>
<h3 class="text-3xl text-gray-500">No slide added</h3>
<img alt="Video not found" src="/notfound.webp" class="w-80" />
</Box>
{/if}
</TabContent>
<TabContent value={tabs[2].value} index={currentTab}>
<iframe
width="100%"
height="569"
src={$lesson.materials.video}
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
/>
{#if $lesson.materials.video_url}
<iframe
width="100%"
height="569"
src={$lesson.materials.video_url}
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
/>
{:else}
<Box>
<h3 class="text-3xl text-gray-500">No youtube video added</h3>
<img alt="Video not found" src="/notfound.webp" class="w-80" />
</Box>
{/if}
</TabContent>
</slot:fragment>
</Tabs>
@@ -1,6 +1,6 @@
import { writable } from 'svelte/store';
import { STATUS } from '../constants';
import { STATUS } from '../Exercise/constants';
// export const questionnaireMetaData = writable({
// answers: {},
@@ -0,0 +1,243 @@
import { writable } from 'svelte/store';
import {
QUESTION_TEMPLATE,
QUESTION_TYPE,
} from '../../../../Question/constants';
import { questionnaireMetaData } from './answers';
import { isUUID } from '../../../../../utils/functions/isUUID';
export const isQuestionnaireFetching = writable(false);
// {'question-id': { option: '', title: '', ... }}
export const questionnaireValidation = writable({});
export const questionnaire = writable({
title: '',
is_title_dirty: false,
description: '',
is_description_dirty: false,
questions: [],
});
export function reset() {
questionnaireMetaData.update((metaData) => {
metaData.answers = {};
metaData.currentQuestionIndex = 0;
metaData.isFinished = false;
return metaData;
});
}
export function validateQuestionnaire(questions) {
const errors = {};
for (const question of questions) {
if (
question.question_type.id === QUESTION_TYPE.TEXTAREA ||
question.deleted_at
) {
continue;
}
const qErrors = errors[question.id] || {};
const hasAnswer = question.options
.filter((o) => !o.deleted_at)
.some((option) => option.is_correct);
if (!hasAnswer) {
qErrors.option = 'Please mark an option as the correct answer';
errors[question.id] = { ...qErrors };
}
}
questionnaireValidation.set(errors);
return errors;
}
export function handleAddQuestion() {
questionnaire.update((q) => {
const { questions } = q;
return {
...q,
questions: [
...questions,
{
...QUESTION_TEMPLATE,
id: `${questions.length + 1}-form`,
name: `${questions.length + 1}-form`,
value: '',
points: 0,
question_type: {
id: QUESTION_TYPE.RADIO,
},
options: [
{
id: '1-form',
label: '',
value: null,
is_correct: false,
},
],
},
],
};
});
}
export function handleAddOption(questionId) {
return () => {
questionnaire.update((_questionnaire) => {
const { questions } = _questionnaire;
return {
..._questionnaire,
questions: questions.map((question) => {
if (question.id === questionId) {
return {
...question,
options: [
...question.options,
{
id: `${question.options.length + 1}-form`,
label: '',
value: '',
is_correct: false,
},
],
};
}
return question;
}),
};
});
};
}
export function handleRemoveOption(questionId, optionId) {
return () => {
questionnaire.update((q) => {
const { questions } = q;
return {
...q,
questions: questions.map((question) => {
if (question.id === questionId) {
return {
...question,
options: question.options.map((option) => {
if (option.id === optionId) {
option.deleted_at = new Date();
}
return option;
}),
};
}
return question;
}),
};
});
};
}
export function handleRemoveQuestion(questionId) {
return () => {
questionnaire.update((q) => {
const { questions } = q;
return {
...q,
questions: questions.map((q) => {
if (q.id === questionId) {
q.deleted_at = new Date();
}
return q;
}),
};
});
};
}
export function handleCode(questionId, shouldAdd = true) {
questionnaire.update((q) => {
const { questions } = q;
return {
...q,
questions: questions.map((question) => {
if (question.id === questionId) {
return {
...question,
code: shouldAdd ? question.code || '' : undefined,
};
}
return question;
}),
};
});
}
export function handleAnswerSelect(questionId, optionId) {
return () => {
questionnaire.update((q) => {
const { questions } = q;
return {
...q,
questions: questions.map((question) => {
if (question.id === questionId) {
return {
...question,
options: question.options.map((option) => {
if (option.id === optionId) {
option.is_correct = !option.is_correct;
option.is_dirty = true;
}
return option;
}),
};
}
return question;
}),
};
});
};
}
export function addDynamicValue(questionId, optionId) {
return (e) => {
questionnaire.update((q) => {
const { questions } = q;
return {
...q,
questions: questions.map((question) => {
if (question.id === questionId) {
return {
...question,
options: question.options.map((option) => {
if (option.id === optionId) {
const label = e.target.value;
if (!isUUID(option.value)) {
option.value = label.split(' ').join('-');
}
option.label = label;
option.is_dirty = true;
}
return option;
}),
};
}
return question;
}),
};
});
};
}
@@ -1,8 +1,13 @@
import { writable } from 'svelte/store';
import {
createLesson,
updateLesson,
deleteLesson,
} from '../../../../../utils/services/courses';
export const lessons = writable([]);
export const lesson = writable({
lesson_id: null,
id: null,
materials: {
note: '',
slide: '',
@@ -10,6 +15,7 @@ export const lesson = writable({
},
exercises: [],
});
export const isLessonDirty = writable(false);
export const tutors = writable([
{
@@ -35,35 +41,71 @@ export const tutors = writable([
]);
export function handleAddLesson() {
const resources = [
{
label: 'lesson',
value: 0,
},
{
label: 'quiz',
value: 0,
},
{
label: 'home task',
value: 0,
},
];
lessons.update((_lessons) => {
return [
..._lessons,
{
id: null,
title: 'Untitled lesson',
to: '/courses/1/lessons/' + (_lessons.length + 1),
resources,
tutor: {
userId: 1,
avatar: 'https://picsum.photos/32/32/?random',
name: '',
},
date: new Date(),
profile: undefined,
call_url: undefined,
lesson_at: new Date(),
is_complete: false,
},
];
});
}
export function handleDelete(lessonId) {
return async () => {
// Need to implement soft delete
if (lessonId) {
const { error } = await deleteLesson(lessonId);
if (error) {
return alert('Please delete all exercises first');
}
}
lessons.update((_lessons) =>
_lessons.filter((lesson) => lesson.id !== lessonId)
);
console.log(`lessonId`, lessonId);
};
}
export async function handleSaveLesson(lesson, course_id) {
const newLesson = {
title: lesson.title,
lesson_at: lesson.lesson_at,
call_url: lesson.call_url,
teacher_id: lesson.profile ? lesson.profile.id : null,
course_id,
is_complete: lesson.is_complete,
};
let updatedLesson;
if (typeof lesson.id === 'string') {
// No need to get the result of update cause we have all in local state
await updateLesson(newLesson, lesson.id);
} else {
const { data } = await createLesson(newLesson);
updatedLesson = data;
}
lessons.update((l) =>
l.map((lesson) => {
if (!lesson.id && updatedLesson) {
lesson.id = updatedLesson[0].id;
}
return lesson;
})
);
}
export async function handleUpdateLessonMaterials(lesson) {
return await updateLesson(lesson.materials, lesson.id);
}
@@ -15,11 +15,11 @@
} from '../../function';
import { lessons } from '../Lesson/store/lessons';
import { course } from '../../store';
import { updateCourse } from '../../../../utils/services/courses';
// export let lessonId;
export let courseId;
export let path;
let show = null;
const { page } = stores();
@@ -30,43 +30,15 @@
};
}
const navItems = [
{
label: 'Overview',
to: getNavItemRoute(courseId),
hideSortIcon: true,
},
{
label: 'Lessons',
to: getLessonsRoute(courseId),
hideSortIcon: false,
isLecture: true,
},
{
label: 'All Exercises',
to: getNavItemRoute(courseId, 'allexercises'),
hideSortIcon: true,
},
{
label: 'Scoreboard',
to: getNavItemRoute(courseId, 'scoreboard'),
hideSortIcon: true,
},
{
label: 'People',
to: getNavItemRoute(courseId, 'people'),
hideSortIcon: true,
},
{
label: 'Timetable',
to: getNavItemRoute(courseId, 'timetable'),
hideSortIcon: true,
},
];
let navItems = [];
function getHref(item) {
return item.to;
// return item.isComplete ? item.to : path || $page.path
// return item.is_complete ? item.to : path || $page.path
}
function handleSaveTitle() {
updateCourse($course.id, { title: $course.title });
}
onMount(() => {
@@ -87,11 +59,51 @@
// localStorage.setItem('hideCourseNav', `${!show}`);
// }
// }
$: {
navItems = [
{
label: 'Overview',
to: getNavItemRoute($course.id),
hideSortIcon: true,
},
{
label: 'Lessons',
to: getLessonsRoute($course.id),
hideSortIcon: false,
isLecture: true,
},
{
label: 'Submissions',
to: getNavItemRoute($course.id, 'submissions'),
hideSortIcon: true,
},
{
label: 'Scoreboard',
to: getNavItemRoute($course.id, 'scoreboard'),
hideSortIcon: true,
},
{
label: 'People',
to: getNavItemRoute($course.id, 'people'),
hideSortIcon: true,
},
{
label: 'Timetable',
to: getNavItemRoute($course.id, 'timetable'),
hideSortIcon: true,
},
];
}
</script>
<div class="root z-10 {!show && 'hide'}">
{#if show}
<PageNav title="React.JS" />
<PageNav
bind:title={$course.title}
onEditComplete={handleSaveTitle}
isTitleEditable={true}
/>
<div>
{#each navItems as navItem}
<Expandable
@@ -103,16 +115,15 @@
{#if navItem.isLecture}
{#each $lessons as item, index}
<a
class="item flex items-center {(item.to ===
(path || $page.path) ||
!(path || $page.path).length > 3) &&
'active'} pl-7 py-2 {!item.isComplete &&
class="item flex items-center {(path || $page.path).includes(
item.id
) && 'active'} pl-7 py-2 {!item.is_complete &&
'cursor-not-allowed'}"
href={getHref(item)}
href={getLessonsRoute($course.id, item.id)}
>
<span class="course-counter"> {getLectureNo(index + 1)} </span>
<span>{item.title}</span>
{#if item.isComplete}
{#if item.is_complete}
<span class="ml-2 success">
<CheckmarkFilled20 class="carbon-icon" />
</span>
@@ -147,7 +158,7 @@
<style lang="scss">
.root {
height: 90vh;
height: 93vh;
display: flex;
flex-direction: column;
min-width: 360px;
+46 -14
View File
@@ -1,22 +1,54 @@
<script>
import marked from "marked";
import PageNav from "../PageNav/index.svelte";
import marked from 'marked';
import PageNav from '../PageNav/index.svelte';
import MODES from '../../utils/constants/mode';
import PrimaryButton from '../PrimaryButton/index.svelte';
import EditContent from '../EditContent/index.svelte';
import { course } from '../Course/store';
import { updateCourse } from '../../utils/services/courses';
const readme = `
# Introduction
Welcome to this course, we are glad you made it here
let mode = MODES.view;
let isDirty = false;
## What you'l learn
1. Build powerful, fast, user-friendly and reactive web apps.
2. Provide amazing user experiences by leveraging the power of JavaScript with ease.
3. Apply for high-paid jobs or work as a freelancer in one the most-demanded sectors you can find in web dev right now.
4. Learn all about React Hooks and React Components.
`;
async function handleModeChange() {
if (mode === MODES.edit) {
mode = MODES.view;
if (isDirty) {
const { overview, id } = $course;
const { data, error } = await updateCourse(id, { overview });
console.log(`data`, data);
console.log(`error`, error);
}
} else {
mode = MODES.edit;
}
}
</script>
<PageNav title="Overview" disableSticky={true} />
<PageNav title="Overview" disableSticky={true}>
<svelte:fragment slot="widget">
<div class="flex items-center">
<PrimaryButton
className="mr-2"
label={mode === MODES.edit ? 'Save' : 'Edit'}
onClick={handleModeChange}
/>
</div>
</svelte:fragment>
</PageNav>
<div class="px-3">
{@html marked(readme, { breaks: true, gfm: true })}
{#if mode === MODES.edit}
<EditContent
writeLabel="Note"
bind:value={$course.overview}
placeholder="Start typing your lesson"
onInputChange={() => {
isDirty = true;
console.log('input change');
}}
/>
{:else}
{@html marked($course.overview || '', { breaks: true, gfm: true })}
{/if}
</div>
+63
View File
@@ -0,0 +1,63 @@
import { writable } from 'svelte/store';
import { lessons } from './components/Lesson/store/lessons';
import { ROLE } from '../../utils/constants/roles';
export const course = writable({});
export const mockGroupMember = {
id: '434534534535',
group_id: '434534534535',
role_id: 1,
profile_id: 'vcvbcvbcvbb----434534534535',
email: null,
profile: {
id: 'vcvbcvbcvbb----434534534535',
fullname: 'Rotimi Best',
username: 'robertblake',
avatar_url: '',
created_at: '2021-08-08T13:42:13+00:00',
updated_at: '2021-08-08T13:42:13+00:00',
},
};
export const group = writable({
id: null,
tutors: [],
students: [],
});
export async function setCourseData(data) {
if (!data || !(Object.values(data) && Object.values(data).length)) return;
console.log(`data`, data);
if (data.group) {
const groupData = Object.assign(data.group, { tutors: [], students: [] });
if (Array.isArray(groupData.members)) {
for (const member of groupData.members) {
if (member.role_id === ROLE.STUDENT) {
groupData.students.push(member);
} else if (member.profile) {
groupData.tutors.push({
...member,
...member.profile,
id: member.id,
});
}
}
}
delete groupData.members;
group.set(groupData);
}
const orderedLessons = (data.lessons || []).sort(
(a, b) => new Date(a.created_at) - new Date(b.created_at)
);
// .map();
lessons.set(orderedLessons);
delete data.lessons;
course.set(data);
}
+20 -17
View File
@@ -2,24 +2,24 @@
import { onMount, afterUpdate } from 'svelte';
import Navigation from '../Course/components/Navigation/index.svelte';
import { lessons, lesson } from '../Course/components/Lesson/store/lessons';
import { questionnaire } from '../Course/components/Lesson/Exercise/store';
import { questionnaireMetaData } from '../Course/components/Lesson/Exercise/store/answers';
// 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';
import { course } from '../Course/store';
import Confetti from '../Confetti/index.svelte';
export let courseData = {};
export let path;
export let isExercisePage = false;
function setStoreWithData() {
lessons.set(courseData.lessons);
if (courseData.lesson) {
lesson.set(courseData.lesson);
}
if (courseData.exercise && courseData.exercise.data) {
questionnaire.set(courseData.exercise.data);
questionnaireMetaData.set(courseData.exercise.answers);
}
// lessons.set(courseData.lessons);
// if (courseData.lesson) {
// lesson.set(courseData.lesson);
// }
// if (courseData.exercise && courseData.exercise.data) {
// questionnaire.set(courseData.exercise.data);
// questionnaireMetaData.set(courseData.exercise.answers);
// }
}
onMount(setStoreWithData);
@@ -28,12 +28,15 @@
</script>
<svelte:head>
<title>{courseData.name}</title>
<title>{$course.title || 'ClassroomIO Course'}</title>
</svelte:head>
<div class="root">
<Navigation courseId={courseData.id} {path} />
<Navigation {path} />
<div class="rightBar">
{#if isExercisePage}
<Confetti />
{/if}
<slot />
</div>
</div>
@@ -46,6 +49,6 @@
.rightBar {
flex-grow: 1;
width: calc(100% - 350px);
width: calc(100% - 360px);
}
</style>
+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 '../../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,
};
}
+9 -24
View File
@@ -1,36 +1,21 @@
<script>
import Education16 from "carbon-icons-svelte/lib/Education16";
import Catalog16 from "carbon-icons-svelte/lib/Catalog16";
import Chip from "../Chip/index.svelte";
export let courses = [
{
chipClassName: "bg-green-600 text-white",
chipLabel: "In progress",
},
{
chipClassName: "bg-red-600 text-white",
chipLabel: "Completed",
},
];
import Education16 from 'carbon-icons-svelte/lib/Education16';
import Catalog16 from 'carbon-icons-svelte/lib/Catalog16';
// import Chip from '../Chip/index.svelte';
import { courses } from './store';
</script>
<div class="courses">
{#each courses as courseData, index}
{#each $courses as courseData}
<div class="course">
<h4 class="course-title">
<a rel="prefetch" href="courses/1"
>QA Automation 0{courses.length - index}</a
>
<Chip
<a rel="prefetch" href="/courses/{courseData.id}">{courseData.title}</a>
<!-- <Chip
value={courseData.chipLabel}
className={courseData.chipClassName}
/>
/> -->
</h4>
<p>
Here is some description about the course that you should know. It will
begin on the first and end on the second of the next month.
</p>
<p>{courseData.description}</p>
<div class="flex mt-3 mb-2">
<span class="flex mr-2"
><Education16 class="mr-2" />
+12
View File
@@ -0,0 +1,12 @@
import { writable } from 'svelte/store';
export const courses = writable([]);
export const createCourseModal = writable({
open: false,
title: '',
description: '',
emails: '',
tutors: '',
students: '',
});
+14 -3
View File
@@ -1,11 +1,12 @@
<script>
import marked from "marked";
import marked from 'marked';
let isWriteMode = true;
export let writeLabel = 'Write';
export let value;
export let placeholder = ``;
const activeClassName = "border-b-2 border-blue-700";
export let onInputChange = () => {};
const activeClassName = 'border-b-2 border-blue-700';
const handleTabClick = (tab) => () => {
isWriteMode = tab === 1;
@@ -33,6 +34,7 @@
bind:value
{placeholder}
class="border border-gray rounded-md p-2"
on:change={onInputChange}
/>
{:else}
<article class="preview prose prose-sm sm:prose p-2">
@@ -58,7 +60,16 @@
d="M14.85 3H1.15C.52 3 0 3.52 0 4.15v7.69C0 12.48.52 13 1.15 13h13.69c.64 0 1.15-.52 1.15-1.15v-7.7C16 3.52 15.48 3 14.85 3zM9 11H7V8L5.5 9.92 4 8v3H2V5h2l1.5 2L7 5h2v6zm2.99.5L9.5 8H11V5h2v3h1.5l-2.51 3.5z"
/>
</svg>
<span>Styling with Markdown is supported</span>
<p>
Styling with Markdown is supported.
<a
target="_blank"
class="text-blue-500 underline"
href="https://guides.github.com/features/mastering-markdown"
>
Learn more
</a>
</p>
</div>
</div>
</div>
+5
View File
@@ -0,0 +1,5 @@
<script>
export let message;
</script>
<p class="text-sm text-red-500">{message}</p>
+2
View File
@@ -6,6 +6,7 @@
export let isEditable = false;
export let disabled = false;
export let className = '';
export let onChange;
</script>
<label
@@ -27,6 +28,7 @@
type="text"
placeholder="Your option"
bind:value={label}
on:change={onChange}
/>
{:else}
<span class="ml-2">{label}</span>
+6 -4
View File
@@ -1,11 +1,12 @@
<script>
export let label;
export let value;
export let checked;
export let name;
export let label = '';
export let value = '';
export let checked = false;
export let name = '';
export let isEditable = false;
export let disabled = false;
export let className = '';
export let onChange = () => {}; // This is to know if element is 'dirty'
</script>
<label
@@ -27,6 +28,7 @@
type="text"
placeholder="Your option"
bind:value={label}
on:change={onChange}
/>
{:else}
<span class="ml-2">{label}</span>
+7 -5
View File
@@ -1,9 +1,10 @@
<script>
export let label;
export let value;
export let label = '';
export let value = '';
export let options = [];
export let valueKey = 'id';
export let labelKey = 'label';
export let isRequired = false;
export let onChange = () => {}; // This is to know if element is 'dirty'
</script>
<label class="block">
@@ -12,10 +13,11 @@
bind:value
class="form-select block w-full mt-1"
required={isRequired}
on:blur={onChange}
>
{#each options as option}
<option value={option[valueKey]}>
{option.text}
<option value={option}>
{option[labelKey]}
</option>
{/each}
</select>
+40 -14
View File
@@ -1,37 +1,60 @@
<script>
export let label;
export let placeholder = "Write your answer...";
export let value;
export let rows = "3";
export let maxRows;
export let label = '';
export let placeholder = 'Write your answer...';
export let value = '';
export let rows = '3';
export let maxRows = 3;
export let className = '';
export let helperMessage = '';
export let errorMessage = '';
export let isRequired = false;
export let onChange = () => {}; // This is to know if element is 'dirty'
$: minHeight = `${1 + parseInt(rows, 10) * 1.2}em`;
$: maxHeight = maxRows ? `${1 + maxRows * 1.2}em` : `auto`;
</script>
<label class="block w-full relative">
<label class="block w-full relative {className}">
{#if label}
<span class="text-gray-700">{label}</span>
<p for="text-field" class="">
{label}
{#if isRequired}
<span class="text-red-700">*</span>
{/if}
</p>
{/if}
<pre
<!-- <pre
aria-hidden="true"
style="min-height: {minHeight}; max-height: {maxHeight}">
{value + '\n'}
</pre>
</pre> -->
<textarea
{rows}
{placeholder}
bind:value
class="form-textarea block w-full bg-gray-100 border-t-0 border-l-0 border-r-0 border-b-1"
required={isRequired}
class="form-textarea mt-1 block w-full bg-gray-100 border-t-0 border-l-0 border-r-0 border-b-1 {!!errorMessage
? 'border-red-500'
: 'border-gray-300'}"
on:change={onChange}
/>
{#if !!errorMessage}
<p class="text-sm text-red-500">
{errorMessage}
</p>
{:else if helperMessage}
<p class="text-sm text-gray-500">
{helperMessage}
</p>
{/if}
<slot name="iconbutton" />
</label>
<style>
pre,
/* pre,
textarea {
font-family: inherit;
padding: 0.5em;
@@ -39,13 +62,16 @@
border: 1px solid #eee;
line-height: 1.2;
overflow: hidden;
}
} */
textarea {
/* textarea {
position: absolute;
width: 100%;
height: 100%;
top: 0;
resize: none;
}
textarea.has-label {
top: 30px;
} */
</style>
+20 -7
View File
@@ -1,16 +1,20 @@
<script>
import { onMount } from 'svelte';
export let label;
export let placeholder;
export let value;
export let onKeyDown;
export let label = '';
export let placeholder = '';
export let value = '';
export let onKeyDown = () => {};
export let className = '';
export let inputClassName = '';
export let type = 'text';
export let autoFocus = false;
export let isRequired = false;
export let isDisabled = false;
export let errorMessage;
export let errorMessage = '';
export let helperMessage = '';
export let autoComplete = true;
export let onChange = () => {}; // This is to know if element is 'dirty'
let ref;
function typeAction(node) {
@@ -31,11 +35,14 @@
<label class="block {className}">
{#if label}
<label for="text-field" class="m-0">
{label} <span class="text-red-700">*</span>
{label}
{#if isRequired}
<span class="text-red-700">*</span>
{/if}
</label>
{/if}
<input
id="text-field"
use:typeAction
class="form-input p-2 mt-1 block w-full bg-white border {inputClassName} {errorMessage
? 'border-red-500'
@@ -46,8 +53,14 @@
bind:this={ref}
required={isRequired}
disabled={isDisabled}
autocomplete={autoComplete ? 'on' : 'off'}
on:blur={onChange}
/>
{#if errorMessage}
<p class="text-sm text-red-500">{errorMessage}</p>
{:else if helperMessage}
<p class="text-sm">
{helperMessage}
</p>
{/if}
</label>
+6 -6
View File
@@ -1,15 +1,15 @@
<script>
import ToolTip from "../ToolTip/index.svelte";
export let onClick;
import ToolTip from '../ToolTip/index.svelte';
export let onClick = () => {};
export let disabled = false;
export let selected = false;
export let buttonClassName = "";
export let buttonClassName = '';
export let contained = false;
export let value;
export let value = '';
export let toolTipProps = {
title: "",
title: '',
hotkeys: [],
direction: "",
direction: '',
};
function handleClick() {
+13 -6
View File
@@ -2,10 +2,11 @@
import { afterUpdate } from 'svelte';
import CloseButton from '../Buttons/Close/index.svelte';
export let open;
export let onClose;
export let modalHeading;
export let width;
export let open = false;
export let onClose = () => {};
export let modalHeading = '';
export let width = '';
export let containerClass = '';
afterUpdate(() => {
if (open) {
@@ -21,7 +22,9 @@
class="dialog fixed overflow-x-hidden overflow-y-auto inset-0 flex justify-center items-center z-50 bg-gray-100 bg-opacity-50"
>
<div aria-hidden="true" class="backdrop" />
<div class="bg-white mx-auto {width} shadow-lg p-5 pt-2 rounded-md">
<div
class="container bg-white mx-auto {width} shadow-lg p-5 pt-2 rounded-md"
>
<div class="flex items-center justify-between mb-4">
<h3 class="text-xl m-0">
{modalHeading}
@@ -29,7 +32,7 @@
<CloseButton onClick={onClose} contained={true} />
</div>
<div class="body h-4/5 overflow-y-auto">
<div class="body h-4/5 overflow-y-auto {containerClass}">
<slot />
</div>
</div>
@@ -40,6 +43,10 @@
.dialog {
background-color: rgba(0, 0, 0, 0.5);
}
.container {
max-width: 1000px;
}
.body {
max-height: 60vh;
}
+72 -53
View File
@@ -1,13 +1,12 @@
<script>
import { goto } from '@sapper/app';
import Avatar from '../Avatar/index.svelte';
export let segment;
let navClass = '';
import { user, profile } from '../../utils/store/user';
import PrimaryButton from '../PrimaryButton/index.svelte';
let user = {
avatar:
'https://pbs.twimg.com/profile_images/1416443682157473795/dGtFbtht_normal.jpg',
label: 'Elon',
};
export let segment;
let navClass = '';
$: navClass = ['discussion'].includes(segment) ? 'hide' : '';
</script>
@@ -26,54 +25,74 @@
/>
</a>
</div>
<li>
<a
class="block"
aria-current={segment === undefined || segment === 'dashbboard'
? 'page'
: undefined}
href="dashbboard"
>
Dashboard
</a>
</li>
<li>
<a
class="block"
aria-current={segment === 'courses' ? 'page' : undefined}
href="courses"
>
Courses
</a>
</li>
<li>
<a
class="block"
aria-current={segment === 'discussions' || segment === 'discussion'
? 'page'
: undefined}
href="discussions"
>
Discussion
</a>
</li>
<li>
<a
class="block"
aria-current={segment === 'people' ? 'page' : undefined}
href="people"
>People
</a>
</li>
{#if $user.isLoggedIn}
<li>
<a
class="block"
aria-current={segment === undefined || segment === 'dashboard'
? 'page'
: undefined}
href="dashboard"
>
Dashboard
</a>
</li>
<li>
<a
class="block"
aria-current={segment === 'courses' ? 'page' : undefined}
href="courses"
>
Courses
</a>
</li>
<li>
<a
class="block"
aria-current={segment === 'discussions' || segment === 'discussion'
? 'page'
: undefined}
href="discussions"
>
Discussion
</a>
</li>
<li>
<a
class="block"
aria-current={segment === 'people' ? 'page' : undefined}
href="people"
>People
</a>
</li>
{/if}
<span class="flex-grow" />
<li>
<a
class="block"
aria-current={segment === 'profile' ? 'page' : undefined}
href="profile"
>
<Avatar src={user.avatar} name={user.label} className="mr-2" />
</a>
{#if $user.isLoggedIn}
<a
class="block"
aria-current={segment === 'profile' ? 'page' : undefined}
href={`/profile/${$profile.id}`}
>
<Avatar
src={$profile.avatar_url}
name={$profile.username}
className="mr-2"
/>
</a>
{:else}
<PrimaryButton
label="Login"
className="py-2 px-4"
onClick={() => goto('/login')}
/>
{/if}
</li>
<!-- <li class="hidden">
<a
+7 -3
View File
@@ -1,10 +1,10 @@
<script>
// import
// border border-gray-300 rounded-md p-3 bg-blue-200 bg-opacity-25
export let width = "max-w-3xl";
export let width = 'max-w-3xl';
</script>
<div class="mt-4 mx-auto {width} relative pb-5">
<div class="root mt-4 mx-auto {width} relative pb-5 pr-5">
{#if $$slots.header}
<div
class="head flex bg-white items-center justify-between sticky right-0 w-full py-3"
@@ -17,8 +17,12 @@
</div>
<style>
.root {
overflow-y: scroll;
height: 85vh;
}
.head {
top: 61px;
top: 0px;
z-index: 1;
}
</style>
+46 -8
View File
@@ -1,16 +1,20 @@
<script>
import NewButton from "../PrimaryContainedButton/index.svelte";
export let title = "";
export let overidableStyle = "";
import NewButton from '../PrimaryContainedButton/index.svelte';
import TextField from '../Form/TextField.svelte';
export let title = '';
export let overidableStyle = '';
export let navItems = [];
export let addButtonHref;
export let addButtonLabel;
export let disableSticky = false;
export let isTitleEditable = false;
export let onEditComplete = () => {};
let dynamicRootClass = "";
let dynamicRootClass = '';
let enterEditTitleMode = false;
$: dynamicRootClass =
Array.isArray(navItems) && navItems.length > 4 ? "bring-down" : "";
Array.isArray(navItems) && navItems.length > 4 ? 'bring-down' : '';
</script>
<div
@@ -19,7 +23,30 @@
style={overidableStyle}
>
{#if !!title}
<h4 class="title">{title}</h4>
{#if isTitleEditable}
{#if !enterEditTitleMode}
<h4
class="title editable-title hover:bg-gray-100 px-3 rounded-md overflow-ellipsis"
on:click={() => (enterEditTitleMode = true)}
>
{title}
</h4>
{:else}
<TextField
bind:value={title}
placeholder="Course title"
onChange={() => {
enterEditTitleMode = false;
if (typeof onEditComplete === 'function') {
onEditComplete();
}
}}
/>
{/if}
{:else}
<h4 class="title">{title}</h4>
{/if}
{/if}
{#if Array.isArray(navItems) && navItems.length}
<div class="flex justify-evenly items-center">
@@ -39,7 +66,7 @@
<style>
.header {
border-bottom: 1px solid var(--border-color);
padding: 15px;
padding: 0 15px;
min-height: 61px;
background-color: #fff;
z-index: 1;
@@ -69,7 +96,7 @@
.active::after {
position: absolute;
content: "";
content: '';
width: 100%;
height: 3px;
background-color: var(--main-primary-color);
@@ -84,6 +111,17 @@
flex-grow: 1;
}
.editable-title {
font-size: 16px;
display: flex;
/* justify-content: center; */
align-items: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 10px;
}
@media only screen and (max-width: 1002px) {
.header {
flex-direction: column;
+6 -6
View File
@@ -1,10 +1,10 @@
<script>
import { VARIANTS, VARIANTS_CLASS } from "./constants";
export let label;
export let className = "";
import { VARIANTS, VARIANTS_CLASS } from './constants';
export let label = '';
export let className = '';
export let onClick = () => {};
export let name = "";
export let type;
export let name = '';
export let type = 'text';
export let variant = VARIANTS.CONTAINED;
export let disablePadding = false;
export let isDisabled = false;
@@ -21,7 +21,7 @@
{type}
disabled={isDisabled}
>
{#if label}
{#if !!label}
{label}
{:else}
<slot />
+1 -1
View File
@@ -11,6 +11,6 @@
<style>
.progress {
transition: width 0.5s linear;
transition: width 1s linear;
}
</style>
@@ -3,12 +3,12 @@
import PrimaryButton from '../../PrimaryButton/index.svelte';
import { VARIANTS } from '../../PrimaryButton/constants';
import Checkbox from '../../Form/Checkbox.svelte';
import TextField from '../../Form/TextField.svelte';
export let title = '';
export let code;
export let name = '';
export let options = [];
export let answers = [];
export let onSubmit = () => {};
export let onPrevious = () => {};
export let defaultValue = [];
@@ -16,6 +16,8 @@
export let disabled = false;
export let isPreview = false;
export let nextButtonProps = {};
export let grade;
export let gradeMax = 0;
function getVal(form, name) {
let values = [];
@@ -33,7 +35,7 @@
function handleFormSubmit(event) {
if (isPreview) return;
const values = getVal(event.target, name);
onSubmit(name, values);
onSubmit(name, values, nextButtonProps.isActive);
event.target.reset();
}
@@ -43,8 +45,8 @@
}
function getValidationClassName(option) {
if (defaultValue.includes(option.id)) {
if (answers.includes(option.id)) {
if (defaultValue.includes(option.value)) {
if (option.is_correct) {
return 'border-green-700';
} else {
return 'border-red-700';
@@ -56,7 +58,20 @@
</script>
<form on:submit|preventDefault={handleFormSubmit}>
<h2>{title}</h2>
<div class="flex items-center justify-between">
<h2 class={!isNaN(grade) && 'w-3/4'}>{title}</h2>
{#if !isNaN(grade)}
<div class="flex items-center">
<TextField
placeholder="Points"
bind:value={grade}
type="number"
className="w-20"
/>
<p class="ml-2 text-lg">/ {gradeMax}</p>
</div>
{/if}
</div>
{#if code}
<CodeSnippet {code} />
@@ -73,8 +88,8 @@
<Checkbox
{name}
className="p-2"
value={option.id}
checked={defaultValue.includes(option.id)}
value={option.value}
checked={defaultValue.includes(option.value)}
label={option.label || option.value}
{disabled}
/>
@@ -3,12 +3,12 @@
import PrimaryButton from '../../PrimaryButton/index.svelte';
import { VARIANTS } from '../../PrimaryButton/constants';
import RadioItem from '../../Form/RadioItem.svelte';
import TextField from '../../Form/TextField.svelte';
export let title = '';
export let code;
export let name = '';
export let options = [];
export let answers = [];
export let onSubmit = () => {};
export let onPrevious = () => {};
export let defaultValue = '';
@@ -16,6 +16,8 @@
export let disabled = false;
export let isPreview = false;
export let nextButtonProps = {};
export let grade;
export let gradeMax = 0;
function getRadioVal(form, name) {
let val;
@@ -33,7 +35,7 @@
function handleFormSubmit(event) {
if (isPreview) return;
const value = getRadioVal(event.target, name);
onSubmit(name, [value]);
onSubmit(name, [value], nextButtonProps.isActive);
event.target.reset();
}
@@ -43,8 +45,8 @@
}
function getValidationClassName(option) {
if (defaultValue.includes(option.id)) {
if (answers.includes(option.id)) {
if (defaultValue.includes(option.value)) {
if (option.is_correct) {
return 'border-green-700';
} else {
return 'border-red-700';
@@ -56,7 +58,20 @@
</script>
<form on:submit|preventDefault={handleFormSubmit}>
<h2>{title}</h2>
<div class="flex items-center justify-between">
<h2 class={!isNaN(grade) && 'w-3/4'}>{title}</h2>
{#if !isNaN(grade)}
<div class="flex items-center">
<TextField
placeholder="Points"
bind:value={grade}
type="number"
className="w-20"
/>
<p class="ml-2 text-lg">/ {gradeMax}</p>
</div>
{/if}
</div>
{#if code}
<CodeSnippet {code} />
@@ -73,9 +88,8 @@
<RadioItem
className="p-2"
{name}
value={option.id}
checked={defaultValue.includes(option.id) &&
answers.includes(option.id)}
value={option.value}
checked={defaultValue.includes(option.value) && option.is_correct}
label={option.label || option.value}
{disabled}
/>
@@ -4,6 +4,7 @@
import PrimaryButton from '../../PrimaryButton/index.svelte';
import { VARIANTS } from '../../PrimaryButton/constants';
import TextArea from '../../Form/TextArea.svelte';
import TextField from '../../Form/TextField.svelte';
export let title = '';
export let code;
@@ -15,11 +16,13 @@
export let isLast = false;
export let isPreview = false;
export let disabled = false;
export let grade;
export let gradeMax = 0;
function handleFormSubmit(event) {
if (isPreview) return;
onSubmit(name, defaultValue);
onSubmit(name, defaultValue, true);
// event.target.reset();
}
@@ -30,7 +33,20 @@
</script>
<form on:submit|preventDefault={handleFormSubmit}>
<h2>{title}</h2>
<div class="flex items-center justify-between">
<h2 class={!isNaN(grade) && 'w-3/4'}>{title}</h2>
{#if !isNaN(grade)}
<div class="flex items-center">
<TextField
placeholder="Points"
bind:value={grade}
type="number"
className="w-20"
/>
<p class="ml-2 text-lg">/ {gradeMax}</p>
</div>
{/if}
</div>
{#if code}
<CodeSnippet {code} />
+7 -7
View File
@@ -1,27 +1,27 @@
export const QUESTION_TYPE = {
RADIO: "RADIO",
CHECKBOX: "CHECKBOX",
TEXTAREA: "TEXTAREA",
RADIO: 1,
CHECKBOX: 2,
TEXTAREA: 3,
};
export const QUESTION_TYPES = [
{
id: QUESTION_TYPE.RADIO,
text: "Single answer",
label: 'Single answer',
},
{
id: QUESTION_TYPE.CHECKBOX,
text: "Multiple answers",
label: 'Multiple answers',
},
{
id: QUESTION_TYPE.TEXTAREA,
text: "Paragraph",
label: 'Paragraph',
},
];
export const QUESTION_TEMPLATE = {
id: 1,
title: "",
title: '',
type: QUESTION_TYPE.RADIO,
answers: [],
options: [
+28 -7
View File
@@ -1,10 +1,13 @@
<script>
// import { onMount } from "svelte";
import CloseButton from "../Buttons/Close/index.svelte";
import TextField from '../Form/TextField.svelte';
import CloseButton from '../Buttons/Close/index.svelte';
export let isTitle = false;
export let onClose;
export let onClose = () => {};
export let scrollToQuestion = false;
export let points = undefined;
export let hasError = false;
export let onPointsChange = () => {};
let ref;
// onMount(() => {
@@ -15,16 +18,20 @@
$: {
if (ref && scrollToQuestion) {
ref.scrollIntoView({ block: "end", behavior: "smooth" });
ref.scrollIntoView({
block: 'start',
behavior: 'smooth',
inline: 'nearest',
});
}
}
</script>
<div
bind:this={ref}
class="{isTitle
? 'border'
: 'border-2'} border-gray border-r-2 rounded-md mb-6 relative root hover:border-blue-700"
class="{isTitle ? 'border' : 'border-2'} {hasError
? 'border-red-700'
: 'border-gray hover:border-blue-700'} border-r-2 rounded-md mb-6 relative root"
>
{#if isTitle}
<div class="title absolute bg-blue-700" />
@@ -38,6 +45,20 @@
<div class="px-4 {isTitle ? 'pt-4' : 'pt-2'} pb-3">
<slot />
</div>
{#if typeof points !== 'undefined'}
<div class="border-gray border-t-2 border-r-0 border-b-0 border-l-0 p-2">
<div class="flex items-center w-40">
<p class="text-sm mr-2">Points:</p>
<TextField
placeholder="Points"
bind:value={points}
type="number"
onChange={onPointsChange}
/>
</div>
</div>
{/if}
</div>
<style>
+2 -2
View File
@@ -4,8 +4,8 @@
export let onChange;
</script>
<div class="w-full flex justify-center flex-col">
<div class="flex items-center">
<div class="w-full flex flex-col">
<div class="flex items-center justify-center">
{#each tabs as tab}
<button
class="relative border-r-1 border-t-0 border-b-0 border-l-0 rounded-none {currentTab ===
+38 -15
View File
@@ -10,7 +10,7 @@
import Tailwindcss from '../components/Tailwindcss.svelte';
import Navigation from '../components/Navigation/index.svelte';
// import SideBar from "../components/SideBar/index.svelte";
import Footer from '../components/Footer/index.svelte';
// import Footer from '../components/Footer/index.svelte';
import Apps from '../components/Apps/index.svelte';
import { handleAuthChange } from '../utils/functions/api';
import { user, profile } from '../utils/store/user';
@@ -24,6 +24,7 @@
let path = $page.path.replace('/', '');
async function getProfile() {
console.log('Get user profile');
// Get user profile
const authUser = supabase.auth.user();
@@ -34,39 +35,61 @@
status,
} = await supabase
.from('profile')
.select(`*`)
.eq('user_id', authUser.id)
.select(`*, org:organizationmember(*)`)
.eq('id', authUser.id)
.single();
if (error && !profileData && status === 406) {
// User wasn't found, create profile
console.log(`User wasn't found, create profile`);
const { data, error } = await supabase
.from('profile')
.insert([{ user_id: authUser.id }]);
const [regexUsernameMatch] = [...authUser.email.matchAll(/(.*)@/g)];
const { data, error } = await supabase.from('profile').insert({
id: authUser.id,
username: regexUsernameMatch[1],
fullname: regexUsernameMatch[1],
});
// Profile created, go to profile page
if (!error && data) {
user.set(authUser);
profile.set(data);
return goto('/');
$user.fetchingUser = false;
$user.isLoggedIn = true;
$user.currentSession = authUser;
profile.set(data[0]);
// If user coming to login page, then
if (path.includes('login') || path.includes('signup')) {
goto('/dashboard');
}
}
} else if (profileData) {
// Profile exists, go to profile page
user.set(authUser);
$user.fetchingUser = false;
$user.isLoggedIn = true;
$user.currentSession = authUser;
console.log(`profileData`, profileData);
profile.set(profileData);
return goto('/');
// If user coming to login page, then
if (path.includes('login') || path.includes('signup')) {
goto('/dashboard');
}
}
}
onMount(() => {
console.log('mounting layout');
const { data: authListener } = supabase.auth.onAuthStateChange(
(event, session) => {
handleAuthChange(event, session);
if (event === 'SIGNED_IN') {
$user.fetchingUser = true;
getProfile();
} else {
console.log('not logged in, go to login');
return goto('/');
}
}
@@ -85,21 +108,21 @@
<!-- <Nav {segment} /> -->
<main>
{#if !['login', 'signup', ''].includes(path)}
{#if !['login', 'signup'].includes(path)}
<Navigation {segment} />
{/if}
<div class="flex justify-between">
<slot />
{#if path.includes('courses')}
<!-- {#if path.includes('courses')}
<Apps />
{/if}
{/if} -->
</div>
</main>
{#if !['about', ''].includes(path)}
<Footer {segment} />
<!-- <Footer {segment} /> -->
{/if}
<style>
+2 -2
View File
@@ -378,8 +378,8 @@ export function getLesson(id) {
lesson_id: null,
materials: {
note: '',
slide: '',
video: '',
slide_url: '',
video_url: '',
},
exercises: getExercises(id),
}
+168 -5
View File
@@ -1,12 +1,114 @@
<script context="module">
import { supabase } from '../utils/functions/supabase';
export async function preload(page, params) {
const user = JSON.parse(params.user);
const { data: orgMember } = await supabase
.from('organizationmember')
.select(`organization_id`)
.eq('profile_id', user.id)
.single();
const { data: allCourses } = await supabase
.from('course')
.select(`id, title, description`)
.eq('organization_id', orgMember.organization_id);
return { allCourses, organizationId: orgMember.organization_id };
}
</script>
<script>
import { stores } from '@sapper/app';
import { onMount } from 'svelte';
// import { stores } from '@sapper/app';
import PageNav from '../components/PageNav/index.svelte';
import Courses from '../components/Courses/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 { profile } from '../utils/store/user';
import { ROLE } from '../utils/constants/roles';
const { page } = stores();
let tab = '';
// const { page } = stores();
$: tab = $page.query.tab || 'mine';
export let allCourses;
export let organizationId;
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,
organization_id: organizationId,
});
// Get profile of author and add as member
const admin = await addGroupMember({
profile_id: $profile.id,
group_id,
role_id: ROLE.ADMIN,
});
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);
});
}
function addGroupMember(member) {
return supabase.from('groupmember').insert(member, {
returning: 'minimal',
});
}
onMount(() => {
courses.set(allCourses);
});
// $: tab = $page.query.tab || 'mine';
</script>
<svelte:head>
@@ -14,7 +116,68 @@
</svelte:head>
<div class="root">
<PageNav title="Courses" />
<PageNav title="Courses">
<div slot="widget">
<PrimaryButton
label="New"
onClick={() => ($createCourseModal.open = true)}
/>
</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>
<Courses />
</div>
-252
View File
@@ -1,252 +0,0 @@
<script context="module">
export async function preload({ params }) {
let [courseId] = params.id;
const res = await this.fetch(`api/course?id=${courseId}`);
const data = await res.json();
if (res.status === 200) {
return { courseId, courseData: data };
} else {
this.error(res.status, data.message);
}
}
</script>
<script>
import { flip } from 'svelte/animate';
import { dndzone } from 'svelte-dnd-action';
import { stores, goto } from '@sapper/app';
import PageNav from '../../../components/PageNav/index.svelte';
import MarkExerciseModal from '../../../components/Course/components/Lesson/Exercise/MarkExerciseModal.svelte';
import Chip from '../../../components/Chip/index.svelte';
import PageBody from '../../../components/PageBody/index.svelte';
import CourseContainer from '../../../components/CourseContainer/index.svelte';
const flipDurationMs = 300;
export let courseData = {};
let sections = [
{
id: 1,
title: 'Submitted',
value: 5,
items: [
{
id: 41,
exerciseTitle: 'Practice props in React.js',
student: {
avatar: 'https://picsum.photos/32/32/?random',
name: 'Brad Traversy',
},
tutor: {
name: 'sergey-semko',
id: 1,
},
lesson: {
number: 1,
},
},
{
id: 42,
exerciseTitle: 'Practice props in React.js',
student: {
avatar: 'https://picsum.photos/32/32/?random',
name: 'James Kola',
},
tutor: {
name: 'sergey-semko',
id: 1,
},
lesson: {
number: 1,
},
},
{
id: 43,
exerciseTitle: 'Practice props in React.js',
student: {
avatar: 'https://picsum.photos/32/32/?random',
name: 'linus-torvalds',
},
tutor: {
name: 'sergey-semko',
id: 1,
},
lesson: {
number: 1,
},
},
{
id: 44,
exerciseTitle: 'Practice props in React.js',
student: {
avatar: 'https://picsum.photos/32/32/?random',
name: 'rotimi-best',
},
tutor: {
name: 'sergey-semko',
id: 1,
},
lesson: {
number: 1,
},
},
{
id: 45,
exerciseTitle: 'Home task: Class and Functional component',
student: {
avatar: 'https://picsum.photos/32/32/?random',
name: 'james-two',
},
tutor: {
name: 'sergey-semko',
id: 1,
},
lesson: {
number: 2,
},
},
{
id: 46,
exerciseTitle: 'Home task: Class and Functional component',
student: {
avatar: 'https://picsum.photos/32/32/?random',
name: 'brarack-obama',
},
tutor: {
name: 'sergey-semko',
id: 1,
},
lesson: {
number: 2,
},
},
{
id: 47,
exerciseTitle: 'Lesson task: Class and Functional component',
student: {
avatar: 'https://picsum.photos/32/32/?random',
name: 'bill-gates',
},
tutor: {
name: 'sergey-semko',
id: 1,
},
lesson: {
number: 3,
},
},
],
},
{
id: 2,
title: 'In Progress',
value: 0,
items: [],
},
{
id: 3,
title: 'Graded',
value: 10,
items: [],
},
];
let openExercise = false;
const { page } = stores();
function handleItemFinalize(columnIdx, newItems) {
sections[columnIdx].items = newItems;
// onFinalUpdate([...sections]);
}
function handleDndConsiderCards(columnIdx) {
return (e) => {
console.warn('got consider');
sections[columnIdx].items = e.detail.items;
};
}
function handleDndFinalizeCards(columnIdx) {
return (e) => handleItemFinalize(columnIdx, e.detail.items);
}
function handleModalClose() {
goto($page.path);
}
$: openExercise = !!$page.query.submissionId;
</script>
<MarkExerciseModal bind:open={openExercise} onClose={handleModalClose} submissionId={$page.query.submissionId}/>
<CourseContainer {courseData}>
<PageNav title="All Exercises" />
<PageBody width="w-11/12 overflow-x-auto overflow-y-hidden">
<div class="flex items-center justify-between w-full">
{#each sections as { id, title, items }, idx (id)}
<div
class="section rounded-md bg-gray-100 border border-gray-50 p-3 h-80 mr-3 overflow-hidden"
animate:flip={{ duration: flipDurationMs }}
>
<div class="flex items-center mb-2">
<Chip value={items.length} />
<p class="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="border border-grey-700 w-full my-2 mx-0 rounded-md bg-white py-2 px-3"
animate:flip={{ duration: flipDurationMs }}
>
<a
class="flex items-center no-underline hover:underline text-black mb-2"
href="{$page.path}?submissionId={item.id}"
>
<img
alt="Placeholder"
class="block rounded-full"
width="24"
height="20"
src={item.student.avatar}
/>
<p class="ml-2 text-sm">{item.student.name}</p>
</a>
<a
class="text-blue-700 text-md font-bold"
href="{$page.path}?submissionId={item.id}"
>{item.exerciseTitle}</a
>
<p class="text-grey text-sm">
#{item.lesson.number} created by {item.tutor.name}
</p>
</div>
{/each}
</div>
</div>
{/each}
</div>
</PageBody>
</CourseContainer>
<style>
.section {
max-width: 355px;
min-width: 355px;
height: 80vh;
}
.content {
height: 95%;
}
</style>
+17 -13
View File
@@ -1,25 +1,29 @@
<script context="module">
export async function preload({ params }) {
let [courseId] = params.id;
import { fetchCourse } from '../../../utils/services/courses';
const res = await this.fetch(`api/course?id=${courseId}`);
const data = await res.json();
export async function preload({ params }, session) {
const {
data,
// error
} = await fetchCourse(params.id, session);
if (res.status === 200) {
return { courseId, courseData: data };
} else {
this.error(res.status, data.message);
}
return { courseData: data };
}
</script>
<script>
import Overview from "../../../components/Course/index.svelte";
import CourseContainer from "../../../components/CourseContainer/index.svelte";
import { onMount } from 'svelte';
import Overview from '../../../components/Course/index.svelte';
import CourseContainer from '../../../components/CourseContainer/index.svelte';
import { setCourseData } from '../../../components/Course/store';
export let courseData = {};
export let courseData;
onMount(async () => {
setCourseData(courseData);
});
</script>
<CourseContainer {courseData}>
<CourseContainer>
<Overview />
</CourseContainer>
@@ -1,27 +1,43 @@
<script context="module">
export async function preload({ params }) {
import {
fetchCourse,
fetchLesson,
} from '../../../../../utils/services/courses';
let prevLessonId;
export async function preload({ params }, session) {
const { id: courseId, lessonParams = [] } = params;
const [lessonId, exerciseRouteName, exerciseId] = lessonParams;
let courseData;
let lessonData;
const res = await this.fetch(
`api/course?id=${courseId}&lessonId=${lessonId}&exerciseId=${exerciseId}`
);
const data = await res.json();
if (!process.browser) {
const [course, lesson] = await Promise.all([
fetchCourse(courseId, session),
fetchLesson(lessonId),
]);
if (res.status === 200) {
return {
courseData: data,
lessonId,
isMaterialsTabActive: !exerciseRouteName,
exerciseId,
};
} else {
this.error(res.status, data.message);
courseData = course.data;
lessonData = lesson.data;
} else if (prevLessonId && prevLessonId !== lessonId) {
const lesson = await fetchLesson(lessonId);
lessonData = lesson.data;
}
prevLessonId = lessonId;
return {
courseData,
lessonData,
isMaterialsTabActive: !exerciseRouteName,
lessonId,
exerciseId,
};
}
</script>
<script>
import { onMount, afterUpdate } from 'svelte';
import { stores } from '@sapper/app';
import CourseContainer from '../../../../../components/CourseContainer/index.svelte';
@@ -31,21 +47,42 @@
import Materials from '../../../../../components/Course/components/Lesson/Materials/index.svelte';
import Exercises from '../../../../../components/Course/components/Lesson/Exercises/index.svelte';
import MODES from '../../../../../utils/constants/mode.js';
import { setCourseData } from '../../../../../components/Course/store';
import { lesson } from '../../../../../components/Course/components/Lesson/store/lessons';
export let courseData;
export let lessonData;
export let lessonId;
export let isMaterialsTabActive;
export let exerciseId;
export let isMaterialsTabActive;
let path;
let mode = MODES.view;
const { page } = stores();
$: path = $page.path.replace(/\/exercises[\/ 0-9]*/, '');
function setLesson() {
if (!lessonData) return;
lesson.update((l) => ({
...l,
id: lessonId,
materials: lessonData,
}));
}
onMount(() => {
setCourseData(courseData);
setLesson();
});
afterUpdate(setLesson);
$: path = $page.path.replace(/\/exercises[\/ 0-9 a-z -]*/, '');
</script>
<CourseContainer {courseData} {path}>
<CourseContainer {path} isExercisePage={!isMaterialsTabActive && exerciseId}>
<PageNav
navItems={[
{
@@ -74,11 +111,11 @@
</svelte:fragment>
</PageNav>
<PageBody>
{#if !isMaterialsTabActive}
<Exercises {exerciseId} path={`${path}/exercises`} />
{:else}
{#if !isMaterialsTabActive}
<Exercises {lessonId} {exerciseId} path={`${path}/exercises`} />
{:else}
<PageBody width="w-11/12 m-auto">
<Materials {lessonId} {mode} />
{/if}
</PageBody>
</PageBody>
{/if}
</CourseContainer>
+139 -52
View File
@@ -1,70 +1,113 @@
<script context="module">
export async function preload({ params }) {
let [courseId, courseNavItem, lessonId] = params.id;
const res = await this.fetch(`api/course?id=${courseId}`);
const data = await res.json();
import { fetchCourse } from '../../../../utils/services/courses';
if (res.status === 200) {
return { courseId, courseData: data };
} else {
this.error(res.status, data.message);
export async function preload({ params }, session) {
if (process.browser) {
return {};
}
const {
data,
// error
} = await fetchCourse(params.id, session);
return { courseData: data };
}
</script>
<script>
import Time24 from "carbon-icons-svelte/lib/Time24";
import Edit24 from "carbon-icons-svelte/lib/Edit24";
import Save24 from "carbon-icons-svelte/lib/Save24";
import CheckmarkFilled24 from "carbon-icons-svelte/lib/CheckmarkFilled24";
import PageNav from "../../../../components/PageNav/index.svelte";
import PageBody from "../../../../components/PageBody/index.svelte";
import PrimaryButton from "../../../../components/PrimaryButton/index.svelte";
import TextField from "../../../../components/Form/TextField.svelte";
import IconButton from "../../../../components/IconButton/index.svelte";
import Select from "../../../../components/Form/Select.svelte";
import CourseContainer from "../../../../components/CourseContainer/index.svelte";
import { onMount } from 'svelte';
import Time24 from 'carbon-icons-svelte/lib/Time24';
import Edit24 from 'carbon-icons-svelte/lib/Edit24';
import Save24 from 'carbon-icons-svelte/lib/Save24';
import Phone24 from 'carbon-icons-svelte/lib/Phone24';
import Delete24 from 'carbon-icons-svelte/lib/Delete24';
import CheckmarkFilled24 from 'carbon-icons-svelte/lib/CheckmarkFilled24';
import PageNav from '../../../../components/PageNav/index.svelte';
import Box from '../../../../components/Box/index.svelte';
import PageBody from '../../../../components/PageBody/index.svelte';
import PrimaryButton from '../../../../components/PrimaryButton/index.svelte';
import TextField from '../../../../components/Form/TextField.svelte';
import IconButton from '../../../../components/IconButton/index.svelte';
import Select from '../../../../components/Form/Select.svelte';
import CourseContainer from '../../../../components/CourseContainer/index.svelte';
import {
lessons,
tutors,
handleAddLesson,
} from "../../../../components/Course/components/Lesson/store/lessons";
handleSaveLesson,
handleDelete,
} from '../../../../components/Course/components/Lesson/store/lessons';
import {
course,
setCourseData,
group,
} from '../../../../components/Course/store';
export let courseData;
let lessonEditing;
let ref;
const resources = [
{
label: 'lesson',
value: 1,
},
{
label: 'quiz',
value: 1,
},
{
label: 'home task',
value: 2,
},
];
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);
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}`;
}
// TODO: CRUD of lessons
function addLesson() {
handleAddLesson();
setTimeout(() => {
lessonEditing = $lessons.length - 1;
if (ref) {
ref.scrollIntoView({ block: "end", behavior: "smooth" });
ref.scrollIntoView({
block: 'start',
behavior: 'smooth',
inline: 'nearest',
});
}
}, 100);
}
onMount(() => {
setCourseData(courseData);
});
$: console.log(`$lessons`, $lessons);
</script>
<CourseContainer {courseData}>
<CourseContainer>
<PageNav title="Lessons">
<div slot="widget">
<PrimaryButton label="Add" onClick={addLesson} />
<PrimaryButton
label="Add"
onClick={addLesson}
isDisabled={!!lessonEditing}
/>
</div>
</PageNav>
<PageBody width="w-11/12 m-auto">
<div class="w-4/5 m-auto">
<div class="w-4/5 mt-4 m-auto">
{#each $lessons as lesson, index}
<div
bind:this={ref}
@@ -73,13 +116,16 @@
<!-- Complete or Not complete icon -->
<div class="absolute -left-6 -top-6 success">
<IconButton
onClick={() => (lesson.isComplete = !lesson.isComplete)}
onClick={() => {
lesson.is_complete = !lesson.is_complete;
handleSaveLesson(lesson, $course.id);
}}
toolTipProps={{
title: `Click to ${lesson.isComplete ? "lock" : "unlock"}`,
direction: "right",
title: `Click to ${lesson.is_complete ? 'lock' : 'unlock'}`,
direction: 'right',
}}
>
{#if lesson.isComplete}
{#if lesson.is_complete}
<CheckmarkFilled24 class="carbon-icon" />
{:else}
<span class="text-2xl">🔒</span>
@@ -90,7 +136,12 @@
<!-- Edit/Save -->
<div class="absolute top-2 right-0">
{#if lessonEditing === index}
<IconButton onClick={() => (lessonEditing = null)}>
<IconButton
onClick={() => {
lessonEditing = null;
handleSaveLesson(lesson, $course.id);
}}
>
<Save24 class="carbon-icon" />
</IconButton>
{:else}
@@ -107,62 +158,98 @@
{#if lessonEditing === index}
<TextField bind:value={lesson.title} autofocus={true} />
{:else}
<h3 class="text-3xl font-semibold m-0 flex items-center">
<a href={lesson.to} class="hover:underline text-black">
<h3 class="text-2xl m-0 flex items-center">
<a
href="/courses/{$course.id}/lessons/{lesson.id}"
class="hover:underline text-black"
>
{lesson.title}
</a>
</h3>
{/if}
<div class="flex items-center mb-2">
<!-- <div class="flex items-center mb-2">
<div class="flex items-center mr-2">
{lesson.resources
.map((r) => `${r.value} ${r.label}`)
.join(", ")}
{resources.map((r) => `${r.value} ${r.label}`).join(', ')}
</div>
</div>
</div> -->
<div class="flex items-center justify-between mt-3 w-full">
<div class="flex items-center justify-between mt-6 w-full">
{#if lessonEditing === index}
<Select
bind:value={lesson.tutor.name}
options={$tutors}
valueKey="text"
bind:value={lesson.profile}
options={$group.tutors}
labelKey="fullname"
/>
{:else if !lesson.profile}
<p class="ml-2 text-sm">No tutor added</p>
{:else}
<a
href="courses/1/people"
class="flex items-center hover:underline text-black"
class="flex items-center hover:underline"
>
<img
alt="Placeholder"
class="block rounded-full"
width="24"
height="20"
src={lesson.tutor.avatar}
src={lesson.profile.avatar_url}
/>
<p class="ml-2 text-sm font-bold">{lesson.tutor.name}</p>
<p class="ml-2 text-sm">
{lesson.profile.fullname}
</p>
</a>
{/if}
<div class="flex items-center">
<div class="flex items-cente">
{#if lessonEditing === index}
<input
type="date"
name="lesson-date-picker"
class="p-2 rounded-md w-40"
value={formatDate(lesson.date)}
on:input={(e) => (lesson.date = e.target.value)}
value={formatDate(lesson.lesson_at)}
on:input={(e) => (lesson.lesson_at = e.target.value)}
/>
{:else}
<Time24 class="carbon-icon" />
<p class="text-md font-semibold ml-2">
{new Date(lesson.date).toDateString()}
<p class="text-md ml-2">
{new Date(lesson.lesson_at).toDateString()}
</p>
{/if}
</div>
<div class="flex items-center">
{#if lessonEditing === index}
<TextField
bind:value={lesson.call_url}
autofocus={true}
className="w-40"
placeholder="https://meet.google.com/mga-dsjs-fmb"
/>
{:else}
<Phone24 class="carbon-icon" />
<a
class="text-md ml-2"
href={lesson.call_url || '#'}
target="_blank"
>
{lesson.call_url ? 'Join lesson' : 'No link'}
</a>
{/if}
</div>
</div>
</div>
<div class="absolute bottom-2 right-0">
<IconButton onClick={handleDelete(lesson.id)}>
<Delete24 class="carbon-icon" />
</IconButton>
</div>
</div>
{:else}
<Box>
<h3 class="text-3xl text-gray-500">No lesson added</h3>
<img alt="No lesson added" src="/notfound2.gif" class="w-80" />
</Box>
{/each}
</div>
</PageBody>
+23 -15
View File
@@ -1,33 +1,41 @@
<script context="module">
export async function preload({ params }) {
let [courseId] = params.id;
import { fetchCourse } from '../../../utils/services/courses';
const res = await this.fetch(`api/course?id=${courseId}`);
const data = await res.json();
if (res.status === 200) {
return { courseId, courseData: data };
} else {
this.error(res.status, data.message);
export async function preload({ params }, session) {
if (process.browser) {
return {};
}
const {
data,
// error
} = await fetchCourse(params.id, session);
return { courseData: data };
}
</script>
<script>
import { stores } from "@sapper/app";
import CourseContainer from "../../../components/CourseContainer/index.svelte";
import PageNav from "../../../components/PageNav/index.svelte";
import UserCard from "../../../components/UserCard/index.svelte";
import { onMount } from 'svelte';
import { stores } from '@sapper/app';
import CourseContainer from '../../../components/CourseContainer/index.svelte';
import PageNav from '../../../components/PageNav/index.svelte';
import UserCard from '../../../components/UserCard/index.svelte';
import { setCourseData } from '../../../components/Course/store';
export let courseData = {};
const { page } = stores();
const teachers = courseData.people;
const teachers = [];
const students = [...teachers, ...teachers, ...teachers];
onMount(() => {
setCourseData(courseData);
});
</script>
<CourseContainer {courseData}>
<CourseContainer>
<PageNav title="People" disableSticky={true} />
<div class="m-3">
<h3>
+20 -12
View File
@@ -1,25 +1,33 @@
<script context="module">
export async function preload({ params }) {
let [courseId] = params.id;
import { fetchCourse } from '../../../utils/services/courses';
const res = await this.fetch(`api/course?id=${courseId}`);
const data = await res.json();
if (res.status === 200) {
return { courseId, courseData: data };
} else {
this.error(res.status, data.message);
export async function preload({ params }, session) {
if (process.browser) {
return {};
}
const {
data,
// error
} = await fetchCourse(params.id, session);
return { courseData: data };
}
</script>
<script>
import CourseContainer from "../../../components/CourseContainer/index.svelte";
import PageNav from "../../../components/PageNav/index.svelte";
import { onMount } from 'svelte';
import CourseContainer from '../../../components/CourseContainer/index.svelte';
import PageNav from '../../../components/PageNav/index.svelte';
import { setCourseData } from '../../../components/Course/store';
export let courseData = {};
onMount(() => {
setCourseData(courseData);
});
</script>
<CourseContainer {courseData}>
<CourseContainer>
<PageNav title="Scoreboard" />
</CourseContainer>
+248
View File
@@ -0,0 +1,248 @@
<script context="module">
import { fetchCourse } from '../../../utils/services/courses';
export async function preload({ params }, session) {
if (process.browser) {
return {};
}
const {
data,
// error
} = await fetchCourse(params.id, session);
return { courseData: data };
}
</script>
<script>
import { onMount } from 'svelte';
import { flip } from 'svelte/animate';
import { dndzone } from 'svelte-dnd-action';
import { stores, goto } from '@sapper/app';
import PageNav from '../../../components/PageNav/index.svelte';
import MarkExerciseModal from '../../../components/Course/components/Lesson/Exercise/MarkExerciseModal.svelte';
import Chip from '../../../components/Chip/index.svelte';
import PageBody from '../../../components/PageBody/index.svelte';
import CourseContainer from '../../../components/CourseContainer/index.svelte';
import { setCourseData, course } from '../../../components/Course/store';
import { fetchSubmissions } from '../../../utils/services/submissions';
const flipDurationMs = 300;
export let courseData = {};
let sections = [
{
id: 1,
title: 'Submitted',
value: 0,
items: [],
},
{
id: 2,
title: 'In Progress',
value: 0,
items: [],
},
{
id: 3,
title: 'Graded',
value: 10,
items: [],
},
];
let submissionIdData = {};
let submissionId;
let openExercise = false;
const { page } = stores();
function handleItemFinalize(columnIdx, newItems) {
console.log('handleItemFinalize columnIdx=>', columnIdx, newItems);
sections[columnIdx].items = newItems;
// onFinalUpdate([...sections]);
}
function handleDndConsiderCards(columnIdx) {
return (e) => {
console.warn(
'handleDndConsiderCards columnIdx',
columnIdx,
e.detail.items
);
sections[columnIdx].items = e.detail.items;
};
}
function handleDndFinalizeCards(columnIdx) {
return (e) => handleItemFinalize(columnIdx, e.detail.items);
}
function handleModalClose() {
goto($page.path);
}
function formatAnswers(data) {
const answers = {};
const questionByIdAndName = {};
for (const question of data.questions) {
questionByIdAndName[question.id] = question.name;
}
for (const answer of data.answers) {
const questionName = questionByIdAndName[answer.question_id];
answers[questionName] =
Array.isArray(answer.answers) && answer.answers.length
? answer.answers
: answer.open_answer;
}
return answers;
}
onMount(async () => {
setCourseData(courseData);
console.log(`courseData`, courseData);
console.log(`$course`, $course);
const { data: submissions } = await fetchSubmissions(
courseData.id || $course.id
);
const sectionById = {};
for (const submission of submissions) {
const { id, exercise, course, answers, groupmember, status_id } =
submission;
const submissionItem = {
id,
exercise: {
id: exercise.id,
title: exercise.title,
},
answers,
student: groupmember && groupmember.profile ? groupmember.profile : {},
lesson: {
id: exercise.lesson.id,
title: exercise.lesson.title,
},
};
submissionIdData[id] = {
questions: exercise.questions,
answers: formatAnswers({ questions: exercise.questions, answers }),
};
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]
: [],
}));
console.log(`submissionIdData`, submissionIdData);
});
$: {
submissionId = $page.query.submissionId;
openExercise = !!submissionId;
}
</script>
<MarkExerciseModal
bind:open={openExercise}
onClose={handleModalClose}
data={submissionIdData[submissionId] || {}}
{submissionId}
/>
<CourseContainer>
<PageNav title="Submitted Exercises" />
<PageBody width="w-11/12 overflow-x-auto overflow-y-hidden">
<div class="flex items-center justify-between w-full">
{#each sections as { id, title, items }, idx (id)}
<div
class="section rounded-md bg-gray-100 border border-gray-50 p-3 h-80 mr-3 overflow-hidden"
animate:flip={{ duration: flipDurationMs }}
>
<div class="flex items-center mb-2">
<Chip value={items.length} />
<p class="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="border border-grey-700 w-full my-2 mx-0 rounded-md bg-white py-2 px-3"
animate:flip={{ duration: flipDurationMs }}
>
<a
class="flex items-center no-underline hover:underline text-black mb-2"
href="{$page.path}?submissionId={item.student.id}"
>
<img
alt="Placeholder"
class="block rounded-full"
width="24"
height="20"
src={item.student.avatar_url}
/>
<p class="ml-2 text-sm">{item.student.username}</p>
</a>
<a
class="text-blue-700 text-md font-bold"
href="{$page.path}?submissionId={item.id}"
>
{item.exercise.title}
</a>
<a
class="flex items-center no-underline hover:underline text-black my-2"
href="{$page.path.replace('submissions', 'lessons')}/{item
.lesson.id}/exercises"
>
<p class="text-grey text-sm">
#{item.lesson.title}
{`${item.tutor ? 'created by' + item.tutor.name : ''}`}
</p>
</a>
</div>
{/each}
</div>
</div>
{/each}
</div>
</PageBody>
</CourseContainer>
<style>
.section {
max-width: 355px;
min-width: 355px;
height: 80vh;
}
.content {
height: 95%;
}
</style>
+20 -12
View File
@@ -1,25 +1,33 @@
<script context="module">
export async function preload({ params }) {
let [courseId] = params.id;
import { fetchCourse } from '../../../utils/services/courses';
const res = await this.fetch(`api/course?id=${courseId}`);
const data = await res.json();
if (res.status === 200) {
return { courseId, courseData: data };
} else {
this.error(res.status, data.message);
export async function preload({ params }, session) {
if (process.browser) {
return {};
}
const {
data,
// error
} = await fetchCourse(params.id, session);
return { courseData: data };
}
</script>
<script>
import CourseContainer from "../../../components/CourseContainer/index.svelte";
import PageNav from "../../../components/PageNav/index.svelte";
import { onMount } from 'svelte';
import CourseContainer from '../../../components/CourseContainer/index.svelte';
import PageNav from '../../../components/PageNav/index.svelte';
import { setCourseData } from '../../../components/Course/store';
export let courseData = {};
onMount(() => {
setCourseData(courseData);
});
</script>
<CourseContainer {courseData}>
<CourseContainer>
<PageNav title="Timetable" />
</CourseContainer>
+5 -1
View File
@@ -80,6 +80,10 @@
];
</script>
<svelte:head>
<title>Dashboard - ClassroomIO</title>
</svelte:head>
<div class="root w-full py-10 bg-gray-100">
<div class="flex items-center justify-evenly max-w-6xl m-auto">
{#each boxes as box}
@@ -138,7 +142,7 @@
<style>
.root {
min-height: 90vh;
min-height: 93vh;
}
.table {
+7 -3
View File
@@ -53,9 +53,7 @@
<title>ClassroomIO - The classroom software</title>
</svelte:head>
<div
class="w-screen flex items-center justify-center flex-col m-2 sm:h-screen sm:m-0"
>
<div class="root w-screen flex items-center justify-center flex-col m-2 sm:m-0">
<img
src="/logo-192.png"
alt="ClassroomIO logo"
@@ -128,4 +126,10 @@
.box {
max-width: 350px;
}
@media (min-width: 640px) {
.root {
height: 93vh;
}
}
</style>
+31 -14
View File
@@ -5,9 +5,11 @@
</script>
<script>
import { Chasing } from 'svelte-loading-spinners';
import TextField from '../components/Form/TextField.svelte';
import PrimaryButton from '../components/PrimaryButton/index.svelte';
import { getSupabase } from '../utils/functions/supabase';
import { user } from '../utils/store/user';
export let config;
@@ -37,7 +39,6 @@
success = true;
email = {};
alert('Check your email for the login link!');
} catch (error) {
errors = {
email: error.error_description || error.message,
@@ -69,33 +70,49 @@
<p class="text-sm">into ClassroomIO</p>
<div class="my-4 w-full">
{#if success}
<p class="text-xl text-center">Please check your email to signin</p>
{#if $user.fetchingUser}
<div class="h-40 text-md flex items-center justify-center">
<Chasing size="60" color="#ff3e00" unit="px" duration="1s" />
</div>
{:else if success}
<div class="h-40 text-md flex items-center justify-center flex-col">
<p class="">
A link has been sent to your email. Please check both your
<strong>spam</strong>and your<strong>inbox</strong>.
</p>
<p class="mt-2 underline">
If our email is in your spam, do make sure you mark our email
address as safe, then move it from your spam to your inbox.
</p>
</div>
{:else}
<TextField
label="Email address"
bind:value={email}
type="email"
autoFocus={true}
placeholder="Enter your email"
placeholder="you@domain.com"
inputClassName="rounded-md w-full px-2 py-4"
isDisabled={loading}
errorMessage={errors.email}
/>
<p class="py-3 text-sm">
<p class="py-3 text-md">
A magic link will be sent to your email, please open the link
</p>
{/if}
</div>
<div class="my-4 w-full flex justify-between items-center">
<a href="/signup" class="text-blue-700 text-sm">Create an account</a>
<PrimaryButton
label={loading ? 'Creating...' : 'Next'}
type="submit"
className="py-2 px-4"
isDisabled={loading}
/>
</div>
{#if !success}
<div class="my-4 w-full flex justify-between items-center">
<a href="/signup" class="text-blue-700 text-sm">Create an account</a>
<PrimaryButton
label={loading ? 'Creating...' : 'Next'}
type="submit"
className="py-2 px-4"
isDisabled={loading}
/>
</div>
{/if}
</form>
</div>
</div>
+15
View File
@@ -0,0 +1,15 @@
function writeJsonHead(res, statusCode) {
res.writeHead(statusCode, {
'Content-Type': 'application/json',
});
}
export function post(req, res) {
const { email } = req.body;
console.log(`req.session`, req.session);
writeJsonHead(res, 200);
return res.end(JSON.stringify({ success: true }));
}
+1 -1
View File
@@ -87,6 +87,6 @@
<style>
.root {
min-height: 90vh;
min-height: 93vh;
}
</style>
-67
View File
@@ -1,67 +0,0 @@
<script context="module">
export function preload(page, { config }) {
return { config };
}
</script>
<script>
import { getSupabase } from '../utils/functions/supabase';
import { user, profile } from '../utils/store/user';
export let config;
let supabase = getSupabase(config);
let loading = true;
let username = null;
let website = null;
let avatar_url = null;
async function updateProfile() {
try {
loading = true;
const user = supabase.auth.user();
const updates = {
id: user.id,
username,
website,
avatar_url,
updated_at: new Date(),
};
let { error } = await supabase.from('profiles').upsert(updates, {
returning: 'minimal', // Don't return the value after inserting
});
if (error) throw error;
} catch (error) {
alert(error.message);
} finally {
loading = false;
}
}
</script>
<form class="form-widget" on:submit|preventDefault={updateProfile}>
<div>
<label for="email">Email</label>
<input id="email" type="text" value={$user.email} disabled />
</div>
<div>
<label for="username">Name</label>
<input id="username" type="text" bind:value={username} />
</div>
<div>
<label for="website">Website</label>
<input id="website" type="website" bind:value={website} />
</div>
<div>
<input
type="submit"
class="button block primary"
value={loading ? 'Loading ...' : 'Update'}
disabled={loading}
/>
</div>
</form>
+199
View File
@@ -0,0 +1,199 @@
<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';
import PrimaryButton from '../../components/PrimaryButton/index.svelte';
import { supabase } from '../../utils/functions/supabase';
import { user, profile } from '../../utils/store/user';
export let profileId;
let loading = false;
let currentProfile = {};
let isOwner = false;
let docs = [];
let initialValueOfUserName;
let initialValueOfFullName;
async function getProfile(profileId) {
loading = true;
// Get user profile
// Check if user has profile
let {
data: profileData,
error,
status,
} = await supabase.from('profile').select(`*`).eq('id', profileId).single();
if (error && !profileData && status === 406 && $user.currentSession) {
// Not found
} else if (profileData) {
// Profile exists, go to profile page
currentProfile = profileData;
initialValueOfUserName = currentProfile.username;
initialValueOfFullName = currentProfile.fullname;
}
loading = false;
}
async function updateProfile() {
try {
loading = true;
const updates = {
fullname: currentProfile.fullname,
username: currentProfile.username,
avatar_url: currentProfile.avatar_url,
};
let { error } = await supabase
.from('profile')
.update(updates, {
returning: 'minimal', // Don't return the value after inserting
})
.match({ id: profileId });
if (error) throw error;
} catch (error) {
alert(error.message);
loading = false;
} finally {
loading = false;
initialValueOfUserName = currentProfile.username;
initialValueOfFullName = currentProfile.fullname;
}
}
onMount(() => {});
$: isOwner = $profile.id === profileId;
$: {
getProfile(profileId);
}
</script>
<svelte:head>
<title>Profile of {currentProfile.username}</title>
</svelte:head>
<section class="root w-11/12 mt-3 m-auto flex items-start justify-between">
{#if currentProfile.id}
<div class="w-1/4 flex items-center flex-col">
<img
alt={currentProfile.username}
src={currentProfile.avatar_url}
height="260"
width="260"
class="rounded-full bg-light-blue-100 mb-4"
/>
{#if isOwner}
<TextField
label="Full name"
bind:value={currentProfile.fullname}
className="w-60 mb-4"
inputClassName="rounded-md"
/>
<TextField
label="Username"
bind:value={currentProfile.username}
className="w-60 mb-4"
inputClassName="rounded-md"
/>
{#if initialValueOfUserName !== currentProfile.username || initialValueOfFullName !== currentProfile.fullname}
<PrimaryButton
label={loading ? 'Updating...' : 'Update profile'}
onClick={updateProfile}
isDisabled={loading}
/>
{/if}
{:else}
<h3 class="text-xl">{currentProfile.username}</h3>
{/if}
</div>
<div class="flex-grow" />
<div class="flex-grow flex flex-wrap mx-3 w-3/5">
<div class="mb-3 w-full">
<h3 class="font-bold text-md m-0">Organizations</h3>
<p class="text-gray-500 text-sm">
This are the organizations you work at
</p>
<div class="my-3 py-2 px-4 border shadow-sm rounded-md">
<h4 class="m-0 font-normal">
<a class="text-blue-700 font-bold" href="/courses"> CitiBim </a>
</h4>
<p class="text-gray-500">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Non aliquam
tenetur magnam. Alias cumque quaerat expedita dolores placeat
laboriosam culpa, suscipit veritatis adipisci eius magni obcaecati.
Sequi doloremque blanditiis earum.
</p>
<div class="mt-2 flex items-center">
<span class="flex mr-2 items-center">
<UserAvatar16 class="mr-2" /> 5
</span>
<span class="flex mr-2 items-center">
<Book16 class="mr-2" /> 5
</span>
</div>
</div>
<div class="my-2 py-2 px-4 border shadow-sm rounded-md">
<h4 class="m-0 font-normal">
<a class="text-blue-700 font-bold" href="/courses">
Climate Change Group
</a>
</h4>
<p class="text-gray-500">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Illo quasi
autem quo possimus illum consequuntur repudiandae nobis error, porro
</p>
<div class="mt-2 flex items-center">
<span class="flex mr-2 items-center">
<UserAvatar16 class="mr-2" /> 30
</span>
<span class="flex mr-2 items-center">
<Book16 class="mr-2" /> 3
</span>
</div>
</div>
</div>
</div>
<!-- {:else if loading}
<Box>
<Chasing size="60" color="#ff3e00" unit="px" duration="1s" />
</Box>
{:else}
<Box>
<div>
<Search32 />
</div>
<h3 class="text-xxl">Profile not found</h3>
</Box> -->
{/if}
</section>
<style>
.root {
max-width: 1204px;
}
</style>
+1 -1
View File
@@ -29,7 +29,7 @@ express() // You can also use Express
sapper.middleware({
session: async (req) => {
const { user } = await supabase.auth.api.getUserByCookie(req);
console.log('cookie calling');
return {
user: JSON.stringify(user),
config,
+5
View File
@@ -0,0 +1,5 @@
export const ROLE = {
ADMIN: 1,
TUTOR: 2,
STUDENT: 3,
};
+11
View File
@@ -0,0 +1,11 @@
export function isUUID(uuid) {
let s = '' + uuid;
s = s.match(
'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
);
if (s === null) {
return false;
}
return true;
}
+4 -4
View File
@@ -1,9 +1,9 @@
import { createClient } from '@supabase/supabase-js';
let _supabase;
export let supabase;
export const getSupabase = (config) => {
if (_supabase) return _supabase;
if (supabase) return supabase;
if (!config && process && process.env) {
config = {
@@ -15,10 +15,10 @@ export const getSupabase = (config) => {
};
}
_supabase = createClient(
supabase = createClient(
config.supabaseConfig.url,
config.supabaseConfig.anonKey
);
return _supabase;
return supabase;
};
+5
View File
@@ -0,0 +1,5 @@
export function validateEmail(email) {
const re =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(String(email).toLowerCase());
}
+264
View File
@@ -0,0 +1,264 @@
import { supabase } from '../../functions/supabase';
import { isUUID } from '../../functions/isUUID';
import { QUESTION_TYPE } from '../../../components/Question/constants';
export async function fetchCourse(courseId, session) {
let jsonUser;
try {
jsonUser = JSON.parse(session.user);
} catch (error) {
console.log('Token is expired probably cause user not found');
return this.redirect(301, '/login');
}
const { data, error } = await supabase
.from('course')
.select(
`
id,
title,
overview,
group(*,
members:groupmember(*,
profile(*)
)
),
lessons:lesson(
id, title,public, lesson_at, call_url, is_complete,created_at,
profile:teacher_id(id, avatar_url, fullname)
)
`
)
.match({ id: courseId })
.single();
if (!data || error) {
console.log(`data`, data);
console.log(`fetchCourse => error`, error);
return this.redirect(301, '/courses');
}
return {
data,
error,
};
}
export async function updateCourse(courseId, course) {
return await supabase
.from('course')
.update(course, { returning: 'minimal' })
.match({ id: courseId });
}
export function fetchLesson(lessonId) {
return supabase
.from('lesson')
.select(`id, note, video_url, slide_url, call_url`)
.eq('id', lessonId)
.single();
}
export function createLesson(lesson) {
return supabase.from('lesson').insert(lesson);
}
export function updateLesson(lesson, lessonId) {
return supabase
.from('lesson')
.update(lesson, { returning: 'minimal' })
.match({ id: lessonId });
}
export function deleteLesson(lessonId) {
// Need to implement soft delete
return supabase.from('lesson').delete().match({ id: lessonId });
}
export function createExercise(exercise) {
return supabase.from('exercise').insert(exercise);
}
function isNew(item) {
return isNaN(item);
}
export async function upsertExercise(questionnaire, exerciseId) {
const {
questions,
title,
description,
is_title_dirty,
is_description_dirty,
} = questionnaire;
if (is_description_dirty || is_title_dirty) {
await supabase
.from('exercise')
.update({
title,
description,
})
.match({ id: exerciseId });
}
const updatedQuestions = [];
for (const question of questions) {
const {
title,
id,
name,
question_type,
options,
deleted_at,
points,
is_dirty,
} = question;
// DELETE /delete/:questionId
if (deleted_at) {
// Delete from server only if this question exists in the database
if (!isNew(id)) {
Promise.all([
supabase.from('option').delete().match({ question_id: id }),
supabase.from('question').delete().match({ id }),
]);
}
// Skip the remaining operation so we filter out this question from the new questions array
continue;
}
// INSERT or UPDATE /update/:questionId or /insert/:questionId
const newQuestion = {
id: isNew(id) ? undefined : id,
name: isNew(id) ? undefined : name,
title,
points,
question_type_id: question_type.id,
exercise_id: exerciseId,
};
let questionSupabaseRes;
if (is_dirty || isNew(id)) {
const res = await supabase.from('question').upsert(newQuestion);
if (res.error) {
console.error(`Upsert question`, error);
}
questionSupabaseRes = Array.isArray(res.data) ? res.data[0] : null;
} else {
questionSupabaseRes = Object.assign(newQuestion);
}
if (questionSupabaseRes) {
const { question_type_id, id, name } = questionSupabaseRes;
// Delete cause this is not a field in the table
delete newQuestion.question_type_id;
newQuestion.question_type = { id: question_type_id };
newQuestion.id = id;
newQuestion.name = name;
newQuestion.options = [];
// Don't map options for 'Paragraph' questions
if (QUESTION_TYPE.TEXTAREA !== question_type_id) {
for (const option of options) {
const { deleted_at, is_dirty } = option;
// DELETE /delete/:optionId
if (deleted_at) {
// if this option exists in the database
if (!isNew(option.id)) {
supabase.from('option').delete().match({ id: option.id });
}
// Skip the remaining operation so we filter out this option from the new option array
continue;
}
// INSERT and UPDATE
const newOption = {
...option,
is_dirty: undefined,
id: isNew(option.id) ? undefined : option.id,
value: isUUID(option.value) ? option.value : undefined, // this value is of UUID type
question_id: newQuestion.id,
};
if (is_dirty || isNew(option.id)) {
const { data } = await supabase.from('option').upsert(newOption);
if (Array.isArray(data)) {
newQuestion.options.push(data[0]);
}
} else {
newQuestion.options.push(newOption);
}
}
}
updatedQuestions.push(newQuestion);
}
}
return updatedQuestions;
}
export async function submitExercise(
answers,
questions,
exerciseId,
courseId,
group,
profile
) {
let groupMember;
if (Array.isArray(group.tutors)) {
groupMember = group.tutors.find(
(student) => student.profile_id === profile.id
);
}
console.log(`groupMember`, groupMember);
if (!groupMember) {
return;
}
const questionsByName = questions.reduce(
(acc, q) => ({ ...acc, [q.name]: q.id }),
{}
);
const questionAnswers = [];
const { data: submission } = await supabase.from('submission').insert({
submitted_by: groupMember.id,
exercise_id: exerciseId,
course_id: courseId,
});
for (const questionName in answers) {
const value = answers[questionName];
const questionAnswer = {
group_member_id: groupMember.id,
question_id: questionsByName[questionName],
open_answer: '',
answers: [],
submission_id: submission[0].id,
};
if (typeof value === 'string') {
questionAnswer.open_answer = value;
} else {
questionAnswer.answers = value;
}
questionAnswers.push(questionAnswer);
}
const res = await supabase.from('question_answer').insert(questionAnswers);
console.log(`res`, res);
}
+33
View File
@@ -0,0 +1,33 @@
import { supabase } from '../../functions/supabase';
export function fetchSubmissionStatus() {
return supabase.from('submissionstatus').select(`*`);
}
export function fetchSubmissions(course_id) {
return supabase
.from('submission')
.select(
`
id,
answers:question_answer(*),
exercise:exercise_id(
id, title,
lesson:lesson_id(id, title),
questions:question(
*,
options:option(*),
question_type:question_type_id(id, label)
)
),
status_id,
course:course_id(*),
groupmember:submitted_by(
profile(*)
)
`
)
.match({
course_id,
});
}
+15 -2
View File
@@ -1,4 +1,17 @@
import { writable } from 'svelte/store';
export const user = writable(false);
export const profile = writable(false);
export const user = writable({
openAuthModal: false,
fetchingUser: false,
isLoggedIn: false,
currentSession: null,
expiresAt: 0,
});
export const profile = writable({
id: null,
avatar_url:
'https://pbs.twimg.com/profile_images/1416443682157473795/dGtFbtht_normal.jpg',
username: 'Elon',
user_id: '',
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB