Editor and backend!

This commit is contained in:
Mawoka
2022-12-21 22:03:37 +01:00
parent 07f242e6cd
commit 0af5aeef95
13 changed files with 331 additions and 219 deletions
+2
View File
@@ -39,11 +39,13 @@
}
const validateInput = async (data: EditorData) => {
// console.log("input", data)
try {
await dataSchema.validate(data, { abortEarly: false });
schemaInvalid = false;
yupErrorMessage = '';
} catch (err) {
console.log('erro!', err.errors);
schemaInvalid = true;
yupErrorMessage = err.errors ? err.errors[0] : '';
}
+10 -4
View File
@@ -12,7 +12,7 @@
import Spinner from '../Spinner.svelte';
import { createTippy } from 'svelte-tippy';
import { getLocalization } from '$lib/i18n';
import Slide from './slide.svelte';
// import Slide from './slide.svelte';
const { t } = getLocalization();
@@ -50,6 +50,8 @@
data.questions[selected_question].type = QuizQuestionType.ABCD;
}
*/
$: console.log();
</script>
<div class="w-full max-h-full pb-20 px-20 h-full">
@@ -67,7 +69,13 @@
/>
</div>
</div>
{#if data.questions[selected_question].time}
{#if data.questions[selected_question].type === QuizQuestionType.SLIDE}
{#await import('./slide.svelte')}
<Spinner my_20={false} />
{:then c}
<svelte:component this={c.default} bind:data={data.questions[selected_question]} />
{/await}
{:else}
<div class="flex flex-col">
<div class="flex justify-center pt-10 w-full">
{#await import('$lib/inline-editor.svelte')}
@@ -179,8 +187,6 @@
{$t('editor_page.right_click_to_delete')}
</p>
</div>
{:else}
<Slide bind:data={data.questions[selected_question]} />
{/if}
</div>
</div>
+9 -1
View File
@@ -28,6 +28,14 @@
type: QuizQuestionType.ABCD
};
const empy_slide: Question = {
type: QuizQuestionType.SLIDE,
time: '120',
question: 'Slide',
image: undefined,
answers: []
};
const setSelectedQuestion = (index: number): void => {
selected_question = index;
if (index === -1) {
@@ -270,7 +278,7 @@
type="button"
class="h-full flex justify-center w-full dark:text-black"
on:click={() => {
data.questions = [...data.questions, { ...{} }];
data.questions = [...data.questions, { ...empy_slide }];
}}
>
<svg
+162 -170
View File
@@ -5,24 +5,26 @@
-->
<script lang="ts">
import TextElement from './slides/types/text.svelte';
import HeadlineElement from './slides/types/text.svelte';
import { ElementTypes } from './types';
import { elementSelection } from '$lib/stores';
import HeadlineElement from './slides/types/headline.svelte';
import { draggable } from '@neodrag/svelte';
import type { Question } from '$lib/quiz_types';
import { ElementTypes, QuizQuestionType } from '$lib/quiz_types';
import ElementSelection from './slides/element_selection.svelte';
import SettingsMenu from './slides/settings_menu.svelte';
import { onMount } from 'svelte';
export let data;
let selected_el_id = undefined;
elementSelection.set({ data: undefined });
export let data: Question = {
type: QuizQuestionType.SLIDE,
time: '120',
question: '',
image: undefined,
answers: []
};
let selected_element = undefined;
const ghost_elements = new Array(72).fill({});
let elements: Array<{
height: number;
width: number;
col: number;
row: number;
type: ElementTypes;
data;
}> = [];
let elements_binds = [];
let elements_binds: Array<HTMLElement> | undefined = [];
let main_el: undefined | HTMLElement;
let settings_menu_open = false;
const get_location = (id: number): { row: number; col: number } => {
const numColumns = 12;
const row = Math.floor(id / numColumns);
@@ -30,176 +32,166 @@
return { row, col };
};
const add_text_field = (id: number) => {};
const change_size = (position: string, id: number, increase: boolean) => {
if (position === 't') {
elements[id].row += increase ? -1 : 1;
elements[id].height += increase ? 1 : -1;
} else if (position === 'b') {
elements[id].height += increase ? 1 : -1;
} else if (position === 'l') {
elements[id].width += increase ? 1 : -1;
elements[id].col += increase ? -1 : 1;
} else if (position === 'r') {
elements[id].width += increase ? 1 : -1;
}
if (elements[id].width < 1 || elements[id].height < 1) {
elements.splice(id, 1);
}
if (elements[id].row < 0) {
elements[id].row = 0;
}
if (elements[id].col < 0) {
elements[id].col = 0;
}
console.log(elements[id]);
const set_correct_position = (
e: CustomEvent<{ offsetX: number; offsetY: number; domRect: DOMRect }>
) => {
const id = parseInt(e.target.getAttribute('el_id'));
data.answers[id].x = e.detail.offsetX / main_el.offsetWidth;
data.answers[id].y = e.detail.offsetY / main_el.offsetHeight;
};
let occupied_fields = [];
elementSelection.subscribe((data) => {
if (data.data && selected_el_id) {
console.log('data!', data.data);
const { row, col } = get_location(selected_el_id);
elements = [...elements, { height: 1, width: 1, col, row, type: data.data, data: '' }];
elementSelection.set({ data: undefined });
occupied_fields.push(selected_el_id);
selected_el_id = undefined;
const set_correct_height = new ResizeObserver((e) => {
for (const i of e) {
const id = parseInt(i.target.getAttribute('el_id'));
data.answers[id].width = i.contentRect.width / main_el.offsetWidth;
data.answers[id].height = i.contentRect.height / main_el.offsetHeight;
}
});
const add_text_field = () => {
data.answers = [
...data.answers,
{
type: selected_element,
data: '',
x: 0,
y: 0,
id: data.answers.length,
height: 0,
width: 0
}
];
selected_element = undefined;
};
const delete_element = (id: number) => {
if (!confirm('Do you really want to delete this element?')) {
return;
}
data.answers.splice(id, 1);
data.answers = data.answers;
elements_binds.splice(id, 1);
elements_binds = elements_binds;
};
$: {
if (selected_element) {
add_text_field();
}
}
const assign_resize_handlers = () => {
for (let i = 0; i < elements_binds.length; i++) {
set_correct_height.observe(elements_binds[i]);
}
};
/* if (data.answers.length !== 0) {
for (let i = 0; i < data.answers.length; i++) {
data.answers[i].width = data.answers[i].width * main_el.offsetWidth;
data.answers[i].height = data.answers[i].height * main_el.offsetHeight;
}
}*/
$: {
elements_binds;
assign_resize_handlers();
}
const set_correct_size = (e: UIEvent) => {
console.log(e, 'resized');
};
onMount(() => {
setTimeout(() => {
for (let i = 0; i < data.answers.length; i++) {
elements_binds[i].style.height = `${
data.answers[i].height * main_el.offsetHeight
}px`;
elements_binds[i].style.width = `${data.answers[i].width * main_el.offsetWidth}px`;
}
}, 100);
});
</script>
<div class="flex h-full">
<div
class="grid grid-cols-12 grid-rows-6 w-full h-full pb-12 absolute overflow-hidden relative max-h-full"
>
{#each elements as el, i}
<div
class="z-20 overflow-auto relative group"
element_id={i}
style="grid-column-start: {el.col + 1}; grid-row-start: {el.row +
1};grid-column-end: {el.col + 1 + el.width}; grid-row-end: {el.row +
1 +
el.height}"
bind:this={elements_binds[i]}
>
<div
class="absolute top-0 left-0 mx-auto justify-center w-full h-0 hidden group-hover:flex"
>
<div>
<button
class="text-black z-30"
on:click={() => {
change_size('t', i, true);
}}
disabled={el.row === 0}
>+
</button>
<button
class="text-black z-30"
on:click={() => {
change_size('t', i, false);
}}>-</button
>
</div>
</div>
<div
class="absolute bottom-6 left-0 mx-auto justify-center w-full h-0 hidden group-hover:flex"
>
<div>
<button
class="text-black z-30"
on:click={() => {
change_size('b', i, true);
}}
disabled={el.row + el.height === 6}
>+
</button>
<button
class="text-black z-30"
on:click={() => {
change_size('b', i, false);
}}>-</button
>
</div>
</div>
<div
class="absolute top-0 left-3 mx-auto justify-center h-full w-0 hidden group-hover:flex"
>
<div class="my-auto">
<button
class="text-black z-30"
on:click={() => {
change_size('l', i, true);
}}
disabled={el.col === 0}
>+
</button>
<button
class="text-black z-30"
on:click={() => {
change_size('l', i, false);
}}>-</button
>
</div>
</div>
<div
class="absolute top-0 right-3 mx-auto justify-center h-full w-0 hidden group-hover:flex"
>
<div class="my-auto">
<button
class="text-black z-30"
on:click={() => {
change_size('r', i, true);
}}
disabled={el.col + el.width === 12}
>+
</button>
<button
class="text-black z-30"
on:click={() => {
change_size('r', i, false);
}}>-</button
>
</div>
</div>
{#if el.type === ElementTypes.Text}
<p>Text</p>
<TextElement />
{:else if el.type === ElementTypes.Headline}
<p>Headline</p>
<HeadlineElement bind:data={el.data} />
{/if}
</div>
{/each}
<div class="flex h-full relative w-full pb-12" bind:this={main_el}>
<div class="absolute top-0 right-0 flex flex-col pr-2 rounded-t-lg">
<button
class="ml-auto"
on:click={() => {
selected_element = selected_element === null ? undefined : null;
}}
type="button"
class:add-button={selected_element === null}
>
Add element
</button>
{#if selected_element === null}
<ElementSelection bind:selected_element />
{/if}
</div>
<div class="grid grid-cols-12 grid-rows-6 w-full h-full pb-12 absolute overflow-hidden">
{#each ghost_elements as el, i}
<div class="w-full border border-gray-600 group">
<button
class="h-full flex justify-center w-full dark:text-black invisible group-hover:visible"
on:click={() => {
selected_el_id = i;
elementSelection.set({ data: null });
}}
<div class="absolute top-0 left-0 flex flex-col pl-2 rounded-t-lg">
<button
class="mr-auto"
on:click={() => {
settings_menu_open = !settings_menu_open;
}}
type="button"
>
Settings
</button>
{#if settings_menu_open}
<SettingsMenu bind:time={data.time} bind:title={data.question} />
{/if}
</div>
{#if main_el}
{#each data.answers as el, i}
<div
use:draggable={{
bounds: 'parent',
handle: '.drag',
defaultPosition: {
x: data.answers[i].x * main_el.offsetWidth,
y: data.answers[i].y * main_el.offsetHeight
}
}}
class="resize h-fit overflow-auto relative min-h-fit"
on:neodrag:end={set_correct_position}
el_id={i}
on:resize={set_correct_size}
bind:this={elements_binds[el.id]}
>
<span
class="absolute drag dark:text-black hover:cursor-grab opacity-30 hover:opacity-100 transition top-0 left-0"
>
{i}
<!-- heroicons/arrows-expand -->
<svg
class="h-12 m-auto"
class="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/>
</svg>
</button>
d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"
/></svg
></span
>
<!-- <button class='absolute drag dark:text-black opacity-30 hover:opacity-100 transition top-0 right-0' on:click={() => {delete_element(i)}}>
&lt;!&ndash; heroicons/trash &ndash;&gt;
<svg class='w-4 h-4' fill='none' stroke='currentColor' viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'>
<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2'
d='M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16'></path>
</svg>
</button>-->
{#if el.type === ElementTypes.Text}
<TextElement bind:text={el.data} />
{:else if el.type === ElementTypes.Headline}
<HeadlineElement bind:data={el.data} />
{/if}
</div>
{/each}
</div>
{/if}
</div>
@@ -4,10 +4,11 @@
- file, You can obtain one at https://mozilla.org/MPL/2.0/.
-->
<script lang="ts">
import { ElementTypes } from '$lib/editor/types';
import { elementSelection } from '$lib/stores';
import { ElementTypes } from '$lib/quiz_types';
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
export let selected_element;
const keybinding_list = {
t: ElementTypes.Text,
h: ElementTypes.Headline
@@ -15,11 +16,14 @@
};
const keypress_listener = (key: KeyboardEvent) => {
if (selected_element !== null) {
return;
}
const sel_type = keybinding_list[key.key];
if (sel_type) {
elementSelection.set({ data: sel_type });
selected_element = sel_type;
} else if (key.code === 'Escape') {
elementSelection.set({ data: undefined });
selected_element = undefined;
}
};
@@ -51,27 +55,23 @@
</script>
<div
class="fixed top-0 left-0 flex justify-center w-screen h-screen bg-black bg-opacity-60 z-50 text-black"
class="bg-white m-auto rounded-lg shadow-lg p-4 flex flex-col dark:bg-gray-600 h-fit"
transition:fade={{ duration: 100 }}
>
<div class="w-5/6 h-5/6 bg-white m-auto rounded-lg shadow-lg p-4 flex flex-col">
<div class="flex justify-center">
<h1 class="text-2xl">Select the element you want to add</h1>
</div>
<ul>
{#each element_list as el}
<li
class="flex flex-row mt-4 w-full bg-gray-200 shadow-xl rounded-lg p-2 hover:bg-gray-300 hover:shadow-2xl hover:cursor-pointer transition"
on:click={() => {
elementSelection.set({ data: el.type });
}}
>
<div class="flex flex-col">
<h3 class="text-xl">{el.name}</h3>
<p>{el.description}</p>
</div>
<kbd class="my-auto ml-auto">{el.shortcut}</kbd>
</li>
{/each}
</ul>
</div>
<ul>
{#each element_list as el}
<li
class="flex flex-row mt-4 w-full bg-gray-200 shadow-xl rounded-lg p-2 hover:bg-gray-300 hover:shadow-2xl hover:cursor-pointer transition dark:bg-gray-800"
on:click={() => {
selected_element = el.type;
}}
>
<div class="flex flex-col">
<h3 class="text-xl">{el.name}</h3>
<p>{el.description}</p>
</div>
<kbd class="my-auto ml-auto">{el.shortcut}</kbd>
</li>
{/each}
</ul>
</div>
@@ -0,0 +1,39 @@
<!--
- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at https://mozilla.org/MPL/2.0/.
-->
<script lang="ts">
import { fade } from 'svelte/transition';
export let title;
export let time;
let time_local = 120;
/*eslint no-unused-vars: ["error", { "argsIgnorePattern": "^_" }]*/
$: time = String(time_local);
if (time) {
time_local = parseInt(time);
}
</script>
<div
class="bg-white m-auto rounded-lg shadow-lg p-4 dark:bg-gray-600 h-fit gap-2 w-fit auto-cols-min flex"
transition:fade={{ duration: 100 }}
>
<label class="w-fit">
Time
<input
bind:value={time}
type="number"
class="w-20 bg-transparent rounded-lg text-lg border-2 border-gray-500 p-1 outline-none"
/>
</label>
<label class="w-fit">
Title
<input
bind:value={title}
type="text"
class="bg-transparent rounded-lg text-lg border-2 border-gray-500 p-1 transition outline-none"
/>
</label>
</div>
@@ -7,4 +7,8 @@
export let data;
</script>
<input bind:value={data} type="text" class="w-full h-full dark:text-black text-4xl text-center" />
<input
bind:value={data}
type="text"
class="w-full h-full dark:text-black text-center text-3xl min-h-fit"
/>
-11
View File
@@ -1,11 +0,0 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
export enum ElementTypes {
Text = 'TEXT',
Headline = 'HEADLINE',
Image = 'IMAGE'
}
+19 -1
View File
@@ -4,6 +4,12 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
export enum ElementTypes {
Text = 'TEXT',
Headline = 'HEADLINE',
Image = 'IMAGE'
}
export interface QuizData {
title: string;
description: string;
@@ -19,7 +25,8 @@ export interface QuizData {
export enum QuizQuestionType {
ABCD = 'ABCD', // eslint-disable-line no-unused-vars
RANGE = 'RANGE', // eslint-disable-line no-unused-vars
VOTING = 'VOTING' // eslint-disable-line no-unused-vars
VOTING = 'VOTING', // eslint-disable-line no-unused-vars
SLIDE = 'SLIDE' // eslint-disable-line no-unused-vars
}
export interface RangeQuizAnswer {
@@ -29,6 +36,16 @@ export interface RangeQuizAnswer {
max_correct: number;
}
export interface Slide {
type: ElementTypes;
x: number;
y: number;
height: number;
width: number;
data: string;
id: number;
}
export interface Question {
time: string;
question: string;
@@ -42,6 +59,7 @@ export interface Answer {
answer: string;
color?: string;
}
export interface VotingAnswer {
answer: string;
image?: string;
+28 -4
View File
@@ -5,6 +5,7 @@
*/
import * as yup from 'yup';
import { ElementTypes } from '$lib/quiz_types';
export const ABCDQuestionSchema = yup
.array()
@@ -34,6 +35,18 @@ export const RangeQuestionSchema = yup.object({
min_correct: yup.number(),
max_correct: yup.number()
});
export const SlideQuestionSchema = yup.array().of(
yup.object({
type: yup.string().required(),
x: yup.number(),
y: yup.number(),
height: yup.number(),
width: yup.number(),
data: yup.string().optional(),
id: yup.number().required()
})
);
export const dataSchema = yup.object({
public: yup.boolean().required(),
type: yup.string(),
@@ -63,12 +76,23 @@ export const dataSchema = yup.object({
.lowercase(),
answers: yup.lazy((v) => {
if (Array.isArray(v)) {
if (typeof v[0].right === 'boolean') {
return ABCDQuestionSchema;
} else {
return VotingQuestionSchema;
try {
if (typeof v[0].right === 'boolean') {
// console.log('ABCDQuestionSchema');
return ABCDQuestionSchema;
} else if (v[0].answer !== undefined) {
// console.log('VotingQuestionSchema');
return VotingQuestionSchema;
} else {
// console.log('SlideQuestionSchema');
return SlideQuestionSchema;
}
} catch {
// console.log('SlideQuestionSchema');
return SlideQuestionSchema;
}
} else {
// console.log('RangeQuestionSchema');
return RangeQuestionSchema;
}
})