Added Editing of Questions

This commit is contained in:
2026-01-02 18:00:57 +01:00
parent 5568a5bb99
commit 48074f7603
23 changed files with 1095 additions and 71 deletions

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import AudioPlayerComponent from "./AudioPlayerComponent.svelte";
import type { AudioMultipleChoiceQuestion } from "./games/games";
import { url } from "./util";
const path = "/sounds/";
@@ -9,10 +10,19 @@
showAnswer: boolean;
showQuestion: boolean;
showPlayer: boolean;
isLegacy?: boolean;
randomize?: boolean;
[key: string]: unknown;
}
let { question, showAnswer, showQuestion, showPlayer }: Props = $props();
let {
question,
showAnswer,
showQuestion,
showPlayer,
isLegacy = true,
randomize = true
}: Props = $props();
function shuffle<T>(array: T[]) {
let currentIndex = array.length;
@@ -28,10 +38,24 @@
}
}
const answer = question.data.choices[0];
const answer = $derived(question.data.choices[0]);
let _choices = [...question.data.choices];
shuffle(_choices);
let _choices = $derived.by(() => {
let c = [...question.data.choices];
if (randomize) shuffle(c);
return c;
});
let audioPath = $derived.by(() => {
if (question.data.audio === null) return undefined;
if (isLegacy) return `${path}${question.data.audio}`;
let audio = question.data.audio;
if (typeof audio === "string") {
return url(`/cdn/${question.owner}/${audio}`);
} else {
return url(`/cdn/${audio?.user}/${audio?._id}`);
}
});
</script>
<div class="mb-4 flex grow flex-col items-center text-6xl">
@@ -42,7 +66,16 @@
{/if}
{#if showPlayer}
<div class="flex w-full flex-col justify-center">
<AudioPlayerComponent src={path + question.data.audio} />
{#if audioPath}
<AudioPlayerComponent src={audioPath} />
{:else}
<div class="flex h-full w-full flex-col items-center justify-center">
<div class="text-[128px] text-gray-300">
<i class="fa-solid fa-file-audio"></i>
</div>
<div class="text-[24px] text-gray-500 select-none">Kein Audio ausgewählt</div>
</div>
{/if}
</div>
{/if}
{#if showQuestion}

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import AudioPlayerComponent from "./AudioPlayerComponent.svelte";
import type { AudioQuestion } from "./games/games";
import { url } from "./util";
const path = "/sounds/";
@@ -9,10 +10,22 @@
showAnswer: boolean;
showQuestion: boolean;
showPlayer: boolean;
isLegacy?: boolean;
[key: string]: unknown;
}
let { question, showAnswer, showQuestion, showPlayer }: Props = $props();
let { question, showAnswer, showQuestion, showPlayer, isLegacy = true }: Props = $props();
let audioPath = $derived.by(() => {
if (question.data.audio === null) return undefined;
if (isLegacy) return `${path}${question.data.audio}`;
let audio = question.data.audio;
if (typeof audio === "string") {
return url(`/cdn/${question.owner}/${audio}`);
} else {
return url(`/cdn/${audio?.user}/${audio?._id}`);
}
});
</script>
<div class="mb-4 flex grow flex-col items-center text-6xl">
@@ -23,7 +36,16 @@
{/if}
{#if showPlayer}
<div class="flex w-full flex-col justify-center">
<AudioPlayerComponent src={path + question.data.audio} />
{#if audioPath}
<AudioPlayerComponent src={audioPath} />
{:else}
<div class="flex h-full w-full flex-col items-center justify-center">
<div class="text-[128px] text-gray-300">
<i class="fa-solid fa-file-audio"></i>
</div>
<div class="text-[24px] text-gray-500 select-none">Kein Audio ausgewählt</div>
</div>
{/if}
</div>
{/if}
{#if showAnswer}

View File

@@ -9,13 +9,15 @@
) => void;
children: Snippet;
class?: string;
disabled?: boolean;
}
let { onclick, children, class: classes }: Props = $props();
let { onclick, children, class: classes, disabled = false }: Props = $props();
</script>
<!-- svelte-ignore a11y_consider_explicit_label -->
<button
{disabled}
type="button"
class="btn {classes}"
onclick={(event) => {

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import type { ImageMultipleChoiceQuestion } from "./games/games";
import { url } from "./util";
const path = "/images/";
@@ -8,6 +9,8 @@
showAnswer: boolean;
showQuestion: boolean;
isBuzzed: boolean;
isLegacy?: boolean;
randomize?: boolean;
[key: string]: unknown;
}
@@ -25,12 +28,33 @@
}
}
let { question, showAnswer, showQuestion, isBuzzed }: Props = $props();
let {
question,
showAnswer,
showQuestion,
isBuzzed,
isLegacy = true,
randomize = true
}: Props = $props();
const answer = question.data.choices[0];
const answer = $derived(question.data.choices[0]);
let _choices = [...question.data.choices];
shuffle(_choices);
let _choices = $derived.by(() => {
let c = [...question.data.choices];
if (randomize) shuffle(c);
return c;
});
let imagePath = $derived.by(() => {
if (question.data.image === null) return undefined;
if (isLegacy) return `${path}${question.data.image}`;
let image = question.data.image;
if (typeof image === "string") {
return url(`/cdn/${question.owner}/${image}`);
} else {
return url(`/cdn/${image?.user}/${image?._id}`);
}
});
</script>
<div class="mb-4 flex w-full grow flex-col items-center gap-2 text-6xl">
@@ -39,11 +63,16 @@
<div class="text-center">{question.data.question}</div>
</div>
<div class="container grow-6">
<img
src={path + question.data.image}
alt={path + question.data.image}
class={isBuzzed ? "blurry" : ""}
/>
{#if imagePath}
<img src={imagePath} alt={imagePath} class={isBuzzed ? "blurry" : ""} />
{:else}
<div class="flex h-full w-full flex-col items-center justify-center">
<div class="text-[128px] text-gray-300">
<i class="fa-solid fa-image"></i>
</div>
<div class="text-[24px] text-gray-500 select-none">Kein Bild ausgewählt</div>
</div>
{/if}
</div>
<div class="flex w-full grow-1 flex-wrap items-center justify-around gap-2">
{#each _choices as choice}

View File

@@ -1,5 +1,7 @@
<script lang="ts">
import type { ImageQuestion } from "./games/games";
import { isRessource } from "./Types";
import { url } from "./util";
const path = "/images/";
@@ -8,10 +10,22 @@
showAnswer: boolean;
showQuestion: boolean;
isBuzzed: boolean;
isLegacy?: boolean;
[key: string]: unknown;
}
let { question, showAnswer, showQuestion, isBuzzed }: Props = $props();
let { question, showAnswer, showQuestion, isBuzzed, isLegacy = true }: Props = $props();
let imagePath = $derived.by(() => {
if (question.data.image === null) return undefined;
if (isLegacy) return `${path}${question.data.image}`;
let image = question.data.image;
if (typeof image === "string") {
return url(`/cdn/${question.owner}/${image}`);
} else {
return url(`/cdn/${image?.user}/${image?._id}`);
}
});
</script>
<div class="mb-4 flex w-full grow flex-col items-center gap-2 text-6xl">
@@ -20,11 +34,16 @@
<div class="text-center">{question.data.question}</div>
</div>
<div class="container grow-6">
<img
src={path + question.data.image}
alt={path + question.data.image}
class={isBuzzed ? "blurry" : ""}
/>
{#if imagePath}
<img src={imagePath} alt={imagePath} class={isBuzzed ? "blurry" : ""} />
{:else}
<div class="flex h-full w-full flex-col items-center justify-center">
<div class="text-[128px] text-gray-300">
<i class="fa-solid fa-image"></i>
</div>
<div class="text-[24px] text-gray-500 select-none">Kein Bild ausgewählt</div>
</div>
{/if}
</div>
{/if}
{#if showAnswer}

View File

@@ -5,6 +5,7 @@
question: MultipleChoiceQuestion;
showAnswer: boolean;
showQuestion: boolean;
randomize?: boolean;
[key: string]: unknown;
}
@@ -22,12 +23,15 @@
}
}
let { question, showAnswer, showQuestion }: Props = $props();
let { question, showAnswer, showQuestion, randomize = true }: Props = $props();
const answer = question.data.choices[0];
const answer = $derived(question.data.choices[0]);
let _choices = [...question.data.choices];
shuffle(_choices);
let _choices = $derived.by(() => {
let c = [...question.data.choices];
if (randomize) shuffle(c);
return c;
});
</script>
<div class="mb-4 flex grow flex-col items-center text-6xl">

View File

@@ -4,7 +4,6 @@
import Modal from "./Modal.svelte";
import { url } from "./util";
import { isDir, isRessource, type Directory, type Ressource } from "./Types";
import { onMount } from "svelte";
interface Props {
show: boolean;

View File

@@ -1,24 +1,36 @@
<script lang="ts">
import type { HTMLInputTypeAttribute } from "svelte/elements";
interface Props {
value: string;
value: any;
label?: string;
type?: HTMLInputTypeAttribute;
readonly?: boolean;
class?: string;
}
let { value = $bindable(), label }: Props = $props();
let {
value = $bindable(),
label,
type = "text",
readonly = false,
class: className
}: Props = $props();
const id = crypto.randomUUID();
</script>
<div>
<div class="w-full grow">
{#if label}
<label for="textfield-{id}" class="">{label}</label>
{/if}
<div>
<input
type="text"
{type}
{readonly}
name="textfield"
id="textfield-{id}"
class="borders mt-2 mb-2 w-full"
class="borders mt-2 mb-2 w-full {className}"
bind:value
/>
</div>

View File

@@ -29,7 +29,10 @@ export function isDir(dir: Directory | Ressource): dir is Directory {
return (dir as Directory).isDir === true;
}
export function isRessource(ressource: Ressource | Directory): ressource is Ressource {
export function isRessource(
ressource: Ressource | Directory | string | null
): ressource is Ressource {
if (ressource === null) return false;
return (ressource as Directory).isDir === undefined;
}

View File

@@ -14,6 +14,7 @@
import Button from "./Button.svelte";
import Modal from "./Modal.svelte";
import Textfield from "./Textfield.svelte";
import { fetchCategory } from "../routes/editor/fetchers";
interface Props {
wall: Wall | FullWall | undefined;
@@ -40,15 +41,7 @@
if (wall && isWall(wall)) {
let cats: Promise<Category>[] = [];
for (const catId of wall.categories) {
cats.push(
axios
.get(url(`/category?id=${catId}`), { withCredentials: true })
.then((response) => {
if (response.status === 200) {
return response.data;
} else throw "Failed to fetch: " + response.status;
})
);
cats.push(fetchCategory(catId));
}
return Promise.all(cats).then((cats) => {
categories = cats;

View File

@@ -1,3 +1,5 @@
import type { Ressource } from "$lib/Types";
const games: Games = [
{
name: "LAN Party",
@@ -1331,8 +1333,10 @@ export type QuestionType =
| "AUDIO_MULTIPLE_CHOICE";
export type Question = {
_id?: string;
points: number;
type: QuestionType;
owner?: string;
};
export type SimpleQuestion = Question & {
@@ -1354,11 +1358,13 @@ export type MultipleChoiceQuestion = Question & {
};
};
export type QuestionRessource = string | Ressource | null;
export type ImageQuestion = Question & {
type: "IMAGE";
data: {
question: string;
image: string;
image: QuestionRessource;
answer: string;
};
};
@@ -1367,7 +1373,7 @@ export type ImageMultipleChoiceQuestion = Question & {
type: "IMAGE_MULTIPLE_CHOICE";
data: {
question: string;
image: string;
image: QuestionRessource;
choices: string[];
};
};
@@ -1376,7 +1382,7 @@ export type AudioQuestion = Question & {
type: "AUDIO";
data: {
question: string;
audio: string;
audio: QuestionRessource;
answer: string;
};
};
@@ -1385,7 +1391,7 @@ export type AudioMultipleChoiceQuestion = Question & {
type: "AUDIO_MULTIPLE_CHOICE";
data: {
question: string;
audio: string;
audio: QuestionRessource;
choices: string[];
};
};

View File

@@ -0,0 +1,12 @@
import type { WallId } from "$lib/Types";
let selectedWallId: WallId | undefined;
export default {
get selectedWallId(): WallId | undefined {
return selectedWallId;
},
set selectedWallId(id: WallId | undefined) {
selectedWallId = id;
}
};

View File

@@ -4,11 +4,13 @@
import Button from "$lib/Button.svelte";
import Modal from "$lib/Modal.svelte";
import Textfield from "$lib/Textfield.svelte";
import type { Game, Wall as WallType } from "$lib/Types";
import type { Game, WallId, Wall as WallType } from "$lib/Types";
import { url } from "$lib/util";
import Wall from "$lib/Wall.svelte";
import axios from "axios";
import { onMount } from "svelte";
import { fetchGame, fetchWalls } from "../fetchers";
import EditorState from "../EditorState";
let game: Game | undefined = $state();
@@ -25,26 +27,20 @@
let error = $state("");
function fetchGame() {
return axios
.get(url(`/game?id=${page.params.gameid}`), { withCredentials: true })
.then((response) => {
if (response.status === 200) {
game = response.data;
} else {
console.error(`Failed to fetch game: ${response.status}`);
function _fetchWalls() {
return fetchWalls(`${page.params.gameid}`)
.then((fetchedWalls) => {
walls = fetchedWalls;
if (selectedWall === undefined) {
if (EditorState.selectedWallId !== undefined) {
for (const wall of walls) {
if (wall._id === EditorState.selectedWallId) selectedWall = wall;
}
} else if (walls.length > 0) selectedWall = walls[0];
}
})
.catch((err) => console.error(err));
}
function fetchWalls() {
return axios
.get(url(`/walls/${page.params.gameid}`), { withCredentials: true })
.then((response) => {
if (response.status === 200) {
walls = response.data;
if (selectedWall === undefined && walls.length > 0) selectedWall = walls[0];
if (selectedWall) {
EditorState.selectedWallId = selectedWall._id;
}
})
.catch((err) => {
@@ -68,7 +64,7 @@
.then((response) => {
if (response.status === 200) {
newWallName = "";
fetchWalls();
_fetchWalls();
return true;
} else return false;
})
@@ -97,7 +93,7 @@
if (response.status === 200) {
newWallName = "";
walltoRename = undefined;
fetchWalls();
_fetchWalls();
return true;
} else {
console.error(`Failed to rename wall: ${response.status}`);
@@ -117,13 +113,16 @@
async function deleteWall() {
if (!wallToDelete) return false;
if (wallToDelete._id === selectedWall?._id) selectedWall = undefined;
if (wallToDelete._id === selectedWall?._id) {
selectedWall = undefined;
EditorState.selectedWallId = undefined;
}
return axios
.delete(url(`/wall/${wallToDelete._id}`), { withCredentials: true })
.then((response) => {
if (response.status === 200) {
wallToDelete = undefined;
fetchWalls();
_fetchWalls();
return true;
} else return false;
})
@@ -138,7 +137,15 @@
}
onMount(() => {
fetchGame().then(() => fetchWalls());
if (page.params.gameid)
fetchGame(page.params.gameid)
.then((fetchedGame) => {
game = fetchedGame;
return _fetchWalls();
})
.catch((err) => {
console.error(err);
});
});
</script>
@@ -164,6 +171,7 @@
class:bg-emerald-200={selectedWall?._id === wall._id}
onclick={() => {
selectedWall = wall;
EditorState.selectedWallId = selectedWall._id;
}}
>
<div>

View File

@@ -0,0 +1,291 @@
<script lang="ts">
import {
isRessource,
type Category,
type Game,
type GeneralQuestion,
type Wall
} from "$lib/Types";
import { onMount, untrack } from "svelte";
import { fetchCategory, fetchGame, fetchQuestion, fetchWall } from "../../../../fetchers";
import { page } from "$app/state";
import { goto } from "$app/navigation";
import {
isAudioMultipleChoiceQuestion,
isAudioQuestion,
isImageMultipleChoiceQuestion,
isImageQuestion,
isMultipleChoiceQuestion,
isSimpleQuestion,
type QuestionType
} from "$lib/games/games";
import SimpleQuestionComponent from "$lib/SimpleQuestionComponent.svelte";
import { convert } from "./questionconverters";
import MultipleChoiceQuestionComponent from "$lib/MultipleChoiceQuestionComponent.svelte";
import ImageQuestionComponent from "$lib/ImageQuestionComponent.svelte";
import ImageMultipleChoiceQuestionComponent from "$lib/ImageMultipleChoiceQuestionComponent.svelte";
import AudioQuestionComponent from "$lib/AudioQuestionComponent.svelte";
import AudioMultipleChoiceQuestionComponent from "$lib/AudioMultipleChoiceQuestionComponent.svelte";
import Button from "$lib/Button.svelte";
import Textfield from "$lib/Textfield.svelte";
import EditorSimple from "./EditorSimple.svelte";
import EditorImage from "./EditorImage.svelte";
import EditorAudio from "./EditorAudio.svelte";
import EditorMultipleChoice from "./EditorMultipleChoice.svelte";
import EditorAudioMultipleChoice from "./EditorAudioMultipleChoice.svelte";
import EditorImageMultipleChoice from "./EditorImageMultipleChoice.svelte";
import axios, { type AxiosResponse } from "axios";
import { url } from "$lib/util";
let init = true;
let game: Game | undefined = $state();
let wall: Wall | undefined = $state();
let wallNumber = $derived.by(() => {
if (game && wall) {
return game.walls.indexOf(wall._id) + 1;
} else return -1;
});
let category: Category | undefined = $state();
let question: GeneralQuestion | undefined = $state();
let questionNumer = $derived.by(() => {
if (category && question) {
for (let i = 0; i < category.questions.length; i++) {
const q = category.questions[i];
if (q._id === question._id) return i + 1;
}
return -1;
} else return -1;
});
let questionType: QuestionType = $state("SIMPLE");
let showAnswer = $state(true);
let saving = $state(false);
function save() {
if (!question) return;
saving = true;
let q: GeneralQuestion | undefined;
let promise: Promise<AxiosResponse> | undefined;
if (question.type === "IMAGE" || question.type === "IMAGE_MULTIPLE_CHOICE") {
promise = axios.post(
url(`/question`),
{
...question,
data: {
...question.data,
image: isRessource(question.data.image) ? question.data.image._id : null
}
},
{ withCredentials: true }
);
} else if (question.type === "AUDIO" || question.type === "AUDIO_MULTIPLE_CHOICE") {
promise = axios.post(
url(`/question`),
{
...question,
data: {
...question.data,
audio: isRessource(question.data.audio) ? question.data.audio._id : null
}
},
{ withCredentials: true }
);
} else if (question.type === "SIMPLE" || question.type === "MULTIPLE_CHOICE") {
promise = axios.post(url(`/question`), question, { withCredentials: true });
}
if (promise)
promise
.then((response) => {
if (response.status !== 200) {
console.error("Failed to save: " + response.status);
alert("Failed to save: " + response.status);
}
})
.catch((err) => {
console.error(err);
alert(`Failed to save: ${err}`);
})
.finally(() => {
saving = false;
});
else {
saving = false;
}
}
$effect(() => {
if (questionType) {
if (init) {
init = false;
return;
}
console.log(questionType);
untrack(() => {
if (question) question = convert(question, questionType);
});
}
});
onMount(() => {
let promises: Promise<unknown>[] = [];
promises.push(
fetchGame(`${page.params.gameid}`).then((fetchedGame) => {
game = fetchedGame;
})
);
promises.push(
fetchWall(`${page.params.wallid}`).then((fetchedWall) => {
wall = fetchedWall;
})
);
promises.push(
fetchCategory(`${page.params.categoryid}`).then((fetchedCategory) => {
category = fetchedCategory;
})
);
promises.push(
fetchQuestion(`${page.params.questionid}`).then((fetchedQuestion) => {
question = fetchedQuestion;
questionType = question.type;
})
);
Promise.all(promises).catch((err) => {
console.error(err);
});
});
</script>
<div class="flex h-full flex-col">
<div class="m-4 flex items-center gap-2">
<h1 class="text-4xl font-bold">{game ? game.name : "Spiel"}</h1>
<!-- svelte-ignore a11y_missing_content -->
<h1 class="text-4xl font-bold"><i class="fa-solid fa-angle-right"></i></h1>
<h1 class="text-4xl font-bold">
{wall
? `${wall.name}
${wallNumber > 0 ? `(Wand ${wallNumber})` : ""}`
: "Wand"}
</h1>
<!-- svelte-ignore a11y_missing_content -->
<h1 class="text-4xl font-bold"><i class="fa-solid fa-angle-right"></i></h1>
<h1 class="text-4xl font-bold">{category ? category.name : "Kategorie"}</h1>
<!-- svelte-ignore a11y_missing_content -->
<h1 class="text-4xl font-bold"><i class="fa-solid fa-angle-right"></i></h1>
<h1 class="text-4xl font-bold">{questionNumer ? `Frage ${questionNumer}` : "Frage ?"}</h1>
<div class="grow"></div>
<button class="btn" type="button" onclick={() => goto(`/editor/${page.params.gameid}`)}
>Zurück</button
>
</div>
{#if question}
<div class="flex grow">
<!-- Sidebar -->
<div class="flex h-full max-w-[600px] min-w-[400px] flex-col gap-4 border-r-1">
<div class="ms-4 me-4 flex justify-between gap-4">
<Button
onclick={() => {
save();
}}>{saving ? "Speichert..." : "Speichern"}</Button
>
<Button
onclick={() => {
showAnswer = !showAnswer;
}}
>
{#if showAnswer}
<div>
<i class="fa-solid fa-exclamation"></i>
</div>
{:else}
<div>
<i class="fa-solid fa-question"></i>
</div>
{/if}
</Button>
</div>
<select
name="QuestionType"
id="QuestionType"
bind:value={questionType}
class="ms-4 me-4 rounded-md border-1 p-1"
>
<option value="SIMPLE">SIMPLE</option>
<option value="MULTIPLE_CHOICE">MULTIPLE_CHOICE</option>
<option value="IMAGE">IMAGE</option>
<option value="IMAGE_MULTIPLE_CHOICE">IMAGE_MULTIPLE_CHOICE</option>
<option value="AUDIO">AUDIO</option>
<option value="AUDIO_MULTIPLE_CHOICE">AUDIO_MULTIPLE_CHOICE</option>
</select>
<div class="ms-4 me-4">
<Textfield type="number" bind:value={question.points} label="Punkte"
></Textfield>
</div>
<div class="ms-4 me-4 grow">
{#if isSimpleQuestion(question)}
<EditorSimple bind:question></EditorSimple>
{:else if isMultipleChoiceQuestion(question)}
<EditorMultipleChoice bind:question></EditorMultipleChoice>
{:else if isImageQuestion(question)}
<EditorImage bind:question></EditorImage>
{:else if isImageMultipleChoiceQuestion(question)}
<EditorImageMultipleChoice bind:question></EditorImageMultipleChoice>
{:else if isAudioQuestion(question)}
<EditorAudio bind:question></EditorAudio>
{:else if isAudioMultipleChoiceQuestion(question)}
<EditorAudioMultipleChoice bind:question></EditorAudioMultipleChoice>{/if}
</div>
</div>
<!-- Display -->
<div class="ms-4 me-4 flex grow flex-col">
{#if isSimpleQuestion(question)}
<SimpleQuestionComponent {question} {showAnswer} showQuestion
></SimpleQuestionComponent>
{:else if isMultipleChoiceQuestion(question)}
<MultipleChoiceQuestionComponent
{question}
{showAnswer}
showQuestion
randomize={false}
></MultipleChoiceQuestionComponent>
{:else if isImageQuestion(question)}
<ImageQuestionComponent
{question}
{showAnswer}
showQuestion
isBuzzed={false}
isLegacy={false}
></ImageQuestionComponent>
{:else if isImageMultipleChoiceQuestion(question)}
<ImageMultipleChoiceQuestionComponent
{question}
{showAnswer}
showQuestion
isBuzzed={false}
isLegacy={false}
randomize={false}
></ImageMultipleChoiceQuestionComponent>
{:else if isAudioQuestion(question)}
<AudioQuestionComponent
{question}
{showAnswer}
showQuestion
showPlayer
isLegacy={false}
></AudioQuestionComponent>
{:else if isAudioMultipleChoiceQuestion(question)}
<AudioMultipleChoiceQuestionComponent
{question}
{showAnswer}
showQuestion
showPlayer
isLegacy={false}
randomize={false}
></AudioMultipleChoiceQuestionComponent>
{/if}
</div>
</div>
{:else}
No question isFileLoadingAllowed, please try again
{/if}
</div>

View File

@@ -0,0 +1,14 @@
<script lang="ts">
import type { AudioQuestion } from "$lib/games/games";
import EditorMediaField from "./EditorMediaField.svelte";
import EditorSimple from "./EditorSimple.svelte";
interface Props {
question: AudioQuestion;
}
let { question = $bindable() }: Props = $props();
</script>
<EditorMediaField bind:question></EditorMediaField>
<EditorSimple bind:question></EditorSimple>

View File

@@ -0,0 +1,14 @@
<script lang="ts">
import type { AudioMultipleChoiceQuestion } from "$lib/games/games";
import EditorMediaField from "./EditorMediaField.svelte";
import EditorMultipleChoice from "./EditorMultipleChoice.svelte";
interface Props {
question: AudioMultipleChoiceQuestion;
}
let { question = $bindable() }: Props = $props();
</script>
<EditorMediaField bind:question></EditorMediaField>
<EditorMultipleChoice bind:question></EditorMultipleChoice>

View File

@@ -0,0 +1,14 @@
<script lang="ts">
import type { ImageQuestion } from "$lib/games/games";
import EditorMediaField from "./EditorMediaField.svelte";
import EditorSimple from "./EditorSimple.svelte";
interface Props {
question: ImageQuestion;
}
let { question = $bindable() }: Props = $props();
</script>
<EditorMediaField bind:question></EditorMediaField>
<EditorSimple bind:question></EditorSimple>

View File

@@ -0,0 +1,14 @@
<script lang="ts">
import type { ImageMultipleChoiceQuestion } from "$lib/games/games";
import EditorMediaField from "./EditorMediaField.svelte";
import EditorMultipleChoice from "./EditorMultipleChoice.svelte";
interface Props {
question: ImageMultipleChoiceQuestion;
}
let { question = $bindable() }: Props = $props();
</script>
<EditorMediaField bind:question></EditorMediaField>
<EditorMultipleChoice bind:question></EditorMultipleChoice>

View File

@@ -0,0 +1,67 @@
<script lang="ts">
import Button from "$lib/Button.svelte";
import {
isAudioMultipleChoiceQuestion,
isAudioQuestion,
type AudioMultipleChoiceQuestion,
type AudioQuestion,
type ImageMultipleChoiceQuestion,
type ImageQuestion
} from "$lib/games/games";
import RessourceManager from "$lib/RessourceManager.svelte";
import Textfield from "$lib/Textfield.svelte";
import { isRessource } from "$lib/Types";
interface Props {
question:
| AudioQuestion
| ImageQuestion
| AudioMultipleChoiceQuestion
| ImageMultipleChoiceQuestion;
}
let { question = $bindable() }: Props = $props();
let showRessourceManager = $state(false);
</script>
<div class="w-full">
{#if isAudioQuestion(question) || isAudioMultipleChoiceQuestion(question)}
<div>Audio:</div>
{:else}
<div>Bild:</div>
{/if}
<div class="flex items-center justify-between gap-2">
{#if isAudioQuestion(question) || isAudioMultipleChoiceQuestion(question)}
<Textfield
readonly
value={question.data.audio && isRessource(question.data.audio)
? question.data.audio.name
: ""}
></Textfield>
{:else}
<Textfield
readonly
value={question.data.image && isRessource(question.data.image)
? question.data.image.name
: ""}
></Textfield>
{/if}
<Button
onclick={() => {
showRessourceManager = true;
}}><i class="fa-solid fa-arrow-up-right-from-square"></i></Button
>
</div>
</div>
<RessourceManager
bind:show={showRessourceManager}
ok={(res) => {
if (isAudioQuestion(question) || isAudioMultipleChoiceQuestion(question)) {
question.data.audio = res;
} else {
question.data.image = res;
}
}}
></RessourceManager>

View File

@@ -0,0 +1,44 @@
<script lang="ts">
import Button from "$lib/Button.svelte";
import type {
AudioMultipleChoiceQuestion,
ImageMultipleChoiceQuestion,
MultipleChoiceQuestion
} from "$lib/games/games";
import Textfield from "$lib/Textfield.svelte";
import { onMount } from "svelte";
interface Props {
question:
| MultipleChoiceQuestion
| AudioMultipleChoiceQuestion
| ImageMultipleChoiceQuestion;
}
let { question = $bindable() }: Props = $props();
</script>
<Textfield bind:value={question.data.question} label="Frage"></Textfield>
<div class="mb-2 flex items-center justify-between">
<div>Antworten</div>
<Button
onclick={() => {
question.data.choices.push("Antwort");
}}><i class="fa-solid fa-plus"></i></Button
>
</div>
<hr />
{#each question.data.choices as _, answerIndex}
<div class="flex items-center gap-2">
<Textfield
bind:value={question.data.choices[answerIndex]}
label={answerIndex === 0 ? "Korrekte Antwort" : `Antwort ${answerIndex + 1}`}
></Textfield>
<Button
class="border-red-600 text-red-600"
onclick={(event) => {
question.data.choices.splice(answerIndex, 1);
}}><i class="fa-solid fa-trash"></i></Button
>
</div>
{/each}

View File

@@ -0,0 +1,13 @@
<script lang="ts">
import type { AudioQuestion, ImageQuestion, SimpleQuestion } from "$lib/games/games";
import Textfield from "$lib/Textfield.svelte";
interface Props {
question: SimpleQuestion | ImageQuestion | AudioQuestion;
}
let { question = $bindable() }: Props = $props();
</script>
<Textfield bind:value={question.data.question} label="Frage"></Textfield>
<Textfield bind:value={question.data.answer} label="Antwort"></Textfield>

View File

@@ -0,0 +1,303 @@
import type {
AudioMultipleChoiceQuestion,
AudioQuestion,
ImageMultipleChoiceQuestion,
ImageQuestion,
MultipleChoiceQuestion,
Question,
QuestionType,
SimpleQuestion
} from "$lib/games/games";
import type { GeneralQuestion } from "$lib/Types";
function defaultConversion(question: Question, type: QuestionType): Question {
return {
points: question.points,
_id: question._id,
owner: question.owner,
type
};
}
export function convert(q: GeneralQuestion, t: QuestionType) {
if (q.type === "SIMPLE") {
switch (t) {
case "SIMPLE":
return q;
case "MULTIPLE_CHOICE":
return simpleToMultipleChoice(q);
case "IMAGE":
return simpleToImage(q);
case "IMAGE_MULTIPLE_CHOICE":
return simpleToImageMultipleChoice(q);
case "AUDIO":
return simpleToAudio(q);
case "AUDIO_MULTIPLE_CHOICE":
return simpleToAudioMultipleChoice(q);
}
} else if (q.type === "MULTIPLE_CHOICE") {
switch (t) {
case "SIMPLE":
return multipleChoiceToSimple(q);
case "MULTIPLE_CHOICE":
return q;
case "IMAGE":
return multipleChoiceToImage(q);
case "IMAGE_MULTIPLE_CHOICE":
return multipleChoiceToImageMultipleChoice(q);
case "AUDIO":
return multipleChoiceToAudio(q);
case "AUDIO_MULTIPLE_CHOICE":
return multipleChoiceToAudioMultipleChoice(q);
}
} else if (q.type === "IMAGE") {
switch (t) {
case "SIMPLE":
return mediaToSimple(q);
case "MULTIPLE_CHOICE":
return simpleToMultipleChoice(q);
case "IMAGE":
return q;
case "IMAGE_MULTIPLE_CHOICE":
return imageToImageMultipleChoice(q);
case "AUDIO":
return simpleToAudio(q);
case "AUDIO_MULTIPLE_CHOICE":
return simpleToMultipleChoice(q);
}
} else if (q.type === "IMAGE_MULTIPLE_CHOICE") {
switch (t) {
case "SIMPLE":
return multipleChoiceToSimple(q);
case "MULTIPLE_CHOICE":
return mediaMultipleChoiceToMultipleChoice(q);
case "IMAGE":
return imageMultipleChoiceToImage(q);
case "IMAGE_MULTIPLE_CHOICE":
return q;
case "AUDIO":
return multipleChoiceToAudio(q);
case "AUDIO_MULTIPLE_CHOICE":
return multipleChoiceToAudioMultipleChoice(q);
}
} else if (q.type === "AUDIO") {
switch (t) {
case "SIMPLE":
return mediaToSimple(q);
case "MULTIPLE_CHOICE":
return simpleToMultipleChoice(q);
case "IMAGE":
return simpleToImage(q);
case "IMAGE_MULTIPLE_CHOICE":
return simpleToImageMultipleChoice(q);
case "AUDIO":
return q;
case "AUDIO_MULTIPLE_CHOICE":
return audioToAudioMultipleChoice(q);
}
} else if (q.type === "AUDIO_MULTIPLE_CHOICE") {
switch (t) {
case "SIMPLE":
return multipleChoiceToSimple(q);
case "MULTIPLE_CHOICE":
return mediaMultipleChoiceToMultipleChoice(q);
case "IMAGE":
return multipleChoiceToImage(q);
case "IMAGE_MULTIPLE_CHOICE":
return multipleChoiceToImageMultipleChoice(q);
case "AUDIO":
return audioMultipleChoiceToAudio(q);
case "AUDIO_MULTIPLE_CHOICE":
return q;
}
}
return q;
}
export function simpleToMultipleChoice(
question: SimpleQuestion | ImageQuestion | AudioQuestion
): MultipleChoiceQuestion {
return {
...(defaultConversion(question, "MULTIPLE_CHOICE") as MultipleChoiceQuestion),
data: {
question: question.data.question,
choices: [question.data.answer]
}
};
}
export function simpleToImage(question: SimpleQuestion | AudioQuestion): ImageQuestion {
return {
...(defaultConversion(question, "IMAGE") as ImageQuestion),
data: {
question: question.data.question,
answer: question.data.answer,
image: null
}
};
}
export function simpleToImageMultipleChoice(
question: SimpleQuestion | AudioQuestion
): ImageMultipleChoiceQuestion {
return {
...(defaultConversion(question, "IMAGE_MULTIPLE_CHOICE") as ImageMultipleChoiceQuestion),
data: {
question: question.data.question,
choices: [question.data.answer],
image: null
}
};
}
export function simpleToAudio(question: SimpleQuestion | ImageQuestion): AudioQuestion {
return {
...(defaultConversion(question, "AUDIO") as AudioQuestion),
data: {
question: question.data.question,
answer: question.data.answer,
audio: null
}
};
}
export function simpleToAudioMultipleChoice(question: SimpleQuestion): AudioMultipleChoiceQuestion {
return {
...(defaultConversion(question, "AUDIO_MULTIPLE_CHOICE") as AudioMultipleChoiceQuestion),
data: {
question: question.data.question,
choices: [question.data.answer],
audio: null
}
};
}
export function mediaToSimple(question: ImageQuestion | AudioQuestion): SimpleQuestion {
return {
...(defaultConversion(question, "SIMPLE") as SimpleQuestion),
data: {
question: question.data.question,
answer: question.data.answer
}
};
}
export function mediaMultipleChoiceToMultipleChoice(
question: ImageMultipleChoiceQuestion | AudioMultipleChoiceQuestion
): MultipleChoiceQuestion {
return {
...(defaultConversion(question, "MULTIPLE_CHOICE") as MultipleChoiceQuestion),
data: {
question: question.data.question,
choices: [...question.data.choices]
}
};
}
export function multipleChoiceToSimple(
question: MultipleChoiceQuestion | ImageMultipleChoiceQuestion | AudioMultipleChoiceQuestion
): SimpleQuestion {
return {
...(defaultConversion(question, "SIMPLE") as SimpleQuestion),
data: {
question: question.data.question,
answer: question.data.choices[0] ? question.data.choices[0] : "Antwort"
}
};
}
export function multipleChoiceToImage(
question: MultipleChoiceQuestion | AudioMultipleChoiceQuestion
): ImageQuestion {
return {
...(defaultConversion(question, "IMAGE") as ImageQuestion),
data: {
question: question.data.question,
answer: question.data.choices[0] ? question.data.choices[0] : "Antwort",
image: null
}
};
}
export function multipleChoiceToImageMultipleChoice(
question: MultipleChoiceQuestion | AudioMultipleChoiceQuestion
): ImageMultipleChoiceQuestion {
return {
...(defaultConversion(question, "IMAGE_MULTIPLE_CHOICE") as ImageMultipleChoiceQuestion),
data: {
question: question.data.question,
choices: [...question.data.choices],
image: null
}
};
}
export function multipleChoiceToAudio(
question: MultipleChoiceQuestion | ImageMultipleChoiceQuestion
): AudioQuestion {
return {
...(defaultConversion(question, "AUDIO") as AudioQuestion),
data: {
question: question.data.question,
answer: question.data.choices[0] ? question.data.choices[0] : "Antwort",
audio: null
}
};
}
export function multipleChoiceToAudioMultipleChoice(
question: MultipleChoiceQuestion | ImageMultipleChoiceQuestion
): AudioMultipleChoiceQuestion {
return {
...(defaultConversion(question, "AUDIO_MULTIPLE_CHOICE") as AudioMultipleChoiceQuestion),
data: {
question: question.data.question,
choices: [...question.data.choices],
audio: null
}
};
}
export function imageToImageMultipleChoice(question: ImageQuestion): ImageMultipleChoiceQuestion {
return {
...(defaultConversion(question, "IMAGE_MULTIPLE_CHOICE") as ImageMultipleChoiceQuestion),
data: {
question: question.data.question,
choices: [question.data.answer],
image: question.data.image
}
};
}
export function imageMultipleChoiceToImage(question: ImageMultipleChoiceQuestion): ImageQuestion {
return {
...(defaultConversion(question, "IMAGE") as ImageQuestion),
data: {
question: question.data.question,
answer: question.data.choices[0] ? question.data.choices[0] : "Antwort",
image: question.data.image
}
};
}
export function audioToAudioMultipleChoice(question: AudioQuestion): AudioMultipleChoiceQuestion {
return {
...(defaultConversion(question, "AUDIO_MULTIPLE_CHOICE") as AudioMultipleChoiceQuestion),
data: {
question: question.data.question,
choices: [question.data.answer],
audio: question.data.audio
}
};
}
export function audioMultipleChoiceToAudio(question: AudioMultipleChoiceQuestion): AudioQuestion {
return {
...(defaultConversion(question, "AUDIO") as AudioQuestion),
data: {
question: question.data.question,
answer: question.data.choices[0] ? question.data.choices[0] : "Antwort",
audio: question.data.audio
}
};
}

View File

@@ -0,0 +1,108 @@
import type {
AudioMultipleChoiceQuestion,
AudioQuestion,
ImageMultipleChoiceQuestion,
ImageQuestion,
MultipleChoiceQuestion,
Question,
SimpleQuestion
} from "$lib/games/games";
import type { Category, Game, Wall } from "$lib/Types";
import { url } from "$lib/util";
import axios from "axios";
export function fetchGame(id: string) {
return axios.get(url(`/game?id=${id}`), { withCredentials: true }).then((response) => {
if (response.status === 200) {
return response.data as Game;
} else {
throw `Failed to fetch game: ${response.status}`;
}
});
}
export function fetchWalls(id: string) {
return axios.get(url(`/walls/${id}`), { withCredentials: true }).then((response) => {
if (response.status === 200) {
return response.data as Wall[];
} else {
throw `Failed to fetch walls: ${response.status}`;
}
});
}
export function fetchWall(id: string) {
return axios.get(url(`/wall?id=${id}`), { withCredentials: true }).then((response) => {
if (response.status === 200) {
return response.data as Wall;
} else {
throw `Failed to fetch wall: ${response.status}`;
}
});
}
export function fetchCategory(id: string) {
return axios.get(url(`/category?id=${id}`), { withCredentials: true }).then((response) => {
if (response.status === 200) {
return response.data as Category;
} else {
throw `Failed to fetch category: ${response.status}`;
}
});
}
export function fetchQuestion(id: string) {
return axios
.get(url(`/question?id=${id}`), { withCredentials: true })
.then(async (response) => {
if (response.status === 200) {
const q = response.data;
console.log(q);
if (
(q as Question).type === "IMAGE" ||
(q as Question).type === "IMAGE_MULTIPLE_CHOICE"
) {
// request ressource
return axios
.get(url(`/ressource?id=${(q as ImageQuestion).data.image}`), {
withCredentials: true
})
.then((ressourceResponse) => {
if (ressourceResponse.status === 200) {
(q as ImageQuestion | ImageMultipleChoiceQuestion).data.image =
ressourceResponse.data;
} else {
(q as ImageQuestion | ImageMultipleChoiceQuestion).data.image =
null;
}
return q as ImageQuestion | ImageMultipleChoiceQuestion;
})
.catch(() => {
return q as ImageQuestion | ImageMultipleChoiceQuestion;
});
} else if (
(q as Question).type === "AUDIO" ||
(q as Question).type === "AUDIO_MULTIPLE_CHOICE"
) {
// request ressource
return axios
.get(url(`/ressource?id=${(q as AudioQuestion).data.audio}`), {
withCredentials: true
})
.then((ressourceResponse) => {
if (ressourceResponse.status === 200) {
(q as AudioQuestion | AudioMultipleChoiceQuestion).data.audio =
ressourceResponse.data;
} else {
(q as AudioQuestion | AudioMultipleChoiceQuestion).data.audio =
null;
}
return q as AudioQuestion | AudioMultipleChoiceQuestion;
})
.catch(() => {
return q as AudioQuestion | AudioMultipleChoiceQuestion;
});
} else return q as SimpleQuestion | MultipleChoiceQuestion;
} else throw `Failed to fetch question: ${response.status}`;
});
}