Sync with Server
This commit is contained in:
@@ -104,7 +104,7 @@
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
height: 90vh;
|
||||
height: 93vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
border-left: 1px solid var(--border-color);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<div class="w-full h-80 flex items-center justify-center flex-col">
|
||||
<slot />
|
||||
</div>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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
-1
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export const statuses = [
|
||||
{
|
||||
chipClassName: 'bg-green-600 text-white',
|
||||
chipLabel: 'In progress',
|
||||
},
|
||||
{
|
||||
chipClassName: 'bg-red-600 text-white',
|
||||
chipLabel: 'Completed',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,67 @@
|
||||
import { validateEmail } from '../../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,
|
||||
};
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const courses = writable([]);
|
||||
|
||||
export const createCourseModal = writable({
|
||||
open: false,
|
||||
title: '',
|
||||
description: '',
|
||||
emails: '',
|
||||
tutors: '',
|
||||
students: '',
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<script>
|
||||
export let message;
|
||||
</script>
|
||||
|
||||
<p class="text-sm text-red-500">{message}</p>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ===
|
||||
|
||||
Reference in New Issue
Block a user