🔀 Merged ClassQuizController

This commit is contained in:
Mawoka
2023-06-30 01:36:48 +02:00
183 changed files with 10232 additions and 3260 deletions
+56 -31
View File
@@ -13,6 +13,8 @@
import { kahoot_icons } from './play/kahoot_mode_assets/kahoot_icons';
import CircularTimer from '$lib/play/circular_progress.svelte';
import Spinner from '$lib/Spinner.svelte';
import { get_foreground_color } from '$lib/helpers';
import MediaComponent from '$lib/editor/MediaComponent.svelte';
export let game_token: string;
export let quiz_data: QuizData;
@@ -20,6 +22,7 @@
export let bg_color;
const { t } = getLocalization();
const default_colors = ['#D6EDC9', '#B07156', '#7F7057', '#4E6E58'];
let question_results = null;
export let final_results: Array<null> | Array<Array<PlayerAnswer>> = [null];
@@ -28,6 +31,7 @@
let shown_question_now: number;
let final_results_clicked = false;
let timer_interval;
let answer_count = 0;
export let control_visible: boolean;
export let player_scores;
@@ -50,6 +54,7 @@
shown_question_now = data.question_index;
timer_res = quiz_data.questions[data.question_index].time;
selected_question = selected_question + 1;
answer_count = 0;
timer(timer_res);
});
const get_question_results = () => {
@@ -92,6 +97,10 @@
}
});
socket.on('player_answer', (_) => {
answer_count += 1;
});
const timer = (time: string) => {
let seconds = Number(time);
timer_interval = setInterval(() => {
@@ -132,7 +141,7 @@
{#if selected_question + 1 === quiz_data.questions.length && ((timer_res === '0' && question_results !== null) || quiz_data?.questions?.[selected_question]?.type === QuizQuestionType.SLIDE)}
{#if JSON.stringify(final_results) === JSON.stringify([null])}
<button on:click={get_final_results} class="admin-button"
>Get final results
>{$t('admin_page.get_final_results')}
</button>
{/if}
{:else if timer_res === '0' || selected_question === -1}
@@ -142,7 +151,7 @@
set_question_number(selected_question + 1);
}}
class="admin-button"
>Next Question ({selected_question + 2})
>{$t('admin_page.next_question', { question: selected_question + 2 })}
</button>
{/if}
{#if question_results === null && selected_question !== -1}
@@ -152,11 +161,11 @@
set_question_number(selected_question + 1);
}}
class="admin-button"
>Next Question ({selected_question + 2})
>{$t('admin_page.next_question', { question: selected_question + 2 })}
</button>
{:else}
<button on:click={get_question_results} class="admin-button"
>Show results
>{$t('admin_page.show_results')}
</button>
{/if}
{/if}
@@ -167,22 +176,22 @@
set_question_number(selected_question + 1);
}}
class="admin-button"
>Next Question ({selected_question + 2})
>{$t('admin_page.next_question', { question: selected_question + 2 })}
</button>
{:else}
<button on:click={show_solutions} class="admin-button"
>Stop time and show solutions
>{$t('admin_page.stop_time_and_solutions')}
</button>
{/if}
{:else}
<!-- <button
on:click={() => {
set_question_number(selected_question + 1);
}}
class='admin-button'
>Next Question ({selected_question + 2}
)
</button>-->
on:click={() => {
set_question_number(selected_question + 1);
}}
class='admin-button'
>Next Question ({selected_question + 2}
)
</button>-->
{/if}
</div>
</div>
@@ -212,37 +221,53 @@
{@html quiz_data.questions[selected_question].question}
</h1>
<!-- <span class='text-center py-2 text-lg'>{$t('admin_page.time_left')}: {timer_res}</span>-->
<div class="mx-auto my-2">
<CircularTimer
bind:text={timer_res}
bind:progress={circular_progress}
color="#ef4444"
/>
<div class="grid grid-cols-3 my-2">
<span />
<div class="m-auto">
<CircularTimer
bind:text={timer_res}
bind:progress={circular_progress}
color="#ef4444"
/>
</div>
<p class="m-auto text-3xl">
{$t('admin_page.answers_submitted', { answer_count: answer_count })}
</p>
</div>
</div>
{#if quiz_data.questions[selected_question].image !== null}
<div>
<img
<div class="flex w-full">
<MediaComponent
src={quiz_data.questions[selected_question].image}
class="max-h-[20vh] object-cover mx-auto mb-8 w-auto"
alt="Content for Question"
muted={false}
css_classes="max-h-[20vh] object-cover mx-auto mb-8 w-auto"
/>
</div>
{/if}
{#if quiz_data.questions[selected_question].type === QuizQuestionType.ABCD || quiz_data.questions[selected_question].type === QuizQuestionType.VOTING}
{#if quiz_data.questions[selected_question].type === QuizQuestionType.ABCD || quiz_data.questions[selected_question].type === QuizQuestionType.VOTING || quiz_data.questions[selected_question].type === QuizQuestionType.CHECK}
<div class="grid grid-rows-2 grid-flow-col auto-cols-auto gap-2 w-full p-4">
{#each quiz_data.questions[selected_question].answers as answer, i}
<div
class="rounded-lg h-fit flex"
style="background-color: {answer.color ?? '#B45309'}"
class="rounded-lg h-fit flex border-2 border-black"
style="background-color: {answer.color ?? default_colors[i]};"
class:opacity-50={!answer.right &&
timer_res === '0' &&
quiz_data.questions[selected_question].type ===
QuizQuestionType.ABCD}
>
<img class="w-14 inline-block pl-4" alt="icon" src={kahoot_icons[i]} />
<span class="text-center text-2xl px-2 py-4 w-full text-black"
>{answer.answer}</span
<img
class="w-14 inline-block pl-4"
alt="icon"
style="color: {get_foreground_color(
answer.color ?? default_colors[i]
)}"
src={kahoot_icons[i]}
/>
<span
class="text-center text-2xl px-2 py-4 w-full"
style="color: {get_foreground_color(
answer.color ?? default_colors[i]
)}">{answer.answer}</span
>
<span class="pl-4 w-10" />
</div>
@@ -262,7 +287,7 @@
</div>
{:else}
<div class="flex justify-center">
<p class="text-2xl">Enter your answer into the input field!</p>
<p class="text-2xl">{$t('admin_page.enter_answer_into_field')}</p>
</div>
{/if}
{/if}
@@ -308,7 +333,7 @@
<div class="h-[30vh] m-auto w-auto mt-12">
<img
class="max-h-full max-w-full block"
src={quiz_data.cover_image}
src="/api/v1/storage/download/{quiz_data.cover_image}"
alt="Not provided"
/>
</div>
@@ -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,279 @@
<!--
- 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 { onMount } from 'svelte';
import { tinykeys } from '$lib/tinykeys';
import { fade } from 'svelte/transition';
import MiniSearch from 'minisearch';
let open = false;
let input = '';
let bg_text = '';
let title_ms: MiniSearch;
let command_ms: MiniSearch;
let selected: null | number = null;
// eslint-disable-next-line no-unused-vars
type ActionFunction = (args: string[]) => void;
const actions: {
id: number;
title: string;
description?: string;
command?: string;
args?: string[];
action: ActionFunction;
}[] = [
{
id: 0,
title: 'Close CommandPalette',
description: 'Closes CommandPalette',
command: 'close',
action: () => close_cp(undefined)
},
{
id: 1,
title: 'Create Quiz',
description: 'Opens editor to create a new quiz',
command: 'newquiz',
args: ['title'],
action: (args) => window.location.assign(`/create?title=${args.join(' ')}`)
},
{
id: 2,
title: 'Import a Quiz',
description: 'Opens the import page',
command: 'import',
args: ['url'],
action: (args) => window.location.assign(`/import?url=${args?.[0] ?? ''}`)
},
{
id: 3,
title: 'Create Quiztivity',
description: 'Opens the editor for quiztivities',
command: 'newquiztivity',
args: ['title'],
action: (args) => window.location.assign(`/quiztivity/create?title=${args.join(' ')}`)
},
{
id: 4,
title: 'View Results',
description: 'Opens the Results viewer',
command: 'results',
action: () => window.location.assign('/results')
},
{
id: 5,
title: 'Explore Quizzes',
description: 'Opens the Explore-page',
command: 'explore',
action: () => window.location.assign('/explore')
},
{
id: 6,
title: 'Dashboard',
description: 'Go to Dashboard',
command: 'dash',
action: () => window.location.assign('/dashboard')
},
{
id: 7,
title: 'Docs',
description: 'Go to documentation',
command: 'docs',
action: () => window.location.assign('/docs')
},
{
id: 8,
title: 'Settings',
description: 'Opens the Settings page',
command: 'settings',
action: () => window.location.assign('/account/settings')
}
];
let visible_items = actions;
const toggle_open = (e: KeyboardEvent | undefined) => {
e.preventDefault();
open = !open;
console.log('TOGGLE!');
};
const close_cp = (e: KeyboardEvent | undefined) => {
if (e) {
e.preventDefault();
}
open = false;
};
const close_on_outside = (e: Event) => {
if (e.target == e.currentTarget) {
open = false;
}
};
const execute_action = () => {
let args = [];
const entry = visible_items[selected];
if (input.startsWith('/')) {
const tokens = input.split(' ');
args = tokens.slice(1);
}
console.log(args);
entry.action(args);
};
const search = (term: string) => {
if (!command_ms || !title_ms) {
return;
}
if (term === '' || term === '/') {
selected = 0;
visible_items = actions;
bg_text = '';
return;
}
let suggestions;
let res;
if (term.startsWith('/')) {
term = term.substring(1);
suggestions = command_ms.autoSuggest(term, { boost: { command: 2 }, prefix: true });
res = command_ms.search(term, { boost: { command: 2 }, prefix: true });
bg_text = suggestions[0]?.suggestion;
bg_text ??= '';
bg_text = `/${bg_text}`;
} else {
suggestions = title_ms.autoSuggest(term, { boost: { command: 2 }, prefix: true });
res = title_ms.search(term, { boost: { command: 2 }, prefix: true });
bg_text = suggestions[0]?.suggestion;
bg_text ??= '';
}
visible_items = [];
console.log(res);
for (const quiz_data of res) {
visible_items.push(actions[quiz_data.id]);
}
visible_items = visible_items;
if (visible_items.length === 1) {
selected = 0;
}
if (visible_items.length === 0) {
selected = null;
}
};
const autocomplete_on_tab = (e: KeyboardEvent) => {
e.preventDefault();
input = bg_text;
};
const on_arrow_down = (e: KeyboardEvent) => {
e.preventDefault();
if (visible_items.length < 1) {
return;
}
if (selected + 1 === visible_items.length) {
return;
}
selected += 1;
};
const on_arrow_up = (e: KeyboardEvent) => {
e.preventDefault();
if (visible_items.length < 1) {
return;
}
if (selected === 0) {
return;
}
selected -= 1;
};
const on_enter = (e: KeyboardEvent) => {
e.preventDefault();
if (selected === null) {
return;
}
execute_action();
input = '';
};
$: search(input);
// $: input = lower_input(input)
$: input = input.toLowerCase();
onMount(async () => {
tinykeys(window, {
'$mod+k': toggle_open,
Escape: close_cp,
Tab: autocomplete_on_tab,
ArrowDown: on_arrow_down,
ArrowUp: on_arrow_up,
Enter: on_enter
});
title_ms = new MiniSearch<any>({
fields: ['title'],
storeFields: ['id']
});
title_ms.addAll(actions);
command_ms = new MiniSearch<any>({
fields: ['command'],
storeFields: ['id']
});
command_ms.addAll(actions);
});
</script>
{#if open}
<div
class="fixed top-0 left-0 w-screen h-screen flex bg-black bg-opacity-50 z-50"
on:click={close_on_outside}
transition:fade={{ duration: 60 }}
>
<div class="m-auto w-1/3 h-2/3 rounded bg-black flex flex-col">
<div class="grid grid-cols-1 grid-rows-1 border-b border-b-white">
<p
class="col-start-1 row-start-1 w-full p-4 outline-none bg-gray-700 rounded-t text-gray-400"
>
{bg_text}
</p>
<input
type="text"
autofocus
class="col-start-1 row-start-1 bg-transparent w-full p-4 outline-none bg-gray-700 rounded"
bind:value={input}
/>
</div>
<div class="flex flex-col p-2 gap-2 overflow-scroll">
{#each visible_items as vi, i}
<div
transition:fade|local={{ duration: 60 }}
class="p-2 transition rounded"
class:bg-[#B07156]={selected === i}
class:bg-gray-700={selected !== i}
on:mouseenter={() => (selected = i)}
on:mousedown={execute_action}
>
<div class="flex">
<h3 class="text-lg my-auto">{vi.title}</h3>
<p
class="font-mono my-auto ml-auto h-fit bg-black bg-opacity-50 rounded p-0.5"
>
/{vi.command}
{#if vi.args}
{#each vi.args as arg}
&lbrace;<span class="text-indigo-400">{arg}</span
>&rbrace;{/each}
{/if}
</p>
</div>
<p class="text-sm">{vi.description}</p>
</div>
{/each}
</div>
</div>
</div>
{/if}
@@ -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')}
{: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
}
+6 -12
View File
@@ -13,7 +13,7 @@
import { QuizQuestionType } from '$lib/quiz_types.js';
import { getLocalization } from '$lib/i18n';
import StartGamePopup from './start_game.svelte';
import { onMount } from 'svelte';
// import { onMount } from 'svelte';
import viewport from './useViewportAction.js';
import Spinner from '$lib/Spinner.svelte';
import GrayButton from '$lib/components/buttons/gray.svelte';
@@ -34,12 +34,6 @@
};
let visibleImages = Array.from(Array(quizzes.length), () => []);
const close_start_game_if_esc_is_pressed = (key: KeyboardEvent) => {
if (key.code === 'Escape') {
start_game = null;
}
};
const copy_id = (quiz_id: string) => {
navigator.clipboard.writeText(quiz_id);
copy_toast_open = true;
@@ -58,9 +52,9 @@
game_in_lobby = await res.json();
}
};
onMount(() => {
document.body.addEventListener('keydown', close_start_game_if_esc_is_pressed);
});
// onMount(() => {
// document.body.addEventListener('keydown', close_start_game_if_esc_is_pressed);
// });
get_game_in_lobby_fn();
</script>
@@ -164,7 +158,7 @@
<div class="h-[20vh] m-auto w-auto">
<img
class="max-h-full max-w-full block"
src={quiz.cover_image}
src="/api/v1/storage/download/{quiz.cover_image}"
alt="Not provided"
loading="lazy"
/>
@@ -308,7 +302,7 @@
{#if visibleImages?.[i]?.[q]}
<img
class="max-h-full max-w-full block"
src={question.image}
src="/api/v1/storage/download/{question.image}"
alt="Not provided"
/>
{/if}
+16 -17
View File
@@ -4,14 +4,16 @@
file, You can obtain one at https://mozilla.org/MPL/2.0/.
-->
<script lang="ts">
import { alertModal } from '$lib/stores';
// import { alertModal } from '$lib/stores';
import { captcha_enabled } from '$lib/config';
import StartGameBackground from './start_game_background.svg';
import { fade } from 'svelte/transition';
import Spinner from '$lib/Spinner.svelte';
import { onMount } from 'svelte';
import { createTippy } from 'svelte-tippy';
import { getLocalization } from '$lib/i18n';
const { t } = getLocalization();
export let quiz_id;
let captcha_selected = false;
let selected_game_mode = 'kahoot';
@@ -52,14 +54,16 @@
);
}
if (res.status !== 200) {
alertModal.set({
/* alertModal.set({
open: true,
title: 'Start failed',
body: `Failed to start game, ${await res.text()}`
});
alertModal.subscribe((_) => {
});*/
/*alertModal.subscribe((_) => {
window.location.assign('/account/login?returnTo=/dashboard');
});
});*/
alert('Starting game failed');
window.location.assign('/account/login?returnTo=/dashboard');
} else {
const data = await res.json();
// eslint-disable-next-line no-undef
@@ -103,9 +107,7 @@
{#if captcha_selected}
<div class="flex justify-center mt-2" in:fade>
<p class="w-1/3">
If enabled, Google's ReCaptcha will load in the browser of players. Only enable
if you really need it, since you need the consent of <b>EVERY</b> player to load
the captcha.
{$t('start_game.captcha_message')}
</p>
<!-- Todo: Add translation -->
</div>
@@ -119,11 +121,9 @@
selected_game_mode = 'kahoot';
}}
>
<h2 class="text-center text-2xl">Normal</h2>
<h2 class="text-center text-2xl">{$t('words.normal')}</h2>
<p>
Question and answer will only be shown on admins screen, like Kahoot!. The
players will only have colored buttons with symbols matching these on the screen
of the admin.
{$t('start_game.normal_mode_description')}
</p>
</div>
<div
@@ -133,15 +133,14 @@
selected_game_mode = 'normal';
}}
>
<h2 class="text-center text-2xl">Old-School</h2>
<h2 class="text-center text-2xl">{$t('start_game.old_school_mode')}</h2>
<p>
Questions and images will be shown on both admins screen and on the screen of
the players
{$t('start_game.old_school_mode_description')}
</p>
</div>
</div>
<div class="flex justify-center items-center my-auto">
<label class="mr-4">Custom Field</label>
<label class="mr-4">{$t('result_page.custom_field')}</label>
<input
bind:value={custom_field}
class="rounded-lg p-2 outline-none placeholder:italic"
@@ -183,7 +182,7 @@
{#if loading}
<Spinner my_20={false} />
{:else}
Start Game
{$t('start_game.start_game')}
{/if}
</button>
</div>
@@ -4,6 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
// Stolen from https://svelte.dev/repl/c6a402704224403f96a3db56c2f48dfc?version=3.55.0
// skipcq: JS-0119
let intersectionObserver;
function ensureIntersectionObserver() {
+10 -36
View File
@@ -4,13 +4,16 @@
- file, You can obtain one at https://mozilla.org/MPL/2.0/.
-->
<script lang="ts">
import { mint } from '$lib/hashcash';
// import { mint } from '$lib/hashcash';
import { dataSchema } from '$lib/yupSchemas';
import type { EditorData, Question } from './quiz_types';
import Sidebar from '$lib/editor/sidebar.svelte';
import SettingsCard from '$lib/editor/settings-card.svelte';
import QuizCard from '$lib/editor/card.svelte';
import Spinner from './Spinner.svelte';
import { getLocalization } from '$lib/i18n';
const { t } = getLocalization();
let schemaInvalid = false;
let yupErrorMessage = '';
@@ -19,22 +22,6 @@
export let quiz_id: string | null;
let selected_question = -1;
let imgur_links_valid = false;
let pow_salt;
const computePOW = async (salt: string) => {
if (pow_salt === undefined) {
return;
}
console.log('Computing POW');
pow_data = await mint(salt, 16, '', 8, false);
pow_salt = undefined;
return;
};
$: {
pow_salt;
computePOW(pow_salt);
}
const validateInput = async (data: EditorData) => {
// console.log("input", data)
@@ -69,7 +56,8 @@
$: imgur_links_valid = checkIfAllQuestionImagesComplyWithRegex(data.questions);
let edit_id;
let confirm_to_leave = true;
let pow_data;
$: console.log('data', data);
const getEditID = async () => {
let res;
@@ -85,7 +73,6 @@
if (res.status === 200) {
const json = await res.json();
edit_id = json.token;
setPOWdata();
} else {
alert('Error!');
}
@@ -120,13 +107,6 @@
alert('Error');
}
};
const setPOWdata = async () => {
const res = await fetch(`/api/v1/editor/pow?edit_id=${edit_id}`);
const data = (await res.json()).data;
console.log(data);
pow_data = await mint(data, 16);
console.log(pow_data);
};
</script>
<svelte:window on:beforeunload={confirmUnload} />
@@ -152,10 +132,10 @@
</p>
{/if}
<button
class="pr-2 align-middle bg-purple-400 pl-2 ml-auto whitespace-nowrap disabled:opacity-60 rounded-br-lg"
class="pr-2 align-middle bg-[#B07156] pl-2 ml-auto whitespace-nowrap disabled:opacity-60 rounded-br-lg"
disabled={schemaInvalid}
>
<span>Save</span>
<span>{$t('words.save')}</span>
<svg
class="w-6 h-6 inline-block"
fill="none"
@@ -174,15 +154,9 @@
</div>
<div class="w-full h-full">
{#if selected_question === -1}
<SettingsCard bind:data bind:pow_salt bind:edit_id bind:pow_data />
<SettingsCard bind:data bind:edit_id />
{:else}
<QuizCard
bind:data
bind:selected_question
bind:edit_id
bind:pow_data
bind:pow_salt
/>
<QuizCard bind:data bind:selected_question bind:edit_id />
{/if}
</div>
</div>
+27 -8
View File
@@ -4,15 +4,20 @@
- file, You can obtain one at https://mozilla.org/MPL/2.0/.
-->
<script lang="ts">
import type { EditorData, Answer } from '../quiz_types';
import type { Answer, EditorData } from '../quiz_types';
import { QuizQuestionType } from '../quiz_types';
import { fade } from 'svelte/transition';
import { reach } from 'yup';
import { ABCDQuestionSchema } from '$lib/yupSchemas';
import { getLocalization } from '$lib/i18n';
import { get_foreground_color } from '$lib/helpers';
const { t } = getLocalization();
const default_colors = ['#D6EDC9', '#B07156', '#7F7057', '#4E6E58'];
export let selected_question: number;
export let check_choice = false;
export let data: EditorData;
if (!Array.isArray(data.questions[selected_question].answers)) {
data.questions[selected_question].answers = [];
@@ -29,17 +34,30 @@
};
const get_empty_answer = (i: number): Answer => {
const color = localStorage.getItem(`quiz_color:${i}:${data.title}`);
return {
answer: '',
color: color,
color: default_colors[i],
right: false
};
};
$: save_colors(data);
data.questions[selected_question].type =
check_choice === true ? QuizQuestionType.CHECK : QuizQuestionType.ABCD;
const set_colors_if_unset = () => {
for (let i = 0; i < data.questions[selected_question].answers.length; i++) {
if (!data.questions[selected_question].answers[i].color) {
data.questions[selected_question].answers[i].color = default_colors[i];
}
}
};
$: {
set_colors_if_unset();
data;
selected_question;
}
</script>
<div class="grid grid-cols-2 gap-4 w-full px-10">
<div class="grid grid-rows-2 grid-flow-col auto-cols-auto gap-4 w-full px-10">
{#if Array.isArray(data.questions[selected_question].answers)}
{#each data.questions[selected_question].answers as answer, index}
<div
@@ -79,14 +97,15 @@
bind:value={answer.answer}
type="text"
class="border-b-2 border-dotted w-5/6 text-center rounded-lg bg-transparent"
style="background-color: {answer.color ?? 'transparent'}"
placeholder="Empty..."
style="background-color: {answer.color}; color: {get_foreground_color(
answer.color
)}"
placeholder={$t('editor.empty')}
/>
<button
type="button"
on:click={() => {
answer.right = !answer.right;
console.log(answer.right);
}}
>
{#if answer.right}
@@ -126,7 +145,7 @@
type="color"
bind:value={answer.color}
on:contextmenu|preventDefault={() => {
answer.color = null;
answer.color = default_colors[index];
}}
/>
</div>
@@ -0,0 +1,115 @@
<!--
- 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 { Answers, Question } from '$lib/quiz_types';
import { QuizQuestionType } from '$lib/quiz_types';
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
import { getLocalization } from '$lib/i18n';
export let questions: Question[];
export let open: boolean;
const { t } = getLocalization();
onMount(() => {
document.body.addEventListener('keydown', close_start_game_if_esc_is_pressed);
});
const close_start_game_if_esc_is_pressed = (key: KeyboardEvent) => {
if (key.code === 'Escape') {
open = false;
}
};
const on_parent_click = (e: Event) => {
if (e.target === e.currentTarget) {
open = false;
}
};
const question_types: {
name: string;
description: string;
answers: Answers;
type: QuizQuestionType;
}[] = [
{
name: $t('words.multiple_choice'),
description: $t('editor.abcd_description'),
answers: [],
type: QuizQuestionType.ABCD
},
{
name: $t('words.voting'),
description: $t('editor.voting_description'),
answers: [],
type: QuizQuestionType.VOTING
},
{
name: $t('words.check_choice'),
description: $t('editor.check_choice_description'),
answers: [],
type: QuizQuestionType.CHECK
},
{
name: $t('words.order'),
description: $t('editor.order_description'),
answers: [],
type: QuizQuestionType.ORDER
},
{
name: $t('words.text'),
description: $t('editor.text_description'),
answers: [],
type: QuizQuestionType.TEXT
},
{
name: $t('words.range'),
description: $t('editor.range_description'),
answers: {
max: 10,
min: 0,
max_correct: 7,
min_correct: 3
},
type: QuizQuestionType.RANGE
}
];
const add_question = (index: number) => {
const empty_question: Question = {
type: question_types[index].type,
time: '20',
question: '',
image: undefined,
answers: question_types[index].answers
};
questions = [...questions, { ...empty_question }];
open = false;
};
</script>
<div
class="fixed top-0 left-0 w-screen h-screen flex bg-black z-50 bg-opacity-50"
on:click={on_parent_click}
transition:fade|local={{ duration: 100 }}
>
<div class="m-auto w-2/3 h-5/6 rounded shadow-2xl bg-white dark:bg-gray-600 p-6">
<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 question_types as qt, i}
<div class="rounded p-6 border-[#B07156] border">
<button
class="text-xl text-black dark:text-white"
on:click={() => {
add_question(i);
}}>{qt.name}</button
>
<p class="text-sm">{qt.description}</p>
</div>
{/each}
</div>
</div>
</div>
@@ -0,0 +1,95 @@
<!--
- 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 { browser } from '$app/environment';
import { fade } from 'svelte/transition';
export let src: string;
export let css_classes = 'max-h-64 h-auto w-auto';
export let muted = true;
export let allow_fullscreen = true;
let type: 'img' | 'video' | undefined = undefined;
let img_data;
const get_media = async () => {
if (!browser) {
return;
}
const res = await fetch(`/api/v1/storage/info/${src}`);
const fileType = res.headers.get('Content-Type');
if (fileType.includes('video')) {
type = 'video';
} else {
type = 'img';
const data = await fetch(`/api/v1/storage/download/${src}`);
img_data = {
data: URL.createObjectURL(await data.blob()),
alt_text: res.headers.get('X-Alt-Text')
};
}
};
const update_url = () => {
media = get_media();
};
let media = get_media();
$: {
src;
update_url();
}
let fullscreen_open = false;
const open_fullscreen = () => {
if (!allow_fullscreen) {
return;
}
fullscreen_open = true;
};
</script>
{#await media}
<p>Placeholder</p>
{:then data}
{#if type === 'img'}
<img
src={img_data.data}
alt={img_data.alt_text ?? 'Not available'}
class={css_classes}
on:click={() => open_fullscreen()}
/>
{:else if type === 'video'}
<video
class={css_classes}
disablepictureinpicture
x-webkit-airplay="deny"
controls
autoplay
loop
{muted}
preload="metadata"
>
<source src="/api/v1/storage/download/{src}" />
</video>
{:else}
<p>Unknown media type</p>
{/if}
{/await}
{#if fullscreen_open}
<div
class="fixed top-0 left-0 z-50 w-screen h-screen bg-black bg-opacity-50 fle p-2"
transition:fade={{ duration: 80 }}
on:click={() => (fullscreen_open = false)}
>
<img
src={img_data.data}
alt={img_data.alt_text ?? 'Not available'}
class="object-cover rounded m-auto max-h-full max-w-full"
/>
</div>
{/if}
@@ -141,7 +141,7 @@
type="text"
class="border-b-2 border-dotted w-5/6 text-center rounded-lg bg-transparent"
style="background-color: {answer.color ?? 'transparent'}"
placeholder="Empty..."
placeholder={$t('editor.empty')}
/>
<input
class="rounded-lg p-1 border-black border"
@@ -14,6 +14,7 @@
min_correct: 3
};
}
/*
const correct_numbers = (data: number[]) => {
console.log(data, data[1] <= data[0])
@@ -74,13 +74,12 @@
bind:value={answer.answer}
type="text"
class="border-b-2 border-dotted w-5/6 text-center rounded-lg bg-transparent"
placeholder="Empty..."
placeholder={$t('editor.empty')}
/>
<button
type="button"
on:click={() => {
answer.case_sensitive = !answer.case_sensitive;
console.log(answer.case_sensitive);
}}
>
{#if answer.case_sensitive}
+33 -16
View File
@@ -4,18 +4,16 @@
- file, You can obtain one at https://mozilla.org/MPL/2.0/.
-->
<script lang="ts">
import type { EditorData, VotingAnswer } from '../quiz_types';
import type { EditorData } from '../quiz_types';
import { fade } from 'svelte/transition';
import { reach } from 'yup';
import { getLocalization } from '$lib/i18n';
import { VotingQuestionSchema } from '$lib/yupSchemas';
import { get_foreground_color } from '$lib/helpers';
const { t } = getLocalization();
const empty_answer: VotingAnswer = {
answer: '',
image: undefined
};
const default_colors = ['#D6EDC9', '#B07156', '#7F7057', '#4E6E58'];
export let selected_question: number;
export let data: EditorData;
@@ -29,16 +27,28 @@
// eslint-disable-next-line no-empty
} catch {}
/*console.log(data.questions[selected_question].answers, 'moIn!', data.questions[selected_question].answers.length);
onMount(() => {
for (let i = 0; i < data.questions[selected_question].answers; i++) {
console.log(data.questions[selected_question].answers[i], 'iterate');
data.questions[selected_question].answers[i].right = undefined;
const set_colors_if_unset = () => {
for (let i = 0; i < data.questions[selected_question].answers.length; i++) {
if (!data.questions[selected_question].answers[i].color) {
data.questions[selected_question].answers[i].color = default_colors[i];
}
}
});*/
};
$: {
set_colors_if_unset();
data;
selected_question;
}
/*console.log(data.questions[selected_question].answers, 'moIn!', data.questions[selected_question].answers.length);
onMount(() => {
for (let i = 0; i < data.questions[selected_question].answers; i++) {
console.log(data.questions[selected_question].answers[i], 'iterate');
data.questions[selected_question].answers[i].right = undefined;
}
});*/
</script>
<div class="grid grid-cols-2 gap-4 w-full px-10">
<div class="grid grid-rows-2 grid-flow-col auto-cols-auto gap-4 w-full px-10">
{#if Array.isArray(data.questions[selected_question].answers)}
{#each data.questions[selected_question].answers as answer, index}
<div
@@ -78,15 +88,16 @@
bind:value={answer.answer}
type="text"
class="border-b-2 border-dotted w-5/6 text-center rounded-lg"
style="background-color: {answer.color ?? 'transparent'}"
placeholder="Empty..."
style="background-color: {answer.color ??
'transparent'}; color: {get_foreground_color(answer.color)}"
placeholder={$t('editor.empty')}
/>
<input
class="rounded-lg p-1 border-black border"
type="color"
bind:value={answer.color}
on:contextmenu|preventDefault={() => {
answer.color = null;
answer.color = default_colors[index];
}}
/>
</div>
@@ -100,7 +111,13 @@
on:click={() => {
data.questions[selected_question].answers = [
...data.questions[selected_question].answers,
{ ...empty_answer }
{
...{
answer: '',
image: undefined,
color: default_colors[data.questions[selected_question].answers.length]
}
}
];
}}
>
+58 -48
View File
@@ -10,22 +10,21 @@
import { reach } from 'yup';
import { dataSchema } from '$lib/yupSchemas';
import Spinner from '../Spinner.svelte';
import { createTippy } from 'svelte-tippy';
// import { createTippy } from 'svelte-tippy';
import { getLocalization } from '$lib/i18n';
// import MediaComponent from "$lib/editor/MediaComponent.svelte";
const { t } = getLocalization();
const tippy = createTippy({
arrow: true,
animation: 'perspective-subtle',
placement: 'top'
});
/* const tippy = createTippy({
arrow: true,
animation: 'perspective-subtle',
placement: 'top'
});*/
export let data: EditorData;
export let selected_question: number;
export let edit_id: string;
export let pow_data;
export let pow_salt: string;
let uppyOpen = false;
let unique = {};
@@ -42,19 +41,45 @@
.toString()
.slice(0, 3);
}
};
const set_unique = () => {
unique = {};
};
$: correctTimeInput(data.questions[selected_question].time);
/*
if (typeof data.questions[selected_question].type !== QuizQuestionType) {
console.log(data.questions[selected_question].type !== QuizQuestionType.ABCD || data.questions[selected_question].type !== QuizQuestionType.RANGE)
data.questions[selected_question].type = QuizQuestionType.ABCD;
$: {
selected_question;
set_unique();
}
*/
let image_url = '';
const update_image_url = () => {
image_url = data.questions[selected_question].image;
};
$: {
update_image_url();
selected_question;
data.questions;
}
const type_to_name = {
RANGE: $t('words.range'),
ABCD: $t('words.multiple_choice'),
VOTING: $t('words.voting'),
TEXT: $t('words.text'),
ORDER: $t('words.order'),
CHECK: $t('words.check_choice')
};
/*
if (typeof data.questions[selected_question].type !== QuizQuestionType) {
console.log(data.questions[selected_question].type !== QuizQuestionType.ABCD || data.questions[selected_question].type !== QuizQuestionType.RANGE)
data.questions[selected_question].type = QuizQuestionType.ABCD;
}
*/
</script>
<div class="w-full max-h-full pb-20 px-20 h-full">
<div class="rounded-lg bg-white w-full h-full border-gray-500 drop-shadow-2xl dark:bg-gray-700">
<div class="rounded-lg bg-white w-full h-full border-gray-500 dark:bg-gray-700 shadow-2xl">
<div class="h-12 bg-gray-300 rounded-t-lg dark:bg-gray-500">
<div class="flex align-middle p-4 gap-3">
<span
@@ -75,6 +100,7 @@
<svelte:component this={c.default} bind:data={data.questions[selected_question]} />
{/await}
{:else}
{@const type = data.questions[selected_question].type}
<div class="flex flex-col">
<div class="flex justify-center pt-10 w-full">
{#key unique}
@@ -103,7 +129,7 @@
class="rounded-full absolute -top-2 -right-2 opacity-70 hover:opacity-100 transition"
type="button"
on:click={() => {
data.questions[selected_question].image = '';
data.questions[selected_question].image = null;
}}
>
<svg
@@ -121,22 +147,11 @@
/>
</svg>
</button>
<img
src={data.questions[selected_question].image}
alt="not available"
class="max-h-64 h-auto w-auto"
/>
{#await import('$lib/editor/MediaComponent.svelte') then c}
<svelte:component this={c.default} bind:src={image_url} />
{/await}
</div>
</div>
{:else if pow_data === undefined}
<a
href="/docs/pow"
target="_blank"
use:tippy={{ content: "Click to learn why it's loading so long." }}
class="cursor-help"
>
<Spinner my_20={false} />
</a>
{:else}
{#await import('$lib/editor/uploader.svelte')}
<Spinner my_20={false} />
@@ -147,8 +162,7 @@
bind:edit_id
bind:data
bind:selected_question
bind:pow_data
bind:pow_salt
video_upload={true}
/>
{/await}
{/if}
@@ -178,40 +192,36 @@
</div>
</div>
<div class="flex justify-center pt-10">
<select
class="p-2 rounded-lg bg-gray-800 focus:ring-2 ring-blue-600 text-white"
name="Answer-Type"
bind:value={data.questions[selected_question].type}
>
<option value={QuizQuestionType.RANGE}>{$t('words.range')}</option>
<option value={QuizQuestionType.ABCD}>{$t('words.multiple_choice')}</option>
<option value={QuizQuestionType.VOTING}>{$t('words.voting')}</option>
<option value={QuizQuestionType.TEXT}>{$t('words.text')}</option>
<option value={QuizQuestionType.ORDER}>{$t('words.order')}</option>
</select>
<p>{type_to_name[String(data.questions[selected_question].type)]}</p>
</div>
<div class="flex justify-center py-10 w-full">
{#if data.questions[selected_question].type === QuizQuestionType.ABCD}
{#if type === QuizQuestionType.ABCD || type === QuizQuestionType.CHECK}
{#await import('$lib/editor/ABCDEditorPart.svelte')}
<Spinner my_20={false} />
{:then c}
<svelte:component this={c.default} bind:data bind:selected_question />
<svelte:component
this={c.default}
bind:data
bind:selected_question
check_choice={type === QuizQuestionType.CHECK}
/>
{/await}
{:else if data.questions[selected_question].type === QuizQuestionType.RANGE}
{:else if type === QuizQuestionType.RANGE}
<p>Range</p>
<RangeEditor bind:selected_question bind:data />
{:else if data.questions[selected_question].type === QuizQuestionType.VOTING}
{:else if type === QuizQuestionType.VOTING}
{#await import('$lib/editor/VotingEditorPart.svelte')}
<Spinner my_20={false} />
{:then c}
<svelte:component this={c.default} bind:data bind:selected_question />
{/await}
{:else if data.questions[selected_question].type === QuizQuestionType.TEXT}
{:else if type === QuizQuestionType.TEXT}
{#await import('$lib/editor/TextEditorPart.svelte')}
<Spinner my_20={false} />
{:then c}
<svelte:component this={c.default} bind:data bind:selected_question />
{/await}
{:else if data.questions[selected_question].type === QuizQuestionType.ORDER}
{:else if type === QuizQuestionType.ORDER}
{#await import('$lib/editor/OrderEditorPart.svelte')}
<Spinner my_20={false} />
{:then c}
+6 -19
View File
@@ -8,9 +8,6 @@
import { getLocalization } from '$lib/i18n';
import Spinner from '$lib/Spinner.svelte';
export let pow_data;
export let pow_salt;
const { t } = getLocalization();
let uppyOpen = false;
@@ -42,7 +39,7 @@
<div
class="dark:bg-gray-700 h-full"
style="background-repeat: no-repeat;background-size: 100% 100%;background-image: {data.background_image
? `url("${data.background_image}")`
? `url("/api/v1/storage/download/${data.background_image}")`
: `unset`}"
>
<div class="flex justify-center pt-10 w-full">
@@ -68,18 +65,14 @@
{#if data.cover_image != undefined && data.cover_image !== ''}
<div class="flex justify-center pt-10 w-full max-h-72 w-full">
<img
src={data.cover_image}
src="/api/v1/storage/download/{data.cover_image}"
alt="not available"
class="max-h-72 h-auto w-auto"
on:contextmenu|preventDefault={() => {
data.cover_image = '';
data.cover_image = null;
}}
/>
</div>
{:else if pow_data === undefined}
<a href="/docs/pow" target="_blank" class="cursor-help">
<Spinner my_20={false} />
</a>
{:else}
{#await import('$lib/editor/uploader.svelte')}
<Spinner my_20={false} />
@@ -89,8 +82,7 @@
bind:modalOpen={uppyOpen}
bind:edit_id
bind:data
bind:pow_data
bind:pow_salt
video_upload={false}
/>
{/await}
{/if}
@@ -180,7 +172,7 @@
</div>
</div>
<div class="flex justify-center pt-10">
<h3>Background-Image</h3>
<h3>{$t('editor.bg_image')}</h3>
</div>
<div class="w-full flex justify-center -mt-8">
{#if data.background_image}
@@ -191,10 +183,6 @@
class="mt-10 bg-red-500 p-2 rounded-lg border-2 border-black transition hover:bg-red-400"
>Remove Background-Image</button
>
{:else if pow_data === undefined}
<a href="/docs/pow" target="_blank" class="cursor-help pt-10">
<Spinner my_20={false} />
</a>
{:else}
{#await import('$lib/editor/uploader.svelte')}
<div class="pt-10">
@@ -207,8 +195,7 @@
bind:edit_id
bind:data
selected_question={-1}
bind:pow_data
bind:pow_salt
video_upload={false}
/>
{/await}
{/if}
+30 -27
View File
@@ -9,6 +9,10 @@
import { reach } from 'yup';
import { ABCDQuestionSchema, dataSchema } from '../yupSchemas';
import { createTippy } from 'svelte-tippy';
import { getLocalization } from '$lib/i18n';
import AddNewQuestionPopup from '$lib/editor/AddNewQuestionPopup.svelte';
const { t } = getLocalization();
export let data: EditorData;
export let selected_question = -1;
@@ -20,13 +24,7 @@
});
let arr_of_cards = Array(data.questions.length);
let propertyCard;
const empty_question: Question = {
question: '',
time: '20',
image: '',
answers: [],
type: QuizQuestionType.ABCD
};
let add_new_question_popup_open = false;
const empy_slide: Question = {
type: QuizQuestionType.SLIDE,
@@ -49,10 +47,10 @@
}
};
/* onMount(() => {
propertyCard.scrollIntoView({
behavior: 'smooth'
});
});*/
propertyCard.scrollIntoView({
behavior: 'smooth'
});
});*/
</script>
<div class="h-screen border-r-2 pt-6 px-6 overflow-scroll">
@@ -78,7 +76,7 @@
{#if data.title}
{@html data.title}
{:else}
<i>No title...</i>
<i>{$t('editor.no_title')}</i>
{/if}
</p>
</div>
@@ -121,7 +119,7 @@
d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>Public</span>
<span>{$t('words.public')}</span>
{:else}
<svg
class="w-5 h-5 inline-block"
@@ -137,7 +135,7 @@
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
/>
</svg>
<span>Private</span>
<span>{$t('words.private')}</span>
{/if}
</button>
</div>
@@ -190,7 +188,7 @@
class:dark:text-black={index === selected_question}
>
{#if question.question === ''}
<span class="italic text-gray-500">No title...</span>
<span class="italic text-gray-500">{$t('editor.no_title')}</span>
{:else}
{@html question.question}
{/if}
@@ -199,17 +197,17 @@
{#if question.image}
<div class="flex justify-center align-middle pb-0.5">
<img
src={question.image}
src="/api/v1/storage/download/{question.image}"
class="h-10 border rounded-lg"
alt="Not available"
use:tippy={{
content: `<img src='${question.image}' alt='Not available' class='rounded'>`,
content: `<img src="/api/v1/storage/download/${question.image}" alt="Not available" class="rounded">`,
allowHTML: true
}}
/>
</div>
{/if}
{#if question.type === QuizQuestionType.ABCD}
{#if question.type === QuizQuestionType.ABCD || question.type === QuizQuestionType.CHECK}
<div class="grid grid-cols-2 gap-2">
{#if Array.isArray(question.answers)}
{#each question.answers as answer}
@@ -222,10 +220,11 @@
'answer'
).isValidSync(answer.answer)}
use:tippy={{
content: answer.answer === '' ? 'Empty...' : answer.answer
content:
answer.answer === '' ? $t('editor.empty') : answer.answer
}}
>{#if answer.answer === ''}
<i>Empty...</i>
<i>{$t('editor.empty')}</i>
{:else}
{answer.answer}
{/if}</span
@@ -252,10 +251,11 @@
'answer'
).isValidSync(answer.answer)}
use:tippy={{
content: answer.answer === '' ? 'Empty...' : answer.answer
content:
answer.answer === '' ? $t('editor.empty') : answer.answer
}}
>{#if answer.answer === ''}
<i>Empty...</i>
<i>{$t('editor.empty')}</i>
{:else}
{answer.answer}
{/if}</span
@@ -275,12 +275,12 @@
>
<button
type="button"
class="h-full flex justify-center w-full dark:text-black flex-col border-r border-black"
class="h-full flex justify-center w-full flex-col border-r border-black dark:text-white"
on:click={() => {
data.questions = [...data.questions, { ...empty_question }];
add_new_question_popup_open = true;
}}
>
<span class="w-full text-center">Question</span>
<span class="w-full text-center">{$t('words.question')}</span>
<svg
class="w-5/6 m-auto"
fill="none"
@@ -298,12 +298,12 @@
</button>
<button
type="button"
class="h-full flex justify-center w-full dark:text-black flex-col"
class="h-full flex justify-center w-full dark:text-white flex-col"
on:click={() => {
data.questions = [...data.questions, { ...empy_slide }];
}}
>
<span class="w-full text-center">Slide</span>
<span class="w-full text-center">{$t('words.slide')}</span>
<svg
class="w-5/6 m-auto"
fill="none"
@@ -321,3 +321,6 @@
</button>
</div>
</div>
{#if add_new_question_popup_open}
<AddNewQuestionPopup bind:questions={data.questions} bind:open={add_new_question_popup_open} />
{/if}
-2
View File
@@ -84,7 +84,6 @@
fill: '#ff000d'
});
}
console.log(canvas.export.toJson());
};
$: {
@@ -146,7 +145,6 @@
interactive: false
}*/
});
console.log(data.answers);
if (data.answers) {
if (typeof data.answers === 'string') {
canvas.import.json(JSON.parse(data.answers));
@@ -15,7 +15,6 @@
const set_available_modifiers = () => {
available_modifiers = [];
console.log(selected_el, 'sel_el');
if (!selected_el) {
return;
}
@@ -43,19 +42,16 @@
const change_color = (e: Event) => {
if (available_modifiers.includes('text_color')) {
console.log('text color');
selected_el.updateText({
fill: e.target.value
});
} else if (available_modifiers.includes('fill_color')) {
console.log('fill color');
selected_el.update({ fill: e.target.value });
}
opened_dropdown = null;
};
const change_fontsize = (e: Event) => {
console.log(selected_el.node.children[1].attrs.fontSize);
selected_el.updateText({
fontSize: e.target.value
});
@@ -7,7 +7,9 @@
import { ElementTypes } from '$lib/quiz_types';
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
import { getLocalization } from '$lib/i18n';
const { t } = getLocalization();
export let selected_element;
const keybinding_list = {
t: ElementTypes.Text,
@@ -40,22 +42,22 @@
shortcut: string;
}> = [
{
name: 'Headline',
description: 'A bold text for headlines',
name: $t('editor.slide.headline'),
description: $t('editor.slide.headline_description'),
type: ElementTypes.Headline,
icon: undefined,
shortcut: 'h'
},
{
name: 'Text',
description: 'Smaller longer text',
name: $t('editor.slide.text'),
description: $t('editor.slide.text_description'),
type: ElementTypes.Text,
icon: undefined,
shortcut: 't'
},
{
name: 'Rectangle',
description: 'Just a rectangle',
name: $t('editor.slide.rectangle'),
description: $t('editor.slide.rectangle_description'),
type: ElementTypes.Rectangle,
icon: undefined,
shortcut: 'r'
@@ -10,7 +10,6 @@
if (!data) {
data = '#ed333b';
}
$: console.log(data);
</script>
<div class="rounded-full w-full h-full flex justify-center p-2" style="background-color: {data}">
@@ -10,7 +10,6 @@
if (!data) {
data = '#ed333b';
}
$: console.log(data);
</script>
<div class="w-full h-full flex justify-center p-2" style="background-color: {data}">
+142 -31
View File
@@ -12,6 +12,7 @@
import Dashboard from '@uppy/dashboard';
import Compressor from '@uppy/compressor';
import { fade } from 'svelte/transition';
import BrownButton from '$lib/components/buttons/brown.svelte';
// CSS imports
import '@uppy/core/dist/style.css';
@@ -21,6 +22,9 @@
import '@uppy/image-editor/dist/style.css';
import type { EditorData } from '../quiz_types';
import { getLocalization } from '$lib/i18n';
import { onMount } from 'svelte';
import Library from '$lib/editor/uploader/Library.svelte';
import Pixabay from '$lib/editor/uploader/Pixabay.svelte';
const { t } = getLocalization();
@@ -28,10 +32,26 @@
export let edit_id: string;
export let data: EditorData;
export let selected_question: number;
export let pow_data;
export let pow_salt: string;
export let video_upload = false;
export let library_enabled = true;
// eslint-disable-next-line no-undef
let video_popup: undefined | WindowProxy = undefined;
let selected_type: AvailableUploadTypes | null = null;
// eslint-disable-next-line no-unused-vars
enum AvailableUploadTypes {
// eslint-disable-next-line no-unused-vars
Image,
// eslint-disable-next-line no-unused-vars
Video,
// eslint-disable-next-line no-unused-vars
Library,
// eslint-disable-next-line no-unused-vars
Pixabay
}
console.log(pow_data);
const uppy = new Uppy()
.use(DropTarget, {
target: document.body
@@ -45,59 +65,150 @@
quality: 0.6
})
.use(XHRUpload, {
endpoint: `/api/v1/editor/image?edit_id=${edit_id}&pow_data=${pow_data}`
endpoint: `/api/v1/storage/`
});
const props = {
inline: true,
restrictions: {
maxFileSize: 2_000_000,
maxFileSize: 10_490_000,
maxNumberOfFiles: 1,
allowedFileTypes: ['.gif', '.jpg', '.jpeg', '.png', '.svg', '.webp']
allowedFileTypes: ['image/*']
// allowedFileTypes: ['.gif', '.jpg', '.jpeg', '.png', '.svg', '.webp']
}
};
let image_id;
uppy.on('upload-success', (file, response) => {
image_id = response.body.id;
pow_salt = response.body.pow_data;
console.log(pow_salt, response.body);
pow_data = undefined;
});
uppy.on('complete', (_) => {
console.log(pow_data);
if (selected_question === undefined) {
data.cover_image = `${window.location.origin}/api/v1/storage/download/${image_id}`;
data.cover_image = image_id;
} else if (selected_question === -1) {
data.background_image = `${window.location.origin}/api/v1/storage/download/${image_id}`;
data.background_image = image_id;
} else {
data.questions[
selected_question
].image = `${window.location.origin}/api/v1/storage/download/${image_id}`;
data.questions[selected_question].image = image_id;
}
console.log(selected_question, data);
modalOpen = false;
selected_type = null;
});
onMount(() => {
window.addEventListener('storage', (e) => {
if (e.key !== 'video_upload_id') {
return;
}
localStorage.removeItem('video_upload_id');
data.questions[selected_question].image = e.newValue;
selected_type = null;
});
});
const upload_video = async () => {
video_popup = window.open(
'/edit/videos',
'_blank',
'popup=true,toolbar=false,menubar=false,location=false,'
);
video_popup.addEventListener('beforeunload', () => {
video_popup = undefined;
});
};
const handle_on_click = (e: Event) => {
if (e.target === e.currentTarget) {
modalOpen = false;
selected_type = null;
}
};
onMount(() => {
window.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Escape') {
modalOpen = false;
selected_type = null;
}
});
});
console.log(edit_id);
</script>
{#if modalOpen}
<div
class="w-full h-full absolute top-0 left-0 bg-opacity-60 z-20 flex justify-center"
transition:fade|local
class="w-screen h-screen fixed top-0 left-0 bg-opacity-50 bg-black z-20 flex justify-center"
on:click={handle_on_click}
transition:fade|local={{ duration: 100 }}
>
<div>
<button
type="button"
class="rounded-t-lg bg-black text-white px-1"
on:click={() => {
modalOpen = false;
}}
>Close
</button>
<div>
<SvelteDashboard {uppy} width="100%" {props} />
{#if selected_type === null}
<div class="m-auto w-1/3 h-auto bg-white dark:bg-gray-700 p-4 rounded">
<h1 class="text-3xl text-center mb-4">{$t('uploader.select_upload_type')}</h1>
<div class="flex flex-row gap-4">
<div class="w-full">
<BrownButton
on:click={() => {
selected_type = AvailableUploadTypes.Image;
}}
>{$t('words.image')}
</BrownButton>
</div>
<div class="w-full">
<BrownButton
disabled={!video_upload}
on:click={() => {
selected_type = AvailableUploadTypes.Video;
}}
>{$t('words.video')}
</BrownButton>
</div>
{#if library_enabled}
<div class="w-full">
<BrownButton
on:click={() => {
selected_type = AvailableUploadTypes.Library;
}}
>{$t('words.library')}
</BrownButton>
</div>
{/if}
<div class="w-full">
<BrownButton
on:click={() => {
selected_type = AvailableUploadTypes.Pixabay;
}}
>Pixabay
</BrownButton>
</div>
</div>
</div>
</div>
{:else if selected_type === AvailableUploadTypes.Image}
<div class="m-auto w-1/3 h-5/6" transition:fade|local={{ duration: 100 }}>
<div>
<SvelteDashboard {uppy} width="100%" {props} />
</div>
</div>
{:else if selected_type === AvailableUploadTypes.Video}
<div
class="m-auto w-1/3 h-auto bg-white dark:bg-gray-700 p-4 rounded"
transition:fade|local={{ duration: 100 }}
>
<h1 class="text-3xl text-center mb-4">{$t('uploader.upload_a_video')}</h1>
{#if video_popup}
<p class="text-center">
{$t('uploader.upload_video_popup_notice')}
</p>
{:else}
<BrownButton on:click={upload_video} type="button"
>{$t('uploader.upload_video')}</BrownButton
>
{/if}
</div>
{:else if selected_type === AvailableUploadTypes.Library}
<div>
<Library bind:data {selected_question} bind:modalOpen />
</div>
{:else if selected_type === AvailableUploadTypes.Pixabay}
<div>
<Pixabay bind:data {selected_question} bind:modalOpen />
</div>
{/if}
</div>
{/if}
<div class="flex justify-center w-full pt-10" transition:fade|local>
@@ -0,0 +1,65 @@
<!--
- 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 { PrivateImageData } from '$lib/quiz_types';
import Spinner from '$lib/Spinner.svelte';
import type { EditorData } from '$lib/quiz_types';
import BrownButton from '$lib/components/buttons/brown.svelte';
import { getLocalization } from '$lib/i18n';
export let data: EditorData;
export let selected_question: number;
export let modalOpen: boolean;
const { t } = getLocalization();
const fetch_images = async (): Promise<PrivateImageData> => {
const response = await fetch('/api/v1/storage/list/last?count=50');
return await response.json();
};
let image_fetch = fetch_images();
const set_image = (id: string) => {
if (selected_question === undefined) {
data.cover_image = id;
} else if (selected_question === -1) {
data.background_image = id;
} else {
data.questions[selected_question].image = id;
}
modalOpen = false;
};
</script>
{#await image_fetch}
<Spinner />
{:then images}
<div class="flex w-screen p-8 h-screen">
<div
class="flex flex-col w-1/3 m-auto overflow-scroll h-full rounded p-4 gap-4 bg-white dark:bg-gray-700"
>
{#each images as image}
<div class="rounded border-2 border-[#B07156] p-2 flex-col flex gap-2">
<div>
<img
src="/api/v1/storage/download/{image.id}"
loading="lazy"
alt={image.alt_text}
class="object-contain w-full h-full max-h-full rounded"
/>
</div>
<p class="text-center">{image.filename ?? 'No name available'}</p>
<BrownButton
on:click={() => {
set_image(image.id);
}}>{$t('words.select')}</BrownButton
>
</div>
{/each}
</div>
</div>
{/await}
@@ -0,0 +1,99 @@
<!--
- 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 { EditorData } from '$lib/quiz_types';
import Spinner from '$lib/Spinner.svelte';
import BrownButton from '$lib/components/buttons/brown.svelte';
import { getLocalization } from '$lib/i18n';
export let data: EditorData;
export let selected_question: number;
export let modalOpen: boolean;
let page = 1;
let search_term = '';
let loading = false;
const { t } = getLocalization();
const set_image = async (id: string) => {
loading = true;
const res = await fetch(`/api/v1/pixabay/save?id=${id}`, {
method: 'POST'
});
const json = await res.json();
const storage_id = json.id;
if (selected_question === undefined) {
data.cover_image = storage_id;
} else if (selected_question === -1) {
data.background_image = storage_id;
} else {
data.questions[selected_question].image = storage_id;
}
modalOpen = false;
};
const fetch_data = async () => {
const res = await fetch(`/api/v1/pixabay/images?page=${page}&query=${search_term}`);
return await res.json();
};
let fetched_data = fetch_data();
</script>
{#await fetched_data}
<Spinner />
{:then data}
{#if loading}
<Spinner />
{:else}
<div class="flex w-screen p-8 h-full mt-8 mb-1">
<div
class="flex flex-col w-1/3 m-auto overflow-scroll h-full rounded p-4 gap-2 bg-white dark:bg-gray-700"
>
<h1 class="text-2xl text-center">{$t('uploader.images_by_pixabay')}</h1>
<div class="flex">
<a href="https://pixabay.com" target="_blank" class="underline mx-auto"
>{$t('uploader.visit_pixabay')}</a
>
</div>
<form
class="w-full flex gap-2"
on:submit|preventDefault={() => (fetched_data = fetch_data())}
>
<input
class="w-full outline-none p-1 rounded dark:bg-gray-500 bg-gray-300"
bind:value={search_term}
/>
<div class="w-fit">
<BrownButton type="submit">{$t('words.search')}</BrownButton>
</div>
</form>
<span class="italic text-center text-sm">{$t('uploader.search_english_only')}</span>
{#each data.hits as image}
<div class="rounded border-2 border-[#B07156] p-2 flex-col flex gap-2">
<div>
<img
src={image.webformatURL}
loading="lazy"
alt="unavailable"
class="object-contain w-full h-full rounded max-h-[80vh]"
/>
</div>
<BrownButton
on:click={() => {
set_image(image.id);
}}>{$t('words.select')}</BrownButton
>
</div>
{/each}
</div>
</div>
{/if}
{/await}
+36
View File
@@ -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 Spinner from '$lib/Spinner.svelte';
export let files: {
id: string;
uploaded_at: string;
mime_type?: string;
hash?: string;
size: number;
deleted_at?: string;
alt_text?: string;
filename?: string;
thumbhash?: string;
server?: string;
quizzes: { id: string }[];
quiztivities: { id: string }[];
}[];
const get_files = async () => {
const res = await fetch('/api/v1/storage/list');
files = await res.json();
};
if (!files) {
get_files();
}
</script>
{#if files}{:else}
<Spinner />
{/if}
+2 -11
View File
@@ -68,22 +68,13 @@
/>
</svg>
<a
href="https://mastodon.online/@Mawoka"
href="https://fosstodon.org/@classquiz"
rel="me"
class="underline text-blue-300 hover:text-blue-500 transition"
>@Mawoka@mastodon.online</a
>@classquiz@fosstodon.org</a
> for updates!
</p>
</div>
<!-- to-[#8dc63f]-->
<span class="w-full h-0.5 bg-gradient-to-r from-[#009444] via-[#39b54a] to-[#8dc63f] block" />
<div class="flex justify-center bg-gray-700">
<p class="text-gray-400 text-center">
<i>Kahoot! and the K! logo are trademarks of Kahoot! AS</i>
</p>
</div>
</footer>
<!--
+2
View File
@@ -42,10 +42,12 @@ export const mint = async (
// eslint-disable-next-line no-constant-condition
while (true) {
// skipcq: JS-0003
const data = new TextEncoder().encode(`${challenge}:${counter.toString(16)}`);
const hashBuffer = await crypto.subtle.digest('SHA-1', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const digest = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
// skipcq: JS-0050
if (digest.slice(0, hex_digits) == zeros) {
result = counter.toString(16);
break;
+42 -2
View File
@@ -10,11 +10,51 @@ export const invertColor = (hexTripletColor: string): string => {
let color_int = parseInt(color, 16); // convert to integer
color_int = 0xffffff ^ color_int; // invert three bytes
color = color_int.toString(16); // convert to hex
color = ('000000' + color).slice(-6); // pad with leading zeros
color = '#' + color; // prepend #
color = `000000${color}`.slice(-6); // pad with leading zeros
color = `#${color}`; // prepend #
return color;
};
export const calculate_score = (q_time: number, time_taken: number): number => {
return q_time / time_taken;
};
export type RGB = [number, number, number];
export const getLuminance = (rgb: RGB): number => {
const [r, g, b] = rgb.map((v) => {
v /= 255;
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
});
return r * 0.2126 + g * 0.7152 + b * 0.0722;
};
export const getContrast = (foregroundColor: RGB, backgroundColor: RGB) => {
const foregroundLuminance = getLuminance(foregroundColor);
const backgroundLuminance = getLuminance(backgroundColor);
return backgroundLuminance < foregroundLuminance
? (backgroundLuminance + 0.05) / (foregroundLuminance + 0.05)
: (foregroundLuminance + 0.05) / (backgroundLuminance + 0.05);
};
export const getRgbColorFromHex = (hex: string): RGB => {
hex = hex.slice(1);
const value = parseInt(hex, 16);
// skipcq: JS-C1002
const r = (value >> 16) & 255;
// skipcq: JS-C1002
const g = (value >> 8) & 255;
// skipcq: JS-C1002
const b = value & 255;
return [r, g, b] as RGB;
};
export const get_foreground_color = (bg_color: string): 'black' | 'white' => {
const bg_rgb = getRgbColorFromHex(bg_color);
const white_rgb: RGB = [255, 255, 255];
const black_rgb: RGB = [0, 0, 0];
const black_contrast = getContrast(black_rgb, bg_rgb);
const white_contrast = getContrast(white_rgb, bg_rgb);
return black_contrast < white_contrast ? 'black' : 'white';
};
+3 -3
View File
@@ -5,7 +5,6 @@
*/
import i18next from 'i18next';
import translations from './translations';
import en from './locales/en.json';
import de from './locales/de.json';
import fr from './locales/fr.json';
@@ -19,11 +18,12 @@ import zh_Hant from './locales/zh_Hant.json';
import pl from './locales/pl.json';
import pt from './locales/pt.json';
import uk from './locales/uk.json';
import nl from './locales/nl.json';
// import uz from './locales/uz.json'
// import zh_Hans from './locales/zh_Hans.json';
import LanguageDetector from 'i18next-browser-languagedetector';
import type { i18n, Resource } from 'i18next';
import type { i18n } from 'i18next';
export class I18nService {
// expose i18next
@@ -49,7 +49,6 @@ export class I18nService {
fallbackLng: 'en',
debug: false,
defaultNS: 'translation',
resources: translations as Resource,
interpolation: {
escapeValue: false
},
@@ -79,6 +78,7 @@ export class I18nService {
this.i18n.addResourceBundle('pl', 'translation', pl);
this.i18n.addResourceBundle('pt', 'translation', pt);
this.i18n.addResourceBundle('uk', 'translation', uk);
this.i18n.addResourceBundle('nl', 'translation', nl);
// this.i18n.addResourceBundle('uz', 'translation', uz);
}
+1
View File
@@ -0,0 +1 @@
{}
+165 -8
View File
@@ -48,7 +48,8 @@
"community_driven_content": "ClassQuiz hängt von der Community ab, die ClassQuiz mit Spenden, Feature-Requests, Übersetzungen und mehr versorgt! Du kannst auch ein Teil der ClassQuiz-Community werden!",
"download_quizzes_content": "Quiz können als einzige Datei heruntergeladen werden und jederzeit importiert werden, sodass du auch einfach auf eine andere Instanz von ClassQuiz umziehen kannst!",
"community_driven": "Von der Community betrieben",
"download_quizzes": "Quiz herunterladen"
"download_quizzes": "Quiz herunterladen",
"how_does_classquiz_work": "Wie funktioniert ClassQuiz überhaupt?"
},
"overview_page": {
"created_at": "Erstellt am",
@@ -161,7 +162,29 @@
"score": "Punkte",
"results": "Ergebnisse",
"note": "Notiz",
"player_plural": "Spieler"
"player_plural": "Spieler",
"slide": "Folie",
"back": "Zurück",
"finish": "Fertig",
"name": "Name",
"point_plural": "Punkte",
"point": "Punkt",
"normal": "Normal",
"selected": "Ausgewählt",
"select": "Auswählen",
"next": "Weiter",
"quiz": "Quiz",
"quiztivity": "Quiztivity",
"check_choice": "Prüfauswahl",
"video": "Video",
"progress": "Fortschritt",
"files_library": "Dateien-Bibliothek",
"upload": "Upload",
"library": "Bibliothek",
"speed": "Geschwindigkeit",
"answer_plural": "Antworten",
"yes": "ja",
"no": "Nein"
},
"editor": {
"time_in_seconds": "Zeit in Sekunden",
@@ -170,7 +193,24 @@
"add_new_question": "Neue Frage hinzufügen",
"delete_question": "Frage löschen",
"delete_answer": "Antwort löschen",
"not_all_links_imgur_links": "Nicht alle Links sind Imgur-Links!"
"not_all_links_imgur_links": "Nicht alle Links sind Imgur-Links!",
"no_title": "Kein Titel...",
"empty": "Leer...",
"bg_image": "Hintergrundbild",
"slide": {
"text": "Text",
"text_description": "Kleiner langer Text",
"rectangle": "Rechteck",
"headline": "Überschrift",
"rectangle_description": "Nur ein Rechteck",
"headline_description": "Ein fetter Text als Überschrift"
},
"abcd_description": "Nur eine Antwort kann ausgewählt werden",
"voting_description": "Antworten geben keine Punkte",
"order_description": "Antworten müssen in die richtige Reihenfolge gebracht werden",
"text_description": "Spieler können text eingeben",
"range_description": "Ein Zahlenbereich kann mit einem Schieberegler ausgewählt werden",
"check_choice_description": "Alle richtigen Antworten müssen für Punkte ausgewählt werden"
},
"import": {
"need_help": "",
@@ -191,7 +231,12 @@
"start_by_showing_first_question": "Beginne damit, die erste Frage zu zeigen!",
"no_answers": "Keine Antworten!",
"stop_time": "Zeit stoppen",
"save_results": "Ergebnisse speichern"
"save_results": "Ergebnisse speichern",
"next_question": "Nächste Frage ({{question}})",
"show_results": "Ergebnisse anzeigen",
"enter_answer_into_field": "Gib die Antwort in das Eingabefeld ein!",
"stop_time_and_solutions": "Zeit stoppen und Ergebnisse zeigen",
"answers_submitted": "{{answer_count}} Antworten abgegen"
},
"import_page": {
"need_help": "Brauchst du Hilfe?",
@@ -214,7 +259,9 @@
"delete_this_session": "Diese Sitzung löschen",
"this_session?": "Diese Sitzung?",
"old_password": "Altes Passwort",
"new_password": "Neues Passwort"
"new_password": "Neues Passwort",
"change_avatar": "Avatar ändern",
"security_settings": "Sicherheitseinstellungen"
},
"explore_page": {
"made_by": "Erstellt von",
@@ -229,7 +276,12 @@
"end_sentence": "Das war's! Das war das Quiz.",
"1st_place": "1. Platz",
"2nd_place": "2. Platz",
"with_out_of": "mit {{correct_questions}} von insgesamt {{total_question_count}}"
"with_out_of": "mit {{correct_questions}} von insgesamt {{total_question_count}}",
"final_result_rank": "{{place}}. Platz: {{username}} mit {{points}} Punkten",
"your_score": "Deine Punktzahl: {{score}}",
"join_description": "Tritt bei unter {{url}} und gib {{pin}} ein.",
"join_by_entering_code": "Tritt bei, indem du folgenden Code eingibst",
"points_added": "Hinzugefügte Punkte"
},
"editor_page": {
"add_an_answer": "Antwort hinzufügen",
@@ -248,7 +300,11 @@
"unknown_error_text": "Das sollte nicht passieren. Es ist wahrscheinlich meine Schuld, oder du hast eine magische Fähigkeit, Fehler zu erschaffen..."
},
"uploader": {
"add_image": "Bild hinzufügen"
"add_image": "Bild hinzufügen",
"upload_a_video": "Ein Video hochladen",
"upload_video": "Video hochladen",
"upload_video_popup_notice": "Das Popup ist offen; guck dir es für weitere Infos an",
"select_upload_type": "Wähle den Upload-Typen aus"
},
"avatar_settings": {
"skin_color": "Hautfarbe",
@@ -264,7 +320,8 @@
"clothe_graphic_type": "Grafik",
"thats_you": "Das bist du!",
"start_over": "Neu anfangen",
"clothe_type": "Kleidung"
"clothe_type": "Kleidung",
"go_back": "Zurückkehren"
},
"results_page": {
"quiz_title": "Quiz-Titel",
@@ -279,5 +336,105 @@
"time_taken": "benötigte Zeit",
"player_name": "Spielername",
"correct_answer_plural": "{{count}} richtige Antworten"
},
"navbar": {
"donate": "Spenden"
},
"security_settings": {
"activate_2fa": "Zwei-Faktor-Authentifizierung aktivieren",
"2fa_activated": "Zwei-Faktor-Authentifizierung ist aktiviert",
"webauthn": "Webauthn",
"webauthn_available": "Webauthn ist verfügbar",
"webauthn_unavailable": "Webauthn ist nicht verfügbar",
"add_security_key": "Sicherheitsschlüssel hinzufügen",
"totp": "Totp",
"totp_available": "Totp ist verfügbar",
"enable_totp": "Totp aktivieren",
"backup_codes": {
"your_backup_code": "Dein Backup-Code",
"save_somewhere_save": "Speichere ihn an einem sicheren Ort!",
"download_code": "Code herunterladen"
},
"totp_setup": {
"do_not_forget_backup_code": "Vergiss nicht, deinen Backup-Code zu speichern!",
"scan_to_set_up": "Scanne diesen QR-Code um Totp einzurichten",
"enter_as_secret_if_no_see_code": "Gib dieses Geheimnis ein, wenn du den QR-Code nicht scannen kannst",
"totp_setup": "Totp-Einrichtung"
},
"backup_code": "Backup-Code",
"get_backup_code": "Backup-Code erhalten",
"2fa_deactivated": "Zwei-Faktor-Authentifizierung ist deaktiviert",
"totp_unavailable": "Totp ist nicht verfügbar",
"disable_totp": "Totp deaktivieren"
},
"view_quiz_page": {
"made_by": "Erstellt von",
"view_on_kahoot": "Auf Kahoot! ansehen"
},
"start_game": {
"start_game": "Spiel starten",
"old_school_mode": "Old-School",
"captcha_message": "Wenn aktiviert, Googles ReCaptcha wird in den Browsern der Spielerinnen und Spieler geladen. Aktiviere dies nur, wenn du es wirklich brauchst, da du die Einverständniserklärung JEDEN SPIELERS brauchst um die Captcha zu laden.",
"old_school_mode_description": "Fragen und Bilder werden sowohl auf dem Bildschirm des Admins als auch auf dem Bildschirm der Spielerinnen und Spieler angezeigt",
"normal_mode_description": "Fragen und Antwortmöglichkeiten werden nur auf dem Bildschirm des Admins angezeigt, wie bei Kahoot!. Die Spielerinnen und Spieler werden nur gefärbte Knöpfe mit passenden Symbolen haben."
},
"quiztivity": {
"editor": {
"select_page_type": "Seitentyp auswählen",
"move_left": "Nach links bewegen",
"add_new": "Hinzufügen",
"delete": "Löschen",
"open_shares_menu": "Teilen-Menü öffnen",
"shares": {
"add_new_share": "Neue Veröffentlichung hinzufügen",
"never_expires": "Läuft nie ab",
"expires_on": "Läuft ab am {{date}}"
},
"move_right": "Nach rechts bewegen",
"title_placeholder": "Titel hier eingeben"
},
"share_expired": "Veröffentlichung abgelaufen",
"memory": {
"editor": {
"add_card": "Karte hinzufügen",
"upload_image": "Bild hochladen",
"add_pair": "Paar hinzufügen"
}
},
"play": {
"memory": {
"try_count": "Versuche: {{try_count}}"
}
}
},
"components": {
"popover": {
"copied_to_clipboard": "In die Zwischenablage kopiert!"
}
},
"public_user_page": {
"joined_on": "Beigetreten am {{date}}",
"no_original_quizzes": "Dieser Nutzer hat keine originellen Quiz"
},
"file_dashboard": {
"not_available": "Nicht verfügbar",
"missing": "NICHT VORHANDEN!",
"unset": "Unbestimmt",
"size": "Größe: {{size}} Mib",
"caption": "Beschreibung: {{caption}}",
"filename": "Dateiname: {{filename}}",
"uploaded": "Hochgeladen am: {{date}}",
"Imported": "Importiert: {{yes_or_no}}",
"edit_details": "Details bearbeiten",
"edit_the_image": "Bild bearbeiten",
"filename_word": "Dateiname",
"storage_usage": "Du hast {{used}} Mib von {{total}} Mib benutzt. Das entspricht {{percent}}% deines Speichers.",
"delete_image": "Bild löschen",
"alt_text": "Alternativtext / Beschreibung",
"imported": "Importiert: {{yes_or_no}}"
},
"video_uploader": {
"time_elapsed": "Abgelaufene Zeit",
"time_remaining": "Zeit übrig"
}
}
+180 -22
View File
@@ -2,14 +2,9 @@
"index_page": {
"slogan": "The open-source quiz-platform!",
"meta": {
"description": "ClassQuiz is a quiz app like Kahoot! for students, which is open source and free to use",
"description": "ClassQuiz is a quiz app to learn interactively for students, which is open source and free to use",
"title": "Home"
},
"features_description": {
"1": "ClassQuiz is a quiz-platform that allows you to create and manage quizzes.",
"2": "The main feature is a Kahoot!-import function that allows you to import quizzes from Kahoot!-quizzes.",
"3": "The editor and function of exporting quiz results as Excel files are particular highlights of the software."
},
"stats": "There are already {{user_count}} users and {{quiz_count}} quizzes on ClassQuiz.",
"see_what_true_and_false": "See what was right or wrong",
"see_how_many_true_and_false": "See how many were right or wrong",
@@ -28,7 +23,6 @@
"get_a_quiz": "1. Get a quiz",
"create_a_quiz_from_scratch": "Create a quiz from scratch with the editor and include pictures and more",
"find_or_explore": "Find (or explore) quizzes made or imported by other people",
"import_quiz_from_kahoot_and_edit": "Import a quiz from Kahoot! and edit it in ClassQuiz",
"play_quiz": "2. Play the quiz",
"select_answer": "Select the answer",
"choose_answer_wisely": "Choose your answer wisely",
@@ -37,7 +31,7 @@
"list_winners": "List winners",
"get_ranking_and_winners": "Get the ranking and see who won",
"why_classquiz": "Why ClassQuiz?",
"no_tracking_content": "Kahoot! tracks you and sends that info to third-parties, but ClassQuiz doesn't.",
"no_tracking_content": "Others track you and sends that info to third-parties, but ClassQuiz doesn't.",
"self_hostable_content": "ClassQuiz can easily be self-hosted, so the data is only in your control!",
"user_friendly_content": "ClassQuiz aims to be simple, so it is can be used by everyone.",
"completely_free_content": "ClassQuiz is completely cost free (for the user), without any paid plans or annoying redirects to the upgrade-page. Donations are highly appreciated.",
@@ -48,12 +42,13 @@
"download_quizzes": "Download Quizzes",
"download_quizzes_content": "Quizzes can be downloaded as one file and imported at any time. This also lets you move your quizzes to other ClassQuiz instances.",
"community_driven": "Community-driven",
"community_driven_content": "ClassQuiz depends on its community for funding, testing ideas, feature requests, translations and more! You can also be a part of the ClassQuiz-community!"
"community_driven_content": "ClassQuiz depends on its community for funding, testing ideas, feature requests, translations and more! You can also be a part of the ClassQuiz-community!",
"how_does_classquiz_work": "How does ClassQuiz even work?"
},
"overview_page": {
"created_at": "Created at",
"question_count": "Question count",
"no_quizzes": "Click the \"Create\"-button, or import a quiz from Kahoot! to get going."
"no_quizzes": "Click the \"Create\"-button, or import a quiz to get going."
},
"edit_page": {
"success_update_title": "Quiz updated.",
@@ -157,16 +152,37 @@
"backup_code": "Backup-code",
"totp": "Totp",
"text": "Text",
"order": "order",
"order": "Order",
"results": "Results",
"note": "Note",
"player_plural": "Players",
"score": "Score",
"version": "Version",
"never": "Never",
"unknown": "Unknown",
"update": "Update",
"score": "Score",
"slide": "Slide",
"name": "Name",
"update": "Update"
"point": "Point",
"point_plural": "Points",
"back": "Back",
"finish": "Finish",
"normal": "Normal",
"selected": "Selected",
"select": "Select",
"quiz": "Quiz",
"quiztivity": "Quiztivity",
"next": "Next",
"check_choice": "Check Choice",
"video": "Video",
"library": "Library",
"progress": "Progress",
"speed": "Speed",
"upload": "Upload",
"files_library": "Files Library",
"answer_plural": "Answers",
"yes": "Yes",
"no": "no"
},
"editor": {
"time_in_seconds": "Time in seconds",
@@ -175,7 +191,24 @@
"add_new_question": "Add new question",
"delete_question": "Delete question",
"delete_answer": "Delete answer",
"not_all_links_imgur_links": "Not all links are Imgur-links!"
"not_all_links_imgur_links": "Not all links are Imgur-links!",
"no_title": "No title...",
"empty": "Empty...",
"bg_image": "Background image",
"slide": {
"headline": "Headline",
"headline_description": "A bold text for headlines",
"text": "Text",
"text_description": "Smaller longer text",
"rectangle": "Rectangle",
"rectangle_description": "Just a rectangle"
},
"abcd_description": "Only one answer can be chosen",
"voting_description": "Answers don't add any points",
"check_choice_description": "All correct answers have to be chosen to score points",
"order_description": "Answers can be brought into the correct order",
"text_description": "Players can enter text",
"range_description": "A number-range can be selected with a slider"
},
"import_page": {
"need_help": "Need help?",
@@ -194,12 +227,18 @@
"get_results": "Get results",
"get_results_and_stop_time": "Get results and stop time",
"get_final_results": "Get final results",
"export_results": "Export results",
"show_next_question": "Show next question",
"start_by_showing_first_question": "Start by showing the first question.",
"no_answers": "No answers!",
"stop_time": "Stop time",
"save_results": "Save results"
"save_results": "Save results",
"next_question": "Next Question ({{question}})",
"show_results": "Show results",
"stop_time_and_solutions": "Stop time and show solutions",
"enter_answer_into_field": "Enter your answer into the input field!",
"answers_submitted": "{{answer_count}} Answers submitted",
"request_export_results": "Request result download",
"download_export_results": "Download results"
},
"password_reset_page": {
"reset_password": "Reset password"
@@ -212,7 +251,10 @@
"last_seen": "Last seen",
"check_location": "Check location",
"delete_this_session": "Delete this session",
"this_session?": "This session?"
"this_session?": "This session?",
"change_avatar": "Change avatar",
"security_settings": "Security-Settings",
"add_api_key": "Add API key"
},
"explore_page": {
"made_by": "Made by",
@@ -227,7 +269,12 @@
"1st_place": "1st Place",
"2nd_place": "2nd Place",
"3rd place": "3rd Place",
"with_out_of": "with {{correct_questions}} out of {{total_question_count}}"
"with_out_of": "with {{correct_questions}} out of {{total_question_count}}",
"final_result_rank": "{{place}}: {{username}} with {{points}} points",
"your_score": "Your score: {{score}}",
"join_description": "Join at {{url}} and enter {{pin}}.",
"join_by_entering_code": "Join by entering the following code",
"points_added": "Points added"
},
"editor_page": {
"add_an_answer": "Add an answer",
@@ -246,7 +293,14 @@
"unknown_error_text": "That shouldn't happen. It's probably my fault, not yours, but maybe you have a magical power to break stuff..."
},
"uploader": {
"add_image": "Add image"
"add_image": "Add image",
"select_upload_type": "Select the Upload Type",
"upload_a_video": "Upload a Video",
"upload_video_popup_notice": "The popup is open; have a look at it for further information",
"upload_video": "Upload Video",
"visit_pixabay": "Visit Pixabay",
"images_by_pixabay": "Images provided by Pixabay",
"search_english_only": "The search works in English only"
},
"avatar_settings": {
"skin_color": "Skin color",
@@ -262,13 +316,17 @@
"clothe_color": "Clothing color",
"clothe_graphic_type": "Graphic",
"thats_you": "That's You!",
"start_over": "Start over"
"start_over": "Start over",
"go_back": "Go back"
},
"results_page": {
"no_results_so_far": "No results saved so far...",
"quiz_title": "Quiz Title",
"date_played": "Date Played",
"player_count": "Player count"
"player_count": "Player count",
"general_overview": {
"sentence": "The quiz \"{{title}}\", which was played on {{date}} had {{player_count}} players with an average score of {{average_score}}."
}
},
"result_page": {
"player_name": "Player name",
@@ -276,7 +334,8 @@
"average_score": "Average score: {{average_score}}",
"correct_answer": "{{count}} correct answer",
"correct_answer_plural": "{{count}} correct answers",
"time_taken": "Time taken"
"time_taken": "Time taken",
"player_score": "Player Score"
},
"controllers": {
"add_new_controller": "Add new controller",
@@ -288,5 +347,104 @@
"already_latest_version": "You're already on the latest version",
"cancel_update": "Cancel Update!",
"update_from_to": "Update from {{current_version}} to {{newest_version}}"
},
"navbar": {
"donate": "Donate"
},
"security_settings": {
"backup_code": "Backup-Code",
"get_backup_code": "Get Backup-Code",
"activate_2fa": "Activate Two Factor Authentication",
"2fa_activated": "Two Factor authentication is activated",
"2fa_deactivated": "Two Factor authentication is deactivated",
"webauthn": "Webauthn",
"webauthn_available": "Webauthn is available",
"webauthn_unavailable": "Webauthn is not available",
"add_security_key": "Add Security-Key",
"totp": "Totp",
"totp_available": "Totp is available",
"totp_unavailable": "Totp is not available",
"disable_totp": "Disable Totp",
"enable_totp": "Enable Totp",
"backup_codes": {
"your_backup_code": "Your Backup-Code",
"save_somewhere_save": "Save this somewhere safe!",
"download_code": "Download code"
},
"totp_setup": {
"scan_to_set_up": "Scan this QR-code to set up the code",
"enter_as_secret_if_no_see_code": "Enter this as the secret if you can't scan the QR-code",
"totp_setup": "Totp-Setup",
"do_not_forget_backup_code": "Do not forget to save your recovery-code!"
}
},
"view_quiz_page": {
"made_by": "Made by",
"view_on_kahoot": "View on the original"
},
"start_game": {
"captcha_message": "If enabled, Google's ReCaptcha will load in the browser of players. Only enable if you really need it, since you need the consent of EVERY player to load the captcha.",
"normal_mode_description": "Question and answer will only be shown on admins screen. The players will only have colored buttons with symbols matching these on the screen of the admin.",
"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!"
}
},
"public_user_page": {
"joined_on": "Joined on {{date}}",
"no_original_quizzes": "This user doesn't have any original quizzes"
},
"file_dashboard": {
"not_available": "Not available",
"missing": "MISSING!",
"unset": "Unset",
"size": "Size: {{size}} Mib",
"caption": "Caption: {{caption}}",
"filename": "Filename: {{filename}}",
"uploaded": "Uploaded: {{date}}",
"imported": "Imported: {{yes_or_no}}",
"edit_details": "Edit details",
"delete_image": "Delete image",
"edit_the_image": "Edit the image",
"filename_word": "Filename",
"alt_text": "Alt(ernate) text / Caption",
"storage_usage": "You've used {{used}} Mib out of {{total}} MiB of storage. That's equivalent to {{percent}}% of your storage."
},
"video_uploader": {
"time_elapsed": "Time elapsed",
"time_remaining": "Time remaining"
}
}
+191 -27
View File
@@ -7,8 +7,8 @@
},
"features_description": {
"1": "ClassQuiz es una plataforma de quiz que permite crear y gestionar quiz.",
"2": "La principal funcionalidad es una función de importación de Kahoot! que permite importar cuestionarios de Kahoot!",
"3": "El editor fácil de usar es un punto destacado, al igual que la función de exportación para descargar los resultados de las pruebas como archivos de Excel."
"2": "La característica principal es una función de importación de Kahoot! que le permite importar cuestionarios desde Kahoot!-quizzes.",
"3": "El editor y la función de exportar los resultados de los cuestionarios como archivos Excel son aspectos especialmente destacados del programa."
},
"stats": "Ya hay {{user_count}} usuarios y {{quiz_count}} cuestionarios en ClassQuiz.",
"see_what_true_and_false": "Ver lo que estaba correcto o incorrecto",
@@ -16,20 +16,20 @@
"get_a_quiz": "1. Haz un quiz",
"create_a_quiz_from_scratch": "Crea un quiz desde cero con el editor e incluye imágenes y más",
"find_or_explore": "Encuentra (o explora) quizzes hechos o importados por otras personas",
"import_quiz_from_kahoot_and_edit": "¡Importa un cuestionario de Kahoot! y editarlo en ClassQuiz",
"import_quiz_from_kahoot_and_edit": "Importar un cuestionario de Kahoot! y editarlo en ClassQuiz",
"no_tracking": "Sin rastreo",
"german_server": "Servidor alemán",
"user_friendly": "Fácil de usar",
"completely_free": "Totalmente gratis",
"quiz_results_downloadable": "Los resultados de los cuestionarios se pueden descargar",
"multilingual": "Multilingüe",
"completely_free_content": "ClassQuiz es gratuito para el usuario, sin planes pagos ni redireccionamientos para una versión paga. Por lo tanto, cualquier donación es apreciada.",
"completely_free_content": "ClassQuiz es completamente gratuito (para el usuario), sin planes de pago ni molestas redirecciones a la página de actualización. Las donaciones son muy apreciadas.",
"see_how_many_true_and_false": "Ver cuántos estaban correctos o equivocados",
"create_or_import": "Crear o importar",
"see_all_quizzes": "Ver todos tus cuestionarios",
"teachers_site": "Portal de profesores",
"students_site": "Portal estudiante",
"multilingual_content": "ClassQuiz ya está completamente disponible en inglés, alemán, turco, francés, bokmål noruego e italiano, mientras que también está disponible parcialmente en indonesio y catalán.",
"multilingual_content": "ClassQuiz está disponible en Inglés, Francés, Alemán, Italiano, Bokmål Noruego, Turco y, en parte, Indonesio y Catalán.",
"select_answer": "Selecciona la respuesta",
"view_results": "Ver los resultados",
"check_if_chosen_wisely": "Comprueba, si has elegido bien",
@@ -37,18 +37,19 @@
"get_ranking_and_winners": "Consigue la clasificación y mira quién ha ganado",
"why_classquiz": "¿Por qué ClassQuiz?",
"self_hostable_content": "ClassQuiz puede ser fácilmente auto-alojado, por lo que los datos sólo están bajo tu control!",
"user_friendly_content": "ClassQuiz está diseñado para ser simple y fácil de usar para todos.",
"user_friendly_content": "ClassQuiz pretende ser sencillo, para que todo el mundo pueda utilizarlo.",
"quiz_results_downloadable_content": "Los resultados de los cuestionarios se pueden exportar fácilmente a una hoja de cálculo de Excel. (No sabía que otros no pudieran hacerlo)",
"dark_mode_content": "Una de las funcionalidades más importantes que puede tener un sitio web!",
"german_server_content": "Los servidores de ClassQuiz se encuentran en Alemania y están alojados con netcup.",
"play_quiz": "2. Haz el quiz",
"choose_answer_wisely": "Elige bien tu respuesta",
"no_tracking_content": "Kahoot! rastrea y comparte su perfil con terceros, pero ClassQuiz no lo hace.",
"no_tracking_content": "Kahoot! te rastrea y envía esa información a terceros, pero ClassQuiz no lo hace.",
"self_hostable": "Autohospedable",
"download_quizzes": "Descargar cuestionarios",
"community_driven_content": "¡ClassQuiz depende de la comunidad que proporciona ClassQuiz con donaciones, solicitudes de funciones, traducciones y más! ¡También puede convertirse en parte de la comunidad de ClassQuiz!",
"community_driven_content": "¡ClassQuiz depende de su comunidad para financiar, probar ideas, solicitar nuevas funciones, traducciones y más! ¡También puedes ser parte de la comunidad ClassQuiz!",
"community_driven": "Impulsado por la comunidad",
"download_quizzes_content": "Los cuestionarios se pueden descargar como un solo archivo e importar en cualquier momento, lo que le permite mover fácilmente sus cuestionarios a otra instancia de ClassQuiz."
"download_quizzes_content": "Los cuestionarios pueden descargarse como un archivo e importarse en cualquier momento. Esto también te permite mover tus cuestionarios a otras instancias de ClassQuiz.",
"how_does_classquiz_work": "¿Cómo funciona ClassQuiz?"
},
"create_page": {
"success": {
@@ -59,7 +60,7 @@
"login_page": {
"modal": {
"success": {
"success_check_mail": "Conectado. Por favor revise su bandeja de entrada de correo electrónico.",
"success_check_mail": "Conectado. Comprueba tu bandeja de entrada del correo electrónico.",
"description": {
"success_check_mail": "Verifique su buzón de correo ya que debería haber recibido un correo electrónico con un enlace para iniciar sesión.",
"success": "Conectado."
@@ -67,24 +68,24 @@
"success": "Conectado."
},
"error": {
"wrong_creds": "Dirección de correo electrónico o contraseña incorrecta.",
"wrong_creds": "Dirección de correo electrónico o contraseña incorrectas.",
"unexpected": "¡Error inesperado!",
"description": {
"wrong_creds": "Por favor, asegúrese de que su contraseña y dirección de correo electrónico sean correctas.",
"wrong_creds": "Por favor, asegúrate de que tu contraseña y tu dirección de correo electrónico son correctas.",
"unexpected": "Se produjo el típico error inesperado!"
}
}
},
"welcome_back": "Bienvenido de nuevo.",
"login_or_create_account": "Ingresar o Crear una cuenta",
"login_or_create_account": "Conectarse o crear una cuenta",
"already_have_account": "¿No tienes una cuenta?",
"use_backup_code": "Usar el código de la copia de seguridad",
"email_or_username": "Correo electrónico o nombre de usuario"
"use_backup_code": "Utilizar el código de seguridad",
"email_or_username": "Correo electrónico o nombre del usuario"
},
"overview_page": {
"created_at": "Creado en",
"question_count": "Recuento de preguntas",
"no_quizzes": "Haga clic en el botón \"Crear\" o importe un cuestionario de Kahoot. para ponerse en marcha."
"no_quizzes": "Haz clic en el botón \"Crear\" o importa un cuestionario de Kahoot! para empezar."
},
"edit_page": {
"success_update_title": "Cuestionario actualizado.",
@@ -121,7 +122,7 @@
"stats": "Estadísticas",
"features": "Características",
"login": "Iniciar sesión",
"email": "Dirección de correo electrónico",
"email": "Correo electrónico",
"username": "Nombre de usuario",
"count": "Cuenta",
"range": "Zona",
@@ -157,11 +158,33 @@
"backup_code": "Código de la copia de seguridad",
"totp": "contraseña de un solo uso (Totp)",
"text": "Texto",
"order": "solicitar",
"order": "Ordenar",
"results": "Resultados",
"note": "Nota",
"player_plural": "Jugadores",
"score": "Puntuación"
"score": "Puntuación",
"slide": "Deslizar",
"name": "Nombre",
"point": "Punto",
"point_plural": "Puntos",
"back": "Atrás",
"finish": "Finalizar",
"normal": "Normal",
"selected": "Seleccionado",
"select": "Seleccionar",
"quiz": "Cuestionario",
"quiztivity": "Quiztivity",
"next": "Siguiente",
"check_choice": "Comprobar la elección",
"video": "Vídeo",
"library": "Biblioteca",
"progress": "Progreso",
"speed": "Velocidad",
"upload": "Subir",
"files_library": "Biblioteca de archivos",
"yes": "Sí",
"answer_plural": "Respuestas",
"no": "no"
},
"admin_page": {
"export_results": "Exportar resultados",
@@ -175,7 +198,14 @@
"get_final_results": "Ver los resultados finales",
"start_by_showing_first_question": "Comienza mostrando la primera pregunta.",
"no_answers": "¡No hay respuestas!",
"save_results": "Guardar los resultados"
"save_results": "Guardar los resultados",
"next_question": "Siguiente pregunta ({{question}})",
"show_results": "Mostrar los resultados",
"stop_time_and_solutions": "Detener el tiempo y mostrar los resultados",
"enter_answer_into_field": "¡Ingresa tu respuesta en el campo de entrada!",
"answers_submitted": "{{answer_count}} Respuestas enviadas",
"download_export_results": "Descargar los resultados",
"request_export_results": "Solicitar la descarga de los resultados"
},
"settings_page": {
"check_location": "Comprobar ubicación",
@@ -185,14 +215,22 @@
"last_seen": "Visto por última vez",
"delete_this_session": "Borrar esta sesión",
"this_session?": "¿Esta sesión?",
"old_password": "Contraseña antigua"
"old_password": "Contraseña antigua",
"change_avatar": "Cambiar el avatar",
"security_settings": "Configuraciones de seguridad",
"add_api_key": "Añadir la clave de la API"
},
"play_page": {
"2nd_place": "2º puesto",
"end_sentence": "¡Eso es todo! Este fue el cuestionario.",
"1st_place": "1er puesto",
"3rd place": "3er puesto",
"with_out_of": "con {{correct_questions}} de {{total_question_count}}"
"with_out_of": "con {{correct_questions}} de {{total_question_count}}",
"final_result_rank": "{{place}}: {{username}} con {{points}} puntos",
"your_score": "Tú puntuación: {{score}}",
"join_description": "Únete en {{url}} e ingresa {{pin}}.",
"join_by_entering_code": "Únete ingresando el siguiente código",
"points_added": "Puntos agregados"
},
"editor_page": {
"right_click_to_delete": "Haz clic con el botón derecho en una respuesta para eliminarla!",
@@ -214,7 +252,24 @@
"not_all_links_imgur_links": "¡No todos los enlaces son de Imgur!",
"right_or_true?": "¿Cierto?",
"add_new_answer": "Añadir nueva respuesta",
"time_in_seconds": "Tiempo en segundos"
"time_in_seconds": "Tiempo en segundos",
"slide": {
"headline": "Titular",
"headline_description": "Texto en negrita para los titulares",
"text": "Texto",
"text_description": "Texto más corto y largo",
"rectangle": "Rectángulo",
"rectangle_description": "Solo un rectangulo"
},
"no_title": "Sin título...",
"empty": "Vacío...",
"bg_image": "Imagen de fondo",
"abcd_description": "Solo se puede seleccionar una respuesta",
"voting_description": "Las respuestas no suman puntos",
"order_description": "Las respuestas se pueden poner en el orden correcto",
"text_description": "Los jugadores pueden ingresar texto",
"range_description": "Se puede seleccionar un rango de números con un control deslizante",
"check_choice_description": "Todas las respuestas correctas deben seleccionarse para obtener puntos"
},
"import_page": {
"need_help": "¿Necesitas ayuda?",
@@ -241,7 +296,11 @@
"search_for_own_quizzes": "Busca tus propios quizzes"
},
"uploader": {
"add_image": "Añadir una imagen"
"add_image": "Añadir una imagen",
"select_upload_type": "Seleccione el tipo de carga",
"upload_a_video": "Cargar un vídeo",
"upload_video_popup_notice": "La ventana emergente está abierta; échale un vistazo para obtener más información",
"upload_video": "Subir un vídeo"
},
"avatar_settings": {
"skin_color": "Color de la piel",
@@ -257,13 +316,17 @@
"clothe_type": "Ropa",
"eyebrow_type": "Cejas",
"clothe_graphic_type": "Gráficos",
"start_over": "Empezar de nuevo"
"start_over": "Empezar de nuevo",
"go_back": "Regresar"
},
"results_page": {
"quiz_title": "Título del cuestionario",
"date_played": "Fecha del juego",
"player_count": "Número de jugadores",
"no_results_so_far": "No hay resultados guardados hasta ahora..."
"no_results_so_far": "No hay resultados guardados hasta ahora...",
"general_overview": {
"sentence": "El cuestionario \"{{title}}\", al que se jugó el {{date}}, tenía {{player_count}} jugadores con una puntuación media de {{average_score}}."
}
},
"result_page": {
"player_name": "Nombre del jugador",
@@ -271,6 +334,107 @@
"correct_answer": "{{count}} respuesta correcta",
"correct_answer_plural": "{{count}} respuestas correctas",
"time_taken": "Tiempo invertido",
"average_score": "Puntuación media: {{average_score}}"
"average_score": "Puntuación media: {{average_score}}",
"player_score": "Puntuación del jugador"
},
"navbar": {
"donate": "Donar"
},
"security_settings": {
"2fa_deactivated": "La verificación en dos pasos está desactivada",
"backup_codes": {
"your_backup_code": "Tu código de seguridad",
"download_code": "Descargar el código",
"save_somewhere_save": "¡Guárdalo en un lugar seguro!"
},
"totp_setup": {
"scan_to_set_up": "Escanea este código QR para configurar el código",
"enter_as_secret_if_no_see_code": "Introduce este secreto si no puedes escanear el código QR",
"totp_setup": "Configuración de Totp",
"do_not_forget_backup_code": "¡No olvides guardar tu código de recuperación!"
},
"backup_code": "Código de seguridad",
"get_backup_code": "Obtener el código de la copia de seguridad",
"activate_2fa": "Habilitar la verificación en dos pasos",
"2fa_activated": "La verificación en dos pasos está activada",
"webauthn": "WebAuthn",
"webauthn_unavailable": "WebAuthn no está disponible",
"webauthn_available": "WebAuthn está disponible",
"add_security_key": "Agregar una clave de seguridad",
"totp": "contraseña de un solo uso (OTP)",
"totp_available": "Totp está disponible",
"totp_unavailable": "Totp no está disponible",
"disable_totp": "Desactivar Totp",
"enable_totp": "Activar Totp"
},
"view_quiz_page": {
"made_by": "Hecho por",
"view_on_kahoot": "¡Ver en Kahoot!"
},
"start_game": {
"captcha_message": "Si está habilitado, el ReCaptcha de Google se cargará en el navegador de los jugadores. Habilítalo solo si realmente lo necesitas, ya que necesitas el consentimiento de TODOS los jugadores para cargar el captcha.",
"normal_mode_description": "¡La pregunta y la respuesta solo se mostrarán en la pantalla de administración como en Kahoot! Los jugadores solo tendrán botones de colores con símbolos correspondientes a los de la pantalla de administración.",
"old_school_mode_description": "Las preguntas y las imágenes se muestran tanto en la pantalla del administrador como en la pantalla del jugador",
"old_school_mode": "De la vieja escuela",
"start_game": "Iniciar el Juego"
},
"quiztivity": {
"memory": {
"editor": {
"upload_image": "Subir una imagen",
"add_card": "Añadir una tarjeta",
"add_pair": "Añadir un par"
}
},
"editor": {
"title_placeholder": "Escribe el título aquí",
"shares": {
"expires_on": "Caduca el {{date}}",
"add_new_share": "Añadir una nueva acción",
"never_expires": "No caduca nunca"
},
"select_page_type": "Selecciona el tipo de página",
"move_left": "Mover hacia la izquierda",
"move_right": "Mover hacia la derecha",
"add_new": "Añadir nuevo",
"delete": "Borrar",
"open_shares_menu": "Abrir el menú Acciones"
},
"play": {
"memory": {
"try_count": "Intentos: {{try_count}}"
}
},
"share_expired": "Expiró lo compartido"
},
"components": {
"popover": {
"copied_to_clipboard": "¡Copiado al portapapeles!"
}
},
"public_user_page": {
"no_original_quizzes": "Este usuario no tiene cuestionarios originales",
"joined_on": "Ingresó el {{date}}"
},
"file_dashboard": {
"not_available": "No disponible",
"missing": "¡DESAPARECIDO!",
"unset": "Desactivar",
"size": "Tamaño: {{size}} Mib",
"uploaded": "Subido: {{date}}",
"Imported": "Importado: {{yes_or_no}}",
"edit_details": "Editar los detalles",
"delete_image": "Borrar la imagen",
"edit_the_image": "Editar la imagen",
"filename_word": "Nombre del archivo",
"alt_text": "Texto alternativo / Leyenda",
"caption": "Leyenda: {{caption}}",
"filename": "Nombre del archivo: {{filename}}",
"storage_usage": "Has utilizado {{used}} Mib de {{total}} MiB del almacenamiento. Eso equivale al {{percent}}% de tu almacenamiento.",
"imported": "Importado: {{yes_or_no}}"
},
"video_uploader": {
"time_elapsed": "Tiempo transcurrido",
"time_remaining": "Duración restante"
}
}
+214 -9
View File
@@ -20,7 +20,9 @@
},
"welcome_back": "Bon retour parmi nous.",
"login_or_create_account": "Se connecter ou créer un compte",
"already_have_account": "Pas encore de compte ?"
"already_have_account": "Pas encore de compte ?",
"email_or_username": "Email ou Nom d'utilisateur",
"use_backup_code": "Utilisez un code de sauvegarde"
},
"words": {
"question": "Question",
@@ -78,7 +80,38 @@
"practice": "Pratiquer",
"error": "Erreur",
"voting": "Vote",
"download": "Télécharger"
"download": "Télécharger",
"text": "Texte",
"order": "Ordre",
"normal": "Normal",
"totp": "TOTP",
"player_plural": "Joueurs",
"name": "Nom",
"point": "Point",
"continue": "Continuer",
"backup_code": "Code de sauvegarde",
"results": "Résultats",
"note": "Note",
"score": "Score",
"slide": "Diapositive",
"point_plural": "Points",
"back": "Retour",
"finish": "Finir",
"selected": "Séléctionné",
"select": "Sélectionner",
"quiz": "Quiz",
"answer_plural": "Réponses",
"yes": "Oui",
"no": "Non",
"quiztivity": "Quiztivité",
"next": "Suivant",
"check_choice": "Choix unique",
"progress": "Progrès",
"video": "Vidéo",
"library": "Bibliothèque",
"speed": "Vitesse",
"upload": "Charger",
"files_library": "Bibliothèque de fichiers"
},
"index_page": {
"slogan": "La plateforme de quiz open-source !",
@@ -116,7 +149,7 @@
"list_winners": "Lister les gagnants",
"get_ranking_and_winners": "Avoir le classement et voir les gagnants",
"why_classquiz": "Pourquoi ClassQuiz ?",
"no_tracking_content": "Si Kahoot! vous espionne avec au moins 2 outils tiers étatsuniens, Classquiz lui ne vous trace pas du tout !",
"no_tracking_content": "Kahoot! vous trace et envoie vos données à des services tiers, mais pas Classquiz.",
"self_hostable_content": "ClassQuiz est facilement auto-hébergeable, vos données sont sous contrôle!",
"user_friendly_content": "ClassQuiz se veut simple d'utilisation, de manière à être utilisable par le plus grand nombre.",
"import_quiz_from_kahoot_and_edit": "Importer un quiz depuis Kahoot! et l'éditer sur ClassQuiz",
@@ -129,7 +162,8 @@
"download_quizzes": "Télécharger des quiz",
"community_driven": "Piloté par la communauté",
"download_quizzes_content": "Les quiz peuvent être téléchargés en un seul fichier et importés à tout moment, ce qui vous permet de déplacer facilement vos quiz vers une autre instance de ClassQuiz.",
"community_driven_content": "ClassQuiz dépend de sa communauté pour le financement, les idées de tests, les demandes de fonctionnalités, les traductions et plus encore ! Vous pouvez aussi faire partie de la communauté ClassQuiz !"
"community_driven_content": "ClassQuiz dépend de sa communauté pour le financement, les idées de tests, les demandes de fonctionnalités, les traductions et plus encore ! Vous pouvez aussi faire partie de la communauté ClassQuiz !",
"how_does_classquiz_work": "Comment fonctionne ClassQuiz ?"
},
"overview_page": {
"created_at": "Crée le",
@@ -159,7 +193,24 @@
"delete_question": "Supprimer la question",
"delete_answer": "Supprimer la réponse",
"right_or_true?": "Vrai?",
"not_all_links_imgur_links": "Tout les liens ne sont pas des liens Imgur!"
"not_all_links_imgur_links": "Tout les liens ne sont pas des liens Imgur!",
"bg_image": "Image d'arrière plan",
"slide": {
"rectangle": "Rectangle",
"rectangle_description": "Juste un rectangle",
"text_description": "Texte plus petit et plus long",
"headline": "Titre",
"headline_description": "Un texte en gras pour les titres",
"text": "Texte"
},
"abcd_description": "Une seule réponse peut être choisie",
"voting_description": "Les réponses n'ajoutent aucun point",
"order_description": "Les réponses peuvent être mises dans le bon ordre",
"text_description": "Les joueurs peuvent saisir du texte",
"range_description": "Une plage de chiffres peut être sélectionnée à l'aide d'un curseur.",
"empty": "Vide...",
"no_title": "Aucun titre...",
"check_choice_description": "Toutes les réponses correctes doivent être choisies pour marquer des points."
},
"import_page": {
"need_help": "Besoin d'aide ?",
@@ -182,7 +233,15 @@
"show_next_question": "Montrer la prochaine question",
"start_by_showing_first_question": "Démarrer en montrant la première question.",
"no_answers": "Pas de réponses !",
"stop_time": "Arrêter le temps"
"stop_time": "Arrêter le temps",
"next_question": "Question suivante ({{question}})",
"show_results": "Montrer les résultats",
"stop_time_and_solutions": "Arrêter le temps et montrer des solutions",
"enter_answer_into_field": "Saisissez votre réponse dans le champ de saisie !",
"answers_submitted": "{{answer_count}} Réponses soumises",
"request_export_results": "Demande de téléchargement des résultats",
"save_results": "Sauvegarder les résultats",
"download_export_results": "Télécharger les résultats"
},
"password_reset_page": {
"reset_password": "Réinitialiser le mot de passe"
@@ -195,7 +254,10 @@
"last_seen": "Dernière consultation",
"check_location": "Vérifier la localisation",
"delete_this_session": "Supprimer cette session",
"this_session?": "Cette session ?"
"this_session?": "Cette session ?",
"security_settings": "Paramètres de sécurité",
"change_avatar": "Changer d'avatar",
"add_api_key": "Ajouter une clé API"
},
"explore_page": {
"made_by": "Créé par",
@@ -210,7 +272,12 @@
"end_sentence": "C'est terminé ! Voilà le quizz.",
"2nd_place": "2ème place",
"3rd place": "3ème place",
"with_out_of": "avec {{correct_questions}} sur {{total_question_count}}"
"with_out_of": "avec {{correct_questions}} sur {{total_question_count}}",
"join_description": "Inscrivez-vous sur {{url}} et entrez {{pin}}.",
"join_by_entering_code": "Participez en entrant le code suivant",
"points_added": "Points ajoutés",
"your_score": "Votre score : {{score}}",
"final_result_rank": "{{place}} : {{nom d'utilisateur}} avec {{points}} points"
},
"editor_page": {
"add_an_answer": "Ajouter une réponse",
@@ -229,6 +296,144 @@
"unknown_error_text": "Cela ne devrait pas arriver. C'est probablement ma faute, pas la tienne, mais peut-être que tu as un pouvoir magique pour casser les choses..."
},
"uploader": {
"add_image": "Ajouter une image"
"add_image": "Ajouter une image",
"select_upload_type": "Sélectionnez le type de téléchargement",
"upload_a_video": "Télécharger une vidéo",
"upload_video": "Télécharger la vidéo",
"upload_video_popup_notice": "La fenêtre contextuelle est ouverte ; jetez-y un coup d'œil pour obtenir de plus amples informations."
},
"quiztivity": {
"editor": {
"delete": "Supprimer",
"shares": {
"never_expires": "N'expire jamais",
"add_new_share": "Ajouter un nouveau partage",
"expires_on": "Expire le {{date}}"
},
"move_left": "Déplacer vers la gauche",
"move_right": "Déplacer à droite",
"select_page_type": "Sélectionner le type de page",
"add_new": "Ajouter un nouveau",
"title_placeholder": "Saisir le titre ici",
"open_shares_menu": "Ouvrir le menu Partage"
},
"memory": {
"editor": {
"add_card": "Ajouter une carte",
"upload_image": "Charger une image",
"add_pair": "Ajouter une paire"
}
},
"play": {
"memory": {
"try_count": "Essais : {{try_count}}"
}
},
"share_expired": "Partage expiré"
},
"file_dashboard": {
"size": "Taille : {{size}} Mo",
"filename": "Nom de fichier : {{filename}}",
"uploaded": "Chargé le : {{date}}",
"imported": "Importé : {{yes_or_no}}",
"edit_details": "Éditer",
"alt_text": "Texte alternatif / Légende",
"delete_image": "Supprimer l'image",
"caption": "Légende : {{caption}}",
"not_available": "Non disponible",
"edit_the_image": "Modifier l'image",
"filename_word": "Nom du fichier",
"storage_usage": "Vous avez utilisé {{used}} Mo sur un total de {{total}} Mo de stockage. Cela équivaut à {{percent}}% de votre espace de stockage.",
"missing": "MANQUANT !",
"unset": "Indéfini"
},
"avatar_settings": {
"skin_color": "Couleur de la peau",
"accessories_type": "Lunettes",
"hat_color": "Couleur du chapeau",
"start_over": "Recommencer",
"facial_hair_type": "Pilosité faciale",
"mouth_type": "Bouche",
"eyebrow_type": "Sourcils",
"clothe_type": "Vêtements",
"clothe_graphic_type": "Graphique",
"facial_hair_color": "Couleur de pilosité faciale",
"thats_you": "C'est vous !",
"top_type": "Haut",
"hair_color": "Couleur des cheveux",
"clothe_color": "Couleur de vêtement",
"go_back": "Retour"
},
"results_page": {
"no_results_so_far": "Aucun résultat enregistré jusqu'à présent...",
"general_overview": {
"sentence": "Le quiz \"{{titre}}\", qui a été joué le {{date}} a eu {{player_count}} joueurs avec un score moyen de {{average_score}}."
},
"quiz_title": "Titre du quiz",
"date_played": "Date jouée",
"player_count": "Nombre de joueurs"
},
"start_game": {
"old_school_mode": "Classique",
"start_game": "Lancer le jeu",
"captcha_message": "Si cette option est activée, le ReCaptcha de Google se chargera dans le navigateur des joueurs. N'activez cette option que si vous en avez vraiment besoin, car vous avez besoin du consentement de CHAQUE joueur pour charger le captcha.",
"old_school_mode_description": "Les questions et les images seront affichées à la fois sur l'écran des administrateurs et sur l'écran des joueurs.",
"normal_mode_description": "Questions et réponses ne seront affichées que sur l'écran de l'administrateur, comme dans Kahoot! Les joueurs n'auront que des boutons de couleur avec des symboles correspondants sur l'écran de l'administrateur."
},
"result_page": {
"average_score": "Score moyen : {{average score}}",
"player_name": "Nom du joueur",
"custom_field": "Champ personnalisé",
"time_taken": "Durée de l'opération",
"player_score": "Score du joueur",
"correct_answer": "{{count}} réponse correcte",
"correct_answer_plural": "{{count}} réponses correctes"
},
"security_settings": {
"totp_setup": {
"enter_as_secret_if_no_see_code": "Entrez ceci comme secret si vous ne pouvez pas scanner le QR-code.",
"scan_to_set_up": "Scanner ce QR-code pour configurer le code",
"totp_setup": "Paramètrage Totp",
"do_not_forget_backup_code": "N'oubliez pas de sauvegarder votre code de récupération !"
},
"activate_2fa": "Activer l'authentification à deux facteurs",
"webauthn_available": "Webauthn est disponible",
"totp": "Totp",
"totp_available": "Totp est disponible",
"totp_unavailable": "Totp n'est pas disponible",
"disable_totp": "Désactiver Totp",
"enable_totp": "Activer Totp",
"backup_codes": {
"your_backup_code": "Votre code de sauvegarde",
"save_somewhere_save": "Sauvegardez-le dans un endroit sûr !",
"download_code": "Télécharger le code"
},
"get_backup_code": "Obtenir le code de sauvegarde",
"2fa_activated": "L'authentification à deux facteurs est activée",
"2fa_deactivated": "L'authentification à deux facteurs est désactivée",
"webauthn": "Webauthn",
"add_security_key": "Ajouter une clé de sécurité",
"webauthn_unavailable": "Webauthn n'est pas disponible",
"backup_code": "Code de sauvegarde"
},
"view_quiz_page": {
"made_by": "Créé par",
"view_on_kahoot": "Voir sur Kahoot!"
},
"video_uploader": {
"time_elapsed": "Temps écoulé",
"time_remaining": "Temps restant"
},
"components": {
"popover": {
"copied_to_clipboard": "Copié dans le presse-papiers !"
}
},
"public_user_page": {
"no_original_quizzes": "Cet utilisateur n'a pas de quiz original",
"joined_on": "Rejoint le {{date}}"
},
"navbar": {
"donate": "Faire un don"
}
}
+22 -4
View File
@@ -1,14 +1,32 @@
{
"index_page": {
"meta": {
"title": "Beranda"
"title": "Beranda",
"description": "ClassQuiz adalah aplikasi kuis seperti Kahoot! untuk siswa, yang bersifat open source dan gratis untuk digunakan"
},
"features_description": {
"2": "Fitur utamanya adalah fungsi impor KAHOOT! yang memungkinkan kamu untuk mengimpor kuis dari kuis KAHOOT!.",
"1": "ClassQuiz adalah platform kuis yang memungkinkan kamu untuk membuat dan mengelola kuis."
"2": "Fitur utamanya adalah fungsi impor Kahoot!- ini memungkinkan Anda mengimpor kuis dari Kahoot!-kuis.",
"1": "ClassQuiz adalah platform kuis yang memungkinkan kamu untuk membuat dan mengelola kuis.",
"3": "Editor dan fungsi mengekspor hasil kuis sebagai file Excel adalah fitur khusus dari perangkat lunak ini."
},
"slogan": "Platform kuis sumber-terbuka!",
"stats": "Sudah ada {{user_count}} pengguna dan {{quiz_count}} kuis di ClassQuiz."
"stats": "Sudah ada {{user_count}} pengguna dan {{quiz_count}} kuis di ClassQuiz.",
"no_tracking": "Tanpa pelacakan",
"self_hostable": "Dapat Dihosting Sendiri",
"german_server": "Server Jerman",
"user_friendly": "Mudah digunakan",
"create_a_quiz_from_scratch": "Buat kuis dari awal dengan editor dan sertakan gambar dan lainnya",
"see_all_quizzes": "Lihat semua kuis Anda",
"teachers_site": "Situs guru",
"see_how_many_true_and_false": "Lihat berapa banyak yang benar atau salah",
"students_site": "Situs siswa",
"see_what_true_and_false": "Lihat mana yang benar atau salah",
"create_or_import": "Buat atau Impor",
"completely_free": "Gratis",
"quiz_results_downloadable": "Hasil kuis dapat diunduh",
"multilingual": "Multibahasa",
"dark_mode": "Mode gelap",
"get_a_quiz": "1. Dapatkan kuis"
},
"overview_page": {
"question_count": "Jumlah pertanyaan"
+121 -28
View File
@@ -2,12 +2,12 @@
"index_page": {
"meta": {
"title": "Home",
"description": "ClassQuiz è un'applicazione per quiz come KAHOOT! per gli studenti, è gratuita e open source"
"description": "ClassQuiz è un'app di quiz per gli studenti come Kahoot!, open source e gratuita"
},
"features_description": {
"1": "ClassQuiz è una webapp per creare e gestire quiz.",
"2": "Una delle caratteristiche principali è la possibilità di importare i quiz di KAHOOT!.",
"3": "L'editor delle domande è molto facile da usare, come l'esportazione dei risultati dei quiz in formato Excel."
"2": "La caratteristica principale è una funzione di importazione di Kahoot!che consente di importare quiz da Kahoot!-quiz.",
"3": "L'editor e la funzione di esportazione dei risultati dei quiz in file Excel sono i punti forti del software."
},
"stats": "In ClassQuiz ci sono già {{user_count}} utenti e {{quiz_count}} quiz.",
"see_what_true_and_false": "Verifica cos'è corretto o sbagliato",
@@ -17,21 +17,52 @@
"students_site": "Sito per lo studente",
"slogan": "La piattaforma per quiz open-source!",
"see_all_quizzes": "I tuoi quiz",
"dark_mode": "Modalità Notte"
"dark_mode": "Modalità Notte",
"quiz_results_downloadable_content": "I risultati dei quiz possono essere facilmente esportati in un foglio Excel. (Non sapevo che altri non potessero farlo)",
"multilingual_content": "ClassQuiz è già disponibile in inglese, francese, tedesco, italiano, norvegese, turco, e parzialmente in indonesiano e in catalano.",
"no_tracking_content": "Kahoot! ti traccia e invia le tue informazioni a terza parti, ma ClassQuiz, no.",
"no_tracking": "Senza tracking",
"self_hostable": "È possibile fare hosting autonomo",
"german_server": "Server tedesco",
"user_friendly": "Facile da usare",
"completely_free": "Completamente gratuito",
"quiz_results_downloadable": "I risultati dei quiz possono essere scaricati",
"multilingual": "Multilingue",
"get_a_quiz": "1. Ottieni un quiz",
"create_a_quiz_from_scratch": "Crea un quiz da zero con l'editor, includi immagini e altro ancora",
"find_or_explore": "Trova (o esplora) dei quiz fatti o importati da altre persone",
"import_quiz_from_kahoot_and_edit": "Importa un quiz da Kahoot! e modificalo su ClassQuiz",
"play_quiz": "2. Gioca al quiz",
"select_answer": "Seleziona la risposta",
"choose_answer_wisely": "Scegli la risposta attentamente",
"view_results": "Visualizza i risultati",
"check_if_chosen_wisely": "Controlla se hai scelto correttamente",
"list_winners": "Elenco vincitori",
"get_ranking_and_winners": "Vedi la classifica e chi ha vinto",
"why_classquiz": "Perché ClassQuiz?",
"self_hostable_content": "Con ClassQuiz si può fare facilmente hosting autonomo, così i dati sono in tuo controllo!",
"user_friendly_content": "ClassQuiz cerca di essere semplice, così può essere usato da tutti.",
"dark_mode_content": "Una delle caratteristiche più importanti che un sito web possa avere!",
"german_server_content": "I server di ClassQuiz si trovano in Germania e sono ospitati da netcup.",
"download_quizzes_content": "I quiz possono essere scaricati come un unico file e importati in qualsiasi momento. Questo ti permette di spostare i tuoi quiz in altre istanze di ClassQuiz.",
"download_quizzes": "Scarica i quiz",
"community_driven": "Guidati dalla comunità",
"community_driven_content": "ClassQuiz dipende dalla sua comunità per finanziamenti, idee di test, richieste di funzionalità, traduzioni e altro! Anche tu puoi far parte della comunità di ClassQuiz!",
"completely_free_content": "ClassQuiz è completamente gratuito (per l'utente), senza piani a pagamento o fastidiose richieste di aggiornamento. Le donazioni sono molto apprezzate."
},
"overview_page": {
"created_at": "Creato il",
"question_count": "Numero domande",
"no_quizzes": "Sembra che tu non abbia ancora quiz. Clicca sul pulsante \"Crea\" o importa un quiz da Kahoot!"
"no_quizzes": "Clicca sul pulsante \"Crea\", o importa un quiz da Kahoot! per iniziare."
},
"edit_page": {
"success_update_title": "Quiz importato con successo!",
"success_update_body": "Quiz aggiornato con successo!"
"success_update_title": "Quiz aggiornato.",
"success_update_body": "Nessuno si aspetta l'inquisizione spagnola."
},
"create_page": {
"success": {
"title": "Quiz creato con successo!",
"body": "Quiz creato con successo!"
"title": "Quiz creato.",
"body": "Che i giochi abbiano inizio."
}
},
"register_page": {
@@ -41,36 +72,38 @@
"already_have_account?": "Hai già un account?"
},
"login_page": {
"welcome_back": "Bentornato!",
"welcome_back": "Bentornato.",
"already_have_account": "Non hai un account?",
"modal": {
"success": {
"success_check_mail": "Login effettuato! Verifica la tua mailbox!",
"success": "Login effettuato!",
"success_check_mail": "Accesso effettuato. Controlla la mail.",
"success": "Accesso effettuato.",
"description": {
"success": "Login effettuato con successo!",
"success_check_mail": "Verifica la tua mailbox, dato che dovresti aver ricevuto una email con il link per accedere."
"success": "Accesso effettuato.",
"success_check_mail": "Per favore apri la mail. Troverai un link che puoi cliccare per effettuare l'accesso."
}
},
"error": {
"wrong_creds": "Email o password errate!",
"wrong_creds": "Indirizzo e-mail o password errati.",
"unexpected": "Errore imprevisto!",
"description": {
"unexpected": "È accaduto il buon vecco errore inatteso!",
"wrong_creds": "Verifica che l'email e la password siano corrette!"
"wrong_creds": "Per favore, assicurati che la tua password e l'indirizzo e-mail siano corretti."
}
}
},
"login_or_create_account": "Effettua il login o crea un account"
"login_or_create_account": "Accedi o crea un account",
"email_or_username": "Email o nome utente",
"use_backup_code": "Usa il codice di backup"
},
"words": {
"answer": "Risposta",
"question": "Domanda",
"stats": "Statistiche",
"features": "Caratteristiche",
"login": "Login",
"email": "Email",
"username": "Username",
"login": "Accedi",
"email": "Indirizzo e-mail",
"username": "Nome utente",
"password": "Password",
"play": "Avvia",
"delete": "Elimina",
@@ -78,7 +111,7 @@
"start": "Avvia",
"create": "Crea",
"import": "Importa",
"logout": "Logout",
"logout": "Esci dall'account",
"title": "Titolo",
"url": "URL",
"submit": "Invia",
@@ -111,10 +144,24 @@
"search": "Cerca",
"private": "Privato",
"question_plural": "Domande",
"game_pin": "PIN",
"game_pin": "PIN del gioco",
"other": "altro",
"other_plural": "altri",
"donating": "donazione"
"donating": "donazione",
"practice": "Esercitati",
"error": "Errore",
"voting": "Votazione",
"download": "Scarica",
"text": "Testo",
"order": "ordine",
"totp": "Password momentanea",
"continue": "Continua",
"backup_code": "Codice di backup",
"results": "Risultati",
"note": "Nota",
"player_plural": "Giocatori",
"score": "Punteggio",
"find": "Trova"
},
"editor": {
"right_or_true?": "Corretto?",
@@ -127,7 +174,13 @@
},
"import_page": {
"need_help": "Hai bisogno di aiuto?",
"visit_docs": "Guarda la documentazione"
"visit_docs": "Guarda la documentazione",
"url_should_look_like_this": "L'URL dovrebbe essere così: https://create.kahoot.it/details/...",
"this_side_classquiz": "Da questa parte puoi importare i quiz esportati da ClassQuiz.",
"side_import_kahoot": "Da questa parte è possibile importare i quiz di Kahoot!.",
"a_kahoot_quiz": "Un quiz di Kahoot",
"classquiz_quiz": "Un quiz di ClassQuiz",
"upload_file_ending": "Carica il file che termina con .cqa"
},
"admin_page": {
"already_registered_as_admin": "C'è già un amministratore registrato per questo quiz.",
@@ -138,8 +191,10 @@
"show_next_question": "Mostra la prossima domanda",
"no_answers": "Nessuna risposta!",
"get_final_results": "Risultati finali",
"start_by_showing_first_question": "Avvia mostrando la prima domanda!",
"get_results_and_stop_time": "Raccogli i risultati e ferma il tempo"
"start_by_showing_first_question": "Avvia mostrando la prima domanda.",
"get_results_and_stop_time": "Raccogli i risultati e ferma il tempo",
"stop_time": "Ferma il cronometro",
"save_results": "Salva i risultati"
},
"password_reset_page": {
"reset_password": "Resetta la password"
@@ -170,14 +225,52 @@
"right_click_to_delete": "Clicca col destro sulla risposta per eliminarla!"
},
"search_page": {
"at_least_3_characters": "Digita almento 3 caratteri..."
"at_least_3_characters": "Digita almento 3 caratteri...",
"nothing_here": "Non c'è nulla qua..."
},
"dashboard": {
"search_for_own_quizzes": "Cerca fra i tuoi quiz"
},
"footer": {
"self_ads": "Realizzato con ❤️ da {{mawoka_link}} e con l'aiuto di {{others_link}}.",
"more_details_here": "Maggiorni informazioni qui",
"more_details_here": "Maggiori informazioni qui",
"donate": "Se ritieni questa applicazione utile, considera la possibilità di fare una {{donate_link}}."
},
"avatar_settings": {
"facial_hair_type": "Peli del viso",
"skin_color": "Colore della pelle",
"top_type": "In alto",
"hair_color": "Colore dei capelli",
"eyebrow_type": "Sopracciglio",
"accessories_type": "Occhiali",
"hat_color": "Colore del cappello",
"clothe_type": "Vestiti",
"thats_you": "Sei tu!",
"clothe_graphic_type": "Grafica",
"facial_hair_color": "Colore dei peli del viso",
"mouth_type": "Bocca",
"clothe_color": "Colore dei vestiti",
"start_over": "Ricomincia da capo"
},
"uploader": {
"add_image": "Aggiungi immagine"
},
"results_page": {
"no_results_so_far": "Nessun risultato salvato finora...",
"quiz_title": "Titolo del quiz",
"date_played": "Data della partita",
"player_count": "Numero dei giocatori"
},
"result_page": {
"player_name": "Nome del giocatore",
"custom_field": "Campo personalizzato",
"average_score": "Punteggio medio: {{average_score}}",
"correct_answer": "{{count}} risposta corretta",
"correct_answer_plural": "{{count}} risposte corrette",
"time_taken": "Tempo impiegato"
},
"error_page": {
"404_text": "La pagina che stavi cercando è sparita o non è mai esistita. Chi lo sa?",
"unknown_error_text": "Questo non dovrebbe succedere. Probabilmente è colpa mia, non tua, oppure forse tu hai il potere magico di rompere le cose..."
}
}
+340
View File
@@ -0,0 +1,340 @@
{
"index_page": {
"features_description": {
"1": "ClassQuiz is een quiz-platform waarmee je quizzen kunt maken en beheren.",
"2": "De belangrijkste functie is een Kahoot!-importfunctie waarmee je quizzen van Kahoot! kunt importeren.",
"3": "De editor en de functie voor het exporteren van testresultaten als Excel-bestanden zijn bijzondere hoogtepunten van de software."
},
"create_or_import": "Maken of Importeren",
"see_all_quizzes": "Bekijk al je quizzen",
"no_tracking": "Geen Tracking",
"german_server": "Duitse Server",
"user_friendly": "Gebruikers Vriendelijk",
"completely_free": "Volledig Kostenloos",
"quiz_results_downloadable": "Quiz-resultaten kunnen worden gedownload",
"multilingual": "Meertalig",
"dark_mode": "Donkere Modus",
"create_a_quiz_from_scratch": "Maak een quiz vanaf niks met de editor en voeg afbeeldingen en meer toe",
"find_or_explore": "Zoek (of verken) quizzen gemaakt of geïmporteerd door andere mensen",
"import_quiz_from_kahoot_and_edit": "Importeer een quiz van Kahoot! en bewerk hem in ClassQuiz",
"play_quiz": "2. Speel de quiz",
"select_answer": "Selecteer een antwoord",
"view_results": "Bekijk de resultaten",
"check_if_chosen_wisely": "Check of je goed had gekozen",
"list_winners": "Winnaarslijst",
"get_ranking_and_winners": "Bekijk het klassement en zie wie er gewonnen heeft",
"why_classquiz": "Waarom ClassQuiz?",
"meta": {
"title": "Home",
"description": "ClassQuiz is een quiz-app zoals Kahoot! voor studenten, die open source en gratis te gebruiken is"
},
"get_a_quiz": "1. Verkrijg een quiz",
"user_friendly_content": "ClassQuiz heeft als doel simpel te zijn, zodat iedereen het kan gebruiken.",
"students_site": "Studenten site",
"teachers_site": "Leraar's site",
"slogan": "Het open-source quizplatform!",
"self_hostable": "Zelf-Hostable",
"multilingual_content": "ClassQuiz is beschikbaar in het Engels, Frans, Duits, Italiaans, Noors Bokmål, Turks en gedeeltelijk Indonesisch en Catalaans.",
"dark_mode_content": "Een van de belangrijkste functies die een website kan hebben!",
"community_driven_content": "ClassQuiz is afhankelijk van haar gemeenschap voor financiering, het testen van ideeën, feature requests, vertalingen en meer! U kunt ook deel uitmaken van de ClassQuiz-community!",
"community_driven": "Gedreven door de community",
"stats": "Er zijn al {{user_count}} gebruikers en {{quiz_count}} quizzen op ClassQuiz.",
"see_how_many_true_and_false": "Kijk hoeveel er goed of fout waren",
"see_what_true_and_false": "Zie wat goed of fout was",
"choose_answer_wisely": "Kies je antwoord verstandig",
"no_tracking_content": "Kahoot! volgt je en stuurt die informatie naar partijen van derden, maar ClassQuiz niet.",
"self_hostable_content": "ClassQuiz kan eenvoudig zelf worden gehost, dus de gegevens zijn alleen in jouw beheer!",
"german_server_content": "De servers van ClassQuiz bevinden zich in Duitsland en worden gehost door netcup.",
"completely_free_content": "ClassQuiz is volledig gratis (voor de gebruiker), zonder betaalde abonnementen of vervelende doorverwijzingen naar de upgrade-pagina. Donaties worden zeer op prijs gesteld.",
"download_quizzes": "Download Quizzen",
"download_quizzes_content": "Quizzen kunnen als één bestand worden gedownload en op elk moment worden geïmporteerd. Zo kun je ook je testen verplaatsen naar andere ClassQuiz instanties.",
"quiz_results_downloadable_content": "Quiz-resultaten kunnen gemakkelijk worden geëxporteerd naar een Excel-spreadsheet. (Wist niet dat anderen dat niet konden)",
"how_does_classquiz_work": "Hoe werkt ClassQuiz eigenlijk?"
},
"overview_page": {
"created_at": "Gemaakt op",
"question_count": "Vragenaantal",
"no_quizzes": "Klik op de \"Creëer\"-knop of importeer een quiz van Kahoot! om aan de slag te gaan."
},
"edit_page": {
"success_update_title": "Quiz bijgewerkt.",
"success_update_body": "Niemand zal het onderzoek verwachten."
},
"create_page": {
"success": {
"title": "Quiz gemaakt.",
"body": "Laat het spel beginnen."
}
},
"register_page": {
"greeting": "Leuk je te ontmoeten!",
"create_account": "Account aanmaken",
"forgot_password?": "Wachtwoord vergeten?",
"already_have_account?": "Heb je al een account?"
},
"login_page": {
"welcome_back": "Welkom terug.",
"login_or_create_account": "Log in of maak een account",
"already_have_account": "Heb je geen account?",
"email_or_username": "Email of Gebruikersnaam",
"modal": {
"success": {
"success_check_mail": "Ingelogd. Controleer jouw e-mail.",
"success": "Ingelogd.",
"description": {
"success": "Ingelogd.",
"success_check_mail": "Controleer jouw e-mail voor een mail met een link waarop je kunt klikken om in te loggen."
}
},
"error": {
"wrong_creds": "Verkeerd e-mailadres of wachtwoord.",
"unexpected": "Onverwachte fout!",
"description": {
"wrong_creds": "Zorg ervoor dat jouw wachtwoord en e-mailadres correct zijn.",
"unexpected": "De goede oude onverwachte fout trad op!"
}
}
},
"use_backup_code": "Gebruik backup-code"
},
"words": {
"question": "Vraag",
"answer": "Antwoord",
"stats": "Statistieken",
"email": "E-mailadres",
"username": "Gebruikersnaam",
"password": "Wachtwoord",
"play": "Speel",
"features": "Mogelijkheden",
"login": "Inloggen",
"edit": "Bewerken",
"delete": "Verwijderen",
"public": "Openbaar",
"url": "URL",
"submit": "Versturen",
"connect": "Verbinden",
"pin": "PIN",
"kick": "Eruit gooien",
"register": "Registeren",
"docs": "Documentatie",
"close": "Sluiten",
"save": "Opslaan",
"description": "Beschrijving",
"image": "Afbeelding",
"repeat_password": "Wachtwoord herhalen",
"overview": "Overzicht",
"report": "Melden",
"explore": "Verkennen",
"screenshot": "Schermafbeelding",
"screenshot_plural": "Schermafbeeldingen",
"view": "Bekijken",
"browser": "Browser",
"correct": "Juist",
"result": "Resultaat",
"result_plural": "Resultaten",
"count": "Aantal",
"range": "Bereik",
"private": "Privé",
"other": "andere",
"other_plural": "anderen",
"donating": "doneren",
"find": "Vind",
"practice": "Oefenen",
"error": "Fout",
"voting": "Stemmen",
"download": "Downloaden",
"continue": "Verder",
"backup_code": "Backup-code",
"totp": "TOTP",
"text": "Tekst",
"order": "volgorde",
"results": "Resultaten",
"note": "Opmerking",
"multiple_choice": "Meerkeuze",
"start": "Start",
"create": "Creëren",
"import": "Importeren",
"logout": "Uitloggen",
"title": "Titel",
"settings": "Instellingen",
"player_plural": "Spelers",
"search": "Zoeken",
"game_pin": "Spel PIN",
"dashboard": "Dashboard",
"question_plural": "Vragen",
"score": "Score",
"name": "Naam",
"point": "Punt",
"slide": "Dia",
"finish": "Afronden",
"point_plural": "Punten",
"back": "Terug"
},
"editor": {
"not_all_links_imgur_links": "Niet alle links zijn Imgur-links!",
"time_in_seconds": "Tijd in seconden",
"delete_answer": "Verwijder antwoord",
"right_or_true?": "Juist?",
"add_new_answer": "Nieuw antwoord toevoegen",
"add_new_question": "Nieuwe vraag toevoegen",
"delete_question": "Verwijder vraag",
"bg_image": "Achtergrondafbeelding",
"no_title": "Geen titel...",
"slide": {
"headline_description": "Een vetgedrukte tekst voor koppen",
"headline": "Kop",
"text_description": "Kleinere langere tekst",
"rectangle_description": "Gewoon een rechthoek",
"text": "Tekst",
"rectangle": "Rechthoek"
},
"empty": "Leeg..."
},
"import_page": {
"url_should_look_like_this": "De URL zou er als volgt uit moeten zien: https://create.kahoot.it/details/...",
"side_import_kahoot": "Aan deze kant kun je quizzen van Kahoot! importeren.",
"upload_file_ending": "Upload het bestand eindigend op .cqa",
"this_side_classquiz": "Aan deze kant kun je quizzen importeren die geëxporteerd zijn vanuit ClassQuiz.",
"visit_docs": "Bekijk de documentatie",
"a_kahoot_quiz": "Een Kahoot!-Quiz",
"need_help": "Hulp nodig?",
"classquiz_quiz": "Een ClassQuiz-Quiz"
},
"admin_page": {
"start_game": "Start spel",
"time_left": "Tijd over",
"get_results": "Verkrijg resultaten",
"no_answers": "Geen antwoorden!",
"stop_time": "Stop de tijd",
"save_results": "Resultaten opslaan",
"start_by_showing_first_question": "Start met het vertonen van de eerste vraag.",
"show_next_question": "Toon volgende vraag",
"already_registered_as_admin": "Er is al een beheerder geregistreerd voor dit spel.",
"get_results_and_stop_time": "Verkrijg resultaten en stop de tijd",
"get_final_results": "Verkrijg definitieve resultaten",
"export_results": "Exporteer resultaten",
"next_question": "Volgende Vraag ({{question}})",
"show_results": "Toon resultaten",
"enter_answer_into_field": "Typ je antwoord in het invoerveld!",
"stop_time_and_solutions": "Stop de tijd en toon oplossingen"
},
"password_reset_page": {
"reset_password": "Wachtwoord resetten"
},
"settings_page": {
"old_password": "Oud wachtwoord",
"new_password": "Nieuw wachtwoord",
"repeat_password": "Herhaal wachtwoord",
"change_password_submit": "Wachtwoord aangepast!",
"last_seen": "Laatst gezien",
"check_location": "Check locatie",
"delete_this_session": "Verwijder deze sessie",
"this_session?": "Deze sessie?",
"change_avatar": "Verander avatar",
"security_settings": "Beveiligingsinstellingen"
},
"explore_page": {
"made_by": "Gemaakt door",
"imported_by": "Geïmporteerd door"
},
"search_page": {
"at_least_3_characters": "Voer minimaal 3 tekens in...",
"nothing_here": "Er is hier niks..."
},
"play_page": {
"end_sentence": "Dat was het! Dit was de quiz.",
"1st_place": "1ste Plek",
"2nd_place": "2de Plek",
"3rd place": "3de Plek",
"with_out_of": "met {{correct_questions}} van de {{total_question_count}}",
"final_result_rank": "{{place}}: {{username}} met {{points}} punten",
"points_added": "Punten toegevoegd",
"your_score": "Jouw score: {{score}}",
"join_by_entering_code": "Neem deel door de volgende code in te voeren",
"join_description": "Neem deel via {{url}} en voer {{pin}} in."
},
"editor_page": {
"add_an_answer": "Voeg een antwoord toe",
"right_click_to_delete": "Klik met de rechtermuisknop op een antwoord om het te verwijderen!"
},
"footer": {
"more_details_here": "Meer details hier",
"donate": "Als je dit nuttig vindt overweeg dan {{donate_link}}.",
"self_ads": "Gemaakt met ❤️ door {{mawoka_link}} en met de hulp van {{others_link}}."
},
"error_page": {
"404_text": "De pagina waarnaar je op zoek was, is verdwenen of heeft zelfs nooit bestaan. Wie weet?",
"unknown_error_text": "Dat zou niet mogen gebeuren. Het is waarschijnlijk mijn schuld, niet de jouwe, maar misschien heb je een magische kracht om dingen te breken..."
},
"uploader": {
"add_image": "Afbeelding toevoegen"
},
"avatar_settings": {
"skin_color": "Huidskleur",
"hair_color": "Haarkleur",
"facial_hair_type": "Gezichtshaar",
"facial_hair_color": "Gezichtshaar kleur",
"mouth_type": "Mond",
"eyebrow_type": "Wenkbrauw",
"accessories_type": "Bril",
"hat_color": "Hoed kleur",
"clothe_type": "Kleding",
"clothe_color": "Kleding kleur",
"clothe_graphic_type": "Grafisch",
"thats_you": "Dat ben jij!",
"start_over": "Opnieuw beginnen",
"top_type": "Top",
"go_back": "Ga terug"
},
"results_page": {
"no_results_so_far": "Geen resultaten opgeslagen tot nu toe...",
"quiz_title": "Quiz Titel",
"date_played": "Datum Gespeeld",
"player_count": "Aantal spelers"
},
"result_page": {
"player_name": "Naam speler",
"custom_field": "Aangepast veld",
"average_score": "Gemiddelde score: {{average_score}}",
"correct_answer_plural": "{{count}} juiste antwoorden",
"time_taken": "Benodigde tijd",
"correct_answer": "{{count}} juist antwoord"
},
"dashboard": {
"search_for_own_quizzes": "Zoek voor je eigen quizzen"
},
"security_settings": {
"webauthn": "Webauthn",
"webauthn_available": "Webauthn is beschikbaar",
"webauthn_unavailable": "Webauthn is niet beschikbaar",
"backup_codes": {
"your_backup_code": "Jouw back-up-code",
"download_code": "Download code",
"save_somewhere_save": "Bewaar dit ergens veilig!"
},
"backup_code": "Backup Code",
"get_backup_code": "Verkrijg Backup Code",
"activate_2fa": "Activeer Tweestapsauthenticatie",
"2fa_activated": "Tweestapsauthenticatie is geactiveerd",
"totp_setup": {
"enter_as_secret_if_no_see_code": "Voer dit in als de 'secret' als u de QR-code niet kunt scannen",
"scan_to_set_up": "Scan deze QR-code om de code in te stellen",
"totp_setup": "Totp-Setup",
"do_not_forget_backup_code": "Vergeet niet je herstelcode op te slaan!"
},
"2fa_deactivated": "Tweestapsauthenticatie is gedeactiveerd",
"add_security_key": "Beveiligingssleutel toevoegen",
"totp": "Totp",
"totp_available": "Totp is beschikbaar",
"totp_unavailable": "Totp is niet beschikbaar",
"disable_totp": "Totp uitschakelen",
"enable_totp": "Totp inschakelen"
},
"navbar": {
"donate": "Doneer"
},
"view_quiz_page": {
"made_by": "Gemaakt door",
"view_on_kahoot": "Bekijk op Kahoot!"
}
}
+68
View File
@@ -0,0 +1,68 @@
{
"register_page": {
"already_have_account?": "Har du allereie ein konto?",
"create_account": "Opprett konto"
},
"overview_page": {
"created_at": "Oppretta"
},
"login_page": {
"welcome_back": "Velkomen attende.",
"modal": {
"error": {
"unexpected": "Uventa feil!"
}
}
},
"words": {
"features": "Funksjonar",
"question": "Spørsmål",
"username": "Brukarnamn",
"password": "Passord",
"play": "Spel",
"public": "Offentleg",
"start": "Start",
"email": "E-postadresse",
"edit": "Rediger",
"delete": "Slett"
},
"index_page": {
"features_description": {
"3": "Redigeringsverktyget og mogleheit for å eksportera resultat som Excel-filer er særlege funksjonar å merka seg.",
"1": "ClassQuiz er ein kvissplatform der du kan laga og handsame kvissar.",
"2": "Den viktigaste funksjonen er at du kan importera kvissar frå Kahoot!"
},
"stats": "Det er allereie {{user_count}} brukarar og {{quiz_count}} kvissar på ClassQuiz.",
"see_how_many_true_and_false": "Sjå kor mange som hadde rett eller feil",
"see_all_quizzes": "Sjå kvissane dine",
"teachers_site": "Læraren si side",
"students_site": "Eleven si side",
"no_tracking": "Inga sporing",
"german_server": "Tjenarmaskin i Tyskland",
"user_friendly": "Brukarvenleg",
"completely_free": "Heilt gratis",
"quiz_results_downloadable": "Du kan lasta ned kvissresultat",
"multilingual": "Fleirspråkleg",
"get_a_quiz": "1. Vel ein kviss",
"find_or_explore": "Finn kvissar laga eller importert av andre",
"import_quiz_from_kahoot_and_edit": "Importer ein kviss frå Kahoot! og rediger han i ClassQuiz",
"play_quiz": "2. Spel kvissen",
"select_answer": "Vel eit svar",
"choose_answer_wisely": "Vel med omhug",
"view_results": "Sjå resultata",
"check_if_chosen_wisely": "Sjekk om du hadde rett",
"list_winners": "List opp vinnarane",
"why_classquiz": "Kvifor ClassQuiz?",
"self_hostable": "Køyr frå eigen vert",
"dark_mode": "Mørk drakt",
"no_tracking_content": "Kahoot! sporar deg og sender info til tredjepart, det gjer ikkje ClassQuiz.",
"slogan": "Den opne kvissplatformen!",
"meta": {
"description": "ClassQuiz er ein kvissapp som Kahoot! for elever, som har open kjeldekode og er gratis",
"title": "Heim"
},
"see_what_true_and_false": "Sjå kva som er rett og gale",
"create_or_import": "Lag eller importer",
"create_a_quiz_from_scratch": "Lag ein ny kviss frå botnen av og ta med bilete og meir"
}
}
+178 -26
View File
@@ -24,7 +24,7 @@
"quiz_results_downloadable": "Wyniki quizu można pobrać",
"multilingual": "Wielojęzyczny",
"dark_mode": "Tryb ciemny",
"get_a_quiz": "1. Pobierz quiz",
"get_a_quiz": "1. Pozyskaj quiz",
"find_or_explore": "Znajdź (lub odkryj) quizy stworzone lub zaimportowane przez innych ludzi",
"import_quiz_from_kahoot_and_edit": "Zaimportuj quiz z Kahoot! i edytuj go w ClassQuiz",
"play_quiz": "2. Uruchom quiz",
@@ -43,12 +43,13 @@
"download_quizzes": "Pobierz quizy",
"download_quizzes_content": "Quizy mogą być pobierane jako jeden plik i importowane w dowolnym momencie. Pozwala to również na przenoszenie quizów do innych instancji ClassQuiz.",
"community_driven_content": "ClassQuiz polega na swojej społeczności w zakresie finansowania, testowania pomysłów, próśb o nowe funkcje, tłumaczeń i nie tylko! Ty też możesz być częścią społeczności ClassQuiz!",
"community_driven": "Napędzane przez społeczność",
"community_driven": "Napędzany przez społeczność",
"students_site": "Strona ucznia",
"create_a_quiz_from_scratch": "Stwórz quiz od podstaw za pomocą edytora i dołącz zdjęcia i więcej",
"create_a_quiz_from_scratch": "Stwórz quiz od podstaw za pomocą edytora, dołącz zdjęcia i więcej",
"multilingual_content": "ClassQuiz jest dostępny w języku angielskim, francuskim, niemieckim, włoskim, norweskim bokmål, tureckim oraz częściowo indonezyjskim i katalońskim.",
"quiz_results_downloadable_content": "Wyniki quizu można łatwo wyeksportować do arkusza kalkulacyjnego Excel. (Nie wiem, dlaczego inni nie mogli tego zrobić)",
"dark_mode_content": "Jedna z najważniejszych funkcji, jakie może mieć strona internetowa!"
"dark_mode_content": "Jedna z najważniejszych funkcji, jakie może mieć strona internetowa!",
"how_does_classquiz_work": "Jak działa ClassQuiz?"
},
"overview_page": {
"question_count": "Liczba pytań",
@@ -78,11 +79,11 @@
"email_or_username": "Adres e-mail lub nazwa użytkownika",
"modal": {
"success": {
"success_check_mail": "Zalogowany. Sprawdź swoją skrzynkę e-mail.",
"success": "Zalogowany.",
"success_check_mail": "Zalogowano. Sprawdź swoją skrzynkę e-mail.",
"success": "Zalogowano.",
"description": {
"success_check_mail": "Sprawdź swoją skrzynkę e-mail i poszukaj wiadomości z linkiem, który możesz kliknąć, aby się zalogować.",
"success": "Zalogowany."
"success": "Zalogowano."
}
},
"error": {
@@ -116,7 +117,7 @@
"screenshot_plural": "Zrzuty ekranu",
"browser": "Przeglądarka",
"view": "Widok",
"result": "Wyniki",
"result": "Wynik",
"result_plural": "Wyniki",
"range": "Zakres",
"multiple_choice": "Wielokrotny wybór",
@@ -124,12 +125,12 @@
"question_plural": "Pytania",
"game_pin": "PIN do gry",
"other": "inne",
"other_plural": "inni",
"donating": "darowizna",
"other_plural": "innych",
"donating": "dotację",
"find": "Szukaj",
"practice": "Ćwiczenia",
"error": "Błąd",
"register": "Zarejestruj",
"register": "Zarejestruj się",
"docs": "Dokumentacja",
"close": "Zamknij",
"save": "Zapisz",
@@ -154,14 +155,36 @@
"image": "Obraz",
"overview": "Przegląd",
"report": "Raport",
"explore": "Odkryj",
"correct": "Prawidłowo",
"explore": "Przeglądaj",
"correct": "Poprawnie",
"count": "Liczba",
"private": "Prywatny",
"backup_code": "Kod zapasowy",
"submit": "Prześlij",
"kick": "Wykop",
"order": "kolejność"
"order": "Kolejność",
"name": "Nazwa",
"point": "Punkt",
"slide": "Slajd",
"point_plural": "Punkty",
"back": "Wróć",
"finish": "Zakończ",
"normal": "Normalny",
"select": "wybierz",
"quiz": "Quiz",
"quiztivity": "Quiztivity",
"check_choice": "Sprawdź wybór",
"selected": "Wybrano",
"next": "Następny",
"progress": "Postęp",
"video": "Film",
"library": "Biblioteka",
"speed": "Prędkość",
"upload": "Prześlij",
"files_library": "Biblioteka plików",
"answer_plural": "Odpowiedzi",
"no": "nie",
"yes": "Tak"
},
"settings_page": {
"last_seen": "Ostatnio widziany",
@@ -171,7 +194,10 @@
"new_password": "Nowe hasło",
"repeat_password": "Powtórz hasło",
"this_session?": "Ta sesja?",
"change_password_submit": "Zmień hasło!"
"change_password_submit": "Zmień hasło!",
"change_avatar": "Zmień awatar",
"security_settings": "Ustawienia bezpieczeństwa",
"add_api_key": "Dodaj klucz API"
},
"editor_page": {
"add_an_answer": "Dodaj odpowiedź",
@@ -179,15 +205,19 @@
},
"footer": {
"self_ads": "Wykonane z ❤️ przez {{mawoka_link}} i z pomocą {{others_link}}.",
"more_details_here": "Więcej szczegółów tutaj",
"donate": "Jeśli uznasz to za przydatne, rozważ {{donate_link}}."
"more_details_here": "Więcej szczegółów znajdziesz tutaj",
"donate": "Jeśli uznasz to za użyteczne, rozważ {{donate_link}}."
},
"error_page": {
"404_text": "Strona, której szukałeś, zniknęła lub nawet nigdy nie istniała. Kto wie?",
"unknown_error_text": "To nie powinno się zdarzyć. To pewnie moja wina, nie twoja, ale może masz magiczną moc niszczenia rzeczy..."
},
"uploader": {
"add_image": "Dodaj obraz"
"add_image": "Dodaj obraz",
"select_upload_type": "Wybierz typ przesyłania",
"upload_a_video": "Prześlij film",
"upload_video_popup_notice": "Wyskakujące okienko jest otwarte; spójrz na nie, aby uzyskać więcej informacji",
"upload_video": "Prześlij film"
},
"editor": {
"time_in_seconds": "Czas w sekundach",
@@ -196,7 +226,24 @@
"delete_question": "Usuń pytanie",
"delete_answer": "Usuń odpowiedź",
"not_all_links_imgur_links": "Nie wszystkie linki prowadzą do Imgur!",
"right_or_true?": "Prawda?"
"right_or_true?": "Prawda?",
"no_title": "Brak tytułu...",
"empty": "Pusto...",
"bg_image": "Obraz tła",
"slide": {
"headline": "Nagłówek",
"headline_description": "Pogrubiony tekst dla nagłówków",
"text": "Tekst",
"text_description": "Mniejszy dłuższy tekst",
"rectangle": "Prostokąt",
"rectangle_description": "Tylko prostokąt"
},
"abcd_description": "Można wybrać tylko jedną odpowiedź",
"voting_description": "Odpowiedzi nie dodają żadnych punktów",
"check_choice_description": "Aby zdobyć punkty, należy wybrać wszystkie poprawne odpowiedzi",
"order_description": "Odpowiedzi można ustawić w odpowiedniej kolejności",
"text_description": "Gracze mogą wprowadzać tekst",
"range_description": "Zakres liczbowy można wybrać za pomocą suwaka"
},
"import_page": {
"need_help": "Potrzebujesz pomocy?",
@@ -220,7 +267,14 @@
"stop_time": "Zatrzymaj czas",
"save_results": "Zapisz wyniki",
"get_results_and_stop_time": "Uzyskaj wyniki i zatrzymaj czas",
"get_final_results": "Wyniki ostateczne"
"get_final_results": "Wyniki ostateczne",
"next_question": "Następne pytanie ({{question}})",
"show_results": "Pokaż wyniki",
"stop_time_and_solutions": "Zatrzymaj czas i pokaż rozwiązania",
"enter_answer_into_field": "Wprowadź swoją odpowiedź w pole wejściowe!",
"answers_submitted": "{{answer_count}} Przesłane odpowiedzi",
"request_export_results": "Żądanie pobrania wyników",
"download_export_results": "Wyniki pobierania"
},
"password_reset_page": {
"reset_password": "Zresetuj hasło"
@@ -234,11 +288,16 @@
"2nd_place": "2. miejsce",
"3rd place": "3. miejsce",
"with_out_of": "z {{correct_questions}} na {{total_question_count}}",
"end_sentence": "To jest to! To był quiz."
"end_sentence": "To jest to! To był quiz.",
"your_score": "Twój wynik: {{score}}",
"join_description": "Dołącz na {{url}} i wpisz {{pin}}.",
"join_by_entering_code": "Dołącz do nas wpisując następujący kod",
"points_added": "Dodane punkty",
"final_result_rank": "{{place}}: {{username}} z {{points}} punktów"
},
"explore_page": {
"made_by": "Wykonane przez",
"imported_by": "Importowane przez"
"imported_by": "Zaimportowane przez"
},
"avatar_settings": {
"skin_color": "Kolorystyka",
@@ -249,18 +308,22 @@
"clothe_type": "Odzież",
"clothe_graphic_type": "Grafika",
"start_over": "Zacznij od nowa",
"thats_you": "To ty!",
"thats_you": "To Ty!",
"clothe_color": "Kolor odzieży",
"hair_color": "Kolor włosów",
"facial_hair_type": "Zarost",
"facial_hair_color": "Kolor zarostu",
"top_type": "Góra"
"top_type": "Góra",
"go_back": "Wstecz"
},
"results_page": {
"quiz_title": "Tytuł quizu",
"player_count": "Liczba graczy",
"no_results_so_far": "Do tej pory nie zapisano żadnych wyników...",
"date_played": "Data gry"
"date_played": "Data gry",
"general_overview": {
"sentence": "W quizie \"{{title}}\", który został rozegrany {{date}} wzięło udział {{player_count}} graczy ze średnim wynikiem {{average_score}}."
}
},
"result_page": {
"player_name": "Nazwa gracza",
@@ -268,9 +331,98 @@
"average_score": "Średni wynik: {{average_score}}",
"correct_answer": "{{count}} poprawna odpowiedź",
"correct_answer_plural": "{count}} poprawne odpowiedzi",
"time_taken": "Czas trwania"
"time_taken": "Czas trwania",
"player_score": "Wynik gracza"
},
"dashboard": {
"search_for_own_quizzes": "Wyszukaj własne quizy"
},
"navbar": {
"donate": "Darowizna"
},
"security_settings": {
"backup_code": "Kod zapasowy",
"get_backup_code": "Pobierz kod zapasowy",
"activate_2fa": "Aktywuj uwierzytelnianie dwuskładnikowe",
"2fa_activated": "Uaktywniono uwierzytelnianie dwuskładnikowe",
"2fa_deactivated": "Uwierzytelnianie dwuskładnikowe jest wyłączone",
"backup_codes": {
"your_backup_code": "Twój kod zapasowy",
"save_somewhere_save": "Zapisz to gdzieś w bezpiecznym miejscu!",
"download_code": "Pobierz kod"
},
"totp_setup": {
"do_not_forget_backup_code": "Nie zapomnij zapisać swojego kodu odzyskiwania!",
"scan_to_set_up": "Zeskanuj ten kod QR, aby ustawić kod"
}
},
"view_quiz_page": {
"made_by": "Wykonane przez",
"view_on_kahoot": "Zobacz na Kahoot!"
},
"start_game": {
"captcha_message": "Jeśli ta opcja jest włączona, Google ReCaptcha będzie ładować się w przeglądarce graczy. Włącz tylko jeśli naprawdę tego potrzebujesz, ponieważ będziesz potrzebował zgody KAŻDEGO gracza na załadowanie captcha.",
"normal_mode_description": "Pytanie i odpowiedź będą wyświetlane tylko na ekranie administratora, jak w Kahoot! Gracze będą mieli do dyspozycji jedynie kolorowe przyciski z symbolami odpowiadającymi tym na ekranie administratora.",
"old_school_mode_description": "Pytania i obrazy będą wyświetlane zarówno na ekranie administratora, jak i na ekranie graczy",
"start_game": "Rozpocznij grę"
},
"quiztivity": {
"editor": {
"move_right": "Przesuń w prawo",
"shares": {
"expires_on": "Wygasa w dniu {{date}}",
"never_expires": "Nigdy nie wygasa",
"add_new_share": "Dodaj nowy udział"
},
"add_new": "Dodaj nowy",
"move_left": "Przesuń w lewo",
"delete": "Usuń",
"select_page_type": "Wybierz typ strony",
"title_placeholder": "Wpisz tytuł",
"open_shares_menu": "Otwórz menu Udziały"
},
"memory": {
"editor": {
"upload_image": "Wyślij obraz",
"add_pair": "Dodaj parę",
"add_card": "Dodaj kartę"
}
},
"play": {
"memory": {
"try_count": "Próby: {{try_count}}"
}
},
"share_expired": "Udział wygasł"
},
"components": {
"popover": {
"copied_to_clipboard": "Skopiowano do schowka!"
}
},
"public_user_page": {
"joined_on": "Dołączył {{date}}",
"no_original_quizzes": "Ten użytkownik nie ma jeszcze żadnych oryginalnych quizów"
},
"file_dashboard": {
"not_available": "Niedostępne",
"size": "Rozmiar: {{size}} Mib",
"uploaded": "Przesłano: {{date}}",
"Imported": "Importowane: {{yes_or_no}}",
"edit_details": "Edytuj szczegóły",
"delete_image": "Usuń obraz",
"edit_the_image": "Edytuj obraz",
"filename_word": "Nazwa pliku",
"storage_usage": "Użyłeś {{used}} Mib z {{łącznie}} MiB pamięci. Odpowiada to {{percent}}% twojej przestrzeni dyskowej.",
"imported": "Importowane: {{yes_or_no}}",
"missing": "ZAGINIONY!",
"unset": "Nieustawiony",
"caption": "Napis: {{caption}}",
"filename": "Nazwa pliku: {{nazwa pliku}}",
"alt_text": "Tekst alternatywny / Napis"
},
"video_uploader": {
"time_elapsed": "Upłynęło",
"time_remaining": "Pozostały czas:"
}
}
+214 -9
View File
@@ -24,7 +24,7 @@
"completely_free": "Tamamen Ücretsiz",
"quiz_results_downloadable": "Quiz sonuçları indirilebilir",
"multilingual": "Çok dilli",
"no_tracking_content": "Kahoot! en az iki Amerikan üçüncü taraflarla sizi takip eder, oysaki ClassQuiz bunu yapmaz!",
"no_tracking_content": "Kahoot! sizi takip eder ve bu bilgiyi üçüncü taraflara gönderir ama ClassQuiz bunu yapmaz.",
"german_server": "Alman Sunucusu",
"user_friendly": "Kullanıcı Dostu",
"list_winners": "Kazananları listele",
@@ -48,7 +48,8 @@
"community_driven_content": "ClassQuiz fonlama, fikirleri test etme, özellik istekleri, çeviriler ve daha fazlası için kendi topluluğuna bağlıdır! Siz de ClassQuiz topluluğunun bir parçası olabilirsiniz!",
"community_driven": "Topluluk güdümlü",
"download_quizzes": "Quizleri İndir",
"download_quizzes_content": "Quizler bir dosya olarak indirilebilir ve her zaman içe aktarılabilir. Bu ayrıca quizlerinizi diğer ClassQuiz sunucularına taşımanıza da olanak sağlar."
"download_quizzes_content": "Quizler bir dosya olarak indirilebilir ve her zaman içe aktarılabilir. Bu ayrıca quizlerinizi diğer ClassQuiz sunucularına taşımanıza da olanak sağlar.",
"how_does_classquiz_work": "ClassQuiz nasıl çalışıyor?"
},
"login_page": {
"already_have_account": "Bir hesabınız yok mu?",
@@ -71,7 +72,9 @@
}
}
},
"welcome_back": "Tekrar hoş geldiniz."
"welcome_back": "Tekrar hoş geldiniz.",
"use_backup_code": "Yedek kod kullanın",
"email_or_username": "E-posta veya Kullanıcı Adı"
},
"words": {
"answer": "Cevap",
@@ -129,7 +132,38 @@
"error": "Hata",
"practice": "Alıştırma",
"find": "Bul",
"download": "İndir"
"download": "İndir",
"answer_plural": "Cevaplar",
"yes": "Evet",
"no": "Hayır",
"normal": "Normal",
"continue": "Devam Et",
"backup_code": "Yedek Kod",
"totp": "TOTP",
"text": "Metin",
"finish": "Bitir",
"order": "Sıralama",
"results": "Sonuçlar",
"note": "Not",
"player_plural": "Oyuncular",
"score": "Skor",
"name": "Ad",
"back": "Geri",
"point": "Puan",
"point_plural": "Puanlar",
"slide": "Slayt",
"quiz": "Quiz",
"next": "Sonraki",
"check_choice": "Seçeneği İşaretle",
"select": "Seç",
"selected": "Seçilmiş",
"quiztivity": "Quiz Aktivitesi",
"library": "Kütüphane",
"speed": "Hız",
"upload": "Yükle",
"video": "Video",
"files_library": "Dosya Kütüphanesi",
"progress": "İlerleme"
},
"register_page": {
"create_account": "Hesap oluştur",
@@ -163,7 +197,15 @@
"show_next_question": "Bir sonraki soruyu göster",
"start_by_showing_first_question": "İlk soruyu göstererek başla.",
"no_answers": "Cevap yok!",
"stop_time": "Zamanı durdur"
"stop_time": "Zamanı durdur",
"answers_submitted": "{{answer_count}} Gönderilen cevaplar",
"request_export_results": "Sonuç indirmeyi talep et",
"download_export_results": "Sonuçları indir",
"save_results": "Sonuçları kaydet",
"show_results": "Sonuçları göster",
"stop_time_and_solutions": "Zamanı durdurun ve çözümleri gösterin",
"enter_answer_into_field": "Cevabınızı giriş alanına girin!",
"next_question": "Sonraki Soru ({{question}})"
},
"editor": {
"time_in_seconds": "Saniye cinsinden zaman",
@@ -172,7 +214,24 @@
"delete_question": "Soruyu sil",
"delete_answer": "Cevabı sil",
"add_new_answer": "Yeni cevap ekle",
"add_new_question": "Yeni soru ekle"
"add_new_question": "Yeni soru ekle",
"slide": {
"text": "Metin",
"headline": "Başlık",
"rectangle": "Dikdörtgen",
"rectangle_description": "Sadece bir dikdörtgen",
"headline_description": "Başlıklar için kalın bir metin",
"text_description": "Daha küçük ve daha uzun metin"
},
"bg_image": "Arkaplan resmi",
"empty": "Boş...",
"abcd_description": "Sadece bir cevap seçilebilir",
"voting_description": "Cevaplar puan eklemez",
"order_description": "Cevaplar doğru sıralamaya getirilebilir",
"range_description": "Kaydırıcı ile bir sayı aralığı seçilebilir",
"check_choice_description": "Puan toplamak için tüm doğru cevapların seçilmesi gerekir",
"text_description": "Oyuncular metin girebilir",
"no_title": "Başlık yok..."
},
"import_page": {
"visit_docs": "Dokümantasyonu ziyaret edin",
@@ -195,7 +254,10 @@
"change_password_submit": "Şifreyi değiştir!",
"last_seen": "Son görüldüğü tarih",
"delete_this_session": "Bu oturumu sil",
"old_password": "Eski şifre"
"old_password": "Eski şifre",
"add_api_key": "API anahtarı ekle",
"change_avatar": "Avatarı değiştir",
"security_settings": "Güvenlik Ayarları"
},
"explore_page": {
"made_by": "Oluşturan:",
@@ -210,7 +272,12 @@
"end_sentence": "Hepsi bu kadar! Quiz buydu.",
"1st_place": "1. Sıra",
"2nd_place": "2. Sıra",
"3rd place": "3. Sıra"
"3rd place": "3. Sıra",
"final_result_rank": "{{place}}: {{username}} ile {{points}} puan",
"join_description": "{{url}} adresinden katılın ve {{pin}} kodunu girin.",
"points_added": "Puanlar eklendi",
"your_score": "Skorunuz: {{score}}",
"join_by_entering_code": "Aşağıdaki kodu girerek katılın"
},
"editor_page": {
"add_an_answer": "Bir cevap ekle",
@@ -229,6 +296,144 @@
"unknown_error_text": "Bu gerçekleşmemeliydi. Muhtemelen benim hatam, sizin değil, ama belki sizde de bir şeyleri kırabilecek büyülü bir güç var..."
},
"uploader": {
"add_image": "Resim ekle"
"add_image": "Resim ekle",
"upload_a_video": "Bir Video Yükleyin",
"upload_video_popup_notice": "Açılır pencere açık; daha fazla bilgi için göz atın",
"select_upload_type": "Yükleme Türünü Seçin",
"upload_video": "Video Yükle"
},
"avatar_settings": {
"start_over": "Baştan başla",
"top_type": "Üst",
"hair_color": "Saç rengi",
"mouth_type": "Ağız",
"eyebrow_type": "Kaş",
"go_back": "Geri git",
"skin_color": "Ten rengi",
"facial_hair_type": "Yüz kılları",
"hat_color": "Şapka rengi",
"clothe_type": "Giysiler",
"clothe_graphic_type": "Grafik",
"thats_you": "Bu sensin!",
"facial_hair_color": "Yüz kıl rengi",
"clothe_color": "Giysi rengi",
"accessories_type": "Gözlük"
},
"security_settings": {
"backup_code": "Yedek Kod",
"activate_2fa": "İki Faktörlü Kimlik Doğrulamayı Etkinleştirme",
"get_backup_code": "Yedekleme Kodu Alın",
"enable_totp": "TOTP'yi etkinleştir",
"webauthn": "Webauthn",
"webauthn_available": "Webauthn kullanılabilir",
"totp": "TOTP",
"totp_available": "Totp kullanılabilir",
"totp_unavailable": "TOTP kullanılamaz",
"disable_totp": "TOTP'yi devre dışı bırak",
"backup_codes": {
"your_backup_code": "Yedekleme Kodunuz",
"save_somewhere_save": "Bunu güvenli bir yere sakla!",
"download_code": "Kodu indirin"
},
"totp_setup": {
"do_not_forget_backup_code": "Kurtarma kodunuzu kaydetmeyi unutmayın!",
"totp_setup": "TOTP Kurulumu",
"enter_as_secret_if_no_see_code": "QR kodunu tarayamıyorsanız bunu sır olarak girin",
"scan_to_set_up": "Kodu ayarlamak için bu QR kodunu tarayın"
},
"2fa_activated": "İki Faktörlü kimlik doğrulama etkinleştirildi",
"2fa_deactivated": "İki Faktörlü kimlik doğrulama devre dışı bırakıldı",
"webauthn_unavailable": "Webauthn kullanılamaz",
"add_security_key": "Güvenlik Anahtarı Ekle"
},
"result_page": {
"time_taken": "Alınan zaman",
"player_name": "Oyuncu adı",
"custom_field": "Özel alan",
"correct_answer_plural": "{{count}} doğru cevaplar",
"correct_answer": "{{count}} doğru cevap",
"player_score": "Oyuncu Skoru",
"average_score": "Ortalama skor: {{average_score}}"
},
"quiztivity": {
"editor": {
"move_left": "Sola hareket et",
"move_right": "Sağa hareket et",
"title_placeholder": "Başlığı buraya girin",
"add_new": "Yeni ekle",
"delete": "Sil",
"shares": {
"expires_on": "{{date}} tarihinde sona erer",
"never_expires": "Asla sona ermez",
"add_new_share": "Yeni Paylaşım Ekle"
},
"open_shares_menu": "Paylaşımlar menüsünü açın",
"select_page_type": "Sayfa Türünü Seçin"
},
"share_expired": "Paylaşım süresi doldu",
"memory": {
"editor": {
"add_card": "Kart ekle",
"upload_image": "Resim yükle",
"add_pair": "Çift ekle"
}
},
"play": {
"memory": {
"try_count": "Denemeler: {{try_count}}"
}
}
},
"file_dashboard": {
"not_available": "Mevcut değil",
"missing": "KAYIP!",
"caption": "Başlık: {{caption}}",
"filename": "Dosya adı: {{filename}}",
"size": "Boyut: {{size}} Mib",
"unset": "Ayarı kaldır",
"edit_details": "Ayrıntıları düzenle",
"edit_the_image": "Görüntüyü düzenleyin",
"filename_word": "Dosya adı",
"uploaded": "Yüklendi: {{tarih}}",
"alt_text": "Alt(ernatif) metin / Başlık",
"imported": "İçeri aktarıldı: {{yes_or_no}}",
"delete_image": "Resmi sil",
"storage_usage": "{{total}} MiB depolama alanınızın {{used}} MiB kadarını kullandınız. Bu, depolama alanınızın %{{percent}} kadarına eşdeğerdir."
},
"video_uploader": {
"time_remaining": "Kalan süre",
"time_elapsed": "Geçen süre"
},
"results_page": {
"no_results_so_far": "Şimdiye kadar kaydedilen sonuç yok...",
"quiz_title": "Quiz Başlığı",
"date_played": "Oynandığı Tarih",
"player_count": "Oyuncu sayısı",
"general_overview": {
"sentence": "Quiz \"{{title}}\", which was played on {{date}} tarihinde oynandı ve {{average_score}} ortalama skoruyla {{player_count}} oyuncusu vardı."
}
},
"navbar": {
"donate": "Bağışta Bulun"
},
"view_quiz_page": {
"made_by": "Oluşturan:",
"view_on_kahoot": "Kahoot'ta görüntüle!"
},
"start_game": {
"captcha_message": "Etkinleştirilirse, Google'ın ReCaptcha'sı oyuncuların tarayıcısına yüklenir. Captcha'yı yüklemek için HER oyuncunun onayına ihtiyacınız olduğundan, yalnızca gerçekten ihtiyacınız varsa etkinleştirin.",
"old_school_mode": "Eski Usul",
"normal_mode_description": "Soru ve cevaplar Kahoot! gibi sadece yöneticilerin ekranında gösterilecektir. Oyuncular sadece yöneticinin ekranında bunlarla eşleşen sembollere sahip renkli düğmelere sahip olacaklar.",
"old_school_mode_description": "Sorular ve görüntüler hem yöneticilerin ekranında hem de oyuncuların ekranında gösterilecektir",
"start_game": "Oyunu Başlat"
},
"public_user_page": {
"joined_on": "{{date}} tarihinde katıldı",
"no_original_quizzes": "Bu kullanıcının hiç orijinal quizi yok"
},
"components": {
"popover": {
"copied_to_clipboard": "Panoya kopyalandı!"
}
}
}
+43 -8
View File
@@ -31,14 +31,23 @@
"stats": "ClassQuiz 上已有 {{user_count}} 位使用者和 {{quiz_count}} 個測驗。",
"features_description": {
"1": "ClassQuiz 是一個可以讓你建立和管理測驗的測驗平台。",
"2": "最主要的功能之一是允許你匯入 Kahoot! 上的測驗。"
"2": "最主要的功能之一是允許你匯入 Kahoot! 上的測驗。",
"3": "編輯器和可將測驗結果匯出為 Excel 檔為此軟體的幾個亮點。"
},
"quiz_results_downloadable": "可被下載的測驗結果",
"find_or_explore": "搜尋 (或瀏覽) 其他人建立和匯入的測驗",
"no_tracking_content": "Kahoot! 會追蹤並將你的資料分享給第三方,但 ClassQuiz 不會。",
"user_friendly_content": "ClassQuiz 旨在簡單,每個人都可以輕鬆使用。",
"dark_mode_content": "一個網站最重要的功能之一!",
"german_server_content": "ClassQuiz 的伺服器位於德國,並由 netcup 提供。"
"german_server_content": "ClassQuiz 的伺服器位於德國,並由 netcup 提供。",
"how_does_classquiz_work": "ClassQuiz 是如何運作的?",
"see_how_many_true_and_false": "看看有多少人是對的或錯的",
"completely_free_content": "ClassQuiz 完全免費 (對用戶而言),沒有任何訂閱機制,也不會惱人地重新定向至升級頁面。你的捐贈我們不勝感激。",
"see_what_true_and_false": "看看什麼是對的,什麼是錯的",
"self_hostable_content": "輕鬆地自行架設 ClassQuiz ,資料只由你掌控!",
"quiz_results_downloadable_content": "測驗結果可以簡單地被匯出為 Excel 試算表。(不知道其他類似的服務沒有提供此功能)",
"community_driven": "社群驅動",
"community_driven_content": "ClassQuiz 依靠它的社群提供金援、測試想法、功能請求、翻譯...等!你也可以成為 ClassQuiz 的社群成員之一!"
},
"edit_page": {
"success_update_title": "測驗已更新。"
@@ -84,7 +93,13 @@
"add_new_question": "新增問題",
"delete_answer": "刪除答案",
"add_new_answer": "新增答案",
"not_all_links_imgur_links": "不是所有連結都是 Imgur 連結!"
"not_all_links_imgur_links": "不是所有連結都是 Imgur 連結!",
"right_or_true?": "正確?",
"no_title": "沒有標題...",
"bg_image": "背景圖片",
"slide": {
"rectangle": "長方形"
}
},
"import_page": {
"need_help": "需要幫助?",
@@ -108,7 +123,11 @@
"no_answers": "沒有答案!",
"start_by_showing_first_question": "開始時顯示第一個問題。",
"save_results": "儲存結果",
"already_registered_as_admin": "此遊戲已有一位管理員。"
"already_registered_as_admin": "此遊戲已有一位管理員。",
"stop_time_and_solutions": "停止計時並顯示解答",
"enter_answer_into_field": "在輸入欄內輸入你的答案!",
"show_results": "顯示結果",
"next_question": "下個問題 ({{question}})"
},
"password_reset_page": {
"reset_password": "重設密碼"
@@ -120,7 +139,8 @@
"change_password_submit": "變更密碼!",
"check_location": "檢查位置",
"delete_this_session": "刪除此工作階段",
"this_session?": "這個工作階段?"
"this_session?": "這個工作階段?",
"security_settings": "安全性設定"
},
"explore_page": {
"made_by": "作者為",
@@ -131,7 +151,9 @@
"3rd place": "第三名",
"with_out_of": "{{total_question_count}} 題中答對 {{correct_questions}} 題",
"1st_place": "第一名",
"end_sentence": "完成!本次測驗結束。"
"end_sentence": "完成!本次測驗結束。",
"your_score": "",
"join_description": "在 {{url}} 加入並輸入 {{pin}}。"
},
"editor_page": {
"add_an_answer": "新增一個答案",
@@ -204,7 +226,14 @@
"connect": "連線",
"results": "結果",
"score": "分數",
"player_plural": "玩家"
"player_plural": "玩家",
"finish": "完成",
"point": "",
"name": "姓名",
"slide": "投影片",
"note": "筆記",
"back": "返回",
"point_plural": ""
},
"search_page": {
"at_least_3_characters": "輸入至少 3 個字元...",
@@ -234,7 +263,8 @@
"facial_hair_type": "鬍子",
"hat_color": "帽子顏色",
"clothe_type": "服裝",
"clothe_color": "服裝顏色"
"clothe_color": "服裝顏色",
"go_back": "返回"
},
"results_page": {
"quiz_title": "測驗標題",
@@ -249,5 +279,10 @@
"time_taken": "花費時間",
"correct_answer": "{{count}} 個正確答案",
"correct_answer_plural": "{{count}} 個正確答案"
},
"security_settings": {
"activate_2fa": "啟用兩步驟驗證",
"2fa_deactivated": "兩步驟驗證未啟用",
"2fa_activated": "兩步驟驗證已啟用"
}
}
-10
View File
@@ -3,13 +3,3 @@
* 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 default {
en: {
common: {
'ClassQuiz is a quiz app like KAHOOT! for students, which is open source and free to use.':
'ClassQuiz is a quiz app like KAHOOT! for students, which is open source and free to use.',
'The open-source quiz-platform!': 'The open-source quiz-platform!'
}
}
};
-1
View File
@@ -73,7 +73,6 @@
})
.then((newEditor) => {
editor = newEditor;
console.log(editor);
editor.setData(text);
editor.model.document.on('change:data', () => {
triggerChange();
+5
View File
@@ -117,6 +117,11 @@
code: 'uk',
name: 'Українська',
flag: '🇺🇦'
},
{
code: 'nl',
name: 'Nederlands',
flag: '🇳🇱'
}
];
const get_selected_language = (): string => {
+2 -2
View File
@@ -109,7 +109,7 @@
<ul id="menu-items" class="lg:flex w-full flex-col lg:flex-row" class:hidden={openMenu}>
<li class="py-2 lg:hidden">
<BrownButton href="https://mawoka.eu/donate" target="_blank"
>Donate <span class="">❤️</span></BrownButton
>{$t('navbar.donate')} <span class="">❤️</span></BrownButton
>
</li>
{#if $signedIn}
@@ -183,7 +183,7 @@
>
<div class="whitespace-nowrap hidden lg:block">
<BrownButton href="https://mawoka.eu/donate" target="_blank"
>Donate <span class="">❤️</span></BrownButton
>{$t('navbar.donate')} <span class="">❤️</span></BrownButton
>
</div>
{#if darkMode}
@@ -5,6 +5,9 @@
-->
<script lang="ts">
import { onMount } from 'svelte';
import { getLocalization } from '$lib/i18n';
const { t } = getLocalization();
export let data;
export let username;
@@ -53,8 +56,11 @@
style="font-size: {player_count_or_five - i / 2}rem"
class="text-center"
>
{i + 1}
: {player} with {data[player]} points
{$t('play_page.final_result_rank', {
place: i + 1,
username: player,
points: data[player]
})}
</p>
{/if}
{/each}
@@ -62,7 +68,7 @@
{#if data[username]}
<div class="fixed bottom-0 left-0 flex justify-center w-full mb-6">
<div class="mx-auto p-2 border-[#B07156] border-4 rounded">
<p>Your score: <b>{data[username]}</b></p>
<p>{$t('play_page.your_score', { score: data[username] })}</p>
</div>
</div>
{/if}
@@ -38,7 +38,13 @@
<div class="grid grid-cols-3 mt-12">
<div class="flex justify-center">
<p class="m-auto text-2xl">
Join at <b>{window.location.host}/play</b> and enter <b>{game_pin}</b>.
{$t('play_page.join_description', {
url:
window.location.host === 'classquiz.de'
? 'cquiz.de'
: `${window.location.host}/play`,
pin: game_pin
})}
</p>
</div>
<img
@@ -49,7 +55,7 @@
{#if cqc_code}
<div class="m-auto">
<div class="flex-col flex justify-center">
<p class="mx-auto">Join by entering the following code</p>
<p class="mx-auto">{$t('play_page.join_by_entering_code')}</p>
<ControllerCodeDisplay code={cqc_code} />
</div>
</div>
+7 -3
View File
@@ -7,6 +7,8 @@
import { flip } from 'svelte/animate';
import { fly } from 'svelte/transition';
import { onMount } from 'svelte';
import { getLocalization } from '$lib/i18n';
const { t } = getLocalization();
export let data;
@@ -91,11 +93,13 @@
<div>
<table class="table-auto text-xl">
<tr>
<th class="p-2 border-r border-r-black border-b-2 border-b-black">Name</th>
<th class="p-2 border-b-2 border-b-black">Points</th>
<th class="p-2 border-r border-r-black border-b-2 border-b-black"
>{$t('words.name')}</th
>
<th class="p-2 border-b-2 border-b-black">{$t('words.point', { count: 2 })}</th>
{#if show_new_score_clicked}
<th in:fly={{ x: 300 }} class="p-2 border-b-2 border-b-black"
>Points added
>{$t('play_page.points_added')}
</th>
{/if}
</tr>
+30 -21
View File
@@ -8,7 +8,7 @@
import { onDestroy, onMount } from 'svelte';
import { browser } from '$app/environment';
import * as Sentry from '@sentry/browser';
import { alertModal } from '../stores';
// import { alertModal } from '../stores';
import { getLocalization } from '$lib/i18n';
import Cookies from 'js-cookie';
import BrownButton from '$lib/components/buttons/brown.svelte';
@@ -33,6 +33,7 @@
onMount(() => {
if (browser) {
prefetch_username();
hcaptcha = window.hcaptcha;
if (hcaptcha.render) {
hcaptchaWidgetID = hcaptcha.render('hcaptcha', {
@@ -54,6 +55,15 @@
}
});
const prefetch_username = async () => {
const res = await fetch('/api/v1/users/me');
if (res.status !== 200) {
return;
}
const json = await res.json();
username = json.username;
};
const set_game_pin = async () => {
let process_var;
try {
@@ -72,20 +82,22 @@
custom_field = json.custom_field;
}
if (res.status === 404) {
alertModal.set({
open: true,
title: 'Game not found',
body: 'The game pin you entered seems invalid.'
});
/* alertModal.set({
open: true,
title: 'Game not found',
body: 'The game pin you entered seems invalid.'
});*/
alert('Game not found');
game_pin = '';
return;
}
if (res.status !== 200) {
alertModal.set({
open: true,
body: `Unknown error with response-code ${res.status}`,
title: 'Unknown Error'
});
/* alertModal.set({
open: true,
body: `Unknown error with response-code ${res.status}`,
title: 'Unknown Error'
});*/
alert('Unknown error');
return;
}
};
@@ -122,16 +134,13 @@
if (import.meta.env.VITE_SENTRY !== null) {
Sentry.captureException(e);
}
alertModal.set({
open: true,
body: "The captcha failed, which is normal, but most of the time it's fixed by reloading!",
title: 'Captcha failed'
});
alertModal.subscribe((data) => {
if (!data.open) {
window.location.reload();
}
});
/* alertModal.set({
open: true,
body: "The captcha failed, which is normal, but most of the time it's fixed by reloading!",
title: 'Captcha failed'
});*/
alert('Captcha failed!');
window.location.reload();
}
} else if (import.meta.env.VITE_RECAPTCHA) {
// eslint-disable-next-line no-undef
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="666.667" height="666.667" preserveAspectRatio="xMidYMid meet" version="1.0" viewBox="0 0 500 500"><metadata>Created by potrace 1.16, written by Peter Selinger 2001-2019</metadata><g fill="#000" stroke="none"><path d="M2345 4799 c-123 -28 -281 -109 -400 -207 -180 -147 -461 -492 -640 -787 -43 -71 -84 -137 -91 -145 -19 -24 -231 -387 -359 -615 -64 -115 -134 -237 -155 -270 -84 -134 -350 -635 -350 -659 0 -3 -16 -40 -36 -83 -66 -142 -85 -187 -119 -278 -48 -130 -83 -251 -100 -346 -19 -109 -19 -317 0 -409 63 -302 296 -519 677 -634 144 -43 251 -62 463 -81 94 -9 230 -22 303 -30 193 -21 1737 -21 1995 0 332 27 538 62 730 126 276 90 474 244 571 443 63 130 76 193 76 377 0 252 -49 426 -229 828 -133 294 -247 511 -436 826 -59 99 -128 216 -152 260 -25 44 -76 130 -113 190 -102 168 -230 384 -263 445 -112 205 -438 640 -554 740 -16 14 -58 50 -94 81 -125 110 -276 192 -414 224 -66 16 -247 18 -310 4z m33 -805 c99 -49 199 -159 338 -369 63 -95 194 -310 194 -318 0 -5 98 -177 110 -192 28 -36 195 -356 235 -450 49 -117 85 -254 85 -326 0 -53 -25 -132 -54 -172 -102 -139 -434 -186 -1201 -173 -442 7 -627 31 -752 95 -106 54 -153 130 -153 250 0 40 7 101 16 134 38 148 198 486 319 672 17 28 49 81 70 120 159 292 320 530 432 640 117 114 247 146 361 89z" transform="translate(0.000000,500.000000) scale(0.100000,-0.100000)"/></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="666.667" height="666.667" preserveAspectRatio="xMidYMid meet" version="1.0" viewBox="0 0 500 500"><metadata>Created by potrace 1.16, written by Peter Selinger 2001-2019</metadata><g fill="currentColor" stroke="none"><path d="M2345 4799 c-123 -28 -281 -109 -400 -207 -180 -147 -461 -492 -640 -787 -43 -71 -84 -137 -91 -145 -19 -24 -231 -387 -359 -615 -64 -115 -134 -237 -155 -270 -84 -134 -350 -635 -350 -659 0 -3 -16 -40 -36 -83 -66 -142 -85 -187 -119 -278 -48 -130 -83 -251 -100 -346 -19 -109 -19 -317 0 -409 63 -302 296 -519 677 -634 144 -43 251 -62 463 -81 94 -9 230 -22 303 -30 193 -21 1737 -21 1995 0 332 27 538 62 730 126 276 90 474 244 571 443 63 130 76 193 76 377 0 252 -49 426 -229 828 -133 294 -247 511 -436 826 -59 99 -128 216 -152 260 -25 44 -76 130 -113 190 -102 168 -230 384 -263 445 -112 205 -438 640 -554 740 -16 14 -58 50 -94 81 -125 110 -276 192 -414 224 -66 16 -247 18 -310 4z m33 -805 c99 -49 199 -159 338 -369 63 -95 194 -310 194 -318 0 -5 98 -177 110 -192 28 -36 195 -356 235 -450 49 -117 85 -254 85 -326 0 -53 -25 -132 -54 -172 -102 -139 -434 -186 -1201 -173 -442 7 -627 31 -752 95 -106 54 -153 130 -153 250 0 40 7 101 16 134 38 148 198 486 319 672 17 28 49 81 70 120 159 292 320 530 432 640 117 114 247 146 361 89z" transform="translate(0.000000,500.000000) scale(0.100000,-0.100000)"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="666.667" height="666.667" preserveAspectRatio="xMidYMid meet" version="1.0" viewBox="0 0 500 500"><metadata>Created by potrace 1.16, written by Peter Selinger 2001-2019</metadata><g fill="#000" stroke="none"><path d="M2410 4863 c-448 -27 -898 -195 -1281 -477 -142 -105 -402 -369 -503 -511 -227 -320 -372 -675 -433 -1060 -26 -159 -25 -567 0 -720 43 -258 108 -467 212 -683 56 -116 166 -307 184 -322 4 -3 24 -30 45 -60 121 -171 352 -402 518 -520 358 -252 719 -395 1128 -446 152 -19 457 -18 607 1 405 53 811 216 1124 451 247 185 471 427 615 664 67 111 74 123 104 189 43 90 53 140 48 230 -8 138 -78 240 -208 303 -57 28 -75 32 -150 32 -76 1 -93 -3 -158 -33 -82 -39 -142 -97 -181 -177 -39 -80 -134 -226 -206 -318 -162 -208 -426 -402 -690 -508 -74 -29 -289 -85 -378 -97 -101 -14 -353 -14 -454 0 -88 12 -253 55 -359 93 -310 112 -619 356 -799 631 -21 33 -49 76 -61 95 -20 30 -53 99 -109 225 -23 52 -62 186 -85 290 -26 120 -38 398 -21 515 46 332 168 616 372 866 129 158 351 337 534 431 261 133 597 199 890 174 379 -33 730 -188 1000 -444 155 -146 259 -284 356 -471 54 -104 117 -166 208 -201 62 -25 78 -27 158 -23 67 4 100 11 138 31 79 40 131 91 167 165 31 63 33 73 33 172 l0 105 -51 105 c-103 208 -281 447 -470 630 -124 119 -145 137 -262 221 -454 329 -1007 487 -1582 452z" transform="translate(0.000000,500.000000) scale(0.100000,-0.100000)"/><path d="M2350 4015 c-191 -30 -337 -78 -507 -165 -90 -47 -250 -152 -273 -179 -3 -4 -29 -26 -57 -50 -107 -88 -273 -306 -340 -447 -46 -96 -49 -174 -10 -249 46 -91 109 -128 217 -128 106 -1 173 46 240 168 199 359 547 568 945 568 265 -1 478 -75 685 -239 250 -199 400 -513 400 -838 0 -219 -74 -454 -193 -615 -161 -217 -371 -366 -606 -427 -167 -43 -386 -43 -556 0 -113 29 -279 107 -358 168 -125 97 -255 245 -310 353 -45 89 -82 131 -142 160 -49 24 -64 27 -125 23 -117 -9 -196 -78 -220 -192 -19 -92 24 -204 141 -367 29 -41 57 -79 63 -86 6 -6 31 -36 56 -65 24 -29 79 -84 121 -122 492 -438 1200 -527 1779 -223 89 47 220 133 265 174 11 10 41 36 66 58 45 39 131 132 192 206 39 48 162 255 183 309 9 25 20 50 25 55 18 25 71 222 89 330 30 192 33 266 15 419 -39 335 -148 594 -354 849 -238 293 -620 502 -1014 556 -104 14 -311 12 -417 -4z" transform="translate(0.000000,500.000000) scale(0.100000,-0.100000)"/><path d="M2394 3420 c-205 -37 -407 -147 -545 -298 -186 -204 -270 -439 -256 -721 7 -147 33 -249 97 -378 190 -386 606 -598 1035 -528 167 27 300 85 441 191 112 85 235 238 274 342 25 65 25 86 1 139 -33 72 -137 107 -208 70 -37 -20 -62 -49 -104 -121 -43 -74 -148 -180 -223 -225 -31 -18 -96 -47 -144 -64 -165 -56 -361 -36 -523 56 -79 44 -193 154 -238 230 -168 285 -110 638 140 848 68 57 109 82 179 108 153 56 260 64 392 31 86 -22 185 -65 233 -102 61 -46 154 -147 184 -199 58 -101 106 -139 174 -139 85 0 151 68 151 153 0 63 -32 129 -111 234 -126 168 -298 286 -508 348 -76 23 -113 28 -240 31 -82 1 -173 -1 -201 -6z" transform="translate(0.000000,500.000000) scale(0.100000,-0.100000)"/></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="666.667" height="666.667" preserveAspectRatio="xMidYMid meet" version="1.0" viewBox="0 0 500 500"><metadata>Created by potrace 1.16, written by Peter Selinger 2001-2019</metadata><g fill="currentColor" stroke="none"><path d="M2410 4863 c-448 -27 -898 -195 -1281 -477 -142 -105 -402 -369 -503 -511 -227 -320 -372 -675 -433 -1060 -26 -159 -25 -567 0 -720 43 -258 108 -467 212 -683 56 -116 166 -307 184 -322 4 -3 24 -30 45 -60 121 -171 352 -402 518 -520 358 -252 719 -395 1128 -446 152 -19 457 -18 607 1 405 53 811 216 1124 451 247 185 471 427 615 664 67 111 74 123 104 189 43 90 53 140 48 230 -8 138 -78 240 -208 303 -57 28 -75 32 -150 32 -76 1 -93 -3 -158 -33 -82 -39 -142 -97 -181 -177 -39 -80 -134 -226 -206 -318 -162 -208 -426 -402 -690 -508 -74 -29 -289 -85 -378 -97 -101 -14 -353 -14 -454 0 -88 12 -253 55 -359 93 -310 112 -619 356 -799 631 -21 33 -49 76 -61 95 -20 30 -53 99 -109 225 -23 52 -62 186 -85 290 -26 120 -38 398 -21 515 46 332 168 616 372 866 129 158 351 337 534 431 261 133 597 199 890 174 379 -33 730 -188 1000 -444 155 -146 259 -284 356 -471 54 -104 117 -166 208 -201 62 -25 78 -27 158 -23 67 4 100 11 138 31 79 40 131 91 167 165 31 63 33 73 33 172 l0 105 -51 105 c-103 208 -281 447 -470 630 -124 119 -145 137 -262 221 -454 329 -1007 487 -1582 452z" transform="translate(0.000000,500.000000) scale(0.100000,-0.100000)"/><path d="M2350 4015 c-191 -30 -337 -78 -507 -165 -90 -47 -250 -152 -273 -179 -3 -4 -29 -26 -57 -50 -107 -88 -273 -306 -340 -447 -46 -96 -49 -174 -10 -249 46 -91 109 -128 217 -128 106 -1 173 46 240 168 199 359 547 568 945 568 265 -1 478 -75 685 -239 250 -199 400 -513 400 -838 0 -219 -74 -454 -193 -615 -161 -217 -371 -366 -606 -427 -167 -43 -386 -43 -556 0 -113 29 -279 107 -358 168 -125 97 -255 245 -310 353 -45 89 -82 131 -142 160 -49 24 -64 27 -125 23 -117 -9 -196 -78 -220 -192 -19 -92 24 -204 141 -367 29 -41 57 -79 63 -86 6 -6 31 -36 56 -65 24 -29 79 -84 121 -122 492 -438 1200 -527 1779 -223 89 47 220 133 265 174 11 10 41 36 66 58 45 39 131 132 192 206 39 48 162 255 183 309 9 25 20 50 25 55 18 25 71 222 89 330 30 192 33 266 15 419 -39 335 -148 594 -354 849 -238 293 -620 502 -1014 556 -104 14 -311 12 -417 -4z" transform="translate(0.000000,500.000000) scale(0.100000,-0.100000)"/><path d="M2394 3420 c-205 -37 -407 -147 -545 -298 -186 -204 -270 -439 -256 -721 7 -147 33 -249 97 -378 190 -386 606 -598 1035 -528 167 27 300 85 441 191 112 85 235 238 274 342 25 65 25 86 1 139 -33 72 -137 107 -208 70 -37 -20 -62 -49 -104 -121 -43 -74 -148 -180 -223 -225 -31 -18 -96 -47 -144 -64 -165 -56 -361 -36 -523 56 -79 44 -193 154 -238 230 -168 285 -110 638 140 848 68 57 109 82 179 108 153 56 260 64 392 31 86 -22 185 -65 233 -102 61 -46 154 -147 184 -199 58 -101 106 -139 174 -139 85 0 151 68 151 153 0 63 -32 129 -111 234 -126 168 -298 286 -508 348 -76 23 -113 28 -240 31 -82 1 -173 -1 -201 -6z" transform="translate(0.000000,500.000000) scale(0.100000,-0.100000)"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

@@ -8,7 +8,7 @@
Created by potrace 1.16, written by Peter Selinger 2001-2019
</metadata>
<g transform="translate(0.000000,500.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
fill="currentColor" stroke="none">
<path d="M1272 4522 c-20 -20 -49 -117 -81 -269 -10 -50 -22 -96 -25 -101 -31
-51 -49 -635 -24 -810 16 -115 18 -116 191 -69 73 19 171 43 217 52 47 9 92
20 101 24 8 5 67 14 130 21 194 20 171 8 255 123 92 124 221 274 304 352 97

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

@@ -6,7 +6,7 @@
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,500.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
fill="currentColor" stroke="none">
<path d="M2175 4799 c-430 -40 -450 -45 -515 -116 -151 -163 -352 -455 -465
-675 -82 -159 -75 -178 48 -129 375 149 747 226 1146 237 447 13 857 -53 1262
-201 191 -71 194 -71 193 -24 -3 97 -365 650 -531 812 -54 52 -80 57 -518 97

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

+41 -40
View File
@@ -13,6 +13,7 @@
import CircularTimer from '$lib/play/circular_progress.svelte';
import { flip } from 'svelte/animate';
import BrownButton from '$lib/components/buttons/brown.svelte';
import { get_foreground_color } from '../helpers';
const { t } = getLocalization();
@@ -31,10 +32,10 @@
}
/* if (typeof question_index === 'string') {
question_index = parseInt(question_index);
} else {
throw new Error('question_index must be a string or number');
}*/
question_index = parseInt(question_index);
} else {
throw new Error('question_index must be a string or number');
}*/
let timer_res = question.time;
let selected_answer: string;
@@ -138,6 +139,7 @@
}
};
$: console.log(slider_value, 'values');
const default_colors = ['#D6EDC9', '#B07156', '#7F7057', '#4E6E58'];
</script>
<div class="h-screen w-screen">
@@ -152,7 +154,7 @@
{#if question.image !== null && game_mode !== 'kahoot'}
<div class="max-h-full">
<img
src={question.image}
src="/api/v1/storage/download/{question.image}"
class="object-cover mx-auto mb-8 max-h-[90%]"
alt="Content for Question"
/>
@@ -176,14 +178,17 @@
<div class="grid grid-rows-2 grid-flow-col auto-cols-auto gap-2 w-full p-4 h-full">
{#each question.answers as answer, i}
<button
class="rounded-lg h-full flex align-middle justify-center disabled:opacity-60 p-3"
style="background-color: {answer.color ?? '#B07156'}"
class="rounded-lg h-full flex align-middle justify-center disabled:opacity-60 p-3 border-2 border-black"
style="background-color: {answer.color ??
default_colors[i]}; color: {get_foreground_color(
answer.color ?? default_colors[i]
)}"
disabled={selected_answer !== undefined}
on:click={() => selectAnswer(answer.answer)}
>
{#if game_mode === 'kahoot'}
<img
class="w-10 inline-block m-auto"
class="h-2/3 inline-block m-auto"
alt="Icon"
src={kahoot_icons[i]}
/>
@@ -246,30 +251,6 @@
</BrownButton>
</div>
</div>
{:else if question.type === QuizQuestionType.ABCD}
{#if solution === undefined}
<Spinner />
{:else}
<div class="grid grid-cols-2 gap-2 w-full p-4">
{#each solution.answers as answer}
{#if answer.right}
<button
class="text-3xl rounded-lg h-fit flex align-middle justify-center p-3 bg-green-600"
disabled
class:opacity-30={answer.answer !== selected_answer}
>{answer.answer}</button
>
{:else}
<button
class="text-3xl rounded-lg h-fit flex align-middle justify-center p-3 bg-red-500"
disabled
class:opacity-30={answer.answer !== selected_answer}
>{answer.answer}</button
>
{/if}
{/each}
</div>
{/if}
{:else if question.type === QuizQuestionType.RANGE}
{#if solution === undefined}
<Spinner />
@@ -286,8 +267,8 @@
{/if}
{:else if question.type === QuizQuestionType.ORDER}
<!-- {#if solution === undefined}
<Spinner />
{:else}-->
<Spinner />
{:else}-->
<div class="flex flex-col w-full h-full gap-4 px-4 py-6">
{#each question.answers as answer, i (answer.id)}
<div
@@ -360,14 +341,34 @@
</div>
</div>
<!--{/if}-->
{:else if question.type === QuizQuestionType.CHECK}
{#await import('./questions/check.svelte')}
<Spinner />
{:then c}
<svelte:component
this={c.default}
bind:question
bind:selected_answer
bind:game_mode
/>
<div class="flex justify-center h-[5%]">
<div class="w-1/2">
<BrownButton
disabled={!selected_answer}
on:click={() => selectAnswer(selected_answer)}
>{$t('words.submit')}
</BrownButton>
</div>
</div>
{/await}
{/if}
<!--{:else if question.type === QuizQuestionType.VOTING}
{#await import('$lib/play/admin/voting_results.svelte')}
<Spinner />
{:then c}
<svelte:component this={c.default} bind:data={question_results}
bind:question={quiz_data.questions[selected_question]} />
{/await}-->
{#await import('$lib/play/admin/voting_results.svelte')}
<Spinner />
{:then c}
<svelte:component this={c.default} bind:data={question_results}
bind:question={quiz_data.questions[selected_question]} />
{/await}-->
{/if}
</div>
@@ -0,0 +1,64 @@
<!--
- 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 { Question } from '$lib/quiz_types';
import { get_foreground_color } from '$lib/helpers';
import { kahoot_icons } from '$lib/play/kahoot_mode_assets/kahoot_icons';
// import CircularTimer from '$lib/play/circular_progress.svelte';
const default_colors = ['#D6EDC9', '#B07156', '#7F7057', '#4E6E58'];
export let question: Question;
export let selected_answer = '';
export let game_mode;
let _selected_answers = [false, false, false, false];
const selectAnswer = (i: number) => {
_selected_answers[i] = !_selected_answers[i];
selected_answer = '';
for (let i = 0; i < _selected_answers.length; i++) {
if (_selected_answers[i]) {
selected_answer += String(i);
}
}
selected_answer = selected_answer;
console.log(_selected_answers, selected_answer);
};
</script>
<div class="w-full h-[95%]">
<!--
<div
class="absolute top-0 bottom-0 left-0 right-0 m-auto rounded-full h-fit w-fit border-2 border-black shadow-2xl z-50"
>
<CircularTimer
bind:text={timer_res}
bind:progress={circular_prgoress}
color="#ef4444"
/>
</div>
-->
<div class="grid grid-rows-2 grid-flow-col auto-cols-auto gap-2 w-full p-4 h-full">
{#each question.answers as answer, i}
<button
class="rounded-lg h-full flex align-middle justify-center disabled:opacity-60 p-3 border-2 border-black transition-all"
style="background-color: {answer.color ??
default_colors[i]}; color: {get_foreground_color(
answer.color ?? default_colors[i]
)}"
on:click={() => selectAnswer(i)}
class:opacity-100={_selected_answers[i]}
class:opacity-50={!_selected_answers[i]}
>
{#if game_mode === 'kahoot'}
<img class="h-2/3 inline-block m-auto" alt="Icon" src={kahoot_icons[i]} />
{:else}
<p class="m-auto">{answer.answer}</p>
{/if}
</button>
{/each}
</div>
</div>
+5 -1
View File
@@ -15,7 +15,11 @@
{#if cover_image}
<div class="flex justify-center align-middle items-center">
<div class="h-[30vh] m-auto w-auto mt-12">
<img class="max-h-full max-w-full block" src={cover_image} alt="Not provided" />
<img
class="max-h-full max-w-full block"
src="/api/v1/storage/download/{cover_image}"
alt="Not provided"
/>
</div>
</div>
{/if}
+53 -10
View File
@@ -10,6 +10,8 @@
import { getLocalization } from '$lib/i18n';
import Spinner from '$lib/Spinner.svelte';
import { flip } from 'svelte/animate';
import BrownButton from '$lib/components/buttons/brown.svelte';
import MediaComponent from '$lib/editor/MediaComponent.svelte';
export let question: Question;
@@ -43,6 +45,8 @@
let text_input;
timer(question.time);
let check_choice_selected = [false, false, false, false];
function shuffleArray(a) {
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
@@ -73,13 +77,13 @@
let order_corrected = false;
const select_complex_answer = () => {
/* const correct_order_ids = []
for (const e of original_order) {
correct_order_ids.push(e.id)
}
const user_set_ids = []
for (const e of answer) {
correct_order_ids.push(e.id)
}*/
for (const e of original_order) {
correct_order_ids.push(e.id)
}
const user_set_ids = []
for (const e of answer) {
correct_order_ids.push(e.id)
}*/
question.answers = original_order;
order_corrected = true;
timer_res = '0';
@@ -90,10 +94,9 @@
<h1 class="text-3xl text-center">{@html question.question}</h1>
{#if question.image !== null}
<div>
<img
<MediaComponent
src={question.image}
class="max-h-[40vh] object-cover mx-auto mb-8 w-auto"
alt="Content for Question"
css_classes="max-h-[40vh] object-cover mx-auto mb-8 w-auto"
/>
</div>
{/if}
@@ -303,5 +306,45 @@
{$t('words.submit')}
</button>
</div>
{:else if question.type === QuizQuestionType.CHECK}
{#if show_results}
<div>
{#each question.answers as answer, i}
<button
disabled
class:bg-green-500={question.answers[i].right}
class:bg-red-500={!question.answers[i].right}
class="p-2 rounded-lg flex justify-center w-full transition my-5 text-black"
>{answer.answer}</button
>
{/each}
</div>
{:else}
<div>
{#each question.answers as answer, i}
<button
disabled={selected_answer !== undefined || timer_res === '0'}
class="p-2 rounded-lg flex justify-center w-full transition bg-amber-300 my-5 disabled:grayscale text-black opacity-50"
class:opacity-100={check_choice_selected[i]}
on:click={() => {
check_choice_selected[i] = !check_choice_selected[i];
}}>{answer.answer}</button
>
{/each}
<BrownButton
on:click={() => {
timer_res = '0';
}}>{$t('words.submit')}</BrownButton
>
{#if timer_res === '0'}
<button
class="bg-orange-500 p-2 rounded-lg flex justify-center w-full transition my-5 text-black"
on:click={() => {
show_results = true;
}}>{$t('admin_page.get_results')}</button
>
{/if}
</div>
{/if}
{/if}
</div>
@@ -46,7 +46,7 @@
{#if data.cover_image != undefined && data.cover_image !== ''}
<div class="flex justify-center pt-10 w-full max-h-72 w-full">
<img
src={data.cover_image}
src="/api/v1/storage/download/{data.cover_image}"
alt="not available"
class="max-h-72 h-auto w-auto"
on:contextmenu|preventDefault={() => {
+27 -8
View File
@@ -31,7 +31,8 @@ export enum QuizQuestionType {
VOTING = 'VOTING', // eslint-disable-line no-unused-vars
SLIDE = 'SLIDE', // eslint-disable-line no-unused-vars
TEXT = 'TEXT', // eslint-disable-line no-unused-vars
ORDER = 'ORDER' // eslint-disable-line no-unused-vars
ORDER = 'ORDER', // eslint-disable-line no-unused-vars
CHECK = 'CHECK' // eslint-disable-line no-unused-vars
}
export interface RangeQuizAnswer {
@@ -57,15 +58,17 @@ export interface Question {
question: string;
type?: QuizQuestionType;
image?: string;
answers:
| Answer[]
| RangeQuizAnswer
| VotingAnswer[]
| string
| TextQuizAnswer[]
| OrderQuizAnswer[];
answers: Answers;
}
export type Answers =
| Answer[]
| RangeQuizAnswer
| VotingAnswer[]
| string
| TextQuizAnswer[]
| OrderQuizAnswer[];
export interface Answer {
right: boolean;
answer: string;
@@ -87,3 +90,19 @@ export interface EditorData {
background_color?: string;
background_image?: string;
}
export interface PrivateImageData {
id: string;
uploaded_at: string;
mime_type: string;
hash?: string;
size?: number;
deleted_at?: string;
alt_text?: string;
filename?: string;
thumbhash?: string;
server?: string;
imported: boolean;
quizzes: { id: string }[];
quiztivities: { id: string }[];
}
@@ -0,0 +1,64 @@
<!--
- 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
},
{
name: 'Multiple Choice',
description: 'Multiple Choice Quiz',
type: QuizTivityTypes.ABCD,
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,90 @@
<!--
- 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 { Abcd } from '$lib/quiztivity/types';
import { getLocalization } from '$lib/i18n';
import BrownButton from '$lib/components/buttons/brown.svelte';
export let data: Abcd | undefined;
if (!data) {
data = {
question: '',
answers: []
};
}
const { t } = getLocalization();
</script>
<div>
<div class="flex justify-center">
<input
class="bg-transparent outline-none text-3xl text-center"
placeholder="Enter question here..."
bind:value={data.question}
/>
</div>
<div class="grid grid-cols-2 m-4 gap-4">
{#each data.answers as answer}
<div class="rounded p-6 bg-gray-700 flex">
<input
bind:value={answer.answer}
class="w-full my-auto bg-transparent outline-none text-center text-white"
placeholder="Enter answer here"
/>
<button
type="button"
on:click={() => {
answer.correct = !answer.correct;
}}
>
{#if answer.correct}
<svg
class="w-6 h-6 inline-block text-white"
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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{:else}
<svg
class="w-6 h-6 inline-block text-white"
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="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{/if}
</button>
</div>
{/each}
{#if data.answers.length < 4}
<div class="rounded p-6 bg-gray-700">
<BrownButton
on:click={() => {
data.answers = [...data.answers, { ...{ answer: '', correct: false } }];
}}>{$t('editor_page.add_an_answer')}</BrownButton
>
</div>
{/if}
</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 { Abcd } from '$lib/quiztivity/types';
export let data: Abcd | undefined;
let selected_answer: number | undefined;
const select_answer = (i: number) => {
selected_answer = i;
console.log(data);
};
</script>
<div>
<h1 class="text-center text-4xl">{data.question}</h1>
<div class="grid grid-cols-1 lg:grid-cols-2 m-4 gap-4">
{#each data.answers as answer, i}
<button
class="rounded p-6 bg-gray-700 flex transition-all"
on:click={() => {
select_answer(i);
}}
class:opacity-50={selected_answer !== undefined && !answer.correct}
class:text-2xl={selected_answer === i}
>
<span class="m-auto text-white">{answer.answer}</span>
</button>
{/each}
</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/.
-->
+182
View File
@@ -0,0 +1,182 @@
<!--
- 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 AbcdEdit from './components/abcd/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;
selected_slide = null;
};
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 if sel_t === QuizTivityTypes.ABCD}
<AbcdEdit 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,247 @@
<!--
- 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();
};
const share_available = (): boolean => {
try {
return browser
? !navigator.canShare({
title: 'title',
url: `${window.location.origin}/quiztivity`
})
: false;
} catch {
return false;
}
};
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={share_available()}
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>
+59
View File
@@ -0,0 +1,59 @@
/*
* 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',
// eslint-disable-next-line no-unused-vars
ABCD = 'ABCD'
}
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 AbcdAnswer {
answer: string;
correct: boolean;
}
export interface Abcd {
question: string;
answers: AbcdAnswer[];
}
export interface QuizTivityPage {
title?: string;
type: QuizTivityTypes;
data: Pdf | Memory | Markdown;
id?: string;
}
export interface Data {
id?: string;
title: string;
pages: QuizTivityPage[];
}
+273
View File
@@ -0,0 +1,273 @@
/*
MIT License
Copyright (c) 2020 Jamie Kyle
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
COPIED FROM https://github.com/jamiebuilds/tinykeys/blob/111955cb6604fb5b8c4f152cb75b7f2cb63da913/src/tinykeys.ts
*/
type KeyBindingPress = [string[], string];
/**
* A map of keybinding strings to event handlers.
*/
export interface KeyBindingMap {
// eslint-disable-next-line no-unused-vars
[keybinding: string]: (event: KeyboardEvent) => void;
}
export interface KeyBindingHandlerOptions {
/**
* Keybinding sequences will wait this long between key presses before
* cancelling (default: 1000).
*
* **Note:** Setting this value too low (i.e. `300`) will be too fast for many
* of your users.
*/
timeout?: number;
}
/**
* Options to configure the behavior of keybindings.
*/
export interface KeyBindingOptions extends KeyBindingHandlerOptions {
/**
* Key presses will listen to this event (default: "keydown").
*/
event?: 'keydown' | 'keyup';
}
/**
* These are the modifier keys that change the meaning of keybindings.
*
* Note: Ignoring "AltGraph" because it is covered by the others.
*/
const KEYBINDING_MODIFIER_KEYS = ['Shift', 'Meta', 'Alt', 'Control'];
/**
* Keybinding sequences should timeout if individual key presses are more than
* 1s apart by default.
*/
const DEFAULT_TIMEOUT = 1000;
/**
* Keybinding sequences should bind to this event by default.
*/
const DEFAULT_EVENT = 'keydown';
/**
* Platform detection code.
* @see https://github.com/jamiebuilds/tinykeys/issues/184
*/
const PLATFORM = typeof navigator === 'object' ? navigator.platform : '';
const APPLE_DEVICE = /Mac|iPod|iPhone|iPad/.test(PLATFORM);
/**
* An alias for creating platform-specific keybinding aliases.
*/
const MOD = APPLE_DEVICE ? 'Meta' : 'Control';
/**
* Meaning of `AltGraph`, from MDN:
* - Windows: Both Alt and Ctrl keys are pressed, or AltGr key is pressed
* - Mac: Option key pressed
* - Linux: Level 3 Shift key (or Level 5 Shift key) pressed
* - Android: Not supported
* @see https://github.com/jamiebuilds/tinykeys/issues/185
*/
const ALT_GRAPH_ALIASES = PLATFORM === 'Win32' ? ['Control', 'Alt'] : APPLE_DEVICE ? ['Alt'] : [];
/**
* There's a bug in Chrome that causes event.getModifierState not to exist on
* KeyboardEvent's for F1/F2/etc keys.
*/
function getModifierState(event: KeyboardEvent, mod: string) {
return typeof event.getModifierState === 'function'
? event.getModifierState(mod) ||
(ALT_GRAPH_ALIASES.includes(mod) && event.getModifierState('AltGraph'))
: false;
}
/**
* Parses a "Key Binding String" into its parts
*
* grammar = `<sequence>`
* <sequence> = `<press> <press> <press> ...`
* <press> = `<key>` or `<mods>+<key>`
* <mods> = `<mod>+<mod>+...`
*/
export function parseKeybinding(str: string): KeyBindingPress[] {
return str
.trim()
.split(' ')
.map((press) => {
let mods = press.split(/\b\+/);
const key = mods.pop() as string;
mods = mods.map((mod) => (mod === '$mod' ? MOD : mod));
return [mods, key];
});
}
/**
* This tells us if a series of events matches a key binding sequence either
* partially or exactly.
*/
function match(event: KeyboardEvent, press: KeyBindingPress): boolean {
// prettier-ignore
return !(
// Allow either the `event.key` or the `event.code`
// MDN event.key: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
// MDN event.code: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code
(
press[1].toUpperCase() !== event.key.toUpperCase() &&
press[1] !== event.code
) ||
// Ensure all the modifiers in the keybinding are pressed.
press[0].find(mod => {
return !getModifierState(event, mod)
}) ||
// KEYBINDING_MODIFIER_KEYS (Shift/Control/etc) change the meaning of a
// keybinding. So if they are pressed but aren't part of the current
// keybinding press, then we don't have a match.
KEYBINDING_MODIFIER_KEYS.find(mod => {
return !press[0].includes(mod) && press[1] !== mod && getModifierState(event, mod)
})
)
}
/**
* Creates an event listener for handling keybindings.
*
* @example
* ```js
* import { createKeybindingsHandler } from "../src/keybindings"
*
* let handler = createKeybindingsHandler({
* "Shift+d": () => {
* alert("The 'Shift' and 'd' keys were pressed at the same time")
* },
* "y e e t": () => {
* alert("The keys 'y', 'e', 'e', and 't' were pressed in order")
* },
* "$mod+d": () => {
* alert("Either 'Control+d' or 'Meta+d' were pressed")
* },
* })
*
* window.addEvenListener("keydown", handler)
* ```
*/
export function createKeybindingsHandler(
keyBindingMap: KeyBindingMap,
options: KeyBindingHandlerOptions = {}
): EventListener {
const timeout = options.timeout ?? DEFAULT_TIMEOUT;
const keyBindings = Object.keys(keyBindingMap).map((key) => {
return [parseKeybinding(key), keyBindingMap[key]] as const;
});
const possibleMatches = new Map<KeyBindingPress[], KeyBindingPress[]>();
let timer: number | null = null;
return (event) => {
// Ensure and stop any event that isn't a full keyboard event.
// Autocomplete option navigation and selection would fire a instanceof Event,
// instead of the expected KeyboardEvent
if (!(event instanceof KeyboardEvent)) {
return;
}
keyBindings.forEach((keyBinding) => {
const sequence = keyBinding[0];
const callback = keyBinding[1];
const prev = possibleMatches.get(sequence);
const remainingExpectedPresses = prev ? prev : sequence;
const currentExpectedPress = remainingExpectedPresses[0];
const matches = match(event, currentExpectedPress);
if (!matches) {
// Modifier keydown events shouldn't break sequences
// Note: This works because:
// - non-modifiers will always return false
// - if the current keypress is a modifier then it will return true when we check its state
// MDN: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState
if (!getModifierState(event, event.key)) {
possibleMatches.delete(sequence);
}
} else if (remainingExpectedPresses.length > 1) {
possibleMatches.set(sequence, remainingExpectedPresses.slice(1));
} else {
possibleMatches.delete(sequence);
callback(event);
}
});
if (timer) {
clearTimeout(timer);
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // skipcq: JS-0372
timer = setTimeout(possibleMatches.clear.bind(possibleMatches), timeout);
};
}
/**
* Subscribes to keybindings.
*
* Returns an unsubscribe method.
*
* @example
* ```js
* import { tinykeys } from "../src/tinykeys"
*
* tinykeys(window, {
* "Shift+d": () => {
* alert("The 'Shift' and 'd' keys were pressed at the same time")
* },
* "y e e t": () => {
* alert("The keys 'y', 'e', 'e', and 't' were pressed in order")
* },
* "$mod+d": () => {
* alert("Either 'Control+d' or 'Meta+d' were pressed")
* },
* })
* ```
*/
export function tinykeys(
target: Window | HTMLElement,
keyBindingMap: KeyBindingMap,
options: KeyBindingOptions = {}
): () => void {
const event = options.event ?? DEFAULT_EVENT;
const onKeyEvent = createKeybindingsHandler(keyBindingMap, options);
target.addEventListener(event, onKeyEvent);
return () => {
target.removeEventListener(event, onKeyEvent);
};
}
+2 -14
View File
@@ -4,6 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
// skipcq: JS-C1003
import * as yup from 'yup';
export const ABCDQuestionSchema = yup
@@ -65,34 +66,21 @@ export const dataSchema = yup.object({
yup.object({
question: yup.string().required('A question-title is required').max(299),
time: yup.number().required().positive('The time has to be positive'),
image: yup
.string()
.nullable()
.matches(
/^(http(|s):\/\/.*(|:)\d*\/api\/v1\/storage\/download\/.{36}--.{36}|https:\/\/i\.imgur\.com\/.{7}.(jpg|png|gif))$|^$/,
"The image-url isn't valid"
)
.lowercase(),
image: yup.string().nullable().lowercase(),
answers: yup.lazy((v) => {
if (Array.isArray(v)) {
if (typeof v[0].right === 'boolean') {
console.log('ABCDQuestionSchema');
return ABCDQuestionSchema;
} else if (typeof v[0].case_sensitive === 'boolean') {
console.log('TextQuestionSchema');
return TextQuestionSchema;
} else if (v[0].id !== undefined) {
console.log('OrderQuestionSchema');
return VotingQuestionSchema;
} else if (v[0].answer !== undefined) {
console.log('VotingQuestionSchema');
return VotingQuestionSchema;
}
} else if (typeof v === 'string' || v instanceof String) {
console.log('StringQuestionSchema');
return yup.string().required("The slide mustn't be empty").nullable();
} else {
console.log('RangeQuestionSchema');
return RangeQuestionSchema;
}
})
+1 -1
View File
@@ -29,7 +29,7 @@
{$t('error_page.404_text')}
</p>
{:else}
<p>
<p class="text-center">
{$t('error_page.unknown_error_text')}
</p>
{/if}
+1 -1
View File
@@ -1,7 +1,7 @@
import { signedIn } from '$lib/stores';
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ locals }) => {
export const load: LayoutServerLoad = ({ locals }) => {
if (locals.email) {
signedIn.set(true);
} else {
+11 -5
View File
@@ -6,12 +6,13 @@
<script>
import '../app.css';
import Navbar from '$lib/navbar.svelte';
import { navbarVisible, pathname, alertModal } from '$lib/stores';
import { navbarVisible, pathname } from '$lib/stores';
import * as Sentry from '@sentry/browser';
import { BrowserTracing } from '@sentry/tracing';
import { initLocalizationContext } from '$lib/i18n';
import { browser } from '$app/environment';
import Alert from '$lib/modals/alert.svelte';
import CommandPalette from '$lib/components/commandpalette.svelte';
// import Alert from '$lib/modals/alert.svelte';
/* afterNavigate(() => {
if (browser) {
@@ -91,9 +92,14 @@
{:else}
<slot />
{/if}
{#if $alertModal.open}
<CommandPalette />
<!--{#if $alertModal.open ?? false}
<div
class="fixed inset-0 h-screen w-screen bg-black z-30 bg-opacity-60 flex items-center justify-center content-center"
class="fixed inset-0 h-screen w-screen bg-black z-30 bg-opacity-60 items-center justify-center content-center"
class:hidden={!$alertModal.open}
class:flex={$alertModal.open}
class:visible={$alertModal.open}
>
<Alert
bind:title={$alertModal.title}
@@ -101,7 +107,7 @@
bind:open={$alertModal.open}
/>
</div>
{/if}
{/if}-->
<style lang="scss">
:global(html:not(.dark)) {
+6 -6
View File
@@ -208,7 +208,7 @@
</div>
</section>
<section>
<h2 class="text-center text-5xl mb-6">How does ClassQuiz even work?</h2>
<h2 class="text-center text-5xl mb-6">{$t('index_page.how_does_classquiz_work')}</h2>
<div class="flex justify-center w-full">
<h3 class="text-center text-3xl rounded-t-lg bg-opacity-40 bg-white py-2 px-6">
@@ -258,7 +258,7 @@
class:opacity-70={selected_create_thing !== SelectedCreateThing.Create}
>
<div
class="rounded-lg bg-emerald-300 w-fit p-1 bg-lime-500 hover:bg-lime-400 transition shadow-lg"
class="rounded-lg w-fit p-1 bg-lime-500 hover:bg-lime-400 transition shadow-lg"
>
<svg
aria-label="Pencil-Icon"
@@ -288,7 +288,7 @@
class:opacity-70={selected_create_thing !== SelectedCreateThing.Find}
>
<div
class="rounded-lg bg-emerald-300 w-fit p-1 bg-lime-500 hover:bg-lime-400 transition shadow-lg"
class="rounded-lg w-fit p-1 bg-lime-500 hover:bg-lime-400 transition shadow-lg"
>
<svg
class="w-8 h-8 text-black"
@@ -309,7 +309,7 @@
<h5 class="text-xl dark:text-black">{$t('words.find')}</h5>
<p class="dark:text-black">{$t('index_page.find_or_explore')}</p>
</div>
<div
<!--<div
class="m-2 rounded-lg p-2 bg-opacity-40 bg-white transition-all cursor-pointer lg:h-full"
on:click={() => {
selected_create_thing = SelectedCreateThing.Import;
@@ -318,7 +318,7 @@
class:opacity-70={selected_create_thing !== SelectedCreateThing.Import}
>
<div
class="rounded-lg bg-emerald-300 w-fit p-1 bg-lime-500 hover:bg-lime-400 transition shadow-lg"
class="rounded-lg w-fit p-1 bg-lime-500 hover:bg-lime-400 transition shadow-lg"
>
<svg
class="w-8 h-8 text-black"
@@ -340,7 +340,7 @@
<p class="dark:text-black">
{$t('index_page.import_quiz_from_kahoot_and_edit')}
</p>
</div>
</div>-->
</div>
</div>
</section>
@@ -5,7 +5,6 @@
-->
<script lang="ts">
import { getLocalization } from '$lib/i18n';
import { alertModal } from '../../../lib/stores';
export let session_data;
export let selected_method;
@@ -37,19 +36,16 @@
try {
data = await res.json();
} catch {
alertModal.set({
open: true,
body: "This shouldn't happen. Please try again.",
title: 'Unknown error'
});
alert("This shouldn't happen");
window.location.reload();
}
if (data.detail === 'wrong credentials') {
alertModal.set({
/* alertModal.set({
open: true,
body: 'Please try again. Your email and or password were incorrect.',
title: 'Wrong Credentials'
});
});*/
alert('Wrong credentials');
}
}
};
@@ -5,7 +5,7 @@
-->
<script lang="ts">
import { getLocalization } from '$lib/i18n';
import { alertModal } from '$lib/stores';
// import { alertModal } from '$lib/stores';
export let session_data;
export let selected_method;
@@ -44,19 +44,21 @@
try {
data = await res.json();
} catch {
alertModal.set({
/* alertModal.set({
open: true,
body: "This shouldn't happen. Please try again.",
title: 'Unknown error'
});
});*/
alert('Unknown error');
window.location.reload();
}
if (data.detail === 'totp wrong') {
alertModal.set({
/* alertModal.set({
open: true,
body: 'Wrong Totp-Code. please try again.',
title: 'Totp Error'
});
});*/
alert('TOTP code was incorrect');
totp = '';
}
}
@@ -6,7 +6,7 @@
<script lang="ts">
import { startAuthentication } from '@simplewebauthn/browser';
import { getLocalization } from '$lib/i18n';
import { alertModal } from '$lib/stores';
// import { alertModal } from '$lib/stores';
const { t } = getLocalization();
export let session_data;
@@ -24,11 +24,12 @@
asseResp = await startAuthentication(data);
} catch (e) {
console.error(e);
alertModal.set({
/* alertModal.set({
open: true,
body: e,
title: 'Unknown error'
});
});*/
alert('Unknown error');
isLoading = false;
}
const res = await fetch(
@@ -51,19 +52,21 @@
try {
data = await res.json();
} catch {
alertModal.set({
/* alertModal.set({
open: true,
body: "This shouldn't happen. Please try again.",
title: 'Unknown error'
});
});*/
alert('Unknown error');
window.location.reload();
}
if (data.detail === 'webauthn failed') {
alertModal.set({
/* alertModal.set({
open: true,
body: 'Webauthn failed. Please try again.',
title: 'Webauthn Error'
});
});*/
alert('Webauthn failed');
}
}
isLoading = false;
@@ -170,7 +170,9 @@
alt="Profile image of {user.username}"
/>
<div class="m-2 flex justify-center">
<BrownButton href="/account/settings/avatar">Change avatar</BrownButton>
<BrownButton href="/account/settings/avatar"
>{$t('settings_page.change_avatar')}</BrownButton
>
</div>
</div>
<div class="grid grid-rows-2 col-start-2 col-end-7">
@@ -184,7 +186,7 @@
<div class="p-4 flex justify-center">
<div class="m-auto">
<BrownButton href="/account/settings/security"
>Security-Settings
>{$t('settings_page.security_settings')}
</BrownButton>
<BrownButton href="/account/controllers">ClassQuizController</BrownButton>
</div>
@@ -220,20 +222,25 @@
</div>
</form>
<div>
<button on:click={add_api_key}>Add API-Key</button>
<div class="w-fit">
<BrownButton on:click={add_api_key}
>{$t('settings_page.add_api_key')}</BrownButton
>
</div>
{#await api_keys}
<Spinner />
{:then keys}
{#each keys as key}
<div>
{key.key}
<button
on:click={() => {
delete_api_key(key.key);
}}
class="admin-button"
>Delete
</button>
<div class="inline-block">
<BrownButton
on:click={() => {
delete_api_key(key.key);
}}
>{$t('words.delete')}
</BrownButton>
</div>
</div>
{/each}
{/await}
@@ -94,7 +94,7 @@
on:click={() => {
index = index - 1;
}}
disabled={index < 1}>Back</BrownButton
disabled={index < 1}>{$t('words.back')}</BrownButton
>
</div>
<div class="mx-auto">
@@ -103,7 +103,7 @@
</h2>
</div>
<div class="ml-auto">
<BrownButton disabled={index < 11}>Finish</BrownButton>
<BrownButton disabled={index < 11}>{$t('words.finish')}</BrownButton>
</div>
</div>
<div class="grid grid-cols-4">
@@ -167,11 +167,11 @@
<Spinner my_20={false} />
{/if}
</BrownButton>
<BrownButton href="/account/settings">Go back</BrownButton>
<BrownButton href="/account/settings">{$t('avatar_settings.go_back')}</BrownButton>
<BrownButton
on:click={() => {
finished = false;
}}>Close</BrownButton
}}>{$t('words.close')}</BrownButton
>
</div>
</div>
@@ -10,6 +10,9 @@
import TotpSetup from './totp_setup.svelte';
import BackupCodes from './backup_codes.svelte';
import BrownButton from '$lib/components/buttons/brown.svelte';
import { getLocalization } from '$lib/i18n';
const { t } = getLocalization();
let user_data: object | undefined;
let security_keys: Array<{ id: number }> | undefined;
@@ -101,15 +104,17 @@
<div class="grid grid-rows-2 h-screen">
<div class="grid grid-cols-2 h-full border-b-2 border-black">
<div class="h-full w-full border-r-2 border-black">
<h2 class="text-center text-2xl">Backup-Code</h2>
<h2 class="text-center text-2xl">{$t('security_settings.backup_code')}</h2>
<div class="flex h-full w-full justify-center">
<div class="m-auto">
<BrownButton on:click={get_backup_code}>Get Backup-Codes</BrownButton>
<BrownButton on:click={get_backup_code}
>{$t('security_settings.get_backup_code')}</BrownButton
>
</div>
</div>
</div>
<div class="h-full w-full">
<h2 class="text-center text-2xl">Activate 2 Factor</h2>
<h2 class="text-center text-2xl">{$t('security_settings.activate_2fa')}</h2>
<div class="flex h-full w-full justify-center flex-col">
<div class="m-auto">
{#if user_data.require_password}
@@ -130,7 +135,7 @@
/>
</button>
<span class="text-sm font-medium text-gray-700 dark:text-white"
>Two Factor authentication is activated</span
>{$t('security_settings.2fa_activated')}</span
>
</div>
{:else}
@@ -151,7 +156,7 @@
/>
</button>
<span class="text-sm font-medium text-gray-700 dark:text-white"
>Two Factor authentication is deactivated</span
>{$t('security_settings.2fa_deactivated')}</span
>
</div>
{/if}
@@ -161,17 +166,19 @@
</div>
<div class="grid grid-cols-2 h-full">
<div class="h-full w-full flex flex-col border-r-2 border-black">
<h2 class="text-center text-2xl">Webauthn</h2>
<h2 class="text-center text-2xl">{$t('security_settings.webauthn')}</h2>
<div class="flex justify-center">
{#if security_keys.length > 0}
<p>Webauthn is available</p>
<p>{$t('security_settings.webauthn_available')}</p>
{:else}
<p>Webauthn is not available</p>
<p>{$t('security_settings.webauthn_unavailable')}</p>
{/if}
</div>
<div class="flex justify-center">
<div class="m-auto">
<BrownButton on:click={add_security_key}>Add Security-Key</BrownButton>
<BrownButton on:click={add_security_key}
>{$t('security_settings.add_security_key')}</BrownButton
>
</div>
</div>
<div class="flex justify-center">
@@ -190,21 +197,25 @@
</div>
</div>
<div class="h-full w-full flex flex-col">
<h2 class="text-center text-2xl">Totp</h2>
<h2 class="text-center text-2xl">{$t('security_settings.totp')}</h2>
<div class="flex justify-center">
{#if totp_activated}
<p>Totp is available</p>
<p>{$t('security_settings.totp_available')}</p>
{:else}
<p>Totp is not available</p>
<p>{$t('security_settings.totp_unavailable')}</p>
{/if}
</div>
<div class="flex justify-center">
<div class="m-auto">
{#if totp_activated}
<BrownButton on:click={disable_totp}>Disable Totp</BrownButton>
<BrownButton on:click={disable_totp}
>{$t('security_settings.disable_totp')}</BrownButton
>
{:else}
<BrownButton on:click={enable_totp}>Enable Totp</BrownButton>
<BrownButton on:click={enable_totp}
>{$t('security_settings.enable_totp')}</BrownButton
>
{/if}
</div>
</div>
@@ -4,6 +4,9 @@
- file, You can obtain one at https://mozilla.org/MPL/2.0/.
-->
<script lang="ts">
import { getLocalization } from '$lib/i18n';
const { t } = getLocalization();
export let backup_code;
let already_downloaded = false;
@@ -30,12 +33,12 @@
on:click={() => {
backup_code = undefined;
}}
>Close
>{$t('words.close')}
</button>
<div
class="bg-white dark:bg-gray-700 rounded-b-lg rounded-tr-lg w-full h-full flex flex-col"
>
<h2 class="text-3xl m-auto">Your Backup-Code</h2>
<h2 class="text-3xl m-auto">{$t('security_settings.backup_codes.your_backup_code')}</h2>
<p
class="select-all font-mono text-xl m-auto"
on:click={() => {
@@ -44,13 +47,14 @@
>
{backup_code}
</p>
<p class="m-auto">Save this somewhere safe!</p>
<p class="m-auto">{$t('security_settings.backup_codes.save_somewhere_save')}</p>
<button
on:click={() => {
download_code(true);
}}
class="m-auto p-2 bg-[#B07156] rounded-lg">Download code</button
>
class="m-auto p-2 bg-[#B07156] rounded-lg"
>{$t('security_settings.backup_codes.download_code')}
</button>
</div>
</div>
</div>
@@ -6,6 +6,9 @@
<script lang="ts">
import QRCode from 'qrcode';
import Spinner from '$lib/Spinner.svelte';
import { getLocalization } from '$lib/i18n';
const { t } = getLocalization();
export let totp_data: { url: string; secret: string } | undefined;
@@ -21,23 +24,25 @@
on:click={() => {
totp_data = undefined;
}}
>Close
>{$t('words.close')}
</button>
<div class="bg-white dark:bg-gray-700 rounded-b-lg rounded-tr-lg w-full h-full">
<div class="grid grid-cols-3 w-full h-full">
<div class="flex flex-col justify-center w-full h-5/6">
<span class="m-auto" />
<div class="h-5/6 flex">
<p class="my-auto ml-auto">Scan this to set up the code</p>
<p class="my-auto ml-auto">
{$t('security_settings.totp_setup.scan_to_set_up')}
</p>
</div>
<div class="flex">
<p class="my-auto ml-auto">
Enter this as the secret if you can't scan the code
{$t('security_settings.totp_setup.enter_as_secret_if_no_see_code')}
</p>
</div>
</div>
<div class="flex flex-col justify-start w-full h-5/6">
<h2 class="text-2xl m-auto">Totp-Setup</h2>
<h2 class="text-2xl m-auto">{$t('security_settings.totp_setup.totp_setup')}</h2>
{#await get_image_url()}
<Spinner my_20={false} />
{:then data}
@@ -52,7 +57,9 @@
<p class="m-auto select-all font-mono">{totp_data.secret}</p>
</div>
<div class="flex justify-center h-5/6 w-full">
<p class="m-auto text-3xl p-4">Do not forget to save your recovery-code!</p>
<p class="m-auto text-3xl p-4">
{$t('security_settings.totp_setup.do_not_forget_backup_code')}
</p>
</div>
</div>
</div>
+26 -18
View File
@@ -28,7 +28,8 @@
// };
export let data;
let game_mode;
let { game_pin, auto_connect, game_token } = data;
let { auto_connect, game_token } = data;
const game_pin = data.game_pin;
let players: Array<Player> = [];
let player_scores = {};
@@ -42,6 +43,7 @@
let success = false;
let dataexport_download_a;
let warnToLeave = true;
let export_token = undefined;
const connect = async () => {
socket.emit('register_as_admin', {
@@ -81,18 +83,17 @@
});
/* socket.on('question_results', (int_data) => {
try {
int_data = JSON.parse(int_data);
} catch (e) {
console.error('Failed to parse question results');
return;
}
question_results = int_data;
});*/
try {
int_data = JSON.parse(int_data);
} catch (e) {
console.error('Failed to parse question results');
return;
}
question_results = int_data;
});*/
socket.on('export_token', (int_data) => {
warnToLeave = false;
dataexport_download_a.href = `/api/v1/quiz/export_data/${int_data}?ts=${new Date().getTime()}&game_pin=${game_pin}`;
dataexport_download_a.click();
export_token = int_data;
setTimeout(() => {
warnToLeave = true;
@@ -151,9 +152,17 @@
{#if control_visible}
<div class="w-screen flex justify-center mt-16">
<div class="w-fit">
<GrayButton on:click={request_answer_export}
>{$t('admin_page.export_results')}</GrayButton
>
{#if export_token === undefined}
<GrayButton on:click={request_answer_export}
>{$t('admin_page.request_export_results')}</GrayButton
>
{:else}
<GrayButton
target="_blank"
href="/api/v1/quiz/export_data/{export_token}?ts={new Date().getTime()}&game_pin={game_pin}"
>{$t('admin_page.download_export_results')}</GrayButton
>
{/if}
</div>
</div>
<div class="w-screen flex justify-center mt-2">
@@ -183,9 +192,6 @@
<FinalResults bind:data={player_scores} bind:show_final_results />
{/if}
{#if !success}
<input placeholder="game id" bind:value={game_token} />
<input placeholder="game pin" bind:value={game_pin} />
<button on:click={connect}>{$t('words.connect')}!</button>
{#if errorMessage !== ''}
<p class="text-red-700">{errorMessage}</p>
{/if}
@@ -199,7 +205,7 @@
{:else}
<SomeAdminScreen
bind:final_results
bind:game_pin
{game_pin}
bind:game_token
bind:quiz_data
bind:game_mode
@@ -212,6 +218,8 @@
<a
on:click|preventDefault={request_answer_export}
href="#"
target="_blank"
bind:this={dataexport_download_a}
download=""
class="absolute -top-3/4 -left-3/4 opacity-0">Download</a
>
+6 -4
View File
@@ -8,8 +8,8 @@
import Editor from '$lib/editor.svelte';
import { getLocalization } from '$lib/i18n';
import { navbarVisible } from '$lib/stores';
import { QuizQuestionType } from '$lib/quiz_types';
import type { Question } from '$lib/quiz_types';
import { page } from '$app/stores';
navbarVisible.set(false);
@@ -31,17 +31,19 @@
onMount(() => {
const from_localstorage = localStorage.getItem('create_game');
if (from_localstorage === null) {
let title = $page.url.searchParams.get('title');
title ??= '';
data = {
description: '',
public: false,
title: '',
title,
questions: [
{
/* {
type: QuizQuestionType.ABCD,
question: '',
time: '20',
answers: [{ right: false, answer: '' }]
}
}*/
]
};
} else {
+177 -56
View File
@@ -8,9 +8,14 @@
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 StartGamePopup from '$lib/dashboard/start_game.svelte';
// import GrayButton from "$lib/components/buttons/gray.svelte";
interface QuizData {
id: string;
@@ -23,59 +28,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,40 +114,120 @@
/>
</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">
{$t('words.settings')}
</BrownButton>
<div class="flex gap-2">
<BrownButton href="/edit/files">{$t('words.files_library')}</BrownButton>
<BrownButton href="/account/settings">
{$t('words.settings')}
</BrownButton>
</div>
</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="/api/v1/storage/download/{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 +237,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')}
@@ -158,3 +276,6 @@
{/await}
</div>
<Footer />
{#if start_game !== null}
<StartGamePopup bind:quiz_id={start_game} />
{/if}
+18
View File
@@ -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;
@@ -0,0 +1,34 @@
<!--
- 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 FileDahboard from "$lib/files/dashboard.svelte"
// import { thumbHashToDataURL } from 'thumbhash';
export let data: PageData;
const files = data.files;
/* const base64ToBytes = (value: string) => {
const base64 = value.replace(/-/g, '+').replace(/_/g, '/');
const decodedData =
typeof Buffer !== 'undefined'
? Buffer.from(base64, 'base64')
: Uint8Array.from(atob(base64), (char) => char.charCodeAt(0));
return new Uint8Array(decodedData);
};*/
// let images_loaded = [];
</script>
<div class="w-full h-full">
<div class="grid grid-cols-2 w-full h-full gap-4">
{#each files as file, i}
<div class="w-full h-full flex" />
{/each}
</div>
</div>
@@ -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/.
*/
import type { PageLoad } from './$types';
export const load = (async ({ fetch }) => {
const res = await fetch('/api/v1/storage/list');
return { files: await res.json() };
}) satisfies PageLoad;
@@ -0,0 +1,8 @@
# Question Types
- [Range](/docs/quiz/question-types/range)
- [Multiple Choice](/docs/quiz/question-types/multiple-choice)
- [Voting](/docs/quiz/question-types/voting)
- [Text](/docs/quiz/question-types/text)
- [Order](/docs/quiz/question-types/order)
- [Check Choice](/docs/quiz/question-types/check-choice)
@@ -0,0 +1,11 @@
# Check Choice
## Summary
- Interface similar to [Multiple Choice](/docs/quiz/question-types/multiple-choice)
- Up to 4 answers
- All answers marked as correct have to be selected
- No points if only one correct one is selected
## Use case
- Select all correct statements
- Select all correct nicknames
@@ -0,0 +1,9 @@
# Multiple Choice
## Summary
- The standard question type everyone knows
- 1-4 answers can be correct
- User can only choose one answer
## Use case
Nothing specific; Main component of a quiz
@@ -0,0 +1,9 @@
# Order
## Summary
- Bring the answers in a correct order
- The order gets randomized before the quiz so that every player has the same random order
- Remember to give some time, since it's kinda slow to move the answers up and down
## Use case
- Order historic events
@@ -0,0 +1,10 @@
# Range Answers
## Summary
- The player has a slider where a number can be set
- The creator can choose the range and the correct range
## Use case
- Guessing a year
- Guessing the population of a country
- Guessing in general
@@ -0,0 +1,9 @@
# Text
## Summary
- Players can enter text
- 4 different correct solutions can be given
- case sensitivity can be set per answer
## Use case
- Test if a name is remembered correctly
@@ -0,0 +1,10 @@
# Voting
## Summary
- Like [Multiple Choice](/docs/quiz/question-types/multiple-choice)
- No points
- No correct answers
- Bar graph shows how many voted for what
## Use case
- Get the opinion of the audience

Some files were not shown because too many files have changed in this diff Show More