CLASSROOMIO-T-49

merge

CLASSROOMIO-T-49: Quiz CRUD

CLASSROOMIO-T-49: Validate quiz question

CLASSROOMIO-T-49: Begin work on quiz preview

CLASSROOMIO-T-49: Complete quiz page

CLASSROOMIO-T-49: Init quiz work

CLASSROOMIO-T-49: Quiz CRUD

CLASSROOMIO-T-49: Validate quiz question

CLASSROOMIO-T-49: Begin work on quiz preview

CLASSROOMIO-T-49: Complete quiz page

CLASSROOMIO-T-49: Init quiz work


Co-authored-by: Rotimi Best <rotimiibitoyeemma@gmail.com>

Merge-request: CLASSROOMIO-MR-14
Merged-by: Best Admin <digdippa@gmail.com>
This commit is contained in:
Rotimi Best
2023-06-17 19:11:29 +00:00
committed by Space Cloud
co-authored by Rotimi Best
parent b532ea6d29
commit 1e71a91a54
70 changed files with 10601 additions and 497 deletions
+1
View File
@@ -0,0 +1 @@
{"imports":{"netlify:edge":"https://edge.netlify.com/v1/index.ts"},"scopes":{}}
@@ -0,0 +1 @@
module.exports = require('./src/netlify/functions/create-org-site.js')
File diff suppressed because one or more lines are too long
+1
View File
@@ -35,6 +35,7 @@
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"marked": "^1.2.7",
"mixpanel-browser": "^2.47.0",
"netlify": "^13.1.0",
"node-sass": "^6.0.0",
"paystack": "^2.0.1",
+11
View File
@@ -0,0 +1,11 @@
{
"name": "server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "rotimi-best",
"license": "ISC"
}
@@ -48,7 +48,7 @@
}
</script>
<div class="p-4 bg-white dark:bg-gray-800-100 border-2 rounded-md mb-3">
<div class="p-4 bg-white dark:bg-gray-800 border-2 rounded-md mb-3">
{#if selectedOptionToView}
<div>
<div class="flex items-center mb-5">
-93
View File
@@ -1,93 +0,0 @@
<script context="module">
import ChevronDown24 from 'carbon-icons-svelte/lib/ChevronDown24';
import TextEditor from '../TextEditor/index.svelte';
let questionTitle = '';
let questionTags = [];
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' },
];
function handleClick() {
things = things.slice(1);
}
</script>
<svelte:head>
<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">Ask a question.</h2>
<div class="my-1 px-1">
<div class="mb-5">
<button on:click={handleClick}> Remove first thing </button>
{#each things as thing}
<p style="color: {thing.color}">{thing.id}</p>
{/each}
<button
class="flex border rounded-lg border-grey py-1 px-3 focus:outline-none focus:border-gray-400 focus:bg-gray-200"
on:click={handleFacultyFilters}
>
<span>Факултеты</span>
<ChevronDown24 class="ml-2" />
</button>
{#if facultyFilterOpened}
<div
class="w-48 bg-white dark:bg-gray-800 border border-grey rounded-lg mt-2 py-3"
>
{#each faculties as faculty}
<div class="block px-4 py-3 border-b">
<input
type="checkbox"
class="border border-gray checked:bg-blue-600 checked:border-transparent mr-1 cursor-pointer"
/>
<span>{faculty}</span>
</div>
{/each}
</div>
{/if}
</div>
<input
class="title rounded-lg border border-1 border-gray p-2 w-full focus:outline-none"
error="false"
autocomplete="off"
placeholder="Title"
aria-label="Title"
aria-describedby="title-input-validation"
type="text"
name="discussion[title]"
id="discussion_title"
value={questionTitle}
/>
</div>
<TextEditor
value={questionDescription}
placeholder="Задайте вопрос или начните разговор"
/>
</div>
@@ -177,7 +177,7 @@
{:else}
<Box>
<AudioConsole32 class="carbon-icon w-80" />
<h3 class="text-3xl text-gray-500 dark:text-gray-200">
<h3 class="text-3xl text-gray-500 dark:text-white">
No Exercise Added
</h3>
</Box>
@@ -81,9 +81,7 @@
/>
{:else}
<Box>
<h3 class="text-3xl text-gray-500 dark:text-gray-200">
No slide added
</h3>
<h3 class="text-3xl text-gray-500 dark:text-white">No slide added</h3>
<img alt="Slide not found" src="/notfound.webp" class="w-80" />
</Box>
{/if}
@@ -101,9 +99,7 @@
<MarkdownRender content={$lesson.materials.note} />
{:else}
<Box>
<h3 class="text-3xl text-gray-500 dark:text-gray-200">
No note added
</h3>
<h3 class="text-3xl text-gray-500 dark:text-white">No note added</h3>
<img alt="Lesson note not found" src="/notfound.webp" class="w-80" />
</Box>
{/if}
@@ -133,7 +129,7 @@
{/each}
{:else}
<Box>
<h3 class="text-3xl text-gray-500 dark:text-gray-200">
<h3 class="text-3xl text-gray-500 dark:text-white">
No youtube video added
</h3>
<img alt="Video not found" src="/notfound.webp" class="w-80" />
@@ -3,7 +3,7 @@
import { copyCourseModal } from '../../store';
import { ROLE } from '../../../../utils/constants/roles';
import getCurrencyFormatter from '../../../../utils/functions/getCurrencyFormatter';
import OverflowMenuHorizontal24 from 'carbon-icons-svelte/lib/OverflowMenuHorizontal24';
import OverflowMenuHorizontal20 from 'carbon-icons-svelte/lib/OverflowMenuHorizontal20';
import ArrowRight from 'carbon-icons-svelte/lib/ArrowRight24';
export let bannerImage: string | undefined;
@@ -60,7 +60,7 @@
isIcon={true}
>
<div class="p-1 rounded-full bg-white dark:bg-gray-600">
<OverflowMenuHorizontal24 class="carbon-icon" />
<OverflowMenuHorizontal20 class="carbon-icon" />
</div>
</Dropdown>
{/if}
@@ -74,7 +74,7 @@
{title}
</h4>
<p
class="description text-left text-sm font-small flex flex-col text-gray-500 dark:text-gray-200 my-3"
class="description text-left text-sm font-small flex flex-col text-gray-500 dark:text-white my-3"
title={description}
>
{description}
+6 -5
View File
@@ -8,7 +8,7 @@
let open = false;
const notIconClass =
'border rounded-lg border-grey py-3 px-3 focus:outline-none focus:border-gray-400 focus:bg-gray-200';
'border rounded-lg border-grey p-3 focus:outline-none focus:border-gray-400 focus:bg-gray-200';
function isOptionLast(index) {
return index === options.length - 1;
@@ -29,7 +29,7 @@
}
</script>
<div class="{classNames} z-10d {!options.length && 'hidden'}">
<div class="{classNames} z-10 {!options.length && 'hidden'}">
<button
class="flex flex-row items-center {!isIcon && notIconClass}"
on:click={(e) => {
@@ -45,12 +45,13 @@
{#if open}
<div
class="dropdown w-48 bg-white dark:bg-gray-800 border border-grey rounded-lg mt-2 py-3 absolute"
class="dropdown whitespace-nowrap bg-white dark:bg-gray-800 border border-grey rounded-lg mt-2 py-3 absolute"
>
{#each options as option, index}
<button
class="block px-4 py-3 {!isOptionLast(index) &&
'border-b'} w-full dark:text-white"
class="block px-4 py-3 text-left hover:bg-blue-100 {!isOptionLast(
index
) && 'border-b'} w-full dark:text-white dark:hover:text-black"
on:click={onClick(option)}
>
{option.label}
+1 -1
View File
@@ -49,7 +49,7 @@
{errorMessage}
</p>
{:else if helperMessage}
<p class="text-sm text-gray-500 dark:text-gray-200">
<p class="text-sm text-gray-500 dark:text-white">
{helperMessage}
</p>
{/if}
+2 -1
View File
@@ -10,6 +10,7 @@
export let className = '';
export let inputClassName = '';
export let labelClassName = '';
export let bgColor = 'bg-gray-200';
export let type = 'text';
export let isPassword = false;
export let autoFocus = false;
@@ -65,7 +66,7 @@
{/if}
<input
use:typeAction
class="form-input dark:text-black p-3 mt-1 block w-full border-none bg-gray-200 {inputClassName} {errorMessage
class="form-input {inputClassName} dark:text-black p-3 mt-1 block w-full border-none {bgColor} {errorMessage
? 'border-red-500'
: ''}"
on:keydown={onKeyDown}
+13
View File
@@ -0,0 +1,13 @@
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="dark"
>
<path
d="M21.75 6H19.5V5.25C19.5 4.85218 19.342 4.47064 19.0607 4.18934C18.7794 3.90804 18.3978 3.75 18 3.75H6C5.60218 3.75 5.22064 3.90804 4.93934 4.18934C4.65804 4.47064 4.5 4.85218 4.5 5.25V6H2.25C1.85218 6 1.47064 6.15804 1.18934 6.43934C0.908035 6.72064 0.75 7.10218 0.75 7.5V9C0.75 9.99456 1.14509 10.9484 1.84835 11.6517C2.19657 11.9999 2.60997 12.2761 3.06494 12.4645C3.51991 12.653 4.00754 12.75 4.5 12.75H4.84219C5.28398 14.1501 6.12634 15.39 7.26516 16.3166C8.40398 17.2431 9.78933 17.8157 11.25 17.9634V20.25H9C8.80109 20.25 8.61032 20.329 8.46967 20.4697C8.32902 20.6103 8.25 20.8011 8.25 21C8.25 21.1989 8.32902 21.3897 8.46967 21.5303C8.61032 21.671 8.80109 21.75 9 21.75H15C15.1989 21.75 15.3897 21.671 15.5303 21.5303C15.671 21.3897 15.75 21.1989 15.75 21C15.75 20.8011 15.671 20.6103 15.5303 20.4697C15.3897 20.329 15.1989 20.25 15 20.25H12.75V17.9606C15.7444 17.6578 18.2288 15.5569 19.1325 12.75H19.5C20.4946 12.75 21.4484 12.3549 22.1516 11.6517C22.8549 10.9484 23.25 9.99456 23.25 9V7.5C23.25 7.10218 23.092 6.72064 22.8107 6.43934C22.5294 6.15804 22.1478 6 21.75 6ZM4.5 11.25C3.90326 11.25 3.33097 11.0129 2.90901 10.591C2.48705 10.169 2.25 9.59674 2.25 9V7.5H4.5V10.5C4.5 10.75 4.51219 11 4.53656 11.25H4.5ZM18 10.4156C18 13.7456 15.3291 16.4756 12.0459 16.5H12C10.4087 16.5 8.88258 15.8679 7.75736 14.7426C6.63214 13.6174 6 12.0913 6 10.5V5.25H18V10.4156ZM21.75 9C21.75 9.59674 21.5129 10.169 21.091 10.591C20.669 11.0129 20.0967 11.25 19.5 11.25H19.4531C19.4839 10.9729 19.4995 10.6944 19.5 10.4156V7.5H21.75V9Z"
fill="#7A797D"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+9 -3
View File
@@ -8,6 +8,7 @@
export let width = '';
export let maxWidth = '';
export let containerClass = '';
export let size = '';
afterUpdate(() => {
if (open) {
@@ -25,8 +26,9 @@
>
<div aria-hidden="true" class="backdrop" />
<div
class="{maxWidth ||
'container'} bg-white dark:bg-gray-800 mx-auto {width} shadow-lg p-5 pt-2 rounded-md"
class="{maxWidth || 'container'} {size === 'sm'
? 'small'
: ''} bg-white dark:bg-gray-800 mx-auto {width} shadow-lg p-5 pt-2 rounded-md"
on:click={(e) => e.stopPropagation()}
>
<div class="flex items-center justify-between mb-4">
@@ -36,7 +38,7 @@
<CloseButton onClick={onClose} contained={true} />
</div>
<div class="body h-4/5 overflow-y-auto {containerClass}">
<div class="body p-1 h-4/5 overflow-y-auto {containerClass}">
<slot />
</div>
</div>
@@ -54,4 +56,8 @@
.body {
max-height: 60vh;
}
.small {
max-width: 388px;
}
</style>
+1 -1
View File
@@ -71,7 +71,7 @@
{discussion.title}
</a>
</h4>
<span class="text-gray-600 dark:text-gray-100">
<span class="text-gray-600 dark:text-white">
{discussion.author} asked {discussion.createdAt}
</span>
</div>
@@ -0,0 +1,69 @@
<script>
import PrimaryButton from '../../PrimaryButton/index.svelte';
import { VARIANTS } from '../../PrimaryButton/constants';
import Modal from '../../Modal/index.svelte';
import { deleteModal, quizesStore } from '../../../utils/store/org';
import { supabase } from '../../../utils/functions/supabase';
export let onDelete = () => {};
function closeModal() {
deleteModal.update((m) => ({
...m,
open: false,
id: null,
isQuestion: false,
}));
}
async function deleteQuiz() {
const { data, error } = await supabase
.from('quiz')
.delete()
.match({ id: $deleteModal.id });
console.log('data', data);
console.log('error', error);
if (error) {
return;
}
$quizesStore = $quizesStore.filter((q) => q.id !== $deleteModal.id);
closeModal();
}
</script>
<Modal
onClose={closeModal}
bind:open={$deleteModal.open}
width="w-2/5"
modalHeading={$deleteModal.isQuestion ? 'Delete Question' : 'Delete Quiz'}
size="sm"
>
<div class="w-full flex flex-col items-center my-10">
<h1 class="dark:text-white text-lg text-center">
{#if $deleteModal.isQuestion}
Are you sure you want to delete this question?
{:else}
Are you sure you want to delete this quiz?
{/if}
You will lose your content and this action can’t be undone.
</h1>
<div class="mt-5">
<PrimaryButton
className="px-6 py-3"
variant={VARIANTS.CONTAINED_DANGER}
label="Delete"
onClick={() => {
if ($deleteModal.isQuestion) {
onDelete();
$deleteModal.open = false;
} else {
deleteQuiz();
}
}}
/>
</div>
</div>
</Modal>
+21
View File
@@ -0,0 +1,21 @@
<script lang="ts">
import { SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte';
</script>
<div
class="root w-full border border-gray-100 dark:bg-gray-700 mr-4 mb-4 rounded-md transition ease-in-out relative flex p-3"
>
<SkeletonPlaceholder style="width: 20%; height: 100%;" />
<div class="w-full mx-5 flex justify-evenly flex-col">
<!-- Title -->
<SkeletonText style="width: 50%;" />
<!-- Description -->
<SkeletonText style="width: 50%; height: 4rem;" />
</div>
</div>
<style>
.root {
height: 180px;
}
</style>
+141
View File
@@ -0,0 +1,141 @@
<script>
import { goto } from '@sapper/app';
import PrimaryButton from '../../PrimaryButton/index.svelte';
import Modal from '../../Modal/index.svelte';
import TextField from '../../Form/TextField.svelte';
import { snackbarStore } from '../../Snackbar/store';
import { SNACKBAR_SEVERITY } from '../../Snackbar/constants';
import { supabase } from '../../../utils/functions/supabase';
import {
currentOrg,
createQuizModal,
currentOrgPath,
quizesStore,
} from '../../../utils/store/org';
import { createQuizValidation } from '../../../utils/functions/validator';
let open = false;
let errors = {
title: '',
};
let isLoading = false;
function handleClose() {
$createQuizModal.id = null;
$createQuizModal.title = '';
$createQuizModal.open = false;
$createQuizModal.openEdit = false;
}
function errorNotification() {
$snackbarStore.message = "Something's not right - Please try later";
$snackbarStore.severity = SNACKBAR_SEVERITY.ERROR;
$snackbarStore.open = true;
}
async function createQuiz() {
errors = createQuizValidation($createQuizModal);
console.log('createQuiz|updateQuiz errors', errors);
if (Object.keys(errors).length) {
return;
}
isLoading = true;
if ($createQuizModal.openEdit) {
const { error } = await supabase
.from('quiz')
.update(
{ title: $createQuizModal.title, updated_at: new Date() },
{
returning: 'minimal',
}
)
.match({ id: $createQuizModal.id });
isLoading = false;
if (error) {
return errorNotification();
} else {
$quizesStore = $quizesStore.map((s) => {
if (s.id === $createQuizModal.id) {
s.title = $createQuizModal.title;
}
return s;
});
handleClose();
}
} else {
const { data, error } = await supabase.from('quiz').insert({
title: $createQuizModal.title,
organization_id: $currentOrg.id,
questions: [
{
id: 1,
label: '',
type: 'multichoice',
options: [
{
id: 'circle',
label: '',
isCorrect: false,
},
{
id: 'triangle',
label: '',
isCorrect: false,
},
],
},
],
});
isLoading = false;
if (error) {
return errorNotification();
} else {
$quizesStore = [...$quizesStore, data[0]];
handleClose();
goto(`${$currentOrgPath}/quiz/${data[0].id}`);
}
}
}
$: open = $createQuizModal.openEdit || $createQuizModal.open;
</script>
<svelte:head>
<title>Quizzes - ClassroomIO</title>
</svelte:head>
<Modal
onClose={handleClose}
bind:open
width="w-2/5"
modalHeading="{$createQuizModal.openEdit ? 'Update' : 'Create'} a Quiz"
>
<form on:submit|preventDefault={createQuiz}>
<TextField
label="Quiz Title"
bind:value={$createQuizModal.title}
autofocus={true}
placeholder="Your quiz title"
className="mb-4"
isRequired={true}
errorMessage={errors.title}
autoComplete={false}
/>
<div class="mt-5 flex items-center justify-end">
<PrimaryButton
className="px-6 py-3"
label={$createQuizModal.openEdit ? 'Save Changes' : 'Continue'}
type="submit"
isDisabled={isLoading}
{isLoading}
/>
</div>
</form>
</Modal>
@@ -0,0 +1,61 @@
<script>
import { onMount } from 'svelte';
import { SyncLoader } from 'svelte-loading-spinners';
import PlayContainer from './Container.svelte';
import PlayHeader from './Header/index.svelte';
import { quizStore, playQuizStore } from '../../../../utils/store/org';
import { STEPS } from '../../../../utils/constants/quiz';
import { genQuizPin } from '../../../../utils/functions/org';
import PrimaryButton from '../../../PrimaryButton/index.svelte';
import { VARIANTS } from '../../../PrimaryButton/constants';
let isGettingPin = true;
function getPin() {
setTimeout(() => {
$quizStore.pin = genQuizPin();
isGettingPin = false;
}, 3000);
}
function goToPlayersStep() {
$playQuizStore.step = STEPS.WAIT_FOR_PLAYERS;
}
onMount(() => {
getPin();
});
</script>
<PlayContainer>
<div slot="header">
<PlayHeader startCount={true} showCountDown={true} />
</div>
<div
slot="body"
class="w-full rounded-md bg-white dark:bg-gray-700 py-7 px-5"
>
<div class="mb-3">
<p>1. Visit</p>
<h3>play.classroomio.com</h3>
</div>
<div class="">
<p>2. Enter Pin</p>
{#if isGettingPin}
<SyncLoader size="50" color="#1d4ed8" unit="px" duration="1s" />
{:else}
<h3>{$quizStore.pin}</h3>
{/if}
</div>
</div>
<div slot="footer" class="flex justify-center items-center">
<p class="font-bold mr-3">Let's go</p>
<PrimaryButton
label="View Players"
variant={VARIANTS.OUTLINED}
onClick={goToPlayersStep}
/>
</div>
</PlayContainer>
@@ -0,0 +1,43 @@
<div class="root flex flex-col justify-between h-full m-auto">
{#if $$slots.header}
<!-- HEADER -->
<div class="header">
<slot name="header" />
</div>
{/if}
{#if $$slots.body}
<!-- BODY -->
<div class="body mb-3">
<slot name="body" />
</div>
{/if}
{#if $$slots.footer}
<!-- FOOTER -->
<div class="footer bg-white dark:bg-gray-700 py-6 px-2 m-auto">
<slot name="footer" />
</div>
{/if}
</div>
<style>
.root {
max-width: 680px;
width: 80%;
height: 95vh;
}
.header {
height: 15%;
}
.body {
min-height: 60%;
height: fit-content;
}
.footer {
max-width: 400px;
}
</style>
@@ -0,0 +1,7 @@
<script>
import CircleContent from './CircleContent.svelte';
export let answers = 0;
</script>
<CircleContent value={answers} label="Answer(s)" />
@@ -0,0 +1,45 @@
<script>
import { slide } from 'svelte/transition';
export let value = false;
export let label = '';
export let className = '';
function fadeSlide(node, options) {
const slideTrans = slide(node, options);
return {
duration: options.duration,
css: (t) => `
${slideTrans.css(t)}
opacity: ${t};
`,
};
}
</script>
<div class="circle flex items-center flex-col justify-center {className}">
{#key value}
<h3 class="text-blue-600" transition:fadeSlide={{ duration: 500 }}>
{value}
</h3>
{/key}
<p class="text-blue-600 text-md">{label}</p>
</div>
<style>
.circle {
width: 100px;
height: 100px;
line-height: 100px;
border-radius: 50%;
font-size: 50px;
text-align: center;
background: #fff;
}
.circle h3 {
font-size: 40px;
margin: 0;
padding: 0;
line-height: 1;
}
</style>
@@ -0,0 +1,39 @@
<script>
import { onMount } from 'svelte';
import CircleContent from './CircleContent.svelte';
export let startCount = false;
export let timelimit = '';
let countDownNo = 0;
let started = false;
function triggerCountDown(start) {
if (!start || started) return;
const getNum = (k) => parseInt(timelimit.replace(k, ''));
if (timelimit.includes('s')) {
countDownNo = getNum('s');
} else if (timelimit.includes('m')) {
countDownNo = getNum('m') * 60;
}
window.refreshIntervalId = setInterval(() => {
if (--countDownNo <= 0) {
clearInterval(window.refreshIntervalId);
}
}, 1000);
started = true;
}
onMount(() => {
if (startCount) {
triggerCountDown(startCount);
}
});
$: triggerCountDown(startCount);
</script>
<CircleContent value={countDownNo} label="Second(s)" className="mr-3" />
@@ -0,0 +1,18 @@
<script>
import { quizStore } from '../../../../../utils/store/org';
import Answer from './Answer.svelte';
import Timer from './Timer.svelte';
export let startCount = false;
export let showCountDown = false;
</script>
<div class="flex justify-between items-center w-full">
<h3 class="text-white">{$quizStore.title}</h3>
{#if showCountDown}
<div class="flex">
<Timer {startCount} timelimit={$quizStore.timelimit} />
<Answer />
</div>
{/if}
</div>
@@ -0,0 +1 @@
players
@@ -0,0 +1 @@
Podium
+102
View File
@@ -0,0 +1,102 @@
<script>
import { fly } from 'svelte/transition';
import ChevronLeft16 from 'carbon-icons-svelte/lib/ChevronLeft16';
import ChevronRight16 from 'carbon-icons-svelte/lib/ChevronRight16';
import { onMount } from 'svelte';
import QuizQuestion from '../QuizQuestion.svelte';
import PlayContainer from './Container.svelte';
import PlayHeader from './Header/index.svelte';
import { quizStore } from '../../../../utils/store/org';
import { themeImages } from '../../../../utils/constants/quiz';
import PrimaryButton from '../../../PrimaryButton/index.svelte';
import { VARIANTS } from '../../../PrimaryButton/constants';
import IconButton from '../../../IconButton/index.svelte';
export let exitPreview = () => {};
let bgImg;
let totalQ = $quizStore.questions.length;
let curQId = 0;
let currentQuestion = $quizStore.questions[0] || {};
function handlePrev() {
const prevQId = curQId - 1;
console.log('prevQId', prevQId);
const prev = $quizStore.questions[prevQId];
console.log('prev', prev);
if (prev) {
currentQuestion = prev;
curQId = prevQId;
}
}
function handleNext() {
const nextQId = curQId + 1;
console.log('nextqid', nextQId);
const next = $quizStore.questions[nextQId];
console.log('next', next);
if (next) {
currentQuestion = next;
curQId = nextQId;
}
}
onMount(() => {
bgImg = themeImages[$quizStore.theme].play;
});
</script>
{#if bgImg}
<div
class="absolute inset-0 z-50 bg-white h-screen w-screen"
in:fly={{ y: 500, duration: 500 }}
out:fly={{ y: 500, duration: 500 }}
>
<div
class="p-5 h-full w-full"
style="background: url({bgImg}) no-repeat center center fixed; -webkit-background-size: cover;-moz-background-size: cover;-o-background-size: cover;background-size: cover;min-height: 100vh;height: fit-content;"
>
<PlayContainer>
<div slot="header">
<PlayHeader startCount={true} showCountDown={true} />
</div>
<div slot="body" class="quiz-body">
{#if currentQuestion}
<QuizQuestion {currentQuestion} isPreview={true} />
{:else}
<h2>No question added</h2>
{/if}
</div>
<div slot="footer" class="flex justify-center flex-col">
{#if currentQuestion}
<PrimaryButton
label="Exit Preview"
variant={VARIANTS.TEXT}
onClick={exitPreview}
className="w-fit"
/>
<div class="flex justify-center items-center">
<IconButton onClick={handlePrev} size="small">
<ChevronLeft16 class="carbon-icon" />
</IconButton>
<p class="mx-3">{curQId + 1} / {totalQ}</p>
<IconButton onClick={handleNext} size="small">
<ChevronRight16 class="carbon-icon" />
</IconButton>
</div>
{/if}
</div>
</PlayContainer>
</div>
</div>
{/if}
<style>
.quiz-body {
width: 500px;
margin: auto;
}
</style>
@@ -0,0 +1 @@
Question
@@ -0,0 +1 @@
Scoreboard
+47
View File
@@ -0,0 +1,47 @@
<script>
import { onMount } from 'svelte';
import { fly } from 'svelte/transition';
import { SyncLoader } from 'svelte-loading-spinners';
import { themeImages, STEPS } from '../../../../utils/constants/quiz';
import { quizStore, playQuizStore } from '../../../../utils/store/org';
import ConnectToPlay from './ConnectToPlay.svelte';
import Players from './Players.svelte';
import Question from './Question.svelte';
import Scoreboard from './Scoreboard.svelte';
import Podium from './Podium.svelte';
let bgImg;
onMount(() => {
bgImg = themeImages[$quizStore.theme].play;
});
</script>
{#if bgImg}
<div
class="absolute inset-0 z-50 bg-white h-screen w-screen"
in:fly={{ y: 500, duration: 500 }}
out:fly={{ y: 500, duration: 500 }}
>
<div
class="p-5 h-full w-full"
style="background: url({bgImg}) no-repeat center center fixed; -webkit-background-size: cover;-moz-background-size: cover;-o-background-size: cover;background-size: cover;min-height: 100vh;height: fit-content;"
>
{#if $playQuizStore.step === STEPS.CONNECT_TO_PLAY}
<ConnectToPlay />
{:else if $playQuizStore.step === STEPS.WAIT_FOR_PLAYERS}
<Players />
{:else if $playQuizStore.step === STEPS.SHOW_NEXT_QUESTION}
<Question />
{:else if $playQuizStore.step === STEPS.SCOREBOARD}
<Scoreboard />
{:else if $playQuizStore.step === STEPS.PODIUM}
<Podium />
{:else}
<div class="w-full m-auto">
<SyncLoader size="20" color="#1d4ed8" unit="px" duration="1s" />
</div>
{/if}
</div>
</div>
{/if}
+110
View File
@@ -0,0 +1,110 @@
<script>
import { goto } from '@sapper/app';
import pluralize from 'pluralize';
import OverflowMenuHorizontal20 from 'carbon-icons-svelte/lib/OverflowMenuHorizontal20';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { currentOrgPath, quizStore } from '../../../utils/store/org';
import Dropdown from '../../Dropdown/index.svelte';
import PrimaryButton from '../../PrimaryButton/index.svelte';
import { VARIANTS } from '../../PrimaryButton/constants';
import {
onRename,
onMakeCopy,
onDelete,
} from '../../../utils/services/org/quiz';
import { themeImages } from '../../../utils/constants/quiz';
dayjs.extend(relativeTime);
export let quiz;
export let totalQuestions;
function getOptions() {
return [
// {
// label: 'Edit',
// onClick: onEdit(quiz),
// },
// {
// label: 'Share',
// onClick: onShare(quiz),
// },
// {
// label: 'Report',
// onClick: onReport(quiz),
// },
{
label: 'Rename',
onClick() {
onRename(quiz);
},
},
// {
// label: 'Make a copy',
// onClick() {
// onMakeCopy(quiz);
// },
// },
{
label: 'Delete',
onClick() {
onDelete(quiz);
},
},
];
}
function startQuiz() {}
</script>
{#if quiz}
<div
class="root w-full border hover:shadow-2xl transition ease-in-out rounded-lg bg-gray-100 dark:bg-gray-700 mr-4 mb-5 relative flex p-3"
>
<img
src={themeImages[quiz.theme]?.card || themeImages.standard.card}
alt="quiz-card"
class=""
style="max-width: 300px;"
/>
<div class="p-5 flex flex-col justify-between w-full">
<div class="flex justify-between">
<h4 class="dark:text-white title text-md font-bold">
<a href="{$currentOrgPath}/quiz/{quiz.id}">{quiz.title}</a>
</h4>
<Dropdown
options={getOptions()}
classNames="absolute top-4 right-4"
isIcon={true}
>
<div class="p-1 rounded-full bg-gray-200 dark:bg-gray-600">
<OverflowMenuHorizontal20 class="carbon-icon active" />
</div>
</Dropdown>
</div>
<div class="flex justify-between">
<div>
<p class="mb-2">{pluralize('question', totalQuestions, true)}</p>
<p>Updated {dayjs(quiz.updated_at).fromNow(true)} ago</p>
</div>
<PrimaryButton
className="px-6 py-3"
variant={VARIANTS.OUTLINED}
label="Start Quiz"
onClick={startQuiz}
/>
</div>
</div>
</div>
{/if}
<style>
.root {
height: 180px;
}
</style>
@@ -0,0 +1,63 @@
<script>
import CheckmarkFilled24 from 'carbon-icons-svelte/lib/CheckmarkFilled24';
import CheckmarkOutline24 from 'carbon-icons-svelte/lib/CheckmarkOutline24';
import { optionImage } from '../../../utils/constants/quiz';
import TextField from '../../Form/TextField.svelte';
import IconButton from '../../IconButton/index.svelte';
export let currentQuestion = {
label: '',
};
export let currentError = {};
export let optionHasError = () => false;
export let isPreview = false;
</script>
{#if isPreview}
<h1 class="text-white mb-5 font-bold">{currentQuestion.label}</h1>
{:else}
<TextField
placeholder="Start typing your question"
bind:value={currentQuestion.label}
className="w-full my-4"
errorMessage={currentError.isLabelEmpty && 'This field is required'}
isRequired={true}
bgColor="bg-white"
/>
{/if}
<!-- Options -->
<div class="flex justify-between w-full flex-wrap">
{#each currentQuestion.options || [] as option}
<div class="w-2/5 bg-white dark:bg-gray-700 rounded p-5 mb-5 relative">
<img src={optionImage[option.id]} alt={option.id} />
{#if isPreview}
<p class="mt-5">{option.label}</p>
{:else}
<TextField
bind:value={option.label}
placeholder="Type answer"
bgColor="bg-white"
className="mt-3"
isDisabled={currentQuestion.type === 'boolean'}
errorMessage={optionHasError(option.id, currentError.options) &&
'Please enter a label'}
/>
<div class="flex justify-end absolute top-2 right-2">
<IconButton
value={option.id}
onClick={() => (option.isCorrect = !option.isCorrect)}
buttonClassName={option.isCorrect && 'success'}
>
{#if option.isCorrect}
<CheckmarkFilled24 class="carbon-icon" />
{:else}
<CheckmarkOutline24 class="carbon-icon" />
{/if}
</IconButton>
</div>
{/if}
</div>
{/each}
</div>
+58
View File
@@ -0,0 +1,58 @@
<script>
import { onMount } from 'svelte';
import Box from '../../Box/index.svelte';
import QuizLoader from './Loader.svelte';
import QuizCard from './QuizCard.svelte';
import CoursesEmptyIcon from '../../Icons/CoursesEmptyIcon.svelte';
import { snackbarStore } from '../../Snackbar/store';
import { SNACKBAR_SEVERITY } from '../../Snackbar/constants';
import { supabase } from '../../../utils/functions/supabase';
import { currentOrg, quizesStore } from '../../../utils/store/org';
let isLoading = false;
async function fetchQuizes(id) {
if (!id) return;
isLoading = true;
const { data, error } = await supabase
.from('quiz')
.select(`*`)
.eq('organization_id', id)
.order('updated_at', { ascending: false });
if (error) {
$snackbarStore.message = "Something's not right - Please try later";
$snackbarStore.severity = SNACKBAR_SEVERITY.ERROR;
$snackbarStore.open = true;
return;
}
console.log('data', data);
quizesStore.set(data);
isLoading = false;
}
$: fetchQuizes($currentOrg.id);
</script>
<div
class="flex items-center justify-center lg:justify-start flex-wrap my-4 m-auto"
>
{#if isLoading}
<QuizLoader />
<QuizLoader />
<QuizLoader />
{:else}
{#each $quizesStore as quiz}
<QuizCard {quiz} totalQuestions={quiz.questions?.length || 0} />
{:else}
<Box>
<CoursesEmptyIcon />
<h3 class="dark:text-white text-2xl my-5">No Quiz Created</h3>
<p class="dark:text-white w-1/3 text-center">
Create interactive quizzes with scoreboard for your students.
</p>
</Box>
{/each}
{/if}
</div>
+1 -1
View File
@@ -71,7 +71,7 @@
><SectionTitle>Add</SectionTitle></Column
>
<Column sm={2} md={6} lg={8}>
<p class="text-md text-gray-500 dark:text-gray-200 mb-5">
<p class="text-md text-gray-500 dark:text-white mb-5">
Add your team mates or collaborators to your organization. Start working
together
</p>
@@ -116,7 +116,7 @@
>
<Column sm={2} md={6} lg={8}>
<h4 class="dark:text-white">Customise your Domain</h4>
<p class="text-sm text-gray-500 dark:text-gray-200">
<p class="text-sm text-gray-500 dark:text-white">
Create a custom URL so your audience can get to your organization easily
</p>
<PrimaryButton
@@ -131,7 +131,7 @@
<Column sm={2} md={2} lg={4}><SectionTitle>Team</SectionTitle></Column>
<Column sm={2} md={6} lg={8}>
<h4 class="dark:text-white">Set up your website</h4>
<p class="text-sm text-gray-500 dark:text-gray-200">
<p class="text-sm text-gray-500 dark:text-white">
Add team mates to your organization so you can both collaborate on
projects.
</p>
@@ -133,7 +133,7 @@
><SectionTitle>Add</SectionTitle></Column
>
<Column sm={2} md={6} lg={8}>
<p class="text-md text-gray-500 dark:text-gray-200 mb-5">
<p class="text-md text-gray-500 dark:text-white mb-5">
Add your team mates or collaborators to your organization. Start working
together
</p>
@@ -173,7 +173,7 @@
{#each $orgTeam as teamMember}
<div class="flex justify-between items-center mb-5">
<div class="flex">
<p class="text-sm text-gray-500 dark:text-gray-200 mr-3">
<p class="text-sm text-gray-500 dark:text-white mr-3">
{teamMember.email}
</p>
<TextChip
+19 -9
View File
@@ -1,12 +1,13 @@
<script>
import { stores } from '@sapper/app';
import Help from 'carbon-icons-svelte/lib/Help20';
import UserMultipleIcon from 'carbon-icons-svelte/lib/UserMultiple20';
import ForumIcon from 'carbon-icons-svelte/lib/Forum20';
import TextChip from '../../components/Chip/Text.svelte';
import OrgSelector from '../../components/OrgSelector/OrgSelector.svelte';
import HomeIcon from '../../components/Icons/HomeIcon.svelte';
import CourseIcon from '../../components/Icons/CourseIcon.svelte';
import QuizIcon from '../../components/Icons/QuizIcon.svelte';
import SiteSettingsIcon from '../../components/Icons/SiteSettingsIcon.svelte';
import AudienceIcon from '../../components/Icons/AudienceIcon.svelte';
import Avatar from '../../components/Avatar/index.svelte';
@@ -21,6 +22,10 @@
path: '',
label: 'Home',
},
{
path: '/quiz',
label: 'Quizzes',
},
{
path: '/courses',
label: 'Courses',
@@ -29,10 +34,10 @@
path: '/community',
label: 'Community',
},
{
path: '/site',
label: 'Site settings',
},
// {
// path: '/site',
// label: 'Site settings',
// },
{
path: '/audience',
label: 'Audience',
@@ -73,7 +78,10 @@
<ul class="my-5">
{#each menuItems as menuItem}
<a href="{$currentOrgPath}{menuItem.path}" class="text-black">
<a
href="{$currentOrgPath}{menuItem.path}"
class="text-black no-underline"
>
<li
class="flex items-center py-3 px-4 mb-2 rounded hover:bg-gray-200 dark:hover:bg-gray-500 {isActive(
$page.path,
@@ -87,7 +95,9 @@
{:else if menuItem.path === '/site'}
<SiteSettingsIcon />
{:else if menuItem.path === '/community'}
<UserMultipleIcon class="carbon-icon" />
<ForumIcon class="carbon-icon" />
{:else if menuItem.path === '/quiz'}
<QuizIcon />
{:else if menuItem.path === '/audience'}
<AudienceIcon />
{/if}
@@ -99,7 +109,7 @@
</div>
<span class="flex-grow" />
<ul class="my-5 pb-5 px-4">
<a href={$currentOrgPath} class="text-black">
<a href={$currentOrgPath} class="text-black no-underline">
<li
class="flex items-center py-3 px-4 mb-2 rounded hover:bg-gray-200 dark:hover:bg-gray-500"
>
@@ -107,7 +117,7 @@
<p class="dark:text-white ml-2">Help</p>
</li>
</a>
<a href="{$currentOrgPath}/settings" class="text-black">
<a href="{$currentOrgPath}/settings" class="text-black no-underline">
<li
class="flex items-center py-3 px-4 mb-2 rounded hover:bg-gray-200 dark:hover:bg-gray-500 {isActive(
$page.path,
+4 -1
View File
@@ -1,6 +1,7 @@
export const VARIANTS = {
CONTAINED: 'CONTAINED',
CONTAINED_INFO: 'CONTAINED_INFO',
CONTAINED_WHITE: 'CONTAINED_WHITE',
CONTAINED_DANGER: 'CONTAINED_DANGER',
CONTAINED_SUCCESS: 'CONTAINED_SUCCESS',
OUTLINED: 'OUTLINED',
@@ -11,12 +12,14 @@ export const VARIANTS = {
export const VARIANTS_CLASS = {
[VARIANTS.CONTAINED]: 'border-none bg-blue-700 hover:bg-blue-900 text-white',
[VARIANTS.CONTAINED_WHITE]:
'border-none hover:border-black-300 hover:bg-blue-700 dark:hover:bg-blue-900 text-black hover:text-white bg-white',
[VARIANTS.CONTAINED_INFO]:
'border-none bg-gray-400 hover:bg-gray-600 text-white',
[VARIANTS.CONTAINED_SUCCESS]: 'bg-green-700 hover:bg-green-900 text-white',
[VARIANTS.CONTAINED_DANGER]: 'bg-red-700 hover:bg-red-900 text-white',
[VARIANTS.OUTLINED]:
'border border-blue-700 hover:border-black-300 hover:bg-blue-200 text-black',
'border border-blue-700 hover:border-black-300 hover:bg-blue-700 text-black hover:text-white',
[VARIANTS.NONE]:
'border-none hover:border-black-300 hover:bg-gray-200 dark:hover:bg-gray-500 text-black',
[VARIANTS.TEXT]: 'text-black hover:underline',
+5 -1
View File
@@ -22,7 +22,11 @@
isLoading ? VARIANTS.OUTLINED : variant
]} {!disablePadding &&
'py-3 px-5'} rounded-none {className} w-fit justify-center sm:w-auto {isLoading &&
'py-3 border-blue-700'} hover:shadow-xl transition delay-150 duration-300 ease-in-out"
'py-3 border-blue-700'} {variant !== VARIANTS.TEXT &&
'hover:shadow-xl'} transition delay-150 duration-300 ease-in-out {variant !==
VARIANTS.CONTAINED_WHITE
? 'dark:text-white'
: ''}"
on:click={onClick}
{name}
{type}
-56
View File
@@ -1,56 +0,0 @@
<script>
// your script goes here
const contributors = [
{
name: 'Koskting',
},
{
name: 'Koskting',
},
{
name: 'Koskting',
},
{
name: 'Koskting',
},
{
name: 'Koskting',
},
];
</script>
<!-- markup (zero or more items) goes here -->
<div
class="sidebar-root p-5 w-100 border border-1 border-gray rounded-md md:mt-12"
>
<h4 class="dark:text-white border-b border-gray text-base font-bold pb-1">
Самые полезные
</h4>
{#each contributors as contributor}
<a class="flex pt-3" href="/{contributor.name}">
<img
class="inline-block overflow-hidden rounded-full mr-1 leading-none align-middle"
src="https://avatars0.githubusercontent.com/u/2854919?s=40&amp;v=4"
width="23"
height="20"
alt="@jamesmosier"
/>
<span class="hover:text-blue-700 hover:underline">{contributor.name}</span
>
</a>
{/each}
</div>
<style>
.sidebar-root {
width: 250px;
min-width: 250px;
height: fit-content;
}
@media only screen and (max-width: 1002px) {
.sidebar-root {
display: none;
}
}
</style>
+4
View File
@@ -86,6 +86,10 @@
fill: green;
}
:global(svg.carbon-icon.error) {
fill: red;
}
.dark :global(svg.carbon-icon) {
fill: #fff;
}
-54
View File
@@ -1,54 +0,0 @@
<script>
export let className, value, editing;
</script>
{#if editing == false}
<span class={`input ${className}`}> {value}</span>
{:else}
<input
type="text"
class={`rounded-lg border border-1 border-gray focus:outline-none input ${className}`}
{value}
/>
{/if}
<style>
.small {
width: 100%;
}
.large {
width: 100%;
text-align: center;
}
.input {
margin: 2px;
position: relative;
}
span.alignRight {
text-align: right;
}
span.current::before {
border-radius: 15px;
content: " ";
width: 10px;
height: 10px;
position: absolute;
left: 0;
top: 25%;
animation: blink 2s step-start 0s infinite;
-webkit-animation: blink 2s step-start 0s infinite;
}
@keyframes blink {
50% {
background-color: red;
}
}
@-webkit-keyframes blink {
50% {
background-color: red;
}
}
</style>
-20
View File
@@ -1,20 +0,0 @@
<script>
import RowItem from "./RowItem.svelte";
export let items, current;
</script>
<div class="rowRoot">
{#each items as item, i}
<RowItem {item} current={i === 3 && current} />
{/each}
</div>
<style>
.rowRoot {
display: flex;
width: 100%;
justify-content: space-evenly;
height: 150px;
}
</style>
-77
View File
@@ -1,77 +0,0 @@
<script>
import Edit24 from 'carbon-icons-svelte/lib/Edit24';
import Save24 from 'carbon-icons-svelte/lib/Save24';
import Input from './Input.svelte';
export let item, current;
let editing = false;
function toggle() {
editing = !editing;
}
</script>
<div class="rowItem">
<div class="rowItemHeader">
<Input className="small" value={item.lessonType} {editing} />
<Input className="small alignRight" value={item.lessonHall} {editing} />
</div>
<div class="rowItemHeader">
<Input
className={`large ${current && 'current'}`}
value={item.subject}
{editing}
/>
</div>
<div class="rowItemHeader">
<div class="rowItemFooter" style="width: 70%">
<Input className="small" value={item.tutorOne} {editing} />
<Input className="small" value={item.tutorTwo} {editing} />
</div>
<button on:click={toggle}>
{#if editing}
<Save24 class="icon" />
{:else}
<Edit24 class="icon" />
{/if}
</button>
</div>
</div>
<style>
.rowItem {
border-right: 1px solid;
border-bottom: 1px solid;
display: flex;
justify-content: space-between;
flex-direction: column;
width: 100%;
min-width: 150px;
}
.rowItem button {
display: none;
height: fit-content;
}
.rowItem:hover button {
display: block;
cursor: pointer;
}
.rowItemHeader {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.rowItemFooter {
display: flex;
justify-content: space-between;
flex-direction: column;
align-items: flex-start;
}
:global(svg.icon) {
fill: red;
}
</style>
-39
View File
@@ -1,39 +0,0 @@
export default [
{
lessonType: '1-9 leture',
lessonHall: '402F',
subject: 'Mathematics',
tutorOne: 'Peter Pitrovich',
tutorTwo: 'Pragma James',
},
{
lessonType: '1-9 Pra.',
lessonHall: '202F',
subject: 'Physics',
tutorOne: 'Anicimov',
tutorTwo: '',
},
{
lessonType: '1-9 leture',
lessonHall: '301F',
subject: 'Algorithm',
tutorOne: 'Babilunga Sharon',
tutorTwo: 'Alex Jonard',
},
{
lessonType: '1-9 leture',
lessonHall: '222F',
subject: 'Computer Aided system',
tutorOne: 'Godovichecko Heroku',
tutorTwo: '',
},
...Array(4 * 4)
.fill(0)
.map((a, i) => ({
lessonType: '1-9 leture',
lessonHall: '222F',
subject: 'Computer Aided system',
tutorOne: 'Godovichecko Heroku',
tutorTwo: '',
})),
];
-93
View File
@@ -1,93 +0,0 @@
<script>
import Star24 from 'carbon-icons-svelte/lib/Star24';
import StarFilled24 from 'carbon-icons-svelte/lib/StarFilled24';
import Link24 from 'carbon-icons-svelte/lib/Link24';
import Row from './Row.svelte';
import data from './data';
let starred = false;
</script>
<div class="sub-header flex justify-between mb-2">
<h4 class="dark:text-white title">АИ-153</h4>
<div class="flex justify-evenly items-center">
{#if starred}
<StarFilled24
class="clickables mr-5 text-sm"
on:click={() => (starred = !starred)}
focusable="true"
tabindex={0}
/>
{:else}
<Star24
class="clickables mr-5 text-sm"
on:click={() => (starred = !starred)}
focusable="true"
tabindex="{1}/"
/>
{/if}
<Link24 class="clickables mr-5 text-sm" />
</div>
</div>
<div class="container">
<div class="col">
<div class="colItem">Пн</div>
<div class="colItem">Вт</div>
<div class="colItem">Ср</div>
<div class="colItem">Чт</div>
<div class="colItem">Пя</div>
</div>
<div class="table">
<Row items={data.slice(0, 4)} />
<Row items={data.slice(4, 8)} />
<Row current items={data.slice(8, 12)} />
<Row items={data.slice(12, 16)} />
<Row items={data.slice(16, 20)} />
</div>
</div>
<style>
.sub-header {
padding: 10px 20px;
}
.table {
border: 1px solid;
border-bottom: none;
border-right: none;
width: 100%;
}
.container {
display: flex;
min-width: fit-content;
overflow: auto;
font-size: 12px;
border: 2px solid #8094e9;
padding: 10px 5px;
border-radius: 10px;
}
.container:hover {
border: 2px solid #384da9;
}
.col {
display: flex;
flex-direction: column;
justify-content: space-evenly;
}
.colItem {
border-right: none;
border-bottom: none;
display: flex;
align-items: center;
height: 100%;
padding: 20px;
width: 30px;
/* margin: 0 auto; */
justify-content: center;
}
:global(svg.clickables):hover {
cursor: pointer;
}
</style>
+1 -1
View File
@@ -22,7 +22,7 @@
<div class="pt-6 text-center space-y-4">
<div class="description font-medium flex flex-col justify-around">
<div class="text-cyan-600">{name}</div>
<div class="text-gray-500 dark:text-gray-200">{title}</div>
<div class="text-gray-500 dark:text-white">{title}</div>
</div>
</div>
{#if telegramLink && mailLink}
+31 -4
View File
@@ -9,6 +9,7 @@
import { derived } from 'svelte/store';
import { stores, goto } from '@sapper/app';
import isEmpty from 'lodash/isEmpty';
import mixpanel from 'mixpanel-browser';
import * as Sentry from '@sentry/browser';
import { Theme } from 'carbon-components-svelte';
import { Moon } from 'svelte-loading-spinners';
@@ -24,8 +25,14 @@
// import SideBar from "../components/SideBar/index.svelte";
// import Footer from '../components/Footer/index.svelte';
import Apps from '../components/Apps/index.svelte';
import PlayQuiz from '../components/Org/Quiz/Play/index.svelte';
import { handleAuthChange } from '../utils/functions/api';
import { isCoursesPage, isOrgPage, isLMSPage } from '../utils/functions/app';
import {
isCoursesPage,
isOrgPage,
isLMSPage,
isQuizPage,
} from '../utils/functions/app';
import showAppsSideBar from '../utils/functions/showAppsSideBar';
import { user, profile } from '../utils/store/user';
import { getSupabase } from '../utils/functions/supabase';
@@ -39,11 +46,15 @@
export let segment;
export let config;
// MIXPANEL
mixpanel.init(config.mixpanelToken, { debug: !config.isProd });
let { page, preloading } = stores();
let supabase = getSupabase(config);
let path = $page.path.replace('/', '');
let theme = 'white';
let skipAuth = false;
const delayedPreloading = derived(preloading, (currentPreloading, set) => {
setTimeout(() => set(currentPreloading), 250);
@@ -89,6 +100,8 @@
// Set user in sentry
Sentry.setUser($profile);
// Set user in Mixpanel
mixpanel.identify($profile.email);
if ($appStore.isStudentDomain) {
const { data, error } = await supabase
@@ -127,6 +140,9 @@
// Set user in sentry
Sentry.setUser($profile);
// Set user in Mixpanel
mixpanel.identify($profile.email);
const orgRes = await getOrganizations($profile.id);
// student redirect
@@ -161,6 +177,8 @@
$appStore.isStudentDomain = debug || answer;
$appStore.siteNameFromDomain = debug ? 'codingdojo' : subdomain;
skipAuth = debug || subdomain === 'play';
}
}
@@ -187,6 +205,7 @@
if (config.isProd) {
handleAuthChange(event, session);
}
// Log key events
console.log(`event`, event);
if (event == 'PASSWORD_RECOVERY') {
console.log('PASSWORD RESET');
@@ -197,6 +216,10 @@
return;
}
// Skip Authentication
if (skipAuth) return;
// Authentication Steps
if (event === 'SIGNED_IN') {
$user.fetchingUser = true;
getProfile();
@@ -236,7 +259,9 @@
<Snackbar />
{#if $appStore.isStudentDomain && !path}
{#if skipAuth}
<PlayQuiz />
{:else if $appStore.isStudentDomain && !path}
<OrgLandingPage {segment} />
{:else}
<main class="dark:bg-gray-800">
@@ -262,7 +287,9 @@
<div class="flex justify-between">
{#if isOrgPage($page.path)}
<div class="org-root w-full flex items-center justify-between">
<OrgSideBar />
{#if !isQuizPage($page.path)}
<OrgSideBar />
{/if}
<div class="org-slot bg-white dark:bg-gray-800">
<slot />
</div>
@@ -299,7 +326,7 @@
height: calc(100vh - 44px);
}
.org-slot {
width: calc(100vw - 250px);
min-width: calc(100vw - 250px);
height: calc(100vh - 44px);
overflow-y: auto;
}
+1 -1
View File
@@ -182,7 +182,7 @@
{:else}
<Box>
<AudioConsole32 class="carbon-icon w-80" />
<h3 class="text-3xl text-gray-500 dark:text-gray-200">
<h3 class="text-3xl text-gray-500 dark:text-white">
No Student Added
</h3>
</Box>
+2 -2
View File
@@ -152,7 +152,7 @@
{#each $lessons as lesson (lesson.id)}
<div
bind:this={ref}
class="group relative m-auto rounded-md border-2 border-gray-100 py-3 px-5 mb-4 flex items-center hover:shadow-2xl shadow-md transition delay-150 duration-300 ease-in-out"
class="group relative m-auto rounded-md border-2 border-gray-100 py-3 px-5 mb-4 flex items-center hover:shadow-md shadow-xl transition delay-150 duration-300 ease-in-out"
>
<!-- Complete or Not complete icon -->
<div class="absolute -left-6 -top-6 success">
@@ -303,7 +303,7 @@
</div>
{:else}
<Box>
<h3 class="text-3xl text-gray-500 dark:text-gray-200">
<h3 class="text-3xl text-gray-500 dark:text-white">
No lesson added
</h3>
<img alt="No lesson added" src="/notfound2.gif" class="w-80" />
+1 -1
View File
@@ -181,7 +181,7 @@
{:else}
<Box>
<AudioConsole32 class="carbon-icon w-80" />
<h3 class="text-3xl text-gray-500 dark:text-gray-200">
<h3 class="text-3xl text-gray-500 dark:text-white">
No Student Added
</h3>
</Box>
+1 -2
View File
@@ -25,7 +25,6 @@
import { formatAnswers } from '../../../components/Course/function';
import { snackbarStore } from '../../../components/Snackbar/store';
import { SNACKBAR_SEVERITY } from '../../../components/Snackbar/constants';
import Date from '../../../components/Form/Date.svelte';
import isSubmissionEarly from '../../../utils/functions/isSubmissionEarly';
import formatDate from '../../../utils/functions/formatDate';
@@ -332,7 +331,7 @@
</p>
</a>
<p
class="dark:text-white text-gray-500 dark:text-gray-200 text-xs"
class="dark:text-white text-gray-500 dark:text-white text-xs"
>
{item.submittedAt}
</p>
+4
View File
@@ -5,6 +5,7 @@
</script>
<script>
import mixpanel from 'mixpanel-browser';
import TextField from '../components/Form/TextField.svelte';
import PrimaryButton from '../components/PrimaryButton/index.svelte';
import { getSupabase } from '../utils/functions/supabase';
@@ -36,6 +37,9 @@
email: fields.email,
password: fields.password,
});
mixpanel.track('Log In', {
'Login Type': 'Referral',
});
console.log('data', data);
if (error) throw error;
} catch (error) {
@@ -287,7 +287,7 @@
{:else}
<div class="py-10 px-5">
<a
class="text-gray-500 dark:text-gray-200 text-md flex items-center"
class="text-gray-500 dark:text-white text-md flex items-center"
href={`${$currentOrgPath}/community`}
>
<ArrowLeft24 class="carbon-icon" /> Go Back
+1 -1
View File
@@ -57,7 +57,7 @@
<section class="w-full max-w-3xl mx-auto">
<div class="p-5">
<a
class="text-gray-500 dark:text-gray-200 text-md flex items-center"
class="text-gray-500 dark:text-white text-md flex items-center"
href={`${$currentOrgPath}/community`}
>
<ArrowLeft24 class="carbon-icon" /> Go Back
+414
View File
@@ -0,0 +1,414 @@
<script context="module">
export async function preload({ params }) {
return {
quizId: params.slug,
};
}
</script>
<script>
import { onMount } from 'svelte';
import { goto } from '@sapper/app';
import cloneDeep from 'lodash/cloneDeep';
import isBoolean from 'lodash/isBoolean';
import ArrowLeft24 from 'carbon-icons-svelte/lib/ArrowLeft24';
import CheckmarkFilled24 from 'carbon-icons-svelte/lib/CheckmarkFilled24';
import WarningFilled20 from 'carbon-icons-svelte/lib/WarningFilled20';
import { Select, SelectItem } from 'carbon-components-svelte';
import { currentOrgPath, deleteModal } from '../../../../utils/store/org';
import PrimaryButton from '../../../../components/PrimaryButton/index.svelte';
import { VARIANTS } from '../../../../components/PrimaryButton/constants';
import {
themeImages,
allOptions,
booleanOptions,
allThemes,
} from '../../../../utils/constants/quiz';
import { quizStore, quizesStore } from '../../../../utils/store/org';
import DeleteModal from '../../../../components/Org/Quiz/DeleteModal.svelte';
import Preview from '../../../../components/Org/Quiz/Play/Preview.svelte';
import QuizQuestion from '../../../../components/Org/Quiz/QuizQuestion.svelte';
import { snackbarStore } from '../../../../components/Snackbar/store';
import { SNACKBAR_SEVERITY } from '../../../../components/Snackbar/constants';
import { supabase } from '../../../../utils/functions/supabase';
export let quizId;
// Questionnaire State
let currentQuestion = $quizStore.questions[0] || {
options: [],
};
// Behavioural State
let openPreview = false;
let type = 'multichoice';
let errors = [];
let currentError = {};
let isFocused = false;
let selectEl = null;
function activeClass(q, cq) {
if (q.id === cq.id) {
return 'bg-gray-200 dark:bg-gray-800';
}
return '';
}
function addQuestion() {
$quizStore.questions = [
...$quizStore.questions,
{
id: new Date().getTime(),
label: '',
type: 'multichoice',
options: [],
},
];
type = 'multichoice';
}
function addOption() {
const cOptIds = currentQuestion.options.map((o) => o.id);
const nextOption = cloneDeep(allOptions).find(
(o) => !cOptIds.includes(o.id)
);
if (!nextOption) return;
currentQuestion.options = [...currentQuestion.options, nextOption];
}
function deleteOption() {
const opt = cloneDeep(currentQuestion.options);
opt.pop();
currentQuestion.options = opt;
}
function handleQuestionTypeChange(type) {
const opt =
type === 'multichoice'
? cloneDeep(allOptions).filter((o, i) => i < 2)
: cloneDeep(booleanOptions);
currentQuestion.type = type;
currentQuestion.options = opt;
// $quizStore.questions = $quizStore.questions.map((q) =>
// q.id === currentQuestion.id ? cloneDeep(currentQuestion) : q
// );
}
function deleteQuestion() {
// Only leave one question
if ($quizStore.questions.length === 1) return;
$quizStore.questions = $quizStore.questions.filter(
(q) => q.id !== currentQuestion.id
);
currentQuestion = $quizStore.questions[0];
}
function previewQuiz() {
openPreview = !openPreview;
}
function validateQuiz() {
const _errors = [];
$quizStore.questions.forEach((q) => {
const labelError = !!((q.label?.length || 0) < 3);
const options = [];
let hasOneAnswer = false;
q.options.forEach((o) => {
const valLabelError = !!(o.label.length < 3);
if (valLabelError) {
options.push({
id: o.id,
error: true,
});
}
if (o.isCorrect) {
hasOneAnswer = true;
}
});
_errors.push({
isLabelEmpty: labelError,
hasOneAnswer,
id: q.id,
options,
});
});
errors = _errors;
currentError = errors.find((e) => e.id === currentQuestion.id) || {};
return errors;
}
async function saveQuiz() {
const _errors = validateQuiz();
if (Array.isArray(_errors) && _errors.length && qHasError(null, _errors)) {
console.error('Please fix all errors', errors);
return;
}
const { data, error } = await supabase
.from('quiz')
.update(
{
...$quizStore,
updated_at: new Date(),
},
{
returning: 'minimal',
}
)
.match({ id: quizId });
console.log('data', data);
console.log('error', error);
if (error) {
$snackbarStore.message = "Something's not right - Please try later";
$snackbarStore.severity = SNACKBAR_SEVERITY.ERROR;
$snackbarStore.open = true;
return;
} else {
$snackbarStore.message = 'Saved Successfully';
$snackbarStore.severity = SNACKBAR_SEVERITY.SUCCESS;
$snackbarStore.open = true;
}
}
function qHasError(qId, _errs) {
return _errs.some((qe) => {
return (
(qId ? qe.id === qId : true) &&
(qe.isLabelEmpty || !qe.hasOneAnswer || !!qe.options.length)
);
});
}
function optionHasError(eId, _errs) {
if (Array.isArray(_errs) && _errs.length) {
return _errs.some((e) => e.id === eId && e.error);
}
return false;
}
onMount(() => {
const quiz = $quizesStore.find((q) => q.id === quizId);
if (!quiz) {
goto(`${$currentOrgPath}/quiz`);
return;
}
quizStore.set(quiz);
currentQuestion = quiz.questions[0];
});
</script>
<svelte:head>
<title>Quiz</title>
</svelte:head>
<DeleteModal onDelete={deleteQuestion} />
{#if openPreview}
<Preview exitPreview={previewQuiz} />
{/if}
<section class="w-screen h-full flex justify-between">
<!-- Questions list -->
<aside class="root w-1/5 p-4 bg-gray-100 dark:bg-gray-700 h-full">
<div class="h-full flex flex-col">
<a
class="text-gray-500 dark:text-white text-md flex items-center"
href={`${$currentOrgPath}/quiz`}
>
<ArrowLeft24 class="carbon-icon" /> Back to Quizzes
</a>
<h3 class="my-3">Quiz</h3>
<div class="mb-3">
{#each $quizStore.questions as question, i}
<button
class="w-full rounded p-3 mb-3 font-bold text-left text-gray-500 dark:text-white flex justify-between {activeClass(
question,
currentQuestion
)}"
on:click={() => {
currentQuestion = question;
type = question.type;
}}
>
Question {i + 1}
{#if qHasError(question.id, errors)}
<WarningFilled20 class="carbon-icon error" />
{/if}
</button>
{/each}
</div>
<div class="w-full flex justify-end">
<PrimaryButton
label="Add Question"
variant={VARIANTS.CONTAINED}
onClick={addQuestion}
/>
</div>
</div>
</aside>
<div class="container w-3/6 h-full">
<div
class="p-5 h-full"
style="background: url({themeImages[$quizStore.theme]
?.editor}) no-repeat center center fixed; -webkit-background-size: cover;-moz-background-size: cover;-o-background-size: cover;background-size: cover;"
>
<div class="content m-auto">
<h1 class="text-white font-bold my-5">{$quizStore.title}</h1>
<QuizQuestion {currentQuestion} {optionHasError} {currentError} />
{#if isBoolean(currentError.hasOneAnswer) && !currentError.hasOneAnswer}
<div class="mb-5">
<p class="text-red-500">
Please select at least one correct answer
</p>
</div>
{/if}
{#if currentQuestion.type !== 'boolean'}
<div class="w-full flex justify-center mb-4">
{#if currentQuestion.options.length < allOptions.length}
<PrimaryButton
label="+ Add more answers"
variant={VARIANTS.CONTAINED_WHITE}
onClick={addOption}
className="mr-5"
/>
{/if}
{#if currentQuestion.options.length > 0}
<PrimaryButton
label="- Remove last answer"
variant={VARIANTS.CONTAINED_WHITE}
onClick={deleteOption}
/>
{/if}
</div>
{/if}
</div>
</div>
</div>
<!-- Quiz Settings -->
<aside class="settings w-1/5 p-4 bg-gray-100 dark:bg-gray-700 h-full">
<div class="py-5">
<h5>Quiz settings</h5>
<PrimaryButton
label="Save Changes"
variant={VARIANTS.CONTAINED}
onClick={saveQuiz}
className="my-3"
/>
<PrimaryButton
label="Preview Quiz"
variant={VARIANTS.OUTLINED}
onClick={previewQuiz}
className="my-3"
/>
<PrimaryButton
label="Delete question"
variant={VARIANTS.TEXT}
onClick={() => {
if ($quizStore.questions.length === 1) return;
$deleteModal.open = true;
$deleteModal.isQuestion = true;
}}
className="my-3"
/>
</div>
<div class="flex flex-col justify-evenly">
<div class="my-3">
<!-- Question type -->
<Select
labelText="Question type"
bind:this={selectEl}
bind:selected={type}
class="flex items-center mb-3"
on:focus={() => (isFocused = true)}
on:blur={() => (isFocused = false)}
on:change={() => {
if (!isFocused) return;
// Blur after change
const onBlur = selectEl?.$$?.callbacks?.blur?.[0];
const onFocus = selectEl?.$$?.callbacks?.focus?.[0];
setTimeout(() => {
onBlur();
onFocus();
}, 1000);
handleQuestionTypeChange(type);
}}
>
<SelectItem value="multichoice" text="Multi-choice answers" />
<SelectItem value="boolean" text="True or False" />
</Select>
<!-- -->
<Select
labelText="Time limit"
bind:selected={$quizStore.timelimit}
class="flex items-center mb-3"
>
<SelectItem value="10 seconds" text="10s" />
<SelectItem value="20 seconds" text="20s" />
<SelectItem value="30 seconds" text="30s" />
<SelectItem value="1 minute" text="1m" />
<SelectItem value="2 minute" text="2m" />
<SelectItem value="3 minute" text="3m" />
</Select>
</div>
<!-- Theme settings -->
<div class="my-3">
<p class="mb-2">Choose a theme</p>
{#each allThemes as _theme}
<div
class="theme rounded-md w-full border cursor-pointer mb-5 relative {$quizStore.theme ===
_theme.id && 'border-blue-700'}"
>
{#if $quizStore.theme === _theme.id}
<CheckmarkFilled24
class="carbon-icon absolute top-4 right-4"
style="fill:white;"
/>
{/if}
<div
class="w-full rounded-md h-full border flex flex-col-reverse"
style="background: url({themeImages[_theme.id]?.card});"
on:click={() => ($quizStore.theme = _theme.id)}
>
<p class="ml-3 mb-3 text-white">{_theme.label}</p>
</div>
</div>
{/each}
</div>
</div>
</aside>
</section>
<style>
.container {
min-width: 696px;
}
.content {
max-width: 550px;
}
.theme {
height: 120px;
max-width: 260px;
}
</style>
+28
View File
@@ -0,0 +1,28 @@
<script>
import Quizzes from '../../../../components/Org/Quiz/Quizzes.svelte';
import NewQuizModal from '../../../../components/Org/Quiz/NewQuizModal.svelte';
import DeleteModal from '../../../../components/Org/Quiz/DeleteModal.svelte';
import PrimaryButton from '../../../../components/PrimaryButton/index.svelte';
import { createQuizModal } from '../../../../utils/store/org';
</script>
<svelte:head>
<title>Interactive Quizzes - ClassroomIO</title>
</svelte:head>
<section class="w-full max-w-6xl mx-auto">
<div class="py-10 px-5">
<div class="flex items-center justify-between mb-10">
<h1 class="dark:text-white text-3xl font-bold">Interactive Quizzes</h1>
<PrimaryButton
label="Create Quiz"
onClick={() => ($createQuizModal.open = true)}
/>
</div>
<NewQuizModal />
<DeleteModal />
<Quizzes />
</div>
</section>
@@ -10,7 +10,7 @@
<section class="w-full max-w-6xl mx-auto">
<div class="py-10 px-5">
<a
class="text-gray-500 dark:text-gray-200 text-md"
class="text-gray-500 dark:text-white text-md"
href={`${$currentOrgPath}/settings`}>Back to Settings</a
>
<div class="flex items-center justify-between mb-10">
+1 -1
View File
@@ -10,7 +10,7 @@
<section class="w-full max-w-6xl mx-auto">
<div class="py-10 px-5">
<a
class="text-gray-500 dark:text-gray-200 text-md"
class="text-gray-500 dark:text-white text-md"
href={`${$currentOrgPath}/settings`}>Back to Settings</a
>
<div class="flex items-center justify-between mb-10">
+4
View File
@@ -6,6 +6,7 @@
<script>
import { goto } from '@sapper/app';
import mixpanel from 'mixpanel-browser';
import TextField from '../components/Form/TextField.svelte';
import PrimaryButton from '../components/PrimaryButton/index.svelte';
import { getSupabase } from '../utils/functions/supabase';
@@ -46,6 +47,9 @@
console.log('data', data);
if (error) throw error;
mixpanel.track('Signed Up', {
'Signup Type': 'Referral',
});
formRef?.reset();
return goto('/login');
+1
View File
@@ -13,6 +13,7 @@ dotenv.config();
const config = {
isProd: process.env.NODE_ENV !== 'development',
telegramToken: process.env.SVELTE_APP_TELEGRAM_API_TOKEN,
mixpanelToken: process.env.SVELTE_APP_MIXPANEL_TOKEN,
supabaseConfig: {
bucketPath:
'https://koxqonvbkeakwvmdegcf.supabase.in/storage/v1/object/sign',
+12 -8
View File
@@ -14,15 +14,7 @@
<link rel="icon" type="image/png" sizes="32x32" href="/logo-32.png">
<link href="/css/quill.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.plyr.io/3.7.3/plyr.css" />
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-C7WBN0S06R"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', 'G-C7WBN0S06R');
</script>
<!--Crisp Chat
<script
@@ -43,6 +35,18 @@
<!-- This contains the contents of the <svelte:head> component, if
the current page has one -->
%sapper.head%
<!-- Mix Panel -->
<script type="text/javascript">
(function (f, b) {
if (!b.__SV) {
var e, g, i, h; window.mixpanel = b; b._i = []; b.init = function (e, f, c) {
function g(a, d) { var b = d.split("."); 2 == b.length && (a = a[b[0]], d = b[1]); a[d] = function () { a.push([d].concat(Array.prototype.slice.call(arguments, 0))) } } var a = b; "undefined" !== typeof c ? a = b[c] = [] : c = "mixpanel"; a.people = a.people || []; a.toString = function (a) { var d = "mixpanel"; "mixpanel" !== c && (d += "." + c); a || (d += " (stub)"); return d }; a.people.toString = function () { return a.toString(1) + ".people (stub)" }; i = "disable time_event track track_pageview track_links track_forms track_with_groups add_group set_group remove_group register register_once alias unregister identify name_tag set_config reset opt_in_tracking opt_out_tracking has_opted_in_tracking has_opted_out_tracking clear_opt_in_out_tracking start_batch_senders people.set people.set_once people.unset people.increment people.append people.union people.track_charge people.clear_charges people.delete_user people.remove".split(" ");
for (h = 0; h < i.length; h++)g(a, i[h]); var j = "set set_once union unset remove delete".split(" "); a.get_group = function () { function b(c) { d[c] = function () { call2_args = arguments; call2 = [c].concat(Array.prototype.slice.call(call2_args, 0)); a.push([e, call2]) } } for (var d = {}, e = ["get_group"].concat(Array.prototype.slice.call(arguments, 0)), c = 0; c < j.length; c++)b(j[c]); return d }; b._i.push([e, f, c])
}; b.__SV = 1.2; e = f.createElement("script"); e.type = "text/javascript"; e.async = !0; e.src = "undefined" !== typeof MIXPANEL_CUSTOM_LIB_URL ? nMIXPANEL_CUSTOM_LIB_URL : "file:" === f.location.protocol && "//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js".match(/^\/\//) ? "https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js" : "//cdn.mxpnl.com/libs/mixpanel-2-latest.min.js"; g = f.getElementsByTagName("script")[0]; g.parentNode.insertBefore(e, g)
}
})(document, window.mixpanel || []);
</script>
</head>
<body>
+110
View File
@@ -0,0 +1,110 @@
import cloneDeep from 'lodash/cloneDeep';
export const themeImages = {
standard: {
card: 'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/quizes/standard-quiz-card.svg',
editor:
'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/quizes/standard-quiz-editor.svg',
play: 'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/quizes/standard-quiz-play.svg',
},
mainland: {
card: 'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/quizes/mainstream-quiz-card.svg',
editor:
'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/quizes/mainstream-quiz-editor.svg',
play: 'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/quizes/mainstream-quiz-play.svg',
},
};
export const defOption = {
id: 1,
label: '',
options: [],
};
export const optionImage = {
circle:
'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/quizes/circle.svg',
spade:
'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/quizes/spade.svg',
square:
'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/quizes/square.svg',
pentagon:
'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/quizes/pentagon.svg',
hexagon:
'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/quizes/hexagon.svg',
triangle:
'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/quizes/triangle.svg',
};
export const allOptions = [
{
id: 'circle',
label: '',
isCorrect: false,
},
{
id: 'triangle',
label: '',
isCorrect: false,
},
{
id: 'spade',
label: '',
isCorrect: false,
},
{
id: 'square',
label: '',
isCorrect: false,
},
{
id: 'pentagon',
label: '',
isCorrect: false,
},
{
id: 'hexagon',
label: '',
isCorrect: false,
},
];
export const booleanOptions = [
{
id: 'triangle',
label: 'True',
image:
'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/quizes/triangle.svg',
},
{
id: 'square',
label: 'False',
image:
'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/quizes/square.svg',
},
];
export const allThemes = [
{
id: 'standard',
label: 'Standard',
},
{
id: 'mainland',
label: 'Mainland Bridge',
},
];
export const defQuestion = {
id: new Date().getTime(),
title: '',
type: 'multichoice',
options: [],
timelimit: '10s',
};
export const STEPS = {
CONNECT_TO_PLAY: 'CONNECT_TO_PLAY',
WAIT_FOR_PLAYERS: 'WAIT_FOR_PLAYERS',
SHOW_NEXT_QUESTION: 'SHOW_NEXT_QUESTION',
SCOREBOARD: 'SCOREBOARD',
PODIUM: 'PODIUM',
};
+4
View File
@@ -18,6 +18,10 @@ export function isOrgPage(path) {
return /org\/[a-z 0-9 -]/.test(path);
}
export function isQuizPage(path) {
return /org\/[a-z 0-9 -]+\/quiz\/[a-z 0-9 -]/.test(path);
}
export function isLMSPage(path) {
return /lms[\/a-z 0-9 -]*/.test(path);
}
+5
View File
@@ -0,0 +1,5 @@
export function genQuizPin() {
var minm = 100000;
var maxm = 999999;
return Math.floor(Math.random() * (maxm - minm + 1)) + minm;
}
+32 -3
View File
@@ -1,6 +1,15 @@
import get from 'lodash/get';
import isNumber from 'lodash/isNumber';
import z from 'zod';
import { validateEmail } from './validateEmail';
const createQuizValidationSchema = z.object({
title: z.string().min(6, {
message: 'Must be 6 or more characters long',
invalid_type_error: 'Must not be empty',
}),
});
const askCommunityValidationSchema = z.object({
title: z.string().min(6, {
message: 'Must be 6 or more characters long',
@@ -71,14 +80,28 @@ export const getConfirmPasswordError = ({ password, confirmPassword }) => {
: null;
};
const processErrors = (error) => {
const processErrors = (error, mapToId) => {
const errors = {};
if (Array.isArray(error?.issues)) {
for (const issue of error.issues) {
const { message, path } = issue;
console.log(issue);
if (path.length) {
if (!path.length) {
continue;
}
if (mapToId) {
let value = '';
path.forEach((p, i) => {
const isLast = !path[i + 1];
const formatP = isNumber(p) ? `[${p}]` : p;
value += !value ? formatP : `.${formatP}`;
});
const key = value.replace('label', 'id');
errors[key] = value;
} else {
errors[path[0]] = message;
}
}
@@ -115,6 +138,12 @@ export const onboardingValidation = (fields = {}, step) => {
return processErrors(error);
};
export const createQuizValidation = (fields = {}) => {
const { error } = createQuizValidationSchema.safeParse(fields);
return processErrors(error);
};
export const askCommunityValidation = (fields = {}) => {
const { error } = askCommunityValidationSchema.safeParse(fields);
+26
View File
@@ -0,0 +1,26 @@
import { createQuizModal, deleteModal } from '../../store/org';
export function onEdit(quiz) {}
export function onShare(quiz) {}
export function onReport(quiz) {}
export function onRename(quiz) {
createQuizModal.update((m) => ({
...m,
id: quiz.id,
title: quiz.title,
openEdit: true,
}));
}
export function onMakeCopy(quiz) {}
export function onDelete(quiz) {
deleteModal.update((m) => ({
...m,
open: true,
id: quiz.id,
}));
}
+31
View File
@@ -1,4 +1,6 @@
import cloneDeep from 'lodash/cloneDeep';
import { writable, derived } from 'svelte/store';
import { allOptions, STEPS, booleanOptions } from '../constants/quiz';
export const orgs = writable([]);
export const currentOrg = writable({
@@ -14,3 +16,32 @@ export const currentOrgPath = derived(
currentOrg,
($currentOrg) => `/org/${$currentOrg.siteName}`
);
// Quiz
export const createQuizModal = writable({
open: false,
openEdit: false,
title: '',
id: null,
});
export const deleteModal = writable({
id: null,
open: false,
isQuestion: false,
});
export const quizesStore = writable([]);
export const quizStore = writable({
uuid: '',
title: '',
questions: [],
timelimit: '10s',
theme: 'standard',
pin: '',
});
export const playQuizStore = writable({
step: STEPS.CONNECT_TO_PLAY,
});