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
@@ -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);
}