Merge pull request #230 from mawoka-myblock/quiztivity
This commit is contained in:
@@ -18,7 +18,11 @@
|
||||
<a
|
||||
{href}
|
||||
{target}
|
||||
class="text-black hover:bg-opacity-80 w-full px-4 py-2 leading-5 text-black transition-all duration-200 transform bg-[#B07156] rounded text-center focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 outline-none"
|
||||
{disabled}
|
||||
class:opacity-50={disabled}
|
||||
class:cursor-not-allowed={disabled}
|
||||
class:pointer-events-none={disabled}
|
||||
class="text-black hover:bg-opacity-80 w-full px-4 py-2 leading-5 transition-all duration-200 transform bg-[#B07156] rounded text-center outline-none"
|
||||
on:click
|
||||
class:flex
|
||||
class:justify-center={flex}
|
||||
@@ -29,7 +33,7 @@
|
||||
<button
|
||||
{disabled}
|
||||
{type}
|
||||
class="text-black hover:opacity-80 w-full px-4 py-2 leading-5 text-black transition-all duration-200 transform bg-[#B07156] rounded text-center focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 outline-none"
|
||||
class="text-black hover:opacity-80 w-full px-4 py-2 leading-5 transition-all duration-200 transform bg-[#B07156] rounded text-center focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 outline-none"
|
||||
on:click
|
||||
class:flex
|
||||
class:justify-center={flex}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<!--
|
||||
- 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 { fly } from 'svelte/transition';
|
||||
import { getLocalization } from '$lib/i18n';
|
||||
import { PopoverTypes } from './smalltop';
|
||||
|
||||
const { t } = getLocalization();
|
||||
|
||||
export let open = false;
|
||||
export let type: PopoverTypes;
|
||||
export let data: undefined | { game_pin: number | string; game_id: string } = undefined;
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<div class="fixed w-screen top-10 z-[60] flex justify-center" transition:fly={{ y: -100 }}>
|
||||
<div
|
||||
class="flex items-center p-4 w-full max-w-xs text-gray-500 bg-white rounded-lg shadow dark:text-gray-400 dark:bg-gray-800"
|
||||
role="alert"
|
||||
>
|
||||
<div class="ml-3 text-sm font-normal">
|
||||
{#if type === PopoverTypes.Copy}{$t('components.popover.copied_to_clipboard')}Copied
|
||||
to clipboard!
|
||||
{:else if type === PopoverTypes.GameInLobby}A game is currently in the lobby. Click <a
|
||||
class="underline"
|
||||
href="/remote?game_pin={data.game_pin}&game_id={data.game_id}">here</a
|
||||
> to join as a remote.
|
||||
{:else}
|
||||
<p>Error!!!</p>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700"
|
||||
data-dismiss-target="#toast-default"
|
||||
aria-label="Close"
|
||||
on:click={() => {
|
||||
open = false;
|
||||
}}
|
||||
>
|
||||
<span class="sr-only">{$t('words.close')}</span>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="w-5 h-5"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
export enum PopoverTypes {
|
||||
Copy,
|
||||
GameInLobby
|
||||
}
|
||||
@@ -169,7 +169,12 @@
|
||||
"point_plural": "Points",
|
||||
"back": "Back",
|
||||
"finish": "Finish",
|
||||
"normal": "Normal"
|
||||
"normal": "Normal",
|
||||
"selected": "Selected",
|
||||
"select": "Select",
|
||||
"quiz": "Quiz",
|
||||
"quiztivity": "Quiztivity",
|
||||
"next": "Next"
|
||||
},
|
||||
"editor": {
|
||||
"time_in_seconds": "Time in seconds",
|
||||
@@ -344,5 +349,39 @@
|
||||
"old_school_mode": "Old-School",
|
||||
"old_school_mode_description": "Questions and images will be shown on both admins screen and on the screen of the players",
|
||||
"start_game": "Start Game"
|
||||
},
|
||||
"quiztivity": {
|
||||
"editor": {
|
||||
"select_page_type": "Select Page Type",
|
||||
"move_left": "Move left",
|
||||
"move_right": "Move right",
|
||||
"title_placeholder": "Enter title here",
|
||||
"add_new": "Add new",
|
||||
"delete": "Delete",
|
||||
"open_shares_menu": "Open Shares menu",
|
||||
"shares": {
|
||||
"add_new_share": "Add new Share",
|
||||
"expires_on": "Expires on {{date}}",
|
||||
"never_expires": "Never expires"
|
||||
}
|
||||
},
|
||||
"memory": {
|
||||
"editor": {
|
||||
"add_card": "Add card",
|
||||
"upload_image": "Upload image",
|
||||
"add_pair": "Add pair"
|
||||
}
|
||||
},
|
||||
"play": {
|
||||
"memory": {
|
||||
"try_count": "Tries: {{try_count}}"
|
||||
}
|
||||
},
|
||||
"share_expired": "Share expired"
|
||||
},
|
||||
"components": {
|
||||
"popover": {
|
||||
"copied_to_clipboard": "Copied to clipboard!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<!--
|
||||
- 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 { QuizTivityTypes } from './types';
|
||||
import { getLocalization } from '$lib/i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
const { t } = getLocalization();
|
||||
export let type: QuizTivityTypes | undefined;
|
||||
|
||||
const PageTypes = [
|
||||
/* {
|
||||
name: 'Pdf',
|
||||
description: 'Upload a PDF!',
|
||||
type: QuizTivityTypes.PDF,
|
||||
svg: undefined
|
||||
},*/
|
||||
{
|
||||
name: 'Memory',
|
||||
description: 'Matching Pairs game',
|
||||
type: QuizTivityTypes.MEMORY,
|
||||
svg: undefined
|
||||
},
|
||||
{
|
||||
name: 'Markdown',
|
||||
description: 'Add content in Markdown format!',
|
||||
type: QuizTivityTypes.MARKDOWN,
|
||||
svg: undefined
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="fixed top-0 left-0 z-50 bg-black bg-opacity-50 flex w-screen h-screen"
|
||||
transition:fade={{ duration: 100 }}
|
||||
>
|
||||
<div class="m-auto w-5/6 h-5/6">
|
||||
<div class="rounded bg-white p-6 dark:bg-gray-600">
|
||||
<h1 class="text-center text-3xl mb-6">{$t('quiztivity.editor.select_page_type')}</h1>
|
||||
<div class="grid grid-cols-4 gap-4 overflow-y-scroll">
|
||||
{#each PageTypes as pt}
|
||||
<div class="rounded p-6 border-[#B07156] border">
|
||||
<button
|
||||
class="text-xl text-black dark:text-white"
|
||||
on:click={() => {
|
||||
type = pt.type;
|
||||
}}>{pt.name}</button
|
||||
>
|
||||
<p class="text-sm">{pt.description}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,37 @@
|
||||
<!--
|
||||
- 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 type { Markdown } from '$lib/quiztivity/types';
|
||||
import { marked } from 'marked';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export let data: Markdown | undefined;
|
||||
|
||||
if (!data) {
|
||||
data = {
|
||||
markdown: ''
|
||||
};
|
||||
}
|
||||
|
||||
let rendered_html = '';
|
||||
|
||||
$: rendered_html = browser ? marked.parse(data.markdown) : '';
|
||||
</script>
|
||||
|
||||
<div class="w-full h-[70vh] flex flex-row p-4 gap-4">
|
||||
<textarea
|
||||
class="w-full resize-none border-[#B07156] border-2 rounded outline-none p-2 bg-opacity-30 bg-white dark:placeholder-gray-300"
|
||||
bind:value={data.markdown}
|
||||
placeholder="Enter your markdown here!"
|
||||
/>
|
||||
<div class="w-full">
|
||||
<div
|
||||
class="aspect-video prose max-w-none border-[#B07156] border-2 rounded p-2 dark:prose-invert"
|
||||
>
|
||||
{@html rendered_html}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,21 @@
|
||||
<!--
|
||||
- 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 type { Markdown } from '$lib/quiztivity/types';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { marked } from 'marked';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export let data: Markdown | undefined;
|
||||
|
||||
let rendered_html = '';
|
||||
|
||||
$: rendered_html = browser ? DOMPurify.sanitize(marked.parse(data.markdown ?? '')) : '';
|
||||
</script>
|
||||
|
||||
<div class="prose dark:prose-invert">
|
||||
{@html rendered_html}
|
||||
</div>
|
||||
@@ -0,0 +1,129 @@
|
||||
<!--
|
||||
- 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 type { Memory } from '$lib/quiztivity/types';
|
||||
import { getLocalization } from '$lib/i18n';
|
||||
import BrownButton from '$lib/components/buttons/brown.svelte';
|
||||
import { flip } from 'svelte/animate';
|
||||
|
||||
const { t } = getLocalization();
|
||||
|
||||
export let data: Memory | undefined;
|
||||
let new_pair_data = {
|
||||
text_1: '',
|
||||
text_2: ''
|
||||
};
|
||||
|
||||
if (!data) {
|
||||
data = {
|
||||
cards: []
|
||||
};
|
||||
}
|
||||
const arraymove = (arr: any[], fromI: number, toI: number) => {
|
||||
const el = arr[fromI];
|
||||
arr.splice(fromI, 1);
|
||||
arr.splice(toI, 0, el);
|
||||
};
|
||||
const move_card_left = (selected_slide: number) => {
|
||||
arraymove(data.cards, selected_slide, selected_slide - 1);
|
||||
data.cards = data.cards;
|
||||
};
|
||||
|
||||
const move_card_right = (selected_slide: number) => {
|
||||
arraymove(data.cards, selected_slide, selected_slide + 1);
|
||||
data.cards = data.cards;
|
||||
};
|
||||
|
||||
const add_card = () => {
|
||||
if (!new_pair_data.text_1 || !new_pair_data.text_2) {
|
||||
return;
|
||||
}
|
||||
const id = (Math.random() + 1).toString(36).substring(7);
|
||||
data.cards = [
|
||||
...data.cards,
|
||||
[
|
||||
{ id, text: new_pair_data.text_1 },
|
||||
{ id, text: new_pair_data.text_2 }
|
||||
]
|
||||
];
|
||||
new_pair_data = {
|
||||
text_1: '',
|
||||
text_2: ''
|
||||
};
|
||||
};
|
||||
|
||||
// $: console.log(new_pair_data.text_1.replaceAll("\n", "7"))
|
||||
</script>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<div class="grid grid-cols-4 w-11/12 gap-4">
|
||||
<div class="border-[#B07156] border-2 rounded">
|
||||
<h2 class="text-center">{$t('quiztivity.memory.editor.add_card')}</h2>
|
||||
<div class="grid grid-cols-2 py-2">
|
||||
<div class="px-2 flex flex-col gap-2">
|
||||
<textarea
|
||||
type="text"
|
||||
class="h-auto resize-none bg-transparent outline-none rounded outline-[#B07156] outline"
|
||||
rows="3"
|
||||
contenteditable="true"
|
||||
bind:value={new_pair_data.text_1}
|
||||
/>
|
||||
<div class="flex justify-center">
|
||||
<span class="h-0.5 bg-black block w-11/12" />
|
||||
</div>
|
||||
<BrownButton>{$t('quiztivity.memory.editor.upload_image')}</BrownButton>
|
||||
</div>
|
||||
<div class="px-2 flex flex-col gap-2">
|
||||
<textarea
|
||||
type="text"
|
||||
class="h-auto resize-none bg-transparent outline-none rounded outline-[#B07156] outline"
|
||||
rows="3"
|
||||
contenteditable="true"
|
||||
bind:value={new_pair_data.text_2}
|
||||
/>
|
||||
<div class="flex justify-center">
|
||||
<span class="h-0.5 bg-black block w-11/12" />
|
||||
</div>
|
||||
<BrownButton>{$t('quiztivity.memory.editor.upload_image')}</BrownButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center p-2">
|
||||
<BrownButton on:click={add_card}
|
||||
>{$t('quiztivity.memory.editor.add_pair')}</BrownButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{#each data.cards as card_pair, i (card_pair[0].id)}
|
||||
<div
|
||||
class="border-[#B07156] border-2 rounded group flex flex-col"
|
||||
animate:flip={{ duration: 200 }}
|
||||
>
|
||||
<div class="grid grid-cols-2 py-2 h-full">
|
||||
{#each card_pair as card}
|
||||
<div class="px-2 flex h-full">
|
||||
<p class="m-auto">{card.text}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="grid grid-cols-2 p-2 gap-2">
|
||||
<BrownButton
|
||||
on:click={() => {
|
||||
move_card_left(i);
|
||||
}}
|
||||
disabled={i === 0}>{$t('quiztivity.editor.move_left')}</BrownButton
|
||||
>
|
||||
<BrownButton
|
||||
on:click={() => {
|
||||
move_card_right(i);
|
||||
}}
|
||||
disabled={i + 1 === data.cards.length}
|
||||
>{$t('quiztivity.editor.move_right')}</BrownButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,101 @@
|
||||
<!--
|
||||
- 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 type { Memory, MemoryCard } from '$lib/quiztivity/types';
|
||||
import { getLocalization } from '$lib/i18n';
|
||||
|
||||
const { t } = getLocalization();
|
||||
export let data: Memory | undefined;
|
||||
|
||||
const shuffle = <Type>(a: Array<Type>): Array<Type> => {
|
||||
for (let i = a.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[a[i], a[j]] = [a[j], a[i]];
|
||||
}
|
||||
return a;
|
||||
};
|
||||
|
||||
let card_opened = {};
|
||||
|
||||
const get_all_cards_as_single_array = (): MemoryCard[] => {
|
||||
console.log('data', data.cards);
|
||||
let final_arr: MemoryCard[] = [];
|
||||
for (let i = 0; i < data.cards.length; i++) {
|
||||
const pair = data.cards[i];
|
||||
console.log('pair', pair);
|
||||
for (let c = 0; c < pair.length; c++) {
|
||||
final_arr.push({ ...pair[c], id: `${i}:${c}` });
|
||||
card_opened[`${i}:${c}`] = false;
|
||||
}
|
||||
}
|
||||
console.log(final_arr);
|
||||
return final_arr;
|
||||
};
|
||||
|
||||
const all_cards_array: MemoryCard[] = get_all_cards_as_single_array();
|
||||
|
||||
const random_card_order = shuffle(all_cards_array);
|
||||
console.log(random_card_order);
|
||||
|
||||
const found_cards: string[] = [];
|
||||
let opened_active_cards: string[] = [];
|
||||
let try_count = 0;
|
||||
let game_finished = false;
|
||||
const flip_card = (id: string) => {
|
||||
if (found_cards.includes(id) || game_finished) {
|
||||
return;
|
||||
}
|
||||
card_opened[id] = true;
|
||||
opened_active_cards.push(id);
|
||||
|
||||
if (opened_active_cards.length === 2) {
|
||||
const [pair_1_id, card_1_id] = opened_active_cards[0].split(':');
|
||||
if (!pair_1_id || !card_1_id) {
|
||||
throw "Mustn't happen";
|
||||
}
|
||||
const [pair_2_id, card_2_id] = opened_active_cards[1].split(':');
|
||||
if (!pair_2_id || !card_2_id) {
|
||||
throw "Mustn't happen";
|
||||
}
|
||||
if (pair_2_id === pair_1_id) {
|
||||
found_cards.push(opened_active_cards[0]);
|
||||
found_cards.push(opened_active_cards[1]);
|
||||
opened_active_cards = [];
|
||||
}
|
||||
}
|
||||
if (opened_active_cards.length === 3) {
|
||||
card_opened[opened_active_cards[0]] = false;
|
||||
card_opened[opened_active_cards[1]] = false;
|
||||
opened_active_cards.splice(0, 2);
|
||||
try_count += 1;
|
||||
}
|
||||
game_finished = random_card_order.length === found_cards.length;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<p class="text-center">{$t('quiztivity.play.memory.try_count', { try_count })}</p>
|
||||
<div class="grid lg:grid-cols-6 grid-cols-2 gap-2 m-4">
|
||||
{#each random_card_order as card}
|
||||
<button
|
||||
class="aspect-square flex border-[#B07156] border-2 rounded"
|
||||
on:click={() => {
|
||||
flip_card(card.id);
|
||||
}}
|
||||
>
|
||||
{#if card_opened[card.id]}
|
||||
<p class="m-auto transition-all">{card.text}</p>
|
||||
{:else}
|
||||
<img
|
||||
src="/android-chrome-512x512.png"
|
||||
alt="ClassQuiz logo"
|
||||
class="m-4 opacity-50 hover:opacity-80 transition-all"
|
||||
/>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
<!--
|
||||
- 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/.
|
||||
-->
|
||||
@@ -0,0 +1,178 @@
|
||||
<!--
|
||||
- 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 type { Data } from './types';
|
||||
import { getLocalization } from '$lib/i18n';
|
||||
import BrownButton from '$lib/components/buttons/brown.svelte';
|
||||
import AddNewSlide from './add_new_slide.svelte';
|
||||
import { QuizTivityTypes } from './types';
|
||||
import PdfEdit from './components/pdf/edit.svelte';
|
||||
import MemoryEdit from './components/memory/edit.svelte';
|
||||
import MarkdownEdit from './components/markdown/edit.svelte';
|
||||
import { flip } from 'svelte/animate';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import SharesPopover from '$lib/quiztivity/shares_popover.svelte';
|
||||
|
||||
const { t } = getLocalization();
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let data: Data;
|
||||
export let saving: boolean;
|
||||
|
||||
let selected_slide = null;
|
||||
let opened_slide = null;
|
||||
let selected_type = undefined;
|
||||
let shares_menu_open = false;
|
||||
|
||||
for (let i = 0; i < data.pages.length; i++) {
|
||||
const id = (Math.random() + 1).toString(36).substring(7);
|
||||
const type: QuizTivityTypes = data.pages[i].type as QuizTivityTypes;
|
||||
data.pages[i] = { ...data.pages[i], id, type };
|
||||
}
|
||||
const handle_slide_add = (type: QuizTivityTypes | undefined | null) => {
|
||||
if (!type) {
|
||||
return;
|
||||
}
|
||||
const id = (Math.random() + 1).toString(36).substring(7);
|
||||
data.pages.push({ title: undefined, data: undefined, type, id });
|
||||
opened_slide = data.pages.length - 1;
|
||||
};
|
||||
$: handle_slide_add(selected_type);
|
||||
|
||||
const delete_slide = () => {
|
||||
console.log(selected_slide);
|
||||
data.pages.splice(selected_slide, 1);
|
||||
data.pages = data.pages;
|
||||
};
|
||||
|
||||
const arraymove = (arr: any[], fromI: number, toI: number) => {
|
||||
const el = arr[fromI];
|
||||
arr.splice(fromI, 1);
|
||||
arr.splice(toI, 0, el);
|
||||
};
|
||||
const move_slide_left = () => {
|
||||
arraymove(data.pages, selected_slide, selected_slide - 1);
|
||||
selected_slide -= 1;
|
||||
data.pages = data.pages;
|
||||
};
|
||||
|
||||
const move_slide_right = () => {
|
||||
arraymove(data.pages, selected_slide, selected_slide + 1);
|
||||
selected_slide += 1;
|
||||
data.pages = data.pages;
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if opened_slide === null}
|
||||
<div>
|
||||
<div class="grid grid-cols-3">
|
||||
{#if data.id}
|
||||
<div class="mr-auto w-fit pl-2">
|
||||
<BrownButton
|
||||
on:click={() => {
|
||||
shares_menu_open = true;
|
||||
}}>{$t('quiztivity.editor.open_shares_menu')}</BrownButton
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<span />
|
||||
{/if}
|
||||
<input
|
||||
class="bg-transparent outline-none text-center mx-auto"
|
||||
placeholder={$t('quiztivity.editor.title_placeholder')}
|
||||
bind:value={data.title}
|
||||
/>
|
||||
<div class="self-end pr-2 w-full">
|
||||
<div class="ml-auto w-fit">
|
||||
<BrownButton
|
||||
on:click={() => {
|
||||
dispatch('save');
|
||||
}}
|
||||
disabled={!data.title}>{$t('words.save')}</BrownButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 w-full p-2">
|
||||
<BrownButton
|
||||
on:click={() => {
|
||||
selected_type = null;
|
||||
}}>{$t('quiztivity.editor.add_new')}</BrownButton
|
||||
>
|
||||
<BrownButton on:click={delete_slide} disabled={selected_slide === null}
|
||||
>{$t('quiztivity.editor.delete')}</BrownButton
|
||||
>
|
||||
<BrownButton
|
||||
on:click={move_slide_left}
|
||||
disabled={selected_slide === null || selected_slide === 0}
|
||||
>{$t('quiztivity.editor.move_left')}</BrownButton
|
||||
>
|
||||
<BrownButton
|
||||
on:click={move_slide_right}
|
||||
disabled={selected_slide === null || selected_slide === data.pages.length - 1}
|
||||
>{$t('quiztivity.editor.move_right')}</BrownButton
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-center mt-6">
|
||||
<div class="grid grid-cols-6 gap-6 w-11/12">
|
||||
{#each data.pages as page, i (page.id)}
|
||||
<div
|
||||
class="border-[#B07156] border-2 rounded aspect-square flex flex-col group"
|
||||
animate:flip={{ duration: 200 }}
|
||||
>
|
||||
<p class="m-auto">{page.type}</p>
|
||||
<div
|
||||
class="grid grid-cols-2 gap-2 p-2 group-hover:opacity-100 transition-all"
|
||||
class:opacity-0={selected_slide !== i}
|
||||
>
|
||||
<BrownButton
|
||||
on:click={() => {
|
||||
selected_slide = selected_slide === i ? null : i;
|
||||
}}
|
||||
>
|
||||
{#if selected_slide === i}{$t('words.selected')}{:else}{$t(
|
||||
'words.select'
|
||||
)}{/if}
|
||||
</BrownButton>
|
||||
<BrownButton
|
||||
on:click={() => {
|
||||
opened_slide = i;
|
||||
}}>{$t('words.edit')}</BrownButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{@const sel_t = data.pages[opened_slide].type}
|
||||
<div class="h-full">
|
||||
<div class="mb-2">
|
||||
<BrownButton
|
||||
on:click={() => {
|
||||
opened_slide = null;
|
||||
}}>{$t('words.back')}</BrownButton
|
||||
>
|
||||
</div>
|
||||
{#if sel_t === QuizTivityTypes.PDF}
|
||||
<PdfEdit />
|
||||
{:else if sel_t === QuizTivityTypes.MEMORY}
|
||||
<MemoryEdit bind:data={data.pages[opened_slide].data} />
|
||||
{:else if sel_t === QuizTivityTypes.MARKDOWN}
|
||||
<MarkdownEdit bind:data={data.pages[opened_slide].data} />
|
||||
{:else}
|
||||
<h1 class="text-8xl">ERROR!</h1>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if selected_type === null}
|
||||
<AddNewSlide bind:type={selected_type} />
|
||||
{/if}
|
||||
{#if shares_menu_open}
|
||||
<SharesPopover id={data.id} bind:open={shares_menu_open} />
|
||||
{/if}
|
||||
@@ -0,0 +1,239 @@
|
||||
<!--
|
||||
- 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 BrownButton from '$lib/components/buttons/brown.svelte';
|
||||
import { getLocalization } from '$lib/i18n';
|
||||
import Spinner from '$lib/Spinner.svelte';
|
||||
import { DateTime } from 'luxon';
|
||||
import { browser } from '$app/environment';
|
||||
import SmallPopover from '$lib/components/popover/smalltop.svelte';
|
||||
import { PopoverTypes } from '$lib/components/popover/smalltop';
|
||||
import { onMount } from 'svelte';
|
||||
import { fade, fly } from 'svelte/transition';
|
||||
|
||||
const { t } = getLocalization();
|
||||
export let open = false;
|
||||
export let id;
|
||||
let popover_open = false;
|
||||
const load_shares = async (): Promise<{
|
||||
id: string;
|
||||
name?: string;
|
||||
expire_in?: number;
|
||||
quiztivity: { id: string };
|
||||
user: { id: string };
|
||||
}> => {
|
||||
const res = await fetch(`/api/v1/quiztivity/${id}/shares`);
|
||||
return await res.json();
|
||||
};
|
||||
const copyToClipboard = (str) => {
|
||||
try {
|
||||
navigator.clipboard.writeText(str);
|
||||
} catch {
|
||||
console.log('Async Clipboard not supported');
|
||||
const el = document.createElement('textarea');
|
||||
el.value = str;
|
||||
el.setAttribute('readonly', '');
|
||||
el.style.position = 'absolute';
|
||||
el.style.left = '-9999px';
|
||||
document.body.appendChild(el);
|
||||
const selected =
|
||||
document.getSelection().rangeCount > 0
|
||||
? document.getSelection().getRangeAt(0)
|
||||
: false;
|
||||
el.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(el);
|
||||
if (selected) {
|
||||
document.getSelection().removeAllRanges();
|
||||
document.getSelection().addRange(<Range>selected);
|
||||
}
|
||||
} finally {
|
||||
popover_open = true;
|
||||
setTimeout(() => {
|
||||
popover_open = false;
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const on_parent_click = (e: Event) => {
|
||||
if (e.target !== e.currentTarget) {
|
||||
return;
|
||||
}
|
||||
open = false;
|
||||
};
|
||||
const close_start_game_if_esc_is_pressed = (key: KeyboardEvent) => {
|
||||
if (key.code === 'Escape') {
|
||||
open = false;
|
||||
}
|
||||
};
|
||||
onMount(() => {
|
||||
document.body.addEventListener('keydown', close_start_game_if_esc_is_pressed);
|
||||
});
|
||||
let never_expires_checked = true;
|
||||
let selected_date = undefined;
|
||||
const create_share = async () => {
|
||||
if (!selected_date && !never_expires_checked) {
|
||||
return;
|
||||
}
|
||||
console.log(selected_date);
|
||||
await fetch('/api/v1/quiztivity/shares/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: undefined,
|
||||
quiztivity: id,
|
||||
expire_in: never_expires_checked
|
||||
? undefined
|
||||
: Math.floor(Math.abs(new Date() - new Date(selected_date)) / 1000 / 60)
|
||||
})
|
||||
});
|
||||
loaded_shares = load_shares();
|
||||
};
|
||||
let loaded_shares = load_shares();
|
||||
|
||||
const delete_share = async (id: string) => {
|
||||
if (!confirm('Do you really want to delete this Share?')) {
|
||||
return;
|
||||
}
|
||||
await fetch(`/api/v1/quiztivity/shares/${id}`, { method: 'DELETE' });
|
||||
loaded_shares = load_shares();
|
||||
};
|
||||
let add_shares_open = false;
|
||||
</script>
|
||||
|
||||
<SmallPopover bind:open={popover_open} type={PopoverTypes.Copy} />
|
||||
<div
|
||||
class="fixed w-full h-full top-0 flex bg-black bg-opacity-50 z-50"
|
||||
on:click={on_parent_click}
|
||||
transition:fade|local={{ duration: 100 }}
|
||||
>
|
||||
<div
|
||||
class="m-auto bg-white dark:bg-gray-600 rounded shadow-2xl flex p-4 flex-col w-2/3 h-5/6 gap-2 overflow-scroll"
|
||||
>
|
||||
<div class="flex justify-center flex-col">
|
||||
<BrownButton
|
||||
on:click={() => {
|
||||
add_shares_open = !add_shares_open;
|
||||
}}>{$t('quiztivity.editor.shares.add_new_share')}</BrownButton
|
||||
>
|
||||
{#if add_shares_open}
|
||||
<form
|
||||
class="flex justify-center p-2 border-b-2 border-l-2 border-r-2 border-[#B07156] flex-col gap-2"
|
||||
transition:fly|local={{ duration: 100, y: -10 }}
|
||||
on:submit|preventDefault={create_share}
|
||||
>
|
||||
<div class="grid grid-cols-2">
|
||||
<input
|
||||
type="datetime-local"
|
||||
class="dark:text-black transition-all mx-auto"
|
||||
disabled={never_expires_checked}
|
||||
bind:value={selected_date}
|
||||
/>
|
||||
<div class="mx-auto">
|
||||
<label for="cb">{$t('quiztivity.editor.shares.never_expires')}</label>
|
||||
<input type="checkbox" id="cb" bind:checked={never_expires_checked} />
|
||||
</div>
|
||||
</div>
|
||||
<BrownButton type="submit" disabled={!selected_date && !never_expires_checked}
|
||||
>{$t('words.submit')}</BrownButton
|
||||
>
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
{#await loaded_shares}
|
||||
<Spinner />
|
||||
{:then shares}
|
||||
{#each shares as share}
|
||||
<div class="grid grid-cols-4 w-full gap-2" in:fade={{ duration: 50 }}>
|
||||
<!-- <p>{share.name ?? "..."}</p>-->
|
||||
<div class="w-full mx-auto">
|
||||
<BrownButton
|
||||
flex={true}
|
||||
disabled={browser
|
||||
? !navigator.canShare({
|
||||
title: 'title',
|
||||
url: `${window.location.origin}/quiztivity`
|
||||
})
|
||||
: false}
|
||||
on:click={() => {
|
||||
navigator.share({
|
||||
title: 'Quiztivity on ClassQuiz',
|
||||
text: 'Play this Quiztivity now on ClassQuiz!',
|
||||
url: `${window.location.origin}/quiztivity/share/${share.id}?ref=share`
|
||||
});
|
||||
}}
|
||||
>
|
||||
<!-- heroicons/share -->
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</BrownButton>
|
||||
</div>
|
||||
<div class="w-full mx-auto">
|
||||
<BrownButton
|
||||
flex={true}
|
||||
on:click={() => {
|
||||
copyToClipboard(
|
||||
`${window.location.origin}/quiztivity/share/${share.id}?ref=copy`
|
||||
);
|
||||
}}
|
||||
>
|
||||
<!-- heroicons/ClipboardCopy -->
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
class="w-5 h-5"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</BrownButton>
|
||||
</div>
|
||||
<p class="m-auto">
|
||||
{#if share.expire_in}
|
||||
{$t('quiztivity.editor.shares.expires_on', {
|
||||
date: DateTime.now()
|
||||
.plus({ minutes: share.expire_in })
|
||||
.toJSDate()
|
||||
.toLocaleString()
|
||||
})}
|
||||
{:else}
|
||||
{$t('quiztivity.editor.shares.never_expires')}
|
||||
{/if}
|
||||
</p>
|
||||
<div class="w-fit my-auto ml-auto">
|
||||
<BrownButton
|
||||
on:click={() => {
|
||||
delete_share(share.id);
|
||||
}}>{$t('words.delete')}</BrownButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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 QuizTivityTypes {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
SLIDE = 'SLIDE',
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
PDF = 'PDF',
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
MEMORY = 'MEMORY',
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
MARKDOWN = 'MARKDOWN'
|
||||
}
|
||||
|
||||
export interface Pdf {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface MemoryCard {
|
||||
image?: string;
|
||||
text?: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface Memory {
|
||||
cards: MemoryCard[][];
|
||||
}
|
||||
|
||||
export interface Markdown {
|
||||
markdown: string;
|
||||
}
|
||||
export interface QuizTivityPage {
|
||||
title?: string;
|
||||
type: QuizTivityTypes;
|
||||
data: Pdf | Memory | Markdown;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface Data {
|
||||
id?: string;
|
||||
title: string;
|
||||
pages: QuizTivityPage[];
|
||||
}
|
||||
@@ -29,7 +29,7 @@
|
||||
{$t('error_page.404_text')}
|
||||
</p>
|
||||
{:else}
|
||||
<p>
|
||||
<p class="text-center">
|
||||
{$t('error_page.unknown_error_text')}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
@@ -8,9 +8,13 @@
|
||||
import { getLocalization } from '$lib/i18n';
|
||||
import Footer from '$lib/footer.svelte';
|
||||
import { navbarVisible, signedIn } from '$lib/stores';
|
||||
import Spinner from '$lib/Spinner.svelte';
|
||||
// import Spinner from "$lib/Spinner.svelte";
|
||||
import Fuse from 'fuse.js';
|
||||
import BrownButton from '$lib/components/buttons/brown.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { fly } from 'svelte/transition';
|
||||
|
||||
// import GrayButton from "$lib/components/buttons/gray.svelte";
|
||||
|
||||
interface QuizData {
|
||||
id: string;
|
||||
@@ -23,59 +27,73 @@
|
||||
questions: Question[];
|
||||
}
|
||||
|
||||
export let data: PageData;
|
||||
let search_term = '';
|
||||
let start_game = null;
|
||||
signedIn.set(true);
|
||||
navbarVisible.set(true);
|
||||
const { t } = getLocalization();
|
||||
|
||||
let quizzes_to_show = [];
|
||||
let quizzes: Array<any>;
|
||||
let items_to_show = [];
|
||||
let all_items: Array<any>;
|
||||
let fuse;
|
||||
/* const minisearch = new MiniSearch({
|
||||
fields: ['title', 'description'],
|
||||
idField: 'id',
|
||||
storeFields: ['id']
|
||||
})*/
|
||||
|
||||
let id_to_position_map = {};
|
||||
|
||||
const getData = async (): Promise<Array<QuizData>> => {
|
||||
const res = await fetch('/api/v1/quiz/list');
|
||||
quizzes_to_show = await res.json();
|
||||
fuse = new Fuse(quizzes_to_show, {
|
||||
keys: ['title', 'description', 'questions.question'],
|
||||
items_to_show = [];
|
||||
for (let i = 0; i < data.quizzes.length; i++) {
|
||||
items_to_show.push({ ...data.quizzes[i], type: 'quiz' });
|
||||
}
|
||||
for (let i = 0; i < data.quiztivities.length; i++) {
|
||||
items_to_show.push({ ...data.quiztivities[i], type: 'quiztivity' });
|
||||
}
|
||||
fuse = new Fuse(items_to_show, {
|
||||
keys: ['title', 'description', 'questions.title'],
|
||||
findAllMatches: true
|
||||
});
|
||||
quizzes = quizzes_to_show;
|
||||
for (let i = 0; i < quizzes.length; i++) {
|
||||
id_to_position_map[quizzes[i].id] = i;
|
||||
all_items = items_to_show;
|
||||
for (let i = 0; i < all_items.length; i++) {
|
||||
id_to_position_map[all_items[i].id] = i;
|
||||
}
|
||||
return quizzes_to_show;
|
||||
return all_items;
|
||||
};
|
||||
|
||||
let suggestions = [];
|
||||
|
||||
const search = () => {
|
||||
if (search_term === '') {
|
||||
quizzes_to_show = [];
|
||||
quizzes_to_show = quizzes;
|
||||
quizzes_to_show = quizzes_to_show;
|
||||
items_to_show = all_items;
|
||||
} else {
|
||||
const res = fuse.search(search_term);
|
||||
console.log(res, 'search_res');
|
||||
quizzes_to_show = [];
|
||||
items_to_show = [];
|
||||
for (const quiz_data of res) {
|
||||
quizzes_to_show.push(quiz_data.item);
|
||||
items_to_show.push(quiz_data.item);
|
||||
}
|
||||
|
||||
quizzes_to_show = quizzes_to_show;
|
||||
items_to_show = items_to_show;
|
||||
}
|
||||
};
|
||||
$: {
|
||||
search_term;
|
||||
// console.log(search_term);
|
||||
search();
|
||||
}
|
||||
|
||||
const deleteQuiz = async (to_delete: string, type: 'quiz' | 'quiztivity') => {
|
||||
if (!confirm('Do you really want to delete this quiz?')) {
|
||||
return;
|
||||
}
|
||||
if (type === 'quiz') {
|
||||
await fetch(`/api/v1/quiz/delete/${to_delete}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
} else {
|
||||
await fetch(`/api/v1/quiztivity/${to_delete}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
}
|
||||
window.location.reload();
|
||||
};
|
||||
let create_button_clicked = false;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -95,13 +113,25 @@
|
||||
/>
|
||||
</svg>
|
||||
{:then quizzes}
|
||||
<div class="flex flex-col w-fit mx-auto">
|
||||
<div class="flex flex-col w-full mx-auto">
|
||||
<!-- <button
|
||||
class='px-4 py-2 font-medium tracking-wide text-gray-500 whitespace-nowrap dark:text-gray-400 capitalize transition-colors dark:bg-gray-700 duration-200 transform bg-[#B07156] rounded-md hover:bg-green-600 focus:outline-none focus:ring focus:ring-blue-300 focus:ring-opacity-80'>
|
||||
Primary
|
||||
</button>-->
|
||||
<div class="w-full grid grid-cols-4 gap-2">
|
||||
<BrownButton href="/create">{$t('words.create')}</BrownButton>
|
||||
class='px-4 py-2 font-medium tracking-wide text-gray-500 whitespace-nowrap dark:text-gray-400 capitalize transition-colors dark:bg-gray-700 duration-200 transform bg-[#B07156] rounded-md hover:bg-green-600 focus:outline-none focus:ring focus:ring-blue-300 focus:ring-opacity-80'>
|
||||
Primary
|
||||
</button>-->
|
||||
<div class="w-full grid lg:grid-cols-4 gap-2 grid-cols-2 px-4">
|
||||
{#if create_button_clicked}
|
||||
<div class="flex gap-2" transition:fly={{ y: 10 }}>
|
||||
<BrownButton href="/create">{$t('words.quiz')}</BrownButton>
|
||||
<BrownButton href="/quiztivity/create">{$t('words.quiztivity')}</BrownButton
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<BrownButton
|
||||
on:click={() => {
|
||||
create_button_clicked = true;
|
||||
}}>{$t('words.create')}</BrownButton
|
||||
>
|
||||
{/if}
|
||||
<BrownButton href="/import">{$t('words.import')}</BrownButton>
|
||||
<BrownButton href="/results">{$t('words.results')}</BrownButton>
|
||||
<BrownButton href="/account/settings">
|
||||
@@ -109,26 +139,91 @@
|
||||
</BrownButton>
|
||||
</div>
|
||||
{#if quizzes.length !== 0}
|
||||
{#await import('$lib/dashboard/main_slider.svelte')}
|
||||
<Spinner />
|
||||
{:then c}
|
||||
<div class="flex justify-center pt-4 w-full">
|
||||
<div class="flex justify-center pt-4 w-full">
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<input
|
||||
bind:value={search_term}
|
||||
class="p-2 rounded-lg outline-none text-center w-96 dark:bg-gray-700"
|
||||
placeholder={$t('dashboard.search_for_own_quizzes')}
|
||||
/>
|
||||
<button
|
||||
on:click={() => {
|
||||
search_term = '';
|
||||
quizzes_to_show = quizzes;
|
||||
suggestions = [];
|
||||
}}
|
||||
<input
|
||||
bind:value={search_term}
|
||||
class="p-2 rounded-lg outline-none text-center w-96 dark:bg-gray-700"
|
||||
placeholder={$t('dashboard.search_for_own_quizzes')}
|
||||
/>
|
||||
<button
|
||||
on:click={() => {
|
||||
search_term = '';
|
||||
items_to_show = all_items;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
class="h-8 inline-block"
|
||||
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="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 mt-4 px-2">
|
||||
{#each items_to_show as quiz}
|
||||
<div
|
||||
class="grid grid-cols-2 lg:grid-cols-3 w-full rounded border-[#B07156] border-2 p-2 h-[20vh] overflow-hidden max-h-[20vh]"
|
||||
>
|
||||
<div class="hidden lg:flex w-auto h-full items-center relative">
|
||||
{#if quiz.cover_image}
|
||||
<img
|
||||
src={quiz.cover_image}
|
||||
alt="user provided"
|
||||
loading="lazy"
|
||||
class="shrink-0 max-w-full max-h-full absolute"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="my-auto mx-auto max-h-full overflow-hidden">
|
||||
<p class="text-xl text-center">{@html quiz.title}</p>
|
||||
<p class="text-sm text-center text-clip overflow-hidden">
|
||||
{@html quiz.description ?? ''}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="grid grid-cols-2 grid-rows-2 ml-auto gap-2 w-fit self-end my-auto"
|
||||
>
|
||||
<BrownButton
|
||||
href={quiz.type === 'quiz'
|
||||
? `/edit?quiz_id=${quiz.id}`
|
||||
: `/quiztivity/edit?id=${quiz.id}`}
|
||||
>{$t('words.edit')}</BrownButton
|
||||
>
|
||||
{#if quiz.type === 'quiz'}
|
||||
<BrownButton
|
||||
on:click={() => {
|
||||
start_game = quiz.id;
|
||||
}}
|
||||
>
|
||||
{$t('words.start')}
|
||||
</BrownButton>
|
||||
{:else}
|
||||
<BrownButton href="/quiztivity/play?id={quiz.id}">
|
||||
{$t('words.play')}
|
||||
</BrownButton>
|
||||
{/if}
|
||||
|
||||
<BrownButton
|
||||
on:click={() => {
|
||||
deleteQuiz(quiz.id, quiz.type);
|
||||
}}
|
||||
flex={true}
|
||||
>
|
||||
<!-- heroicons/trash -->
|
||||
<svg
|
||||
class="h-8 inline-block"
|
||||
class="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -138,15 +233,34 @@
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
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"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</BrownButton>
|
||||
<BrownButton
|
||||
href="/api/v1/eximport/{quiz.id}"
|
||||
flex={true}
|
||||
disabled={quiz.type !== 'quiz'}
|
||||
><!-- heroicons/download -->
|
||||
<svg
|
||||
class="w-5 h-5"
|
||||
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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||
/>
|
||||
</svg>
|
||||
</BrownButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<svelte:component this={c.default} bind:quizzes={quizzes_to_show} />
|
||||
{/await}
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p>
|
||||
{$t('overview_page.no_quizzes')}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async ({ fetch }) => {
|
||||
const quiz_res = await fetch('/api/v1/quiz/list?page_size=100');
|
||||
const quizzes = await quiz_res.json();
|
||||
const quiztivity_res = await fetch('/api/v1/quiztivity/');
|
||||
const quiztivities = await quiztivity_res.json();
|
||||
return {
|
||||
quizzes,
|
||||
quiztivities
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
@@ -7,7 +7,7 @@
|
||||
import Editor from '$lib/editor.svelte';
|
||||
import { getLocalization } from '$lib/i18n';
|
||||
import { navbarVisible } from '$lib/stores';
|
||||
import { QuizQuestionType } from '../../lib/quiz_types';
|
||||
import { QuizQuestionType } from '$lib/quiz_types';
|
||||
|
||||
navbarVisible.set(false);
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<!--
|
||||
- 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 type { Data } from '$lib/quiztivity/types';
|
||||
import Editor from '$lib/quiztivity/editor.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let data: Data = { pages: [], id: undefined, title: '' };
|
||||
let saving = false;
|
||||
|
||||
const save_quiztivity = async () => {
|
||||
saving = true;
|
||||
console.log(data);
|
||||
const stringification = JSON.stringify(data);
|
||||
console.log(stringification);
|
||||
|
||||
const res = await fetch('/api/v1/quiztivity/create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: stringification
|
||||
});
|
||||
if (res.ok) {
|
||||
await goto('/dashboard');
|
||||
} else {
|
||||
alert("Couldn't save");
|
||||
}
|
||||
saving = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="h-full">
|
||||
<Editor bind:data on:save={save_quiztivity} bind:saving />
|
||||
</div>
|
||||
@@ -0,0 +1,45 @@
|
||||
<!--
|
||||
- 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 type { PageData } from './$types';
|
||||
import Editor from '$lib/quiztivity/editor.svelte';
|
||||
import SharesPopover from '$lib/quiztivity/shares_popover.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let saving = false;
|
||||
|
||||
let quiztivity = data.quiztivity;
|
||||
let shares_menu_open = false;
|
||||
|
||||
const save_quiztivity = async () => {
|
||||
saving = true;
|
||||
const stringification = JSON.stringify(quiztivity);
|
||||
|
||||
const res = await fetch(`/api/v1/quiztivity/${quiztivity.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: stringification
|
||||
});
|
||||
if (res.ok) {
|
||||
await goto('/dashboard');
|
||||
} else {
|
||||
alert("Couldn't save");
|
||||
}
|
||||
saving = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="h-full">
|
||||
<Editor bind:data={quiztivity} on:save={save_quiztivity} bind:saving />
|
||||
</div>
|
||||
|
||||
{#if shares_menu_open}
|
||||
<SharesPopover id={quiztivity.id} bind:open={shares_menu_open} />
|
||||
{/if}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
import type { PageLoad } from './$types';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { Data } from '$lib/quiztivity/types';
|
||||
|
||||
export const load = (async ({ url, fetch }) => {
|
||||
const id = url.searchParams.get('id');
|
||||
if (!id) {
|
||||
throw error(400, 'id missing');
|
||||
}
|
||||
const resp = await fetch(`/api/v1/quiztivity/${id}`);
|
||||
if (!resp.ok) {
|
||||
throw error(404, 'quiztivity not found');
|
||||
}
|
||||
const data: Data = await resp.json();
|
||||
return {
|
||||
quiztivity: data
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
@@ -0,0 +1,53 @@
|
||||
<!--
|
||||
- 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 { navbarVisible } from '$lib/stores';
|
||||
import { page } from '$app/stores';
|
||||
import { getLocalization } from '$lib/i18n';
|
||||
|
||||
navbarVisible.set(true);
|
||||
let status = $page.status;
|
||||
const { t } = getLocalization();
|
||||
</script>
|
||||
|
||||
<!--
|
||||
- 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/.
|
||||
-->
|
||||
|
||||
<!--
|
||||
- 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/.
|
||||
-->
|
||||
|
||||
<svelte:head>
|
||||
<title>{$t('words.error')} - {status}</title>
|
||||
</svelte:head>
|
||||
<h1 class="text-6xl text-center">{status}</h1>
|
||||
|
||||
{#if status === 404}
|
||||
<p class="text-center">
|
||||
{$t('error_page.404_text')}
|
||||
</p>
|
||||
{:else if status === 410}
|
||||
<p class="text-center">
|
||||
{$t('quiztivity.share_expired')}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-center">
|
||||
{$t('error_page.unknown_error_text')}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-center mt-8">
|
||||
<img
|
||||
class="rounded-lg"
|
||||
src="https://http.cat/{status}"
|
||||
alt="Cat representing the {status}-http error code"
|
||||
/>
|
||||
</div>
|
||||
@@ -0,0 +1,38 @@
|
||||
<!--
|
||||
- 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 type { PageData } from './$types';
|
||||
import type { QuizTivityPage } from '$lib/quiztivity/types';
|
||||
import { QuizTivityTypes } from '$lib/quiztivity/types';
|
||||
import NavigationBar from './navigation_bar.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
let quiztivity = data.quiztivity;
|
||||
for (let i = 0; i < quiztivity.pages.length; i++) {
|
||||
const id = (Math.random() + 1).toString(36).substring(7);
|
||||
const type: QuizTivityTypes = quiztivity.pages[i].type as QuizTivityTypes;
|
||||
quiztivity.pages[i] = { ...quiztivity.pages[i], id, type };
|
||||
}
|
||||
|
||||
let current_slide_index = 0;
|
||||
let current_slide: QuizTivityPage = quiztivity.pages[current_slide_index];
|
||||
$: current_slide = quiztivity.pages[current_slide_index];
|
||||
</script>
|
||||
|
||||
<div class="w-full h-full flex flex-col overflow-scroll">
|
||||
<div class="w-full h-full">
|
||||
{#if current_slide.type === QuizTivityTypes.MARKDOWN}
|
||||
{#await import('$lib/quiztivity/components/markdown/play.svelte') then c}
|
||||
<svelte:component this={c.default} data={current_slide.data} />
|
||||
{/await}
|
||||
{:else if current_slide.type === QuizTivityTypes.MEMORY}
|
||||
{#await import('$lib/quiztivity/components/memory/play.svelte') then c}
|
||||
<svelte:component this={c.default} data={current_slide.data} />
|
||||
{/await}
|
||||
{/if}
|
||||
</div>
|
||||
<NavigationBar bind:current_slide_index question_count={quiztivity.pages.length} />
|
||||
</div>
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
import type { PageLoad } from './$types';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { Data } from '$lib/quiztivity/types';
|
||||
|
||||
export const load = (async ({ url, fetch }) => {
|
||||
const id = url.searchParams.get('id');
|
||||
const share = url.searchParams.get('share') === 'true';
|
||||
if (!id) {
|
||||
throw error(400, 'id missing');
|
||||
}
|
||||
let resp: Response;
|
||||
if (share) {
|
||||
resp = await fetch(`/api/v1/quiztivity/shares/${id}`);
|
||||
} else {
|
||||
resp = await fetch(`/api/v1/quiztivity/${id}`);
|
||||
}
|
||||
if (!resp.ok) {
|
||||
throw error(resp.status);
|
||||
}
|
||||
const data: Data = await resp.json();
|
||||
return {
|
||||
quiztivity: data
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
@@ -0,0 +1,36 @@
|
||||
<!--
|
||||
- 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 BrownButton from '$lib/components/buttons/brown.svelte';
|
||||
import { getLocalization } from '$lib/i18n';
|
||||
|
||||
export let current_slide_index: number;
|
||||
export let question_count: number;
|
||||
|
||||
const { t } = getLocalization();
|
||||
</script>
|
||||
|
||||
<div class="mt-auto h-[7vh] bg-opacity-50 bg-black shadow-2xl flex justify-between">
|
||||
<div class="my-auto justify-start ml-4">
|
||||
<BrownButton
|
||||
disabled={current_slide_index < 1}
|
||||
on:click={() => {
|
||||
current_slide_index -= 1;
|
||||
}}>{$t('words.back')}</BrownButton
|
||||
>
|
||||
</div>
|
||||
<div class="my-auto">
|
||||
{current_slide_index + 1}/{question_count}
|
||||
</div>
|
||||
<div class="my-auto justify-end mr-4">
|
||||
<BrownButton
|
||||
disabled={current_slide_index === 1}
|
||||
on:click={() => {
|
||||
current_slide_index += 1;
|
||||
}}>{$t('words.next')}</BrownButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load = (({ params }) => {
|
||||
const quiz_id = params.share_id;
|
||||
throw redirect(301, `/quiztivity/play?id=${quiz_id}&share=true`);
|
||||
}) satisfies PageServerLoad;
|
||||
Reference in New Issue
Block a user