Merge pull request #139 from mawoka-myblock/135-add-a-new-question-type-voting-questions

This commit is contained in:
Mawoka
2022-10-04 16:48:21 +02:00
committed by GitHub
19 changed files with 512 additions and 155 deletions
+11 -2
View File
@@ -85,24 +85,33 @@ class RangeQuizAnswer(BaseModel):
max_correct: int
class VotingQuizAnswer(BaseModel):
answer: str
image: str | None = None
color: str | None
class QuizQuestionType(str, Enum):
ABCD = "ABCD"
RANGE = "RANGE"
VOTING = "VOTING"
class QuizQuestion(BaseModel):
question: str
time: str # in Secs
type: None | QuizQuestionType = QuizQuestionType.ABCD
answers: list[ABCDQuizAnswer] | RangeQuizAnswer
answers: list[ABCDQuizAnswer] | RangeQuizAnswer | list[VotingQuizAnswer]
image: str | None = None
@validator("answers")
def answers_not_none_if_abcd_type(cls, v, values):
if values["type"] == QuizQuestionType.ABCD and len(v) == 0:
if values["type"] == QuizQuestionType.ABCD and type(v[0]) != ABCDQuizAnswer:
raise ValueError("Answers can't be none if type is ABCD")
if values["type"] == QuizQuestionType.RANGE and type(v) != RangeQuizAnswer:
raise ValueError("Answer must be from type RangeQuizAnswer if type is RANGE")
if values["type"] == QuizQuestionType.VOTING and type(v[0]) != VotingQuizAnswer:
raise ValueError("Answer must be from type VotingQuizAnswer if type is VOTING")
return v
+2 -1
View File
@@ -17,6 +17,7 @@ from classquiz.db.models import (
RangeQuizAnswer,
ABCDQuizAnswer,
QuizQuestionType,
VotingQuizAnswer,
)
from classquiz.auth import check_api_key
from classquiz.socket_server import ReturnQuestion, sio
@@ -35,7 +36,7 @@ class _ABCDQuizAnswer(ABCDQuizAnswer):
class _QuizQuestion(QuizQuestion):
answers: list[ABCDQuizAnswer] | RangeQuizAnswer
answers: list[ABCDQuizAnswer] | RangeQuizAnswer | list[VotingQuizAnswer]
class _GetLivePlayGame(PlayGame):
+18 -9
View File
@@ -11,7 +11,7 @@ import socketio
from cryptography.fernet import Fernet
from classquiz.config import redis, settings
from classquiz.db.models import PlayGame, QuizQuestionType, GameSession, GamePlayer, QuizQuestion
from classquiz.db.models import PlayGame, QuizQuestionType, GameSession, GamePlayer, QuizQuestion, VotingQuizAnswer
from pydantic import BaseModel, ValidationError, validator
from datetime import datetime
@@ -113,8 +113,10 @@ async def join_game(sid: str, data: dict):
{"username": data.username, "sid": sid},
room=redis_res.admin,
)
lol = fernet.encrypt(datetime.now().isoformat().encode("utf-8")).decode("utf-8")
await sio.emit("time_sync", lol, room=sid)
# +++ Time-Sync +++
encrypted_datetime = fernet.encrypt(datetime.now().isoformat().encode("utf-8")).decode("utf-8")
await sio.emit("time_sync", encrypted_datetime, room=sid)
# --- Time-Sync ---
sio.enter_room(sid, data.game_pin)
@@ -183,14 +185,16 @@ class RangeQuizAnswerWithoutSolution(BaseModel):
class ReturnQuestion(QuizQuestion):
answers: list[ABCDQuizAnswerWithoutSolution] | RangeQuizAnswerWithoutSolution
answers: list[ABCDQuizAnswerWithoutSolution] | RangeQuizAnswerWithoutSolution | list[VotingQuizAnswer]
@validator("answers")
def answers_not_none_if_abcd_type(cls, v, values):
if values["type"] == QuizQuestionType.ABCD and len(v) == 0:
if values["type"] == QuizQuestionType.ABCD and type(v[0]) != ABCDQuizAnswerWithoutSolution:
raise ValueError("Answers can't be none if type is ABCD")
if values["type"] == QuizQuestionType.RANGE and type(v) != RangeQuizAnswerWithoutSolution:
raise ValueError("Answer must be from type RangeQuizAnswer if type is RANGE")
if values["type"] == QuizQuestionType.VOTING and type(v[0]) != VotingQuizAnswer:
print("Answer must be from type VotingQuizAnswer if type is VOTING")
return v
@@ -204,13 +208,17 @@ async def set_question_number(sid, data: str):
game_data.current_question = int(float(data))
await redis.set(f"game:{session['game_pin']}", game_data.json())
await redis.set(f"game:{session['game_pin']}:current_time", datetime.now().isoformat())
# print(game_data.dict(include={"questions"})["questions"][int(float(data))])
temp_return = game_data.dict(include={"questions"})["questions"][int(float(data))]
if game_data.questions[int(float(data))].type == QuizQuestionType.VOTING:
for i in range(len(temp_return["answers"])):
temp_return["answers"][i] = VotingQuizAnswer(**temp_return["answers"][i])
print(temp_return)
await sio.emit(
"set_question_number",
{
"question_index": int(float(data)),
"question": ReturnQuestion(
**game_data.dict(include={"questions"})["questions"][int(float(data))]
).dict(),
"question": ReturnQuestion(**temp_return).dict(),
},
room=game_pin,
)
@@ -258,6 +266,8 @@ async def submit_answer(sid: str, data: dict):
<= game_data.questions[int(float(data.question_index))].answers.max_correct
):
answer_right = True
elif game_data.questions[int(float(data.question_index))].type == QuizQuestionType.VOTING:
answer_right = False
else:
raise NotImplementedError
latency = int(float((await sio.get_session(sid))["ping"]))
@@ -317,7 +327,6 @@ async def submit_answer(sid: str, data: dict):
async def get_final_results(sid: str, _data: dict):
session: dict = await sio.get_session(sid)
game_data = PlayGame(**json.loads(await redis.get(f"game:{session['game_pin']}")))
results = {}
if not session["admin"]:
return
results = await generate_final_results(game_data, session["game_pin"])
+26 -64
View File
@@ -77,7 +77,7 @@ devDependencies:
'@sentry/tracing': 7.11.1
'@sveltejs/adapter-auto': 1.0.0-next.80
'@sveltejs/adapter-node': 1.0.0-next.86
'@sveltejs/kit': 1.0.0-next.504_svelte@3.49.0+vite@3.1.2
'@sveltejs/kit': 1.0.0-next.508_svelte@3.49.0+vite@3.1.2
'@tailwindcss/typography': 0.5.4_tailwindcss@3.1.8
'@types/canvas-confetti': 1.4.3
'@types/cookie': 0.5.1
@@ -579,10 +579,10 @@ packages:
- supports-color
dev: true
/@sveltejs/kit/1.0.0-next.504_svelte@3.49.0+vite@3.1.2:
/@sveltejs/kit/1.0.0-next.508_svelte@3.49.0+vite@3.1.2:
resolution:
{
integrity: sha512-KrhlSHT3aCVnhRgUoN6aGIjIw3nWEdNwfoZcE1x65F5D7Ju/K9D8dQwyal9v0aBAZyN9nFuXxsYGaMLE9sppfw==
integrity: sha512-qUnuhuL82meE0lSwrsS/FIvtDoE83Au4SxVLCim3FzoCnJc7bFZ9vYRbRUYxAjfYKRMjKVUrp9iHFObn9LmnUg==
}
engines: { node: '>=16.14' }
hasBin: true
@@ -598,13 +598,12 @@ packages:
kleur: 4.1.5
magic-string: 0.26.3
mime: 3.0.0
node-fetch: 3.2.10
sade: 1.8.1
set-cookie-parser: 2.5.1
sirv: 2.0.2
svelte: 3.49.0
tiny-glob: 0.2.9
undici: 5.9.1
undici: 5.11.0
vite: 3.1.2_sass@1.54.4
transitivePeerDependencies:
- diff-match-patch
@@ -1398,6 +1397,16 @@ packages:
}
dev: true
/busboy/1.6.0:
resolution:
{
integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==
}
engines: { node: '>=10.16.0' }
dependencies:
streamsearch: 1.1.0
dev: true
/callsites/3.1.0:
resolution:
{
@@ -1717,14 +1726,6 @@ packages:
css-tree: 1.1.3
dev: true
/data-uri-to-buffer/4.0.0:
resolution:
{
integrity: sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==
}
engines: { node: '>= 12' }
dev: true
/debug/4.3.4:
resolution:
{
@@ -2503,17 +2504,6 @@ packages:
svelte: 3.49.0
dev: true
/fetch-blob/3.2.0:
resolution:
{
integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==
}
engines: { node: ^12.20 || >= 14.13 }
dependencies:
node-domexception: 1.0.0
web-streams-polyfill: 3.2.1
dev: true
/file-entry-cache/6.0.1:
resolution:
{
@@ -2570,16 +2560,6 @@ packages:
}
dev: true
/formdata-polyfill/4.0.10:
resolution:
{
integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==
}
engines: { node: '>=12.20.0' }
dependencies:
fetch-blob: 3.2.0
dev: true
/fraction.js/4.2.0:
resolution:
{
@@ -3358,14 +3338,6 @@ packages:
}
dev: true
/node-domexception/1.0.0:
resolution:
{
integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
}
engines: { node: '>=10.5.0' }
dev: true
/node-fetch/2.6.7:
resolution:
{
@@ -3381,18 +3353,6 @@ packages:
whatwg-url: 5.0.0
dev: true
/node-fetch/3.2.10:
resolution:
{
integrity: sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA==
}
engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 }
dependencies:
data-uri-to-buffer: 4.0.0
fetch-blob: 3.2.0
formdata-polyfill: 4.0.10
dev: true
/node-gyp-build/4.5.0:
resolution:
{
@@ -4540,6 +4500,14 @@ packages:
deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility'
dev: true
/streamsearch/1.1.0:
resolution:
{
integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
}
engines: { node: '>=10.0.0' }
dev: true
/string-width/4.2.3:
resolution:
{
@@ -4960,12 +4928,14 @@ packages:
}
dev: true
/undici/5.9.1:
/undici/5.11.0:
resolution:
{
integrity: sha512-6fB3a+SNnWEm4CJbgo0/CWR8RGcOCQP68SF4X0mxtYTq2VNN8T88NYrWVBAeSX+zb7bny2dx2iYhP3XHi00omg==
integrity: sha512-oWjWJHzFet0Ow4YZBkyiJwiK5vWqEYoH7BINzJAJOLedZ++JpAlCbUktW2GQ2DS2FpKmxD/JMtWUUWl1BtghGw==
}
engines: { node: '>=12.18' }
dependencies:
busboy: 1.6.0
dev: true
/update-browserslist-db/1.0.5_browserslist@4.21.3:
@@ -5057,14 +5027,6 @@ packages:
pbf: 3.2.1
dev: true
/web-streams-polyfill/3.2.1:
resolution:
{
integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==
}
engines: { node: '>= 8' }
dev: true
/webidl-conversions/3.0.1:
resolution:
{
+29 -11
View File
@@ -114,8 +114,8 @@
{#if selected_question + 1 === quiz_data.questions.length && timer_res === '0' && question_results !== null}
{#if JSON.stringify(final_results) === JSON.stringify([null])}
<button on:click={get_final_results} class="admin-button"
>Get final results</button
>
>Get final results
</button>
{/if}
{:else if timer_res === '0' || selected_question === -1}
{#if (selected_question + 1 !== quiz_data.questions.length && question_results !== null) || selected_question === -1}
@@ -129,13 +129,13 @@
{/if}
{#if question_results === null && selected_question !== -1}
<button on:click={get_question_results} class="admin-button"
>Show results</button
>
>Show results
</button>
{/if}
{:else if selected_question !== -1}
<button on:click={show_solutions} class="admin-button"
>Stop time and show solutions</button
>
>Stop time and show solutions
</button>
{:else}
<!-- <button
on:click={() => {
@@ -181,20 +181,28 @@
</div>
{/if}
{#if game_mode === 'kahoot'}
{#if quiz_data.questions[selected_question].type === QuizQuestionType.ABCD}
{#if quiz_data.questions[selected_question].type === QuizQuestionType.ABCD || quiz_data.questions[selected_question].type === QuizQuestionType.VOTING}
<div class="grid grid-cols-2 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:opacity-50={!answer.right && timer_res === '0'}
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"
class:text-4xl={answer.right && timer_res === '0'}
class:underline={answer.right && timer_res === '0'}
>{answer.answer}</span
class:text-4xl={answer.right &&
timer_res === '0' &&
quiz_data.questions[selected_question].type !==
QuizQuestionType.ABCD}
class:underline={answer.right &&
timer_res === '0' &&
quiz_data.questions[selected_question].type !==
QuizQuestionType.ABCD}>{answer.answer}</span
>
<span class="pl-4 w-10" />
</div>
@@ -221,6 +229,16 @@
<h1 class="text-3xl">{$t('admin_page.no_answers')}</h1>
</div>
{/if}
{:else if quiz_data.questions[selected_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}
{:else}
{#await import('$lib/play/admin/results.svelte')}
<Spinner />
@@ -247,6 +247,18 @@
numbers between {question.answers.min} and {question
.answers.max} can be selected.
</p>
{:else if question.type === QuizQuestionType.VOTING}
<div class="grid grid-cols-2 gap-4 m-4 p-6">
{#each question.answers as answer}
<div
class="p-1 rounded-lg py-4 dark:bg-gray-500 bg-gray-300"
>
<h4 class="text-center">
{answer.answer}
</h4>
</div>
{/each}
</div>
{/if}
</div>
</SwiperSlide>
@@ -66,10 +66,10 @@
<!-- <RangeSlider bind:value={range_arr} bind:min={answer.min} bind:max={answer.max} range={true} slider={lol} /> -->
{#await import('svelte-range-slider-pips')}
<Spinner />
<Spinner my_20={false} />
{:then c}
{#await sleep(100)}
<Spinner />
<Spinner my_20={false} />
{:then _}
<svelte:component
this={c.default}
@@ -0,0 +1,88 @@
<!--
- 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, VotingAnswer } from '../quiz_types';
import { fade } from 'svelte/transition';
import { reach } from 'yup';
import { getLocalization } from '$lib/i18n';
import { VotingQuestionSchema } from '$lib/yupSchemas';
const { t } = getLocalization();
const empty_answer: VotingAnswer = {
answer: '',
image: undefined
};
export let selected_question: number;
export let data: EditorData;
try {
if (typeof data.questions[selected_question].answers[0].right === 'boolean') {
data.questions[selected_question].answers = [];
}
// 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;
}
});*/
</script>
<div class="grid grid-cols-2 gap-4 w-full px-10">
{#if Array.isArray(data.questions[selected_question].answers)}
{#each data.questions[selected_question].answers as answer, index}
<div
out:fade={{ duration: 150 }}
class="p-4 rounded-lg flex justify-center w-full transition"
class:bg-yellow-500={!reach(VotingQuestionSchema, 'answer').isValidSync(
answer.answer
)}
class:dark:bg-gray-500={answer.answer}
class:bg-gray-300={answer.answer}
>
<input
bind:value={answer.answer}
type="text"
on:contextmenu|preventDefault={() => {
data.questions[selected_question].answers.splice(index, 1);
data.questions[selected_question].answers =
data.questions[selected_question].answers;
}}
class="border-b-2 border-dotted w-5/6 text-center rounded-lg"
style="background-color: {answer.color ?? 'transparent'}"
placeholder="Empty..."
/>
<input
class="rounded-lg p-1"
type="color"
bind:value={answer.color}
on:contextmenu|preventDefault={() => {
answer.color = null;
}}
/>
</div>
{/each}
{/if}
{#if data.questions[selected_question].answers.length < 4}
<button
class="p-4 rounded-lg bg-transparent border-gray-500 border-2 hover:bg-gray-300 transition dark:hover:bg-gray-600"
type="button"
in:fade={{ duration: 150 }}
on:click={() => {
data.questions[selected_question].answers = [
...data.questions[selected_question].answers,
{ ...empty_answer }
];
}}
>
<span class="italic text-center">{$t('editor_page.add_an_answer')}</span>
</button>
{/if}
</div>
+10 -3
View File
@@ -96,11 +96,11 @@
use:tippy={{ content: "Click to learn why it's loading so long." }}
class="cursor-help"
>
<Spinner />
<Spinner my_20={false} />
</a>
{:else}
{#await import('$lib/editor/uploader.svelte')}
<Spinner />
<Spinner my_20={false} />
{:then c}
<svelte:component
this={c.default}
@@ -146,17 +146,24 @@
>
<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>
</select>
</div>
<div class="flex justify-center pt-10 w-full">
{#if data.questions[selected_question].type === QuizQuestionType.ABCD}
{#await import('$lib/editor/ABCDEditorPart.svelte')}
<Spinner />
<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.RANGE}
<RangeEditor bind:selected_question bind:data />
{:else if data.questions[selected_question].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}
{/if}
</div>
<p class="italic text-center mt-auto pt-4">{$t('editor_page.right_click_to_delete')}</p>
+2 -2
View File
@@ -67,11 +67,11 @@
</div>
{:else if pow_data === undefined}
<a href="/docs/pow" target="_blank" class="cursor-help">
<Spinner />
<Spinner my_20={false} />
</a>
{:else}
{#await import('$lib/editor/uploader.svelte')}
<Spinner />
<Spinner my_20={false} />
{:then c}
<svelte:component
this={c.default}
+24
View File
@@ -207,6 +207,30 @@
and {question.answers.max_correct} are correct, where numbers between {question
.answers.min} and {question.answers.max} can be selected.
</p>
{:else if question.type === QuizQuestionType.VOTING}
{#if Array.isArray(question.answers)}
<div class="grid grid-cols-2 gap-2">
{#each question.answers as answer}
<span
class="whitespace-nowrap truncate rounded-lg p-0.5 text-sm text-center border border-gray-700"
class:dark:bg-gray-500={answer.answer}
class:bg-gray-300={answer.answer}
class:bg-yellow-500={!reach(
ABCDQuestionSchema,
'answer'
).isValidSync(answer.answer)}
use:tippy={{
content: answer.answer === '' ? 'Empty...' : answer.answer
}}
>{#if answer.answer === ''}
<i>Empty...</i>
{:else}
{answer.answer}
{/if}</span
>
{/each}
</div>
{/if}
{:else}
<p>Unknown Question Type (shouldn't happen)</p>
{/if}
@@ -0,0 +1,50 @@
<!--
- 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';
export let data;
export let question: Question;
let quiz_answers = [];
for (const i of question.answers) {
quiz_answers.push(i.answer);
}
console.log('mounted', question);
let sorted_data = {};
for (const i of quiz_answers) {
sorted_data[i] = 0;
}
for (const i of data) {
sorted_data[i.answer] += 1;
}
</script>
<div class="flex justify-center h-full w-full">
<div
class="m-auto grid grid-rows-3 w-fit gap-4"
style="grid-template-columns: repeat({quiz_answers.length}, minmax(0, 1fr));"
>
{#each quiz_answers as answer}
<span class="text-center self-end"
>{#if sorted_data[answer] > 0}{sorted_data[answer]}{/if}</span
>
{/each}
{#each quiz_answers as answer}
<div
class="w-20 bg-black self-end flex justify-center"
style="height: {(sorted_data[answer] * 20) / data.length}rem"
/>
{/each}
{#each quiz_answers as answer}
<div class="w-20">
<p class="-rotate-45">{answer}</p>
</div>
{/each}
</div>
</div>
+8 -1
View File
@@ -113,7 +113,7 @@
</div>
{/if}
{#if timer_res !== '0'}
{#if question.type === QuizQuestionType.ABCD}
{#if question.type === QuizQuestionType.ABCD || QuizQuestionType.VOTING}
{#if game_mode === 'normal'}
<div class="grid grid-cols-2 gap-2 w-full p-4">
{#each question.answers as answer}
@@ -202,4 +202,11 @@
{/if}
</p>
{/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}-->
{/if}
+59 -43
View File
@@ -91,48 +91,64 @@
{/if}
</div>
{/if}
{:else if timer_res === '0'}
{#await import('svelte-range-slider-pips')}
<Spinner />
{:then c}
<div class="grayscale pointer-events-none w-full">
<svelte:component
this={c.default}
bind:values={slider_values}
bind:min={question.answers.min}
bind:max={question.answers.max}
pips
float
all="label"
/>
</div>
{/await}
{:else}
{#await import('svelte-range-slider-pips')}
<Spinner />
{:then c}
<div class:pointer-events-none={selected_answer !== undefined}>
<svelte:component
this={c.default}
bind:values={slider_value}
bind:min={question.answers.min}
bind:max={question.answers.max}
pips
float
all="label"
/>
</div>
<div class="flex justify-center">
<button
class="w-1/2 text-3xl bg-amber-700 my-2 disabled:opacity-60 border border-white"
disabled={selected_answer !== undefined}
on:click={() => {
selected_answer = slider_value[0];
timer_res = '0';
}}
>{$t('words.submit')}
</button>
</div>
{/await}
{:else if question.type === QuizQuestionType.RANGE}
{#if timer_res === '0'}
{#await import('svelte-range-slider-pips')}
<Spinner />
{:then c}
<div class="grayscale pointer-events-none w-full">
<svelte:component
this={c.default}
bind:values={slider_values}
bind:min={question.answers.min}
bind:max={question.answers.max}
pips
float
all="label"
/>
</div>
{/await}
{:else}
{#await import('svelte-range-slider-pips')}
<Spinner />
{:then c}
<div class:pointer-events-none={selected_answer !== undefined}>
<svelte:component
this={c.default}
bind:values={slider_value}
bind:min={question.answers.min}
bind:max={question.answers.max}
pips
float
all="label"
/>
</div>
<div class="flex justify-center">
<button
class="w-1/2 text-3xl bg-amber-700 my-2 disabled:opacity-60 border border-white"
disabled={selected_answer !== undefined}
on:click={() => {
selected_answer = slider_value[0];
timer_res = '0';
}}
>{$t('words.submit')}
</button>
</div>
{/await}
{/if}
{:else if question.type === QuizQuestionType.VOTING}
{#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"
on:click={() => {
selected_answer = i;
timer_res = '0';
}}>{answer.answer}</button
>
{/each}
{#if timer_res === '0'}
<p>No correct answers, since this is a poll-question</p>
{/if}
{/if}
</div>
+8 -2
View File
@@ -18,7 +18,8 @@ export interface QuizData {
export enum QuizQuestionType {
ABCD = 'ABCD', // eslint-disable-line no-unused-vars
RANGE = 'RANGE' // eslint-disable-line no-unused-vars
RANGE = 'RANGE', // eslint-disable-line no-unused-vars
VOTING = 'VOTING' // eslint-disable-line no-unused-vars
}
export interface RangeQuizAnswer {
@@ -33,7 +34,7 @@ export interface Question {
question: string;
type?: QuizQuestionType;
image?: string;
answers: Answer[] | RangeQuizAnswer;
answers: Answer[] | RangeQuizAnswer | VotingAnswer[];
}
export interface Answer {
@@ -41,6 +42,11 @@ export interface Answer {
answer: string;
color?: string;
}
export interface VotingAnswer {
answer: string;
image?: string;
color?: string;
}
export interface EditorData {
public: boolean;
+22 -3
View File
@@ -17,6 +17,17 @@ export const ABCDQuestionSchema = yup
.min(2, 'You need at least 2 answers')
.max(16, "You can't have more than 16 answers");
export const VotingQuestionSchema = yup
.array()
.of(
yup.object({
answer: yup.string().required('You need an answer'),
image: yup.string().optional().nullable()
})
)
.min(2, 'You need at least 2 answers')
.max(16, "You can't have more than 16 answers");
export const RangeQuestionSchema = yup.object({
min: yup.number(),
max: yup.number(),
@@ -50,9 +61,17 @@ export const dataSchema = yup.object({
"The image-url isn't valid"
)
.lowercase(),
answers: yup.lazy((v) =>
Array.isArray(v) ? ABCDQuestionSchema : RangeQuestionSchema
)
answers: yup.lazy((v) => {
if (Array.isArray(v)) {
if (typeof v[0].right === 'boolean') {
return ABCDQuestionSchema;
} else {
return VotingQuestionSchema;
}
} else {
return RangeQuestionSchema;
}
})
})
)
.min(1, 'You need at least one question')
+1 -12
View File
@@ -9,6 +9,7 @@
import { getLocalization } from '$lib/i18n';
import { navbarVisible } from '$lib/stores';
import { QuizQuestionType } from '$lib/quiz_types';
import type { Question } from '$lib/quiz_types';
navbarVisible.set(false);
@@ -21,18 +22,6 @@
questions: Question[];
}
interface Question {
question: string;
time: string;
type?: QuizQuestionType;
answers: Answer[];
}
interface Answer {
right: boolean;
answer: string;
}
let responseData = {
open: false
};
+129
View File
@@ -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 { navbarVisible } from '$lib/stores';
navbarVisible.set(false);
const data = [
{
username: 'Mawoka',
answer: 'A very long answe-thing',
right: false,
time_taken: 1681.631,
score: 0
},
{
username: 'Mawoka',
answer: 'A very long answe-thing',
right: false,
time_taken: 1681.631,
score: 0
},
{
username: 'Mawoka',
answer: 'A very long answe-thing',
right: false,
time_taken: 1681.631,
score: 0
},
{
username: 'Mawoka',
answer: 'A very long answe-thing',
right: false,
time_taken: 1681.631,
score: 0
},
{
username: 'Mawoka',
answer: 'A very long answe-thing',
right: false,
time_taken: 1681.631,
score: 0
},
{
username: 'Mawoka',
answer: 'A very long answe-thing',
right: false,
time_taken: 1681.631,
score: 0
},
{
username: 'Mawoka',
answer: 'A very long answe-thing',
right: false,
time_taken: 1681.631,
score: 0
},
{
username: 'Mawoka',
answer: 'A very long answe-thing',
right: false,
time_taken: 1681.631,
score: 0
},
{
username: 'Mawoka',
answer: 'A very long answe-thing',
right: false,
time_taken: 1681.631,
score: 0
},
{
username: 'Mawoka',
answer: 'A very long answe-thing',
right: false,
time_taken: 1681.631,
score: 0
},
{
username: 'dewads',
answer: 'A very long answe-thing',
right: false,
time_taken: 1681.631,
score: 0
},
{
username: 'dsadsa',
answer: 'der',
right: false,
time_taken: 2613.532,
score: 0
}
];
const quiz_answers = ['A very long answe-thing', 'dieter', 'moin', 'der'];
let sorted_data = {};
for (const i of quiz_answers) {
sorted_data[i] = 0;
}
for (const i of data) {
sorted_data[i.answer] += 1;
}
</script>
<div class="flex justify-center h-screen w-full">
<div
class="m-auto grid grid-rows-3 w-fit gap-4"
style="grid-template-columns: repeat({quiz_answers.length}, minmax(0, 1fr));"
>
{#each quiz_answers as answer}
<span class="text-center self-end"
>{#if sorted_data[answer] > 0}{sorted_data[answer]}{/if}</span
>
{/each}
{#each quiz_answers as answer}
<div
class="w-20 bg-black self-end flex justify-center"
style="height: {(sorted_data[answer] * 20) / data.length}rem"
/>
{/each}
{#each quiz_answers as answer}
<div class="w-20">
<p class="-rotate-45">{answer}</p>
</div>
{/each}
</div>
</div>
@@ -169,6 +169,17 @@
and {question.answers.max_correct} are correct, where numbers between {question
.answers.min} and {question.answers.max} can be selected.
</p>
{:else if question.type === QuizQuestionType.VOTING}
<div class="grid grid-cols-2 gap-4 m-4 p-6">
{#each question.answers as answer, index_answer}
<div class="p-1 rounded-lg py-4 dark:bg-gray-500 bg-gray-300">
<h4 class="text-center">
{quiz.questions[index_question].answers[index_answer]
.answer}
</h4>
</div>
{/each}
</div>
{/if}
</div>
</CollapsSection>