CLASSROOMIO-T-190: Templates v1

This commit is contained in:
Rotimi Best
2023-10-04 08:18:33 +01:00
parent 8cb70091fa
commit 5703a1b784
25 changed files with 654 additions and 73 deletions
@@ -1,5 +1,4 @@
<script>
import { marked } from 'marked';
import PageNav from '$lib/components/PageNav/index.svelte';
import CloseButton from '$lib/components/Buttons/Close/index.svelte';
import TextField from '$lib/components/Form/TextField.svelte';
@@ -13,24 +12,23 @@
{
avatar: 'https://picsum.photos/32/32/?random',
name: 'Evgeniy',
message: "I'am looking at this and it looks great",
message: "I'am looking at this and it looks great"
},
{
avatar: 'https://picsum.photos/32/32/?random',
name: 'Sergey Semko',
message:
'Thanks for the feedback, what do you think about my website. https://rotimibest.com',
message: 'Thanks for the feedback, what do you think about my website. https://rotimibest.com'
},
{
avatar: 'https://picsum.photos/32/32/?random',
name: 'Vitalii Marushko',
message: 'Всем привет',
message: 'Всем привет'
},
{
avatar: 'https://picsum.photos/32/32/?random',
name: 'Natasha',
message: 'Привет Виталий',
},
message: 'Привет Виталий'
}
];
function handleSend() {
@@ -43,15 +41,15 @@
{
avatar: 'https://picsum.photos/32/32/?random',
name: 'You',
message,
},
message
}
];
message = null;
setTimeout(() => {
bodyRef.scrollTo({
top: bodyRef.scrollHeight,
top: bodyRef.scrollHeight
});
}, 100);
}
@@ -83,7 +81,7 @@
<p class="dark:text-white ml-2 text-sm font-bold">{chat.name}</p>
</div>
<article class="prose prose-sm sm:prose pt-2 pl-8">
{@html marked(chat.message)}
{@html chat.message}
</article>
</div>
{/each}
@@ -91,11 +89,7 @@
<div class="footer">
<!-- <div class="flex items-center justify-between leading-none pl-2"> -->
<TextField
placeholder="Say something"
bind:value={message}
onKeyDown={handleKeyDown}
/>
<TextField placeholder="Say something" bind:value={message} onKeyDown={handleKeyDown} />
<!-- <SendButton onClick={handleSend} /> -->
<!-- </div> -->
</div>
+18 -14
View File
@@ -24,21 +24,25 @@
let appBarRef;
let appContentRef;
const ResizableSidebar = window.innerWidth >= 1024;
const getResizableSidebar = () => {
if (!browser) return;
return window.innerWidth >= 1024;
};
function handleClose() {
selectedApp = null;
if (ResizableSidebar) {
if (getResizableSidebar()) {
appBarRef.style.width = '60px';
}
}
function handleAppClick(appName) {
if (!selectedApp && ResizableSidebar) {
if (!selectedApp && getResizableSidebar()) {
if (window.innerWidth <= 1024) {
appBarRef.style.width = '300px';
} else {
appBarRef.style.width = '400px';
appBarRef.style.width = '500px';
}
}
if (appName === selectedApp) {
@@ -50,7 +54,7 @@
}
function handleCursor(event) {
if (!ResizableSidebar) return;
if (!getResizableSidebar()) return;
if (!resize && selectedApp) {
const isNearLeftBorder = event.clientX - appContentRef.getBoundingClientRect().left < 8;
const isNearRightBorder = appContentRef.getBoundingClientRect().right - event.clientX < 8;
@@ -64,7 +68,7 @@
}
function startDragging(event) {
if (!ResizableSidebar) return;
if (!getResizableSidebar()) return;
if (event.button === 0 && selectedApp) {
event.preventDefault();
const isNearLeftBorder = event.clientX - appContentRef.getBoundingClientRect().left < 8;
@@ -84,13 +88,13 @@
}
function stopDragging() {
if (!ResizableSidebar) return;
if (!getResizableSidebar()) return;
isDragging = false;
resize = false;
}
function dragSidebar(event) {
if (!ResizableSidebar) return;
if (!getResizableSidebar()) return;
if (!isDragging || !selectedApp) return;
const deltaX = startX - event.clientX + 60;
let newWidth = initialWidth + deltaX;
@@ -104,7 +108,7 @@
}
onMount(() => {
if (ResizableSidebar) {
if (getResizableSidebar()) {
appBarRef.addEventListener('mousedown', startDragging);
document.addEventListener('mousemove', dragSidebar);
document.addEventListener('mouseup', stopDragging);
@@ -131,27 +135,27 @@
});
onDestroy(() => {
if (ResizableSidebar) {
if (getResizableSidebar()) {
appBarRef.removeEventListener('mousedown', startDragging);
document.removeEventListener('mousemove', dragSidebar);
document.removeEventListener('mouseup', stopDragging);
document.removeEventListener('mousemove', handleCursor);
}
});
$: ResizableSidebar;
$: getResizableSidebar();
</script>
<div
class={`${$apps.open ? 'open dark:bg-slate-800' : 'close dark:bg-slate-800'} root`}
class={`${$apps.open ? 'open dark:bg-black' : 'close dark:bg-black'} root`}
bind:this={appBarRef}
>
<div class={`apps`}>
<div class="apps">
<div class="lg:hidden">
<IconButton
buttonClassName="lg:hidden"
toolTipProps={{ title: 'Settings', hotkeys: ['A', '0'] }}
onClick={() => {
handleAppClick;
handleAppClick();
$apps.dropdown = !$apps.dropdown;
}}
>
+1 -1
View File
@@ -3,7 +3,7 @@
</script>
<p
class="org-avatar text-xs bg-primary-200 text-primary-700 font-bold py-1 px-1 rounded-md text-center"
class="org-avatar text-xs bg-primary-200 text-primary-700 font-medium py-1 px-2 rounded-md text-center"
>
{label}
</p>
@@ -117,7 +117,11 @@
</div>
<div class="flex items-center lg:mb-0">
<input type="date" name="lesson-date-picker" class="p-2 my-2 rounded-md sm:w-[179px]" />
<input
type="date"
name="lesson-date-picker"
class="p-2 my-2 rounded-md sm:w-[179px] dark:bg-neutral-800 dark:text-white"
/>
</div>
<div class="flex items-center mb-3 lg:mb-0">
@@ -0,0 +1,196 @@
<script lang="ts">
import Modal from '$lib/components/Modal/index.svelte';
import TextField from '$lib/components/Form/TextField.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import CheckmarkFilledIcon from 'carbon-icons-svelte/lib/CheckmarkFilled.svelte';
import CheckmarkOutlineIcon from 'carbon-icons-svelte/lib/CheckmarkOutline.svelte';
import ComingSoon from '$lib/components/ComingSoon/index.svelte';
import { Tag } from 'carbon-components-svelte';
import { TEMPLATES, TAGS } from '$lib/mocks';
import type { ExerciseTemplate } from '$lib/utils/types';
export let open = false;
export let handleAddExercise = () => {};
export let handleCancelAddExercise = () => {};
export let handleTemplateCreate = (template: ExerciseTemplate) => Promise<void>;
export let title = '';
enum Type {
SCRATCH = 0,
TEMPLATE = 1,
AI = 2
}
let step = 1;
let type: Type = Type.TEMPLATE;
const options = [
{
title: 'From Scratch',
subtitle: 'Create your exercise from scratch if you have your questions already prepared',
type: Type.SCRATCH,
isDisabled: false
},
{
title: 'Use a Template',
subtitle: 'Select from 100+ templates of predefined exercise to help you get started',
type: Type.TEMPLATE,
isDisabled: false
},
{
title: 'Use AI',
subtitle: 'You can generate an exercise with AI from your notes',
type: Type.AI,
isDisabled: true
}
];
const tags = Object.values(TAGS);
let selectedTag = tags[0];
let selectedTemplateId = '';
let isTemplateFinishedLoading = false;
function handleNext() {
step = 1;
}
</script>
<Modal
onClose={handleCancelAddExercise}
bind:open
modalHeading="New Exercise"
maxWidth="max-w-4xl"
width="w-4/5"
>
{#if step === 0}
<div>
<h2 class="text-2xl font-medium my-5">How do you want to create your exercise?</h2>
<div class="flex gap-2 justify-between my-8">
{#each options as option}
<button
class="w-[261px] h-[240px] p-5 rounded-md dark:bg-neutral-700 border-2 {option.type ===
type
? 'border-primary-400'
: `border-gray-200 dark:border-gray-400 ${
!option.isDisabled && 'hover:scale-90'
}`} flex flex-col {option.isDisabled &&
'cursor-not-allowed opacity-60'} transition-all ease-in-out"
type="button"
on:click={!option.isDisabled ? () => (type = option.type) : undefined}
>
<div class="w-full flex flex-row-reverse h-[70%]">
{#if option.type === type}
<CheckmarkFilledIcon
size={16}
class="carbon-icon text-primary-600 dark:text-primary-200"
/>
{:else if !option.isDisabled}
<CheckmarkOutlineIcon size={16} class="carbon-icon" />
{/if}
</div>
<div>
<p class="font-bold text-start flex items-center">
<span class="mr-2 text-sm">{option.title}</span>
{#if option.isDisabled}
<ComingSoon />
{/if}
</p>
<p class="text-xs font-light text-start">{option.subtitle}</p>
</div>
</button>
{/each}
</div>
<div class="mt-8 flex items-center flex-row-reverse">
<PrimaryButton className="px-6 py-3" label="Next" onClick={handleNext} />
</div>
</div>
{:else if step === 1}
{#if type === Type.SCRATCH}
<div class="flex items-center justify-center w-96 m-auto min-h-[300px]">
<div class="w-full">
<h2 class="text-2xl font-medium my-5">Give your exercise a title</h2>
<TextField
bind:value={title}
autoFocus={true}
placeholder="Exercise name"
className="my-4"
/>
<div class="mt-5 flex items-center flex-row-reverse">
<PrimaryButton className="px-6 py-3" label="Finish" onClick={handleAddExercise} />
</div>
</div>
</div>
{:else if type === Type.TEMPLATE}
<div>
<h2 class="text-2xl font-medium my-5">Select a template</h2>
<div>
<div class="mb-5 flex items-center gap-2">
{#each tags as tag}
<Tag
type={selectedTag === tag ? undefined : 'outline'}
class={selectedTag === tag ? 'bg-primary-500 text-white dark:text-black' : ''}
interactive
on:click={() => {
selectedTag = tag;
selectedTemplateId = '';
}}>{tag}</Tag
>
{/each}
</div>
<div class="flex flex-wrap items-start gap-2">
{#each TEMPLATES[selectedTag] as template}
<button
class="w-[161px] h-[140px] p-5 rounded-md dark:bg-neutral-700 border {template.id ===
selectedTemplateId
? 'border-primary-400'
: `border-gray-200 dark:border-gray-400`} flex flex-col transition-all ease-in-out"
type="button"
on:click={() => (selectedTemplateId = template.id)}
>
<div class="flex flex-col justify-evenly h-full">
<p class="font-bold text-sm text-start flex items-center">
{template.title}
</p>
<div class="flex flex-col items-start justify-between gap-1">
<p class="text-xs font-normal">
{template.questions} Questions
</p>
<p class="text-xs font-normal text-start">
{template.points} Points
</p>
</div>
</div>
</button>
{/each}
</div>
<div class="mt-5 flex items-center flex-row-reverse">
<PrimaryButton
isDisabled={!selectedTemplateId}
className="px-6 py-3"
label="Finish"
isLoading={isTemplateFinishedLoading}
onClick={async () => {
isTemplateFinishedLoading = true;
const template = TEMPLATES[selectedTag].find((t) => t.id === selectedTemplateId);
if (!template) return;
console.log('Selected template', template);
await handleTemplateCreate(template.data);
isTemplateFinishedLoading = true;
}}
/>
</div>
</div>
</div>
{/if}
<div />
{/if}
</Modal>
@@ -1,4 +1,4 @@
<script>
<script lang="ts">
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import Box from '$lib/components/Box/index.svelte';
@@ -7,19 +7,19 @@
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import PageBody from '$lib/components/PageBody/index.svelte';
import Modal from '$lib/components/Modal/index.svelte';
import TextField from '$lib/components/Form/TextField.svelte';
import Exercise from '../Exercise/index.svelte';
import { supabase } from '$lib/utils/functions/supabase';
import { createExercise } from '$lib/utils/services/courses';
import { createExercise, createExerciseFromTemplate } from '$lib/utils/services/courses';
import { QUESTION_TYPES } from '$lib/components/Question/constants';
import { lesson } from '../store/lessons';
import { questionnaire, isQuestionnaireFetching, handleAddQuestion } from '../store/exercise';
import { questionnaire, isQuestionnaireFetching } from '../store/exercise';
import { exerciseMode } from '../Exercise/store';
import NewExerciseModal from '$lib/components/Course/components/Lesson/Exercises/NewExerciseModal.svelte';
import type { ExerciseTemplate } from '$lib/utils/types';
export let path;
export let exerciseId;
export let lessonId;
export let path = '';
export let exerciseId = '';
export let lessonId = '';
export let isStudent = true;
let open = false;
@@ -30,7 +30,21 @@
description: ''
};
async function handleTemplateCreate(template: ExerciseTemplate) {
const newExercise = await createExerciseFromTemplate(lessonId, template);
console.log('newExercise', newExercise);
if (!newExercise) return;
lesson.update((_lesson) => ({
..._lesson,
exercises: [..._lesson.exercises, newExercise]
}));
handleCancelAddExercise();
goto(path + '/' + newExercise.id);
}
async function handleAddExercise() {
// createExerciseFromTemplate(lessonId);
const { data, error } = await createExercise({
title: newExercise.title,
lesson_id: lessonId
@@ -66,7 +80,7 @@
isFetching = false;
}
async function getExercise(exerciseId) {
async function getExercise(exerciseId: string | undefined) {
if (!exerciseId) return;
isFetching = true;
isQuestionnaireFetching.update(() => true);
@@ -138,13 +152,13 @@
{#if exerciseId}
<Exercise {exerciseId} {goBack} />
{:else}
<Modal onClose={handleCancelAddExercise} bind:open modalHeading="Create an Exercise" width="w-80">
<TextField bind:value={newExercise.title} autoFocus={true} placeholder="Exercise name" />
<div class="mt-5 flex items-center">
<PrimaryButton className="px-6 py-3" label="Submit" onClick={handleAddExercise} />
</div>
</Modal>
<NewExerciseModal
bind:open
{handleCancelAddExercise}
{handleAddExercise}
{handleTemplateCreate}
bind:title={newExercise.title}
/>
<PageBody>
<slot:fragment slot="header">
@@ -37,8 +37,6 @@
let sidebarRef;
let menuContentRef;
const ResizableSidebar = window.innerWidth >= 1024;
const toggleSidebar = () => {
$menu.hidden = !$menu.hidden;
};
@@ -96,7 +94,7 @@
}
function dragSidebar(event) {
if (!ResizableSidebar) return;
if (!(window.innerWidth >= 1024)) return;
if (!isDragging) return;
const deltaX = event.clientX - startX;
@@ -119,7 +117,7 @@
}
onMount(() => {
if (ResizableSidebar) {
if (window.innerWidth >= 1024) {
sidebarRef.addEventListener('mousedown', startDragging);
document.addEventListener('mousemove', dragSidebar);
document.addEventListener('mouseup', stopDragging);
@@ -145,7 +143,10 @@
});
onDestroy(() => {
if (ResizableSidebar) {
if (!browser) {
return;
}
if (window.innerWidth >= 1024) {
sidebarRef.removeEventListener('mousedown', startDragging);
document.removeEventListener('mousemove', dragSidebar);
document.removeEventListener('mouseup', stopDragging);
@@ -227,7 +228,6 @@
}
];
}
$: ResizableSidebar;
</script>
<aside
+1 -1
View File
@@ -45,7 +45,7 @@
bind:this={textareaRef}
{placeholder}
style="height: {textAreaHeight}"
class="border border-gray rounded-md p-2 dark:bg-gray-500"
class="border border-gray rounded-md p-2 dark:bg-neutral-800 dark:text-white"
on:change={onInputChange}
/>
{:else}
+2 -2
View File
@@ -7,12 +7,12 @@
</script>
<label class="block {className}">
<span class="text-gray-700 dark:text-white">{label}</span>
<span class="text-gray-700 dark:bg-neutral-800 dark:text-white">{label}</span>
<input
bind:value
{placeholder}
type="date"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 dark:bg-neutral-800 dark:text-white"
on:blur={onChange}
/>
</label>
+1 -1
View File
@@ -14,6 +14,6 @@
on:input={onInput}
{placeholder}
type="datetime-local"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 dark:bg-neutral-700 dark:text-white"
/>
</label>
+2 -2
View File
@@ -21,14 +21,14 @@
{/if}
<select
bind:value
class="form-select block w-auto mt-1 dark:text-gray-800 {selectClassName}"
class="form-select block w-auto mt-1 dark:bg-neutral-800 dark:text-white {selectClassName}"
required={isRequired}
on:blur={() => {}}
on:change={onChange}
class:customColor={!!selectClassName}
>
{#each options as option}
<option class="bg-white dark:bg-black text-black" value={option}>
<option class="bg-white text-black" value={option}>
{option[labelKey]}
</option>
{/each}
+2 -2
View File
@@ -10,7 +10,7 @@
// export let maxRows = 3;
export let className = '';
export let labelClassName = '';
export let bgColor = 'bg-gray-50 focus:bg-primary-50 dark:bg-white';
export let bgColor = 'bg-gray-50 focus:bg-primary-50 dark:bg-neutral-800 dark:text-white';
export let helperMessage = '';
export let errorMessage = '';
export let isRequired = false;
@@ -59,7 +59,7 @@
bind:value
bind:this={ref}
required={isRequired}
class="form-textarea dark:bg-gray-500 dark:text-gray-800 border-l-0 border-r-0 border-t-0 border-b-2 border-gray-200 focus:border-l-0 focus:border-r-0 rounded-t-md focus:border-t-0 focus:border-b-2 focus:border-primary-600 mt-1 block w-full
class="form-textarea border-l-0 border-r-0 border-t-0 border-b-2 border-gray-200 focus:border-l-0 focus:border-r-0 rounded-t-md focus:border-t-0 focus:border-b-2 focus:border-primary-600 mt-1 block w-full
{bgColor} {!!errorMessage ? 'border-red-500' : 'border-gray-300'}"
on:change={onChange}
/>
+1 -1
View File
@@ -11,7 +11,7 @@
export let className = '';
export let inputClassName = '';
export let labelClassName = '';
export let bgColor = 'bg-gray-100 focus:bg-primary-50 dark:bg-white';
export let bgColor = 'bg-gray-100 focus:bg-primary-50 dark:bg-neutral-800 dark:text-white';
export let type = 'text';
export let isPassword = false;
export let autoFocus = false;
+1 -1
View File
@@ -30,7 +30,7 @@
direction={toolTipProps.direction}
>
<button
class="root {color} {selected && 'active'} {size} {contained && 'contained'} {buttonClassName}"
class="root {color} {selected && 'active'} {size} {contained && 'contained '} {buttonClassName}"
{disabled}
{type}
on:click={handleClick}
+1 -1
View File
@@ -36,7 +36,7 @@
role="presentation"
>
<div class="flex items-center justify-between mb-4">
<h3 class="dark:text-white text-xl m-0">
<h3 class="dark:text-white text-md font-medium m-0">
{modalHeading}
</h3>
<div class="rounded-full">
+5 -6
View File
@@ -54,13 +54,12 @@
</IconButton>
</li>
<li class="hidden md:block">
<IconButton onClick={openSidebar}>
<Menu size={16} class=" text-white" />
</IconButton>
</li>
{#if isCoursePage}
<li class="hidden md:block">
<IconButton onClick={openSidebar}>
<Menu size={16} class=" text-white" />
</IconButton>
</li>
<div class="hidden md:block">
<IconButton
onClick={() => {
+71
View File
@@ -0,0 +1,71 @@
import { QuestionTypes } from '../utils';
import type { ExerciseTemplate } from '$lib/utils/types';
const template: ExerciseTemplate = {
title: 'HTML Elements Quiz',
description: 'Test your knowledge of HTML elements.',
questionnaire: {
questions: [
{
title: 'What does the <p> element stand for?',
name: 'p_element',
points: 2,
order: 1,
question_type: QuestionTypes[0], // RADIO
options: [
{ label: 'Paragraph', is_correct: true },
{ label: 'Page', is_correct: false },
{ label: 'Preformatted', is_correct: false }
]
},
{
title: 'Which HTML element is used for creating an unordered list?',
name: 'ul_element',
points: 2,
order: 2,
question_type: QuestionTypes[0], // RADIO
options: [
{ label: '<ul>', is_correct: true },
{ label: '<ol>', is_correct: false },
{ label: '<li>', is_correct: false }
]
},
{
title: 'What does the <a> element do?',
name: 'a_element',
points: 2,
order: 3,
question_type: QuestionTypes[1], // CHECKBOX
options: [
{ label: 'Defines a hyperlink', is_correct: true },
{ label: 'Creates a table', is_correct: false },
{ label: 'Adds an image', is_correct: false },
{ label: 'Defines a form', is_correct: false }
]
},
{
title: 'Which HTML element is used for creating a line break?',
name: 'br_element',
points: 2,
order: 4,
question_type: QuestionTypes[0], // RADIO
options: [
{ label: '<br>', is_correct: true },
{ label: '<p>', is_correct: false },
{ label: '<hr>', is_correct: false }
]
},
{
title: 'What does the <h1> to <h6> elements represent?',
name: 'h_elements',
points: 2,
order: 5,
question_type: QuestionTypes[2], // TEXTAREA
options: []
}
]
}
};
export default template;
+71
View File
@@ -0,0 +1,71 @@
import { QuestionTypes } from '../utils';
import type { ExerciseTemplate } from '$lib/utils/types';
const template: ExerciseTemplate = {
title: 'HTML Attributes Quiz',
description: 'Test your knowledge of HTML attributes.',
questionnaire: {
questions: [
{
title: 'Which HTML attribute is used to define inline styles?',
name: 'inline_style',
points: 2,
order: 1,
question_type: QuestionTypes[0], // RADIO
options: [
{ label: 'class', is_correct: false },
{ label: 'style', is_correct: true },
{ label: 'src', is_correct: false }
]
},
{
title: "What is the purpose of the 'href' attribute in the <a> element?",
name: 'href_attribute',
points: 2,
order: 2,
question_type: QuestionTypes[2], // TEXTAREA
options: []
},
{
title: 'Which attribute is used to specify an alternate text for an image?',
name: 'alt_attribute',
points: 2,
order: 3,
question_type: QuestionTypes[0], // RADIO
options: [
{ label: 'title', is_correct: false },
{ label: 'alt', is_correct: true },
{ label: 'src', is_correct: false }
]
},
{
title: "What does the 'target' attribute in the <a> element control?",
name: 'target_attribute',
points: 2,
order: 4,
question_type: QuestionTypes[1], // CHECKBOX
options: [
{ label: 'The size of the link', is_correct: false },
{ label: 'The position of the link', is_correct: false },
{ label: 'Where the linked document will open', is_correct: true },
{ label: 'The color of the link', is_correct: false }
]
},
{
title: 'Which attribute is used to specify the URL of a linked resource?',
name: 'href_attribute2',
points: 2,
order: 5,
question_type: QuestionTypes[0], // RADIO
options: [
{ label: 'src', is_correct: false },
{ label: 'href', is_correct: true },
{ label: 'alt', is_correct: false }
]
}
]
}
};
export default template;
+16
View File
@@ -0,0 +1,16 @@
import htmlElements from './001_html_elements';
import htmlAttributes from './002_html_attributes';
export const HTML_IDS = {
HTML_ELEMENTS: 'HTML_ELEMENTS',
HTML_ATTRIBUTES: 'HTML_ATTRIBUTES',
HTML_HEADINGS: 'HTML_HEADINGS',
HTML_PARAGRAPH: 'HTML_PARAGRAPH'
};
export const HTML_TEMPLATES = {
[HTML_IDS.HTML_ELEMENTS]: htmlElements,
[HTML_IDS.HTML_ATTRIBUTES]: htmlAttributes,
[HTML_IDS.HTML_HEADINGS]: htmlElements,
[HTML_IDS.HTML_PARAGRAPH]: htmlElements
};
+48
View File
@@ -0,0 +1,48 @@
import { HTML_IDS, HTML_TEMPLATES } from './html';
export const TAGS = {
HTML: 'HTML',
CSS: 'CSS',
JS: 'JS',
Typescript: 'Typescript',
ReactJS: 'ReactJS',
VueJS: 'VueJS'
};
export const TEMPLATES = {
[TAGS.HTML]: [
{
id: HTML_IDS.HTML_ELEMENTS,
title: 'Learn HTML Elements',
questions: 10,
points: 20,
data: HTML_TEMPLATES[HTML_IDS.HTML_ELEMENTS]
},
{
id: HTML_IDS.HTML_ATTRIBUTES,
title: 'HTML Attributes',
questions: 5,
points: 10,
data: HTML_TEMPLATES[HTML_IDS.HTML_ATTRIBUTES]
},
{
id: HTML_IDS.HTML_HEADINGS,
title: 'Practice HTML Headings',
questions: 15,
points: 50,
data: HTML_TEMPLATES[HTML_IDS.HTML_ELEMENTS]
},
{
id: HTML_IDS.HTML_PARAGRAPH,
title: 'Understand p tag',
questions: 15,
points: 20,
data: HTML_TEMPLATES[HTML_IDS.HTML_ELEMENTS]
}
],
[TAGS.CSS]: [],
[TAGS.JS]: [],
[TAGS.Typescript]: [],
[TAGS.ReactJS]: [],
[TAGS.VueJS]: []
};
+16
View File
@@ -0,0 +1,16 @@
import type { CustomQuestionType } from '$lib/utils/types';
export const QuestionTypes: CustomQuestionType[] = [
{
id: 1,
label: 'RADIO'
},
{
id: 2,
label: 'CHECKBOX'
},
{
id: 3,
label: 'TEXTAREA'
}
];
+14
View File
@@ -176,6 +176,20 @@ export const onboardingValidation = (fields = {}, step) => {
return processErrors(error);
};
// export const createTemplateExerciseValidation = (fields = {}) => {
// const schema = z.object({
// orgName: z.string().min(5, {
// message: 'Must be 5 or more characters long',
// }),
// siteName: z.string().min(5, {
// message: 'Must be 5 or more characters long',
// })
// });
// const { error } = schema.safeParse(fields);
// return processErrors(error);
// };
export const createOrgValidation = (fields = {}) => {
const schema = z.object({
orgName: z.string().min(5, {
+31 -2
View File
@@ -7,10 +7,13 @@ import type {
Group,
Groupmember,
Exercise,
LessonCompletion
LessonCompletion,
ExerciseTemplate
} from '$lib/utils/types';
import { STATUS } from '$lib/utils/constants/course';
import type { PostgrestError, PostgrestSingleResponse } from '@supabase/supabase-js';
import type { PostgrestSingleResponse } from '@supabase/supabase-js';
import htmlElements from '$lib/mocks/html/001_html_elements';
export async function fetchCourse(courseId?: Course['id'], slug?: Course['slug']) {
let match: { slug?: string; id?: string; status?: string } = {};
@@ -189,6 +192,32 @@ function isNew(item: any) {
return isNaN(item);
}
export async function createExerciseFromTemplate(
lessonId: string,
template: ExerciseTemplate
): Promise<any | undefined> {
const { data, error } = await createExercise({
title: template.title,
description: template.description,
lesson_id: lessonId
});
if (error) {
console.error('Something went wrong', error);
return;
}
const { id } = data[0] || {};
if (!id) {
console.error('Something went wrong, no id', error);
return;
}
await upsertExercise(template.questionnaire, id);
return data[0];
}
export async function upsertExercise(questionnaire: any, exerciseId: Exercise['id']) {
const {
questions,
@@ -0,0 +1,82 @@
import type { ExerciseTemplate, CustomQuestionType } from '$lib/utils/types';
const QuestionTypes: CustomQuestionType[] = [
{
id: 1,
label: 'RADIO'
},
{
id: 2,
label: 'CHECKBOX'
},
{
id: 3,
label: 'TEXTAREA'
}
];
const quiz: ExerciseTemplate = {
title: 'HTML Elements Quiz',
description: 'Test your knowledge of HTML elements.',
questionnaire: {
questions: [
{
title: 'What does the <p> element stand for?',
name: 'p_element',
points: 2,
order: 1,
question_type: QuestionTypes[0], // RADIO
options: [
{ label: 'Paragraph', is_correct: true },
{ label: 'Page', is_correct: false },
{ label: 'Preformatted', is_correct: false }
]
},
{
title: 'Which HTML element is used for creating an unordered list?',
name: 'ul_element',
points: 2,
order: 2,
question_type: QuestionTypes[0], // RADIO
options: [
{ label: '<ul>', is_correct: true },
{ label: '<ol>', is_correct: false },
{ label: '<li>', is_correct: false }
]
},
{
title: 'What does the <a> element do?',
name: 'a_element',
points: 2,
order: 3,
question_type: QuestionTypes[1], // CHECKBOX
options: [
{ label: 'Defines a hyperlink', is_correct: true },
{ label: 'Creates a table', is_correct: false },
{ label: 'Adds an image', is_correct: false },
{ label: 'Defines a form', is_correct: false }
]
},
{
title: 'Which HTML element is used for creating a line break?',
name: 'br_element',
points: 2,
order: 4,
question_type: QuestionTypes[0], // RADIO
options: [
{ label: '<br>', is_correct: true },
{ label: '<p>', is_correct: false },
{ label: '<hr>', is_correct: false }
]
},
{
title: 'What does the <h1> to <h6> elements represent?',
name: 'h_elements',
points: 2,
order: 5,
question_type: QuestionTypes[2], // TEXTAREA
options: []
}
]
}
};
+23
View File
@@ -12,6 +12,29 @@ export interface GroupPerson {
profile_id: string;
role_id: Role['id'];
}
export interface CustomQuestionType {
id: number;
label: any;
}
export interface ExerciseTemplate {
title: string;
description: string;
questionnaire: {
questions: {
title: string;
name: string;
points: number;
order: number;
question_type: CustomQuestionType;
options: {
label: string;
is_correct: boolean;
}[];
}[];
};
}
//===========================================
interface CourseMetadata {