Compare commits
7 Commits
4405c23bee
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b24e43e142 | |||
| 48074f7603 | |||
| 5568a5bb99 | |||
| 7be5921ef6 | |||
| dc2766f0ef | |||
| 7d231730a6 | |||
| 956571e470 |
@@ -21,7 +21,7 @@ services:
|
||||
volumes:
|
||||
- jeopardyserver_data_volume:/data
|
||||
mongo:
|
||||
image: mongo:8.0.14
|
||||
image: mongo:8.0.17
|
||||
restart: always
|
||||
environment:
|
||||
MONGO_INITDB_ROOT_USERNAME: jeopardyadmin
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "jeopardy",
|
||||
"version": "1.0.5",
|
||||
"version": "2.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "jeopardy",
|
||||
"version": "1.0.5",
|
||||
"version": "2.0.0",
|
||||
"dependencies": {
|
||||
"axios": "^1.12.2",
|
||||
"cookie": "^1.0.2"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "jeopardy",
|
||||
"private": true,
|
||||
"version": "1.0.5",
|
||||
"version": "2.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
51
src/lib/Button.svelte
Normal file
51
src/lib/Button.svelte
Normal file
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
interface Props {
|
||||
onclick?: (
|
||||
event: MouseEvent & {
|
||||
currentTarget: EventTarget & HTMLButtonElement;
|
||||
}
|
||||
) => void;
|
||||
children: Snippet;
|
||||
class?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
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) => {
|
||||
if (onclick) onclick(event);
|
||||
}}>{@render children()}</button
|
||||
>
|
||||
|
||||
<style>
|
||||
.btn {
|
||||
border: 1px solid black;
|
||||
border-radius: 5px;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
color: grey !important;
|
||||
border-color: gray !important;
|
||||
cursor: unset !important;
|
||||
}
|
||||
|
||||
.btn:disabled:hover {
|
||||
background-color: unset !important;
|
||||
cursor: unset !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Game, Wall } from "./games/games";
|
||||
import type { Game, Wall } from "./Types";
|
||||
|
||||
interface Player {
|
||||
name: string;
|
||||
@@ -8,10 +8,9 @@ interface Player {
|
||||
const baseRoute = "/connected/display/running";
|
||||
let players: Player[] = $state([]);
|
||||
let currentPlayer: string = $state("");
|
||||
// eslint-disable-next-line prefer-const
|
||||
let game: Game | undefined = undefined;
|
||||
let game: Game | undefined = $state();
|
||||
let gameIndex: number = -1;
|
||||
let wall: Wall | undefined = undefined;
|
||||
let wall: Wall | undefined = $state();
|
||||
let wallIndex: number = -1;
|
||||
|
||||
export default {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
46
src/lib/Textfield.svelte
Normal file
46
src/lib/Textfield.svelte
Normal file
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLInputTypeAttribute } from "svelte/elements";
|
||||
|
||||
interface Props {
|
||||
value: any;
|
||||
label?: string;
|
||||
type?: HTMLInputTypeAttribute;
|
||||
readonly?: boolean;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
value = $bindable(),
|
||||
label,
|
||||
type = "text",
|
||||
readonly = false,
|
||||
class: className
|
||||
}: Props = $props();
|
||||
|
||||
const id = crypto.randomUUID();
|
||||
</script>
|
||||
|
||||
<div class="w-full grow">
|
||||
{#if label}
|
||||
<label for="textfield-{id}" class="">{label}</label>
|
||||
{/if}
|
||||
<div>
|
||||
<input
|
||||
{type}
|
||||
{readonly}
|
||||
name="textfield"
|
||||
id="textfield-{id}"
|
||||
class="borders mt-2 mb-2 w-full {className}"
|
||||
bind:value
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.borders {
|
||||
border: 1px solid black;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
padding: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,14 @@
|
||||
export type VisitedQuestions = number[][];
|
||||
import type {
|
||||
AudioMultipleChoiceQuestion,
|
||||
AudioQuestion,
|
||||
FullWall,
|
||||
ImageMultipleChoiceQuestion,
|
||||
ImageQuestion,
|
||||
MultipleChoiceQuestion,
|
||||
SimpleQuestion
|
||||
} from "./games/games";
|
||||
|
||||
export type VisitedQuestions = QuestionId[];
|
||||
|
||||
export type Directory = {
|
||||
name: string;
|
||||
@@ -19,17 +29,56 @@ 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;
|
||||
}
|
||||
|
||||
export type GameId = string;
|
||||
export type _id = string;
|
||||
|
||||
export type GameId = _id;
|
||||
|
||||
export type Game = {
|
||||
name: string;
|
||||
owner: string;
|
||||
owner: _id;
|
||||
_id: GameId;
|
||||
walls: WallId[];
|
||||
};
|
||||
|
||||
export type WallId = string;
|
||||
export type WallId = _id;
|
||||
|
||||
export type Wall = {
|
||||
_id: WallId;
|
||||
name: string;
|
||||
owner: _id;
|
||||
categories: CategoryId[];
|
||||
};
|
||||
|
||||
export function isWall(wall: Wall | FullWall): wall is Wall {
|
||||
return (wall as Wall)._id !== undefined;
|
||||
}
|
||||
|
||||
export function isFullWall(wall: Wall | FullWall): wall is FullWall {
|
||||
return !Object.hasOwn(wall, "_id");
|
||||
}
|
||||
|
||||
export type CategoryId = _id;
|
||||
|
||||
export type Category = {
|
||||
_id: CategoryId;
|
||||
name: string;
|
||||
owner: _id;
|
||||
questions: { _id: QuestionId; points: number }[];
|
||||
};
|
||||
|
||||
export type QuestionId = _id;
|
||||
|
||||
export type GeneralQuestion =
|
||||
| SimpleQuestion
|
||||
| MultipleChoiceQuestion
|
||||
| ImageQuestion
|
||||
| ImageMultipleChoiceQuestion
|
||||
| AudioQuestion
|
||||
| AudioMultipleChoiceQuestion;
|
||||
|
||||
@@ -1,47 +1,145 @@
|
||||
<script lang="ts">
|
||||
import type { Wall } from "$lib/games/games";
|
||||
import type { VisitedQuestions } from "./Types";
|
||||
import axios from "axios";
|
||||
import {
|
||||
isWall,
|
||||
type Category,
|
||||
type CategoryId,
|
||||
type QuestionId,
|
||||
type VisitedQuestions,
|
||||
type Wall
|
||||
} from "./Types";
|
||||
import { url } from "./util";
|
||||
import { onMount } from "svelte";
|
||||
import type { FullWall } from "./games/games";
|
||||
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 | undefined;
|
||||
wall: Wall | FullWall | undefined;
|
||||
onclick?: (catIndex: number, questionIndex: number) => unknown;
|
||||
onclickIds?: (catId: CategoryId, questionId: QuestionId) => unknown;
|
||||
visited: VisitedQuestions;
|
||||
[key: string]: unknown;
|
||||
isEditor?: boolean;
|
||||
}
|
||||
|
||||
function isVisited(catIndex: number, queIndex: number): boolean {
|
||||
return visited[catIndex] && visited[catIndex].includes(queIndex);
|
||||
let { wall, onclick, onclickIds, visited, isEditor = false }: Props = $props();
|
||||
|
||||
let categories: Category[] = $state([]);
|
||||
|
||||
let showRenameCategory = $state(false);
|
||||
let catToRename: Category | undefined = $state();
|
||||
let newCatName = $state("");
|
||||
let error = $state("");
|
||||
|
||||
async function fetchCategories(wall: Wall | FullWall | undefined) {
|
||||
if (wall && isWall(wall)) {
|
||||
let cats: Promise<Category>[] = [];
|
||||
for (const catId of wall.categories) {
|
||||
cats.push(fetchCategory(catId));
|
||||
}
|
||||
return Promise.all(cats).then((cats) => {
|
||||
categories = cats;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let { wall, onclick, visited }: Props = $props();
|
||||
async function renameCategory() {
|
||||
if (!newCatName || !catToRename) return false;
|
||||
return axios
|
||||
.post(
|
||||
url(`/category/rename`),
|
||||
{
|
||||
categoryid: catToRename._id,
|
||||
name: newCatName
|
||||
},
|
||||
{ withCredentials: true }
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
newCatName = "";
|
||||
catToRename = undefined;
|
||||
fetchCategories(wall);
|
||||
return true;
|
||||
} else {
|
||||
console.error(`Failed to rename category: ${response.status}`);
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function renameCategoryCancel() {
|
||||
newCatName = "";
|
||||
catToRename = undefined;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
fetchCategories(wall);
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
fetchCategories(wall);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if wall != undefined}
|
||||
<div class="grid h-full grow grid-flow-col grid-cols-5 grid-rows-6 gap-4 pb-4">
|
||||
{#each wall.categories as category, catIndex}
|
||||
<div class="flex items-center justify-center text-3xl font-semibold">
|
||||
{#if isWall(wall)}
|
||||
{#each categories as category, catIndex}
|
||||
<div class="flex items-center justify-center gap-2 text-3xl font-semibold">
|
||||
<div>{category.name}</div>
|
||||
{#if isEditor}
|
||||
<Button
|
||||
onclick={() => {
|
||||
catToRename = category;
|
||||
newCatName = category.name;
|
||||
showRenameCategory = true;
|
||||
}}><i class="fa-solid fa-pen"></i></Button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{#each category.questions as question, queIndex}
|
||||
{#each category.questions as question}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
class="card {isVisited(catIndex, queIndex) ? 'visited' : ''}"
|
||||
class="card {visited.includes(`${question._id}`) ? 'visited' : ''}"
|
||||
role="button"
|
||||
aria-pressed="false"
|
||||
tabindex="0"
|
||||
onclick={() => {
|
||||
if (onclick) onclick(catIndex, queIndex);
|
||||
if (onclickIds) onclickIds(category._id, question._id);
|
||||
}}
|
||||
>
|
||||
<div class="text-6xl font-thin">{question.points}</div>
|
||||
<div class="text-6xl font-thin">
|
||||
{question.points >= 0 ? question.points : "???"}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
{:else}
|
||||
Legacy Not Supported
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<p>Wall is undefined</p>
|
||||
{/if}
|
||||
|
||||
<Modal bind:showModal={showRenameCategory} okFn={renameCategory} cancelFn={renameCategoryCancel}>
|
||||
{#snippet header()}
|
||||
<h2 class="text-3xl">Kategorie umbenennen</h2>
|
||||
{/snippet}
|
||||
<div>
|
||||
<Textfield bind:value={newCatName} label="Name"></Textfield>
|
||||
{#if error.length > 0}
|
||||
<div class="text-red-700">{error}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.visited {
|
||||
background-color: gray;
|
||||
|
||||
@@ -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[];
|
||||
};
|
||||
};
|
||||
@@ -1425,16 +1431,16 @@ export type Category = {
|
||||
)[];
|
||||
};
|
||||
|
||||
export type Wall = {
|
||||
export type FullWall = {
|
||||
name: string;
|
||||
categories: Category[];
|
||||
};
|
||||
|
||||
export type Game = {
|
||||
export type FullGame = {
|
||||
name: string;
|
||||
walls: Wall[];
|
||||
walls: FullWall[];
|
||||
};
|
||||
|
||||
export type Games = Game[];
|
||||
export type Games = FullGame[];
|
||||
|
||||
export default games;
|
||||
|
||||
@@ -2,29 +2,17 @@
|
||||
let { children } = $props();
|
||||
|
||||
import { error } from "@sveltejs/kit";
|
||||
import games from "$lib/games/games";
|
||||
// import games from "$lib/games/games";
|
||||
import { page } from "$app/state";
|
||||
import DisplayStateSvelte from "$lib/DisplayState.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { fetchGame } from "../../../../editor/fetchers";
|
||||
|
||||
console.log("game:", page.params.game);
|
||||
|
||||
let paramGame = page.params.game;
|
||||
if (paramGame === undefined) {
|
||||
error(404);
|
||||
}
|
||||
const gameIndex = parseInt(paramGame);
|
||||
if (isNaN(gameIndex)) {
|
||||
error(404);
|
||||
}
|
||||
if (DisplayStateSvelte.gameIndex !== gameIndex) {
|
||||
const game = games[gameIndex];
|
||||
if (game) {
|
||||
onMount(() => {
|
||||
fetchGame(`${page.params.game}`).then((game) => {
|
||||
DisplayStateSvelte.game = game;
|
||||
DisplayStateSvelte.gameIndex = gameIndex;
|
||||
} else {
|
||||
error(404);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex grow flex-col pr-4 pl-4">
|
||||
|
||||
@@ -6,27 +6,11 @@
|
||||
import ws from "$lib/websocket.svelte";
|
||||
import { MessageType } from "$lib/MessageType";
|
||||
import type { VisitedQuestions } from "$lib/Types";
|
||||
import { onMount } from "svelte";
|
||||
import { fetchWall } from "../../../../../editor/fetchers";
|
||||
|
||||
console.log("wall:", page.params.wall);
|
||||
|
||||
let paramWall = page.params.wall;
|
||||
if (paramWall === undefined) {
|
||||
error(404);
|
||||
}
|
||||
const wallIndex = parseInt(paramWall);
|
||||
if (isNaN(wallIndex)) {
|
||||
error(404);
|
||||
}
|
||||
if (DisplayStateSvelte.wallIndex !== wallIndex) {
|
||||
const wall = DisplayStateSvelte.game?.walls[wallIndex];
|
||||
if (wall) {
|
||||
DisplayStateSvelte.wall = wall;
|
||||
DisplayStateSvelte.gameIndex = wallIndex;
|
||||
} else {
|
||||
error(404);
|
||||
}
|
||||
}
|
||||
|
||||
let visited: VisitedQuestions = $state([]);
|
||||
|
||||
$effect(() => {
|
||||
@@ -40,6 +24,12 @@
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
fetchWall(`${page.params.wall}`).then((wall) => {
|
||||
DisplayStateSvelte.wall = wall;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<Wall wall={DisplayStateSvelte.wall} {visited} />
|
||||
|
||||
@@ -9,65 +9,22 @@
|
||||
isImageMultipleChoiceQuestion,
|
||||
isImageQuestion,
|
||||
isMultipleChoiceQuestion,
|
||||
isSimpleQuestion
|
||||
isSimpleQuestion,
|
||||
type Question
|
||||
} from "$lib/games/games";
|
||||
import ws from "$lib/websocket.svelte";
|
||||
import { MessageType } from "$lib/MessageType";
|
||||
import { untrack } from "svelte";
|
||||
import { onMount, untrack } from "svelte";
|
||||
import MultipleChoiceQuestionComponent from "$lib/MultipleChoiceQuestionComponent.svelte";
|
||||
import ImageQuestionComponent from "$lib/ImageQuestionComponent.svelte";
|
||||
import AudioQuestionComponent from "$lib/AudioQuestionComponent.svelte";
|
||||
import AudioMultipleChoiceQuestionComponent from "$lib/AudioMultipleChoiceQuestionComponent.svelte";
|
||||
import ImageMultipleChoiceQuestionComponent from "$lib/ImageMultipleChoiceQuestionComponent.svelte";
|
||||
import { fetchCategory, fetchQuestion } from "../../../../../../../editor/fetchers";
|
||||
import type { Category, GeneralQuestion } from "$lib/Types";
|
||||
|
||||
console.log("wall:", page.params.wall);
|
||||
|
||||
let paramWall = page.params.wall;
|
||||
if (paramWall === undefined) {
|
||||
error(404);
|
||||
}
|
||||
const wallIndex = parseInt(paramWall);
|
||||
if (isNaN(wallIndex)) {
|
||||
error(404);
|
||||
}
|
||||
if (DisplayStateSvelte.wallIndex !== wallIndex) {
|
||||
const wall = DisplayStateSvelte.game?.walls[wallIndex];
|
||||
if (wall) {
|
||||
DisplayStateSvelte.wall = wall;
|
||||
DisplayStateSvelte.gameIndex = wallIndex;
|
||||
} else {
|
||||
error(404);
|
||||
}
|
||||
}
|
||||
if (page.params.category === undefined) {
|
||||
error(404);
|
||||
}
|
||||
const categoryIndex = parseInt(page.params.category);
|
||||
if (isNaN(categoryIndex)) {
|
||||
error(404);
|
||||
}
|
||||
|
||||
const category = DisplayStateSvelte.wall?.categories[categoryIndex];
|
||||
|
||||
if (category === undefined) {
|
||||
error(404);
|
||||
}
|
||||
|
||||
if (page.params.question === undefined) {
|
||||
error(404);
|
||||
}
|
||||
const questionIndex = parseInt(page.params.question);
|
||||
if (isNaN(questionIndex)) {
|
||||
error(404);
|
||||
}
|
||||
|
||||
const question = category.questions[questionIndex];
|
||||
|
||||
console.log(question);
|
||||
|
||||
if (question === undefined) {
|
||||
error(404);
|
||||
}
|
||||
let category: Category | undefined = $state();
|
||||
let question: GeneralQuestion | undefined = $state();
|
||||
|
||||
let showAnswer = $state(false);
|
||||
let showQuestion = $state(false);
|
||||
@@ -118,9 +75,21 @@
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
fetchCategory(`${page.params.category}`)
|
||||
.then((cat) => {
|
||||
category = cat;
|
||||
return fetchQuestion(`${page.params.question}`);
|
||||
})
|
||||
.then((que) => {
|
||||
question = que;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="mt-4 flex grow flex-col">
|
||||
{#if category && question}
|
||||
<div class="mt-4 flex grow flex-col">
|
||||
<div class="mb-4 flex justify-between text-4xl">
|
||||
<div>{category.name}</div>
|
||||
<div>
|
||||
@@ -135,25 +104,42 @@
|
||||
{:else if isMultipleChoiceQuestion(question)}
|
||||
<MultipleChoiceQuestionComponent {question} {showAnswer} {showQuestion} />
|
||||
{:else if isImageQuestion(question)}
|
||||
<ImageQuestionComponent {question} {showAnswer} {showQuestion} {isBuzzed} />
|
||||
<ImageQuestionComponent
|
||||
{question}
|
||||
{showAnswer}
|
||||
{showQuestion}
|
||||
{isBuzzed}
|
||||
isLegacy={false}
|
||||
/>
|
||||
{:else if isImageMultipleChoiceQuestion(question)}
|
||||
<ImageMultipleChoiceQuestionComponent
|
||||
{question}
|
||||
{showAnswer}
|
||||
{showQuestion}
|
||||
{isBuzzed}
|
||||
isLegacy={false}
|
||||
/>
|
||||
{:else if isAudioQuestion(question)}
|
||||
<AudioQuestionComponent {question} {showAnswer} {showQuestion} showPlayer={false} />
|
||||
<AudioQuestionComponent
|
||||
{question}
|
||||
{showAnswer}
|
||||
{showQuestion}
|
||||
showPlayer={false}
|
||||
isLegacy={false}
|
||||
/>
|
||||
{:else if isAudioMultipleChoiceQuestion(question)}
|
||||
<AudioMultipleChoiceQuestionComponent
|
||||
{question}
|
||||
{showAnswer}
|
||||
{showQuestion}
|
||||
showPlayer={false}
|
||||
isLegacy={false}
|
||||
/>
|
||||
{:else}
|
||||
<p>Type of question unknown</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
Loading...
|
||||
{/if}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import games from '$lib/games/games';
|
||||
|
||||
export function load() {
|
||||
return {
|
||||
games
|
||||
};
|
||||
}
|
||||
@@ -1,14 +1,39 @@
|
||||
<script lang="ts">
|
||||
let { data } = $props();
|
||||
import type { Game } from "$lib/Types";
|
||||
import { url } from "$lib/util";
|
||||
import axios from "axios";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
let games: Game[] = $state([]);
|
||||
|
||||
function fetchGames() {
|
||||
axios
|
||||
.get(url("/games"), { withCredentials: true })
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
games = response.data;
|
||||
games.sort((a, b) => a.name.localeCompare(b.name));
|
||||
} else {
|
||||
console.error("Could not fetch games: " + response.status);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchGames();
|
||||
});
|
||||
</script>
|
||||
|
||||
<h1 class="m-4 mb-8 text-7xl font-bold">Games</h1>
|
||||
|
||||
<div class="flex flex-col space-y-4">
|
||||
{#each data.games as game, i}
|
||||
{#each games as game}
|
||||
<a
|
||||
class="ms-4 me-4 rounded-xl border-2 p-4 hover:cursor-pointer hover:bg-emerald-200"
|
||||
href="/connected/games/{i}">{game.name}</a
|
||||
href="/connected/games/{game._id}">{game.name}</a
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import games from '$lib/games/games';
|
||||
|
||||
export function load({ params }) {
|
||||
const index = parseInt(params.game);
|
||||
if (isNaN(index)) {
|
||||
error(404);
|
||||
}
|
||||
const game = games[index];
|
||||
if (game === undefined) error(404);
|
||||
else return game;
|
||||
}
|
||||
@@ -6,10 +6,11 @@
|
||||
isMultipleChoiceQuestion,
|
||||
isSimpleQuestion,
|
||||
isImageQuestion,
|
||||
type Game,
|
||||
isAudioQuestion,
|
||||
isAudioMultipleChoiceQuestion,
|
||||
isImageMultipleChoiceQuestion
|
||||
isImageMultipleChoiceQuestion,
|
||||
type FullGame,
|
||||
type Question
|
||||
} from "$lib/games/games";
|
||||
import ws from "$lib/websocket.svelte";
|
||||
import { page } from "$app/state";
|
||||
@@ -18,12 +19,14 @@
|
||||
import Scoreboard from "$lib/Scoreboard.svelte";
|
||||
import SimpleQuestionComponent from "$lib/SimpleQuestionComponent.svelte";
|
||||
import PlusMinusButton from "$lib/PlusMinusButton.svelte";
|
||||
import type { VisitedQuestions } from "$lib/Types.js";
|
||||
import type { _id, Category, Game, VisitedQuestions, Wall as WallType } from "$lib/Types.js";
|
||||
import MultipleChoiceQuestionComponent from "$lib/MultipleChoiceQuestionComponent.svelte";
|
||||
import ImageQuestionComponent from "$lib/ImageQuestionComponent.svelte";
|
||||
import AudioQuestionComponent from "$lib/AudioQuestionComponent.svelte";
|
||||
import AudioMultipleChoiceQuestionComponent from "$lib/AudioMultipleChoiceQuestionComponent.svelte";
|
||||
import ImageMultipleChoiceQuestionComponent from "$lib/ImageMultipleChoiceQuestionComponent.svelte";
|
||||
import { fetchCategory, fetchGame, fetchQuestion, fetchWall } from "../../../editor/fetchers";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
interface SaveData {
|
||||
players: Player[];
|
||||
@@ -57,7 +60,7 @@
|
||||
|
||||
class GameManager {
|
||||
public state: GameState = $state(GameState.INIT);
|
||||
public game: Game;
|
||||
public game: Game | undefined = $state();
|
||||
public players: Player[] = $state([
|
||||
{
|
||||
name: "Player 1",
|
||||
@@ -74,37 +77,42 @@
|
||||
]);
|
||||
|
||||
public currentPlayer = $state(0);
|
||||
public currentWall = $state(0);
|
||||
public currentWallIndex = $state(0);
|
||||
public currentWall: WallType | undefined = $state();
|
||||
public visitedQuestions: VisitedQuestions = $state([]);
|
||||
public currentCategory = $state(0);
|
||||
public currentQuestion = $state(0);
|
||||
public currentCategoryId = $state("");
|
||||
public currentCategory: Category | undefined = $state();
|
||||
public currentQuestionId = $state("");
|
||||
public currentQuestion: Question | undefined = $state();
|
||||
|
||||
public answerIsShowing = $state(false);
|
||||
public questionIsShowing = $state(false);
|
||||
public isBuzzed = $state(false);
|
||||
|
||||
constructor(game: Game) {
|
||||
setGame(game: Game) {
|
||||
this.game = game;
|
||||
}
|
||||
|
||||
save(): void {
|
||||
if (!this.game) return;
|
||||
const saveData = {
|
||||
players: this.players,
|
||||
currentPlayer: this.currentPlayer,
|
||||
currentWall: this.currentWall,
|
||||
currentWall: this.currentWallIndex,
|
||||
visitedQuestions: this.visitedQuestions
|
||||
};
|
||||
localStorage.setItem(`saveGame-${this.game.name}`, JSON.stringify(saveData));
|
||||
localStorage.setItem(`saveGame-${this.game._id}`, JSON.stringify(saveData));
|
||||
}
|
||||
|
||||
load(): void {
|
||||
const saveDataString = localStorage.getItem(`saveGame-${this.game.name}`);
|
||||
if (!this.game) return;
|
||||
const saveDataString = localStorage.getItem(`saveGame-${this.game._id}`);
|
||||
if (saveDataString === null) return;
|
||||
try {
|
||||
const saveData: SaveData = JSON.parse(saveDataString);
|
||||
this.players = saveData.players;
|
||||
this.currentPlayer = saveData.currentPlayer;
|
||||
this.currentWall = saveData.currentWall;
|
||||
this.currentWallIndex = saveData.currentWall;
|
||||
this.visitedQuestions = saveData.visitedQuestions;
|
||||
console.log(saveData);
|
||||
} catch (e) {
|
||||
@@ -113,10 +121,14 @@
|
||||
}
|
||||
|
||||
startGame(): void {
|
||||
if (!this.game) return;
|
||||
this.currentPlayer = Math.floor(Math.random() * this.players.length);
|
||||
this.state = GameState.CHOOSING_QUESTION;
|
||||
fetchWall(this.game.walls[this.currentWallIndex]).then((wall) => {
|
||||
this.currentWall = wall;
|
||||
this.sendStart();
|
||||
this.sendCurrentState();
|
||||
});
|
||||
}
|
||||
|
||||
sendStart(): void {
|
||||
@@ -136,7 +148,7 @@
|
||||
sendWall(): void {
|
||||
ws.sendMessage({
|
||||
type: MessageType.GOTO,
|
||||
route: `/${page.params.game}/${this.currentWall}`
|
||||
route: `/${page.params.game}/${this.currentWall?._id}`
|
||||
});
|
||||
this.sendVisited();
|
||||
}
|
||||
@@ -154,30 +166,39 @@
|
||||
}
|
||||
|
||||
sendCurrentQuestion(): void {
|
||||
if (!this.game) return;
|
||||
ws.sendMessage({
|
||||
type: MessageType.GOTO,
|
||||
route: `/${page.params.game}/${this.currentWall}/${this.currentCategory}/${this.currentQuestion}`
|
||||
route: `/${this.game._id}/${this.currentWall?._id}/${this.currentCategoryId}/${this.currentQuestionId}`
|
||||
});
|
||||
}
|
||||
|
||||
sendEnd(): void {
|
||||
ws.sendMessage({
|
||||
type: MessageType.GOTO,
|
||||
route: `/${page.params.game}/${this.currentWall}/end`
|
||||
route: `/${page.params.game}/${this.currentWall?._id}/end`
|
||||
});
|
||||
}
|
||||
|
||||
tileClicked(categoryIndex: number, questionIndex: number) {
|
||||
console.log("Cat", categoryIndex, "Que", questionIndex);
|
||||
console.log(gameManager.wall.categories[categoryIndex]?.questions[questionIndex]);
|
||||
tileClicked(catId: _id, queId: _id) {
|
||||
console.log("Cat", catId, "Que", queId);
|
||||
|
||||
this.currentCategory = categoryIndex;
|
||||
this.currentQuestion = questionIndex;
|
||||
this.currentCategoryId = catId;
|
||||
this.currentQuestionId = queId;
|
||||
this.answerIsShowing = false;
|
||||
|
||||
this.sendCurrentQuestion();
|
||||
fetchCategory(this.currentCategoryId)
|
||||
.then((category) => {
|
||||
this.currentCategory = category;
|
||||
|
||||
return fetchQuestion(this.currentQuestionId);
|
||||
})
|
||||
.then((question) => {
|
||||
this.currentQuestion = question;
|
||||
|
||||
this.sendCurrentQuestion();
|
||||
this.state = GameState.SHOW_QUESTION;
|
||||
});
|
||||
}
|
||||
|
||||
addPlayer() {
|
||||
@@ -236,20 +257,21 @@
|
||||
}
|
||||
|
||||
plus(player: Player) {
|
||||
if (!this.answerIsShowing) return;
|
||||
if (!this.answerIsShowing || !this.currentQuestion) return;
|
||||
if (player.name === this.currentPlayerToName()) {
|
||||
player.points += this.question.points * 2;
|
||||
player.points += this.currentQuestion.points * 2;
|
||||
} else {
|
||||
player.points += this.question.points;
|
||||
player.points += this.currentQuestion.points;
|
||||
}
|
||||
this.sendPlayers();
|
||||
}
|
||||
|
||||
minus(player: Player) {
|
||||
if (!this.currentQuestion) return;
|
||||
if (player.name === this.currentPlayerToName()) {
|
||||
player.points -= this.question.points * 2;
|
||||
player.points -= this.currentQuestion.points * 2;
|
||||
} else {
|
||||
player.points -= this.question.points;
|
||||
player.points -= this.currentQuestion.points;
|
||||
}
|
||||
this.sendPlayers();
|
||||
}
|
||||
@@ -267,12 +289,8 @@
|
||||
}
|
||||
|
||||
finishQuestion(): void {
|
||||
if (this.visitedQuestions[this.currentCategory] == undefined) {
|
||||
this.visitedQuestions[this.currentCategory] = [this.currentQuestion];
|
||||
} else if (
|
||||
!this.visitedQuestions[this.currentCategory].includes(this.currentQuestion)
|
||||
) {
|
||||
this.visitedQuestions[this.currentCategory].push(this.currentQuestion);
|
||||
if (!this.visitedQuestions.includes(this.currentQuestionId)) {
|
||||
this.visitedQuestions.push(this.currentQuestionId);
|
||||
}
|
||||
this.setupGoingBack();
|
||||
this.nextPlayer();
|
||||
@@ -286,28 +304,21 @@
|
||||
}
|
||||
|
||||
wallIsDone(): boolean {
|
||||
let visitedNum = 0;
|
||||
for (const questions of this.visitedQuestions) {
|
||||
if (questions != undefined) {
|
||||
visitedNum += questions.length;
|
||||
}
|
||||
}
|
||||
let totalNum = 0;
|
||||
for (const category of this.wall.categories) {
|
||||
totalNum += category.questions.length;
|
||||
}
|
||||
console.log(`${visitedNum} >= ${totalNum}`);
|
||||
return visitedNum >= totalNum;
|
||||
return this.visitedQuestions.length >= 25;
|
||||
}
|
||||
|
||||
goToNextWall(): void {
|
||||
if (this.currentWall + 1 >= this.game.walls.length) {
|
||||
if (!this.game) return;
|
||||
if (this.currentWallIndex + 1 >= this.game.walls.length) {
|
||||
this.goToEndScreen();
|
||||
} else {
|
||||
this.currentWall += 1;
|
||||
this.currentWallIndex += 1;
|
||||
this.visitedQuestions = [];
|
||||
fetchWall(this.game.walls[this.currentWallIndex]).then((wall) => {
|
||||
this.currentWall = wall;
|
||||
this.sendWall();
|
||||
this.state = GameState.CHOOSING_QUESTION;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,32 +335,27 @@
|
||||
currentPlayerToName(): string {
|
||||
return this.players[this.currentPlayer].name;
|
||||
}
|
||||
|
||||
get wall() {
|
||||
return this.game.walls[this.currentWall];
|
||||
}
|
||||
|
||||
get category() {
|
||||
return this.wall.categories[this.currentCategory];
|
||||
}
|
||||
let gameManager = new GameManager();
|
||||
|
||||
get question() {
|
||||
return this.category.questions[this.currentQuestion];
|
||||
}
|
||||
}
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
let gameManager = new GameManager(data);
|
||||
onMount(() => {
|
||||
fetchGame(`${page.params.game}`).then((game) => {
|
||||
gameManager.game = game;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col">
|
||||
{#if gameManager.game}
|
||||
<div class="flex h-full flex-col">
|
||||
<h1 class="ms-4 text-7xl font-bold">{gameManager.game.name}</h1>
|
||||
{#if gameManager.state === GameState.INIT}
|
||||
<div class="p-4">
|
||||
<div class="flex items-center">
|
||||
<h2 class="grow pb-4 text-5xl">Spieler</h2>
|
||||
<button class="btn me-4" onclick={() => gameManager.load()}>Load SaveGame</button>
|
||||
<button class="btn me-4" onclick={() => gameManager.load()}
|
||||
>Load SaveGame</button
|
||||
>
|
||||
<button
|
||||
class="btn"
|
||||
disabled={!startDisabled}
|
||||
@@ -366,7 +372,9 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<button class="btn" onclick={() => gameManager.addPlayer()}>Spieler hinzufügen</button>
|
||||
<button class="btn" onclick={() => gameManager.addPlayer()}
|
||||
>Spieler hinzufügen</button
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex grow">
|
||||
@@ -379,53 +387,57 @@
|
||||
{#if gameManager.state === GameState.SHOW_QUESTION}
|
||||
<div class="flex grow flex-col">
|
||||
<div class="m-4 flex justify-between text-4xl">
|
||||
<div>{gameManager.category.name}</div>
|
||||
<div>{gameManager.currentCategory?.name}</div>
|
||||
<div>
|
||||
{gameManager.question.points} Punkte
|
||||
{gameManager.currentQuestion?.points} Punkte
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex grow ps-4 pe-4">
|
||||
{#if gameManager.question === undefined}
|
||||
{#if gameManager.currentQuestion === undefined}
|
||||
<p>Question is undefined</p>
|
||||
{:else if isSimpleQuestion(gameManager.question)}
|
||||
{:else if isSimpleQuestion(gameManager.currentQuestion)}
|
||||
<SimpleQuestionComponent
|
||||
question={gameManager.question}
|
||||
question={gameManager.currentQuestion}
|
||||
showAnswer={true}
|
||||
showQuestion={true}
|
||||
/>
|
||||
{:else if isMultipleChoiceQuestion(gameManager.question)}
|
||||
{:else if isMultipleChoiceQuestion(gameManager.currentQuestion)}
|
||||
<MultipleChoiceQuestionComponent
|
||||
question={gameManager.question}
|
||||
question={gameManager.currentQuestion}
|
||||
showAnswer={true}
|
||||
showQuestion={true}
|
||||
/>
|
||||
{:else if isImageQuestion(gameManager.question)}
|
||||
{:else if isImageQuestion(gameManager.currentQuestion)}
|
||||
<ImageQuestionComponent
|
||||
question={gameManager.question}
|
||||
question={gameManager.currentQuestion}
|
||||
showAnswer={true}
|
||||
showQuestion={true}
|
||||
isBuzzed={false}
|
||||
isLegacy={false}
|
||||
/>
|
||||
{:else if isImageMultipleChoiceQuestion(gameManager.question)}
|
||||
{:else if isImageMultipleChoiceQuestion(gameManager.currentQuestion)}
|
||||
<ImageMultipleChoiceQuestionComponent
|
||||
question={gameManager.question}
|
||||
question={gameManager.currentQuestion}
|
||||
showAnswer={true}
|
||||
showQuestion={true}
|
||||
isBuzzed={false}
|
||||
isLegacy={false}
|
||||
/>
|
||||
{:else if isAudioQuestion(gameManager.question)}
|
||||
{:else if isAudioQuestion(gameManager.currentQuestion)}
|
||||
<AudioQuestionComponent
|
||||
question={gameManager.question}
|
||||
question={gameManager.currentQuestion}
|
||||
showAnswer={true}
|
||||
showPlayer={true}
|
||||
showQuestion={true}
|
||||
isLegacy={false}
|
||||
/>
|
||||
{:else if isAudioMultipleChoiceQuestion(gameManager.question)}
|
||||
{:else if isAudioMultipleChoiceQuestion(gameManager.currentQuestion)}
|
||||
<AudioMultipleChoiceQuestionComponent
|
||||
question={gameManager.question}
|
||||
question={gameManager.currentQuestion}
|
||||
showAnswer={true}
|
||||
showPlayer={true}
|
||||
showQuestion={true}
|
||||
isLegacy={false}
|
||||
/>
|
||||
{:else}
|
||||
<p>Type of question unknown</p>
|
||||
@@ -477,16 +489,21 @@
|
||||
</div>
|
||||
</div>
|
||||
{:else if gameManager.state === GameState.END}
|
||||
<div class="flex grow items-center justify-center text-7xl"><div>ENDE</div></div>
|
||||
<div class="flex grow items-center justify-center text-7xl">
|
||||
<div>ENDE</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grow ps-4 pe-4">
|
||||
<Wall
|
||||
wall={gameManager.game.walls[gameManager.currentWall]}
|
||||
onclick={(cat, que) => gameManager.tileClicked(cat, que)}
|
||||
wall={gameManager.currentWall}
|
||||
onclickIds={(cat, que) => gameManager.tileClicked(cat, que)}
|
||||
visited={gameManager.visitedQuestions}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
Loading Game...
|
||||
{/if}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import Button from "$lib/Button.svelte";
|
||||
import Modal from "$lib/Modal.svelte";
|
||||
import Textfield from "$lib/Textfield.svelte";
|
||||
import type { Game } from "$lib/Types";
|
||||
import { url } from "$lib/util";
|
||||
import axios from "axios";
|
||||
@@ -11,7 +13,9 @@
|
||||
let error = $state("");
|
||||
|
||||
let showNewGame = $state(false);
|
||||
let showRenameGame = $state(false);
|
||||
let newGameName = $state("");
|
||||
let gameToRename: Game | undefined = $state();
|
||||
|
||||
let showDeleteGame = $state(false);
|
||||
let gameToDelete: Game | undefined = $state();
|
||||
@@ -19,7 +23,7 @@
|
||||
async function addNewGame() {
|
||||
if (!newGameName) return false;
|
||||
return axios
|
||||
.post(url("game"), { name: newGameName }, { withCredentials: true })
|
||||
.post(url("/game"), { name: newGameName }, { withCredentials: true })
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
fetchGames();
|
||||
@@ -33,6 +37,39 @@
|
||||
newGameName = "";
|
||||
}
|
||||
|
||||
async function renameGame() {
|
||||
if (!newGameName || !gameToRename) return false;
|
||||
return axios
|
||||
.post(
|
||||
url(`/game/rename`),
|
||||
{
|
||||
gameid: gameToRename._id,
|
||||
name: newGameName
|
||||
},
|
||||
{ withCredentials: true }
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
newGameName = "";
|
||||
gameToRename = undefined;
|
||||
fetchGames();
|
||||
return true;
|
||||
} else {
|
||||
console.error(`Failed to rename game: ${response.status}`);
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function renameGameCancel() {
|
||||
newGameName = "";
|
||||
gameToRename = undefined;
|
||||
}
|
||||
|
||||
async function deleteGame() {
|
||||
if (gameToDelete === undefined) return false;
|
||||
return axios
|
||||
@@ -59,7 +96,7 @@
|
||||
|
||||
function fetchGames() {
|
||||
axios
|
||||
.get(url("games"), { withCredentials: true })
|
||||
.get(url("/games"), { withCredentials: true })
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
games = response.data;
|
||||
@@ -79,11 +116,13 @@
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="mr-4 flex items-center gap-4">
|
||||
<h1 class="m-4 mb-8 text-7xl font-bold">Editor</h1>
|
||||
<button class="btn" type="button" onclick={() => (showNewGame = true)}
|
||||
><i class="fa-solid fa-plus"></i> Neues Spiel</button
|
||||
>
|
||||
<div class="grow"></div>
|
||||
<button class="btn" type="button" onclick={() => goto("/")}>Zurück</button>
|
||||
</div>
|
||||
{#if games.length > 0}
|
||||
<div class="flex flex-col space-y-4 overflow-y-auto">
|
||||
@@ -91,7 +130,7 @@
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="ms-4 me-4 flex items-center justify-between rounded-xl border-2 p-2 hover:cursor-pointer hover:bg-emerald-200"
|
||||
class="ms-4 me-4 flex items-center gap-2 rounded-xl border-2 p-2 hover:cursor-pointer hover:bg-emerald-200"
|
||||
onclick={() => {
|
||||
goto(`/editor/${game._id}`);
|
||||
}}
|
||||
@@ -99,15 +138,22 @@
|
||||
<div>
|
||||
{game.name}
|
||||
</div>
|
||||
<!-- svelte-ignore a11y_consider_explicit_label -->
|
||||
<button
|
||||
type="button"
|
||||
class="btn border-red-600 text-red-600"
|
||||
<div class="grow"></div>
|
||||
<Button
|
||||
onclick={(event) => {
|
||||
event.stopPropagation();
|
||||
newGameName = game.name;
|
||||
gameToRename = game;
|
||||
showRenameGame = true;
|
||||
}}><i class="fa-solid fa-pen"></i></Button
|
||||
>
|
||||
<Button
|
||||
class="border-red-600 text-red-600"
|
||||
onclick={(event) => {
|
||||
event.stopPropagation();
|
||||
gameToDelete = game;
|
||||
showDeleteGame = true;
|
||||
}}><i class="fa-solid fa-trash"></i></button
|
||||
}}><i class="fa-solid fa-trash"></i></Button
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
@@ -125,16 +171,19 @@
|
||||
<h2 class="text-3xl">Neues Spiel</h2>
|
||||
{/snippet}
|
||||
<div>
|
||||
<label for="directory" class="">Name</label>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
name="directory"
|
||||
id="directory"
|
||||
class="borders mt-2 mb-2 w-full"
|
||||
bind:value={newGameName}
|
||||
/>
|
||||
<Textfield bind:value={newGameName} label="Name"></Textfield>
|
||||
{#if error.length > 0}
|
||||
<div class="text-red-700">{error}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal bind:showModal={showRenameGame} okFn={renameGame} cancelFn={renameGameCancel}>
|
||||
{#snippet header()}
|
||||
<h2 class="text-3xl">Spiel umbenennen</h2>
|
||||
{/snippet}
|
||||
<div>
|
||||
<Textfield bind:value={newGameName} label="Name"></Textfield>
|
||||
{#if error.length > 0}
|
||||
<div class="text-red-700">{error}</div>
|
||||
{/if}
|
||||
@@ -151,12 +200,3 @@
|
||||
<div class="text-red-700">{error}</div>
|
||||
{/if}
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.borders {
|
||||
border: 1px solid black;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
padding: 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
12
src/routes/editor/EditorState.ts
Normal file
12
src/routes/editor/EditorState.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
253
src/routes/editor/[gameid]/+page.svelte
Normal file
253
src/routes/editor/[gameid]/+page.svelte
Normal file
@@ -0,0 +1,253 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/state";
|
||||
import Button from "$lib/Button.svelte";
|
||||
import Modal from "$lib/Modal.svelte";
|
||||
import Textfield from "$lib/Textfield.svelte";
|
||||
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();
|
||||
|
||||
let walls: WallType[] = $state([]);
|
||||
let selectedWall: WallType | undefined = $state();
|
||||
|
||||
let showNewWall = $state(false);
|
||||
let showRenameWall = $state(false);
|
||||
let newWallName = $state("");
|
||||
let walltoRename: WallType | undefined = $state();
|
||||
|
||||
let showDeleteWall = $state(false);
|
||||
let wallToDelete: WallType | undefined = $state();
|
||||
|
||||
let error = $state("");
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
if (selectedWall) {
|
||||
EditorState.selectedWallId = selectedWall._id;
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
async function addNewWall() {
|
||||
if (!newWallName) return false;
|
||||
return axios
|
||||
.post(
|
||||
url(`/wall`),
|
||||
{
|
||||
gameid: page.params.gameid,
|
||||
name: newWallName
|
||||
},
|
||||
{
|
||||
withCredentials: true
|
||||
}
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
newWallName = "";
|
||||
_fetchWalls();
|
||||
return true;
|
||||
} else return false;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function addNewWallCancel() {
|
||||
newWallName = "";
|
||||
}
|
||||
|
||||
async function renameWall() {
|
||||
if (!newWallName || !walltoRename) return false;
|
||||
return axios
|
||||
.post(
|
||||
url(`/wall/rename`),
|
||||
{
|
||||
wallid: walltoRename._id,
|
||||
name: newWallName
|
||||
},
|
||||
{ withCredentials: true }
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
newWallName = "";
|
||||
walltoRename = undefined;
|
||||
_fetchWalls();
|
||||
return true;
|
||||
} else {
|
||||
console.error(`Failed to rename wall: ${response.status}`);
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function renameWallCancel() {
|
||||
newWallName = "";
|
||||
walltoRename = undefined;
|
||||
}
|
||||
|
||||
async function deleteWall() {
|
||||
if (!wallToDelete) return false;
|
||||
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();
|
||||
return true;
|
||||
} else return false;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function deleteWallCancel() {
|
||||
wallToDelete = undefined;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (page.params.gameid)
|
||||
fetchGame(page.params.gameid)
|
||||
.then((fetchedGame) => {
|
||||
game = fetchedGame;
|
||||
return _fetchWalls();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col">
|
||||
<div class="mr-4 flex items-center gap-4">
|
||||
<h1 class="m-4 text-4xl font-bold">{game ? game.name : "Spiel"}</h1>
|
||||
<div class="grow"></div>
|
||||
<button class="btn" type="button" onclick={() => goto("/editor")}>Zurück</button>
|
||||
</div>
|
||||
<div class="flex grow">
|
||||
<!-- Sidebar -->
|
||||
<div class="flex h-full max-w-[600px] min-w-[400px] flex-col gap-4 border-r-1">
|
||||
<div>
|
||||
<Button class="ms-4 me-4" onclick={() => (showNewWall = true)}
|
||||
>Wand hinzufügen</Button
|
||||
>
|
||||
</div>
|
||||
{#each walls as wall}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="ms-4 me-4 flex items-center gap-2 rounded-xl border-2 p-2 hover:cursor-pointer hover:bg-emerald-200"
|
||||
class:bg-emerald-200={selectedWall?._id === wall._id}
|
||||
onclick={() => {
|
||||
selectedWall = wall;
|
||||
EditorState.selectedWallId = selectedWall._id;
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{wall.name}
|
||||
</div>
|
||||
<div class="grow"></div>
|
||||
<Button
|
||||
onclick={(event) => {
|
||||
event.stopPropagation();
|
||||
newWallName = wall.name;
|
||||
walltoRename = wall;
|
||||
showRenameWall = true;
|
||||
}}><i class="fa-solid fa-pen"></i></Button
|
||||
>
|
||||
<Button
|
||||
class="border-red-600 text-red-600"
|
||||
onclick={(event) => {
|
||||
event.stopPropagation();
|
||||
wallToDelete = wall;
|
||||
showDeleteWall = true;
|
||||
}}><i class="fa-solid fa-trash"></i></Button
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<!-- Wall -->
|
||||
{#if selectedWall}
|
||||
<div class="ms-4 me-4 grow">
|
||||
<Wall
|
||||
isEditor
|
||||
wall={selectedWall}
|
||||
visited={[]}
|
||||
onclickIds={(catId, queId) => {
|
||||
console.log(catId, queId);
|
||||
if (selectedWall)
|
||||
goto(
|
||||
`/editor/${page.params.gameid}/${selectedWall._id}/${catId}/${queId}`
|
||||
);
|
||||
}}
|
||||
></Wall>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal bind:showModal={showNewWall} okFn={addNewWall} cancelFn={addNewWallCancel}>
|
||||
{#snippet header()}
|
||||
<h2 class="text-3xl">Neue Wand</h2>
|
||||
{/snippet}
|
||||
<div>
|
||||
<Textfield bind:value={newWallName} label="Name"></Textfield>
|
||||
{#if error.length > 0}
|
||||
<div class="text-red-700">{error}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal bind:showModal={showRenameWall} okFn={renameWall} cancelFn={renameWallCancel}>
|
||||
{#snippet header()}
|
||||
<h2 class="text-3xl">Wand umbenennen</h2>
|
||||
{/snippet}
|
||||
<div>
|
||||
<Textfield bind:value={newWallName} label="Name"></Textfield>
|
||||
{#if error.length > 0}
|
||||
<div class="text-red-700">{error}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal bind:showModal={showDeleteWall} okFn={deleteWall} cancelFn={deleteWallCancel}>
|
||||
{#snippet header()}
|
||||
<h2 class="text-3xl">Wand löschen</h2>
|
||||
{/snippet}
|
||||
|
||||
<div>Soll die Wand {wallToDelete?.name} wirklich gelöscht werden?</div>
|
||||
{#if error.length > 0}
|
||||
<div class="text-red-700">{error}</div>
|
||||
{/if}
|
||||
</Modal>
|
||||
15
src/routes/editor/[gameid]/EditorSimpleQuestion.svelte
Normal file
15
src/routes/editor/[gameid]/EditorSimpleQuestion.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import type { SimpleQuestion } from "$lib/games/games";
|
||||
import Textfield from "$lib/Textfield.svelte";
|
||||
|
||||
interface Props {
|
||||
question: SimpleQuestion;
|
||||
}
|
||||
|
||||
let { question }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<Textfield bind:value={question.data.question}></Textfield>
|
||||
<Textfield bind:value={question.data.answer}></Textfield>
|
||||
</div>
|
||||
@@ -0,0 +1,298 @@
|
||||
<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">
|
||||
<div class="flex items-center gap-2">
|
||||
<Textfield type="number" bind:value={question.points} label="Punkte"
|
||||
></Textfield>
|
||||
<Button
|
||||
onclick={() => {
|
||||
if (question) question.points *= -1;
|
||||
}}><i class="fa-solid fa-plus-minus"></i></Button
|
||||
>
|
||||
</div>
|
||||
</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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
108
src/routes/editor/fetchers.ts
Normal file
108
src/routes/editor/fetchers.ts
Normal 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}`;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user