CLASSROOMIO-T-190: Templates v1
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
};
|
||||
@@ -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]: []
|
||||
};
|
||||
@@ -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'
|
||||
}
|
||||
];
|
||||
@@ -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, {
|
||||
|
||||
@@ -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: []
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user