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 <[email protected]> Merge-request: CLASSROOMIO-MR-14 Merged-by: Best Admin <[email protected]>
This commit is contained in:
committed by
Space Cloud
co-authored by
Rotimi Best
parent
b532ea6d29
commit
1e71a91a54
@@ -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 it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -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",
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 |
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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&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>
|
||||
@@ -86,6 +86,10 @@
|
||||
fill: green;
|
||||
}
|
||||
|
||||
:global(svg.carbon-icon.error) {
|
||||
fill: red;
|
||||
}
|
||||
|
||||
.dark :global(svg.carbon-icon) {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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: '',
|
||||
})),
|
||||
];
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export function genQuizPin() {
|
||||
var minm = 100000;
|
||||
var maxm = 999999;
|
||||
return Math.floor(Math.random() * (maxm - minm + 1)) + minm;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user