Added Editor Game and Wall Display

This commit is contained in:
2026-01-02 02:59:40 +01:00
parent dc2766f0ef
commit 7be5921ef6
9 changed files with 372 additions and 38 deletions

View File

@@ -1,4 +1,4 @@
import type { Game, Wall } from "./games/games"; import type { FullGame, FullWall } from "./games/games";
interface Player { interface Player {
name: string; name: string;
@@ -9,9 +9,9 @@ const baseRoute = "/connected/display/running";
let players: Player[] = $state([]); let players: Player[] = $state([]);
let currentPlayer: string = $state(""); let currentPlayer: string = $state("");
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
let game: Game | undefined = undefined; let game: FullGame | undefined = undefined;
let gameIndex: number = -1; let gameIndex: number = -1;
let wall: Wall | undefined = undefined; let wall: FullWall | undefined = undefined;
let wallIndex: number = -1; let wallIndex: number = -1;
export default { export default {
@@ -29,7 +29,7 @@ export default {
currentPlayer = str; currentPlayer = str;
}, },
baseRoute, baseRoute,
get game(): Game | undefined { get game(): FullGame | undefined {
return game; return game;
}, },
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -42,7 +42,7 @@ export default {
set gameIndex(i: number) { set gameIndex(i: number) {
gameIndex = i; gameIndex = i;
}, },
get wall(): Wall | undefined { get wall(): FullWall | undefined {
return wall; return wall;
}, },
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

18
src/lib/Textfield.svelte Normal file
View File

@@ -0,0 +1,18 @@
<script lang="ts">
interface Props {
value: string;
}
let { value = $bindable() }: Props = $props();
</script>
<input type="text" name="textfield" class="borders mt-2 mb-2 w-full" bind:value />
<style>
.borders {
border: 1px solid black;
border-radius: 5px;
display: flex;
padding: 2px;
}
</style>

View File

@@ -1,3 +1,13 @@
import type {
AudioMultipleChoiceQuestion,
AudioQuestion,
FullWall,
ImageMultipleChoiceQuestion,
ImageQuestion,
MultipleChoiceQuestion,
SimpleQuestion
} from "./games/games";
export type VisitedQuestions = number[][]; export type VisitedQuestions = number[][];
export type Directory = { export type Directory = {
@@ -23,13 +33,49 @@ export function isRessource(ressource: Ressource | Directory): ressource is Ress
return (ressource as Directory).isDir === undefined; return (ressource as Directory).isDir === undefined;
} }
export type GameId = string; export type _id = string;
export type GameId = _id;
export type Game = { export type Game = {
name: string; name: string;
owner: string; owner: _id;
_id: GameId; _id: GameId;
walls: WallId[]; 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;

View File

@@ -1,10 +1,21 @@
<script lang="ts"> <script lang="ts">
import type { Wall } from "$lib/games/games"; import axios from "axios";
import type { VisitedQuestions } from "./Types"; 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";
interface Props { interface Props {
wall: Wall | undefined; wall: Wall | FullWall | undefined;
onclick?: (catIndex: number, questionIndex: number) => unknown; onclick?: (catIndex: number, questionIndex: number) => unknown;
onclickIds?: (catId: CategoryId, questionId: QuestionId) => unknown;
visited: VisitedQuestions; visited: VisitedQuestions;
[key: string]: unknown; [key: string]: unknown;
} }
@@ -13,30 +24,80 @@
return visited[catIndex] && visited[catIndex].includes(queIndex); return visited[catIndex] && visited[catIndex].includes(queIndex);
} }
let { wall, onclick, visited }: Props = $props(); let { wall, onclick, onclickIds, visited }: Props = $props();
let categories: Category[] = $state([]);
async function fetchCategories(wall: 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;
})
);
}
return Promise.all(cats).then((cats) => {
categories = cats;
});
}
onMount(() => {
if (wall && isWall(wall)) fetchCategories(wall);
});
</script> </script>
{#if wall != undefined} {#if wall != undefined}
<div class="grid h-full grow grid-flow-col grid-cols-5 grid-rows-6 gap-4 pb-4"> <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} {#if isWall(wall)}
<div class="flex items-center justify-center text-3xl font-semibold"> {#each categories as category, catIndex}
<div>{category.name}</div> <div class="flex items-center justify-center text-3xl font-semibold">
</div> <div>{category.name}</div>
{#each category.questions as question, queIndex}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
class="card {isVisited(catIndex, queIndex) ? 'visited' : ''}"
role="button"
aria-pressed="false"
tabindex="0"
onclick={() => {
if (onclick) onclick(catIndex, queIndex);
}}
>
<div class="text-6xl font-thin">{question.points}</div>
</div> </div>
{#each category.questions as question, queIndex}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
class="card {isVisited(catIndex, queIndex) ? 'visited' : ''}"
role="button"
aria-pressed="false"
tabindex="0"
onclick={() => {
if (onclickIds) onclickIds(category._id, question._id);
}}
>
<div class="text-6xl font-thin">
{question.points >= 0 ? question.points : "???"}
</div>
</div>
{/each}
{/each} {/each}
{/each} {:else}
{#each wall.categories as category, catIndex}
<div class="flex items-center justify-center text-3xl font-semibold">
<div>{category.name}</div>
</div>
{#each category.questions as question, queIndex}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
class="card {isVisited(catIndex, queIndex) ? 'visited' : ''}"
role="button"
aria-pressed="false"
tabindex="0"
onclick={() => {
if (onclick) onclick(catIndex, queIndex);
}}
>
<div class="text-6xl font-thin">
{question.points >= 0 ? question.points : "???"}
</div>
</div>
{/each}
{/each}
{/if}
</div> </div>
{:else} {:else}
<p>Wall is undefined</p> <p>Wall is undefined</p>

View File

@@ -1425,16 +1425,16 @@ export type Category = {
)[]; )[];
}; };
export type Wall = { export type FullWall = {
name: string; name: string;
categories: Category[]; categories: Category[];
}; };
export type Game = { export type FullGame = {
name: string; name: string;
walls: Wall[]; walls: FullWall[];
}; };
export type Games = Game[]; export type Games = FullGame[];
export default games; export default games;

View File

@@ -6,10 +6,10 @@
isMultipleChoiceQuestion, isMultipleChoiceQuestion,
isSimpleQuestion, isSimpleQuestion,
isImageQuestion, isImageQuestion,
type Game,
isAudioQuestion, isAudioQuestion,
isAudioMultipleChoiceQuestion, isAudioMultipleChoiceQuestion,
isImageMultipleChoiceQuestion isImageMultipleChoiceQuestion,
type FullGame
} from "$lib/games/games"; } from "$lib/games/games";
import ws from "$lib/websocket.svelte"; import ws from "$lib/websocket.svelte";
import { page } from "$app/state"; import { page } from "$app/state";
@@ -57,7 +57,7 @@
class GameManager { class GameManager {
public state: GameState = $state(GameState.INIT); public state: GameState = $state(GameState.INIT);
public game: Game; public game: FullGame;
public players: Player[] = $state([ public players: Player[] = $state([
{ {
name: "Player 1", name: "Player 1",
@@ -83,7 +83,7 @@
public questionIsShowing = $state(false); public questionIsShowing = $state(false);
public isBuzzed = $state(false); public isBuzzed = $state(false);
constructor(game: Game) { constructor(game: FullGame) {
this.game = game; this.game = game;
} }

View File

@@ -79,11 +79,13 @@
</script> </script>
<div class="flex h-full flex-col"> <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> <h1 class="m-4 mb-8 text-7xl font-bold">Editor</h1>
<button class="btn" type="button" onclick={() => (showNewGame = true)} <button class="btn" type="button" onclick={() => (showNewGame = true)}
><i class="fa-solid fa-plus"></i> Neues Spiel</button ><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> </div>
{#if games.length > 0} {#if games.length > 0}
<div class="flex flex-col space-y-4 overflow-y-auto"> <div class="flex flex-col space-y-4 overflow-y-auto">

View File

@@ -0,0 +1,192 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { page } from "$app/state";
import Modal from "$lib/Modal.svelte";
import Textfield from "$lib/Textfield.svelte";
import type { Game, 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";
let game: Game | undefined = $state();
let walls: WallType[] = $state([]);
let selectedWall: WallType | undefined = $state();
let showNewWall = $state(false);
let newWallName = $state("");
let showDeleteWall = $state(false);
let wallToDelete: WallType | undefined = $state();
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}`);
}
})
.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];
}
})
.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 deleteWall() {
if (!wallToDelete) return false;
if (wallToDelete._id === selectedWall?._id) selectedWall = 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(() => {
fetchGame().then(() => fetchWalls());
});
</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="btn ms-4 me-4" type="button" 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 justify-between rounded-xl border-2 p-2 hover:cursor-pointer hover:bg-emerald-200"
class:bg-emerald-200={selectedWall?._id === wall._id}
onclick={() => {
selectedWall = wall;
}}
>
<div>
{wall.name}
</div>
<!-- svelte-ignore a11y_consider_explicit_label -->
<button
type="button"
class="btn 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
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>
<label for="directory" class="">Name</label>
<div>
<Textfield bind:value={newWallName}></Textfield>
</div>
{#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>

View 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>