Added Editor Game and Wall Display
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { Game, Wall } from "./games/games";
|
||||
import type { FullGame, FullWall } from "./games/games";
|
||||
|
||||
interface Player {
|
||||
name: string;
|
||||
@@ -9,9 +9,9 @@ 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: FullGame | undefined = undefined;
|
||||
let gameIndex: number = -1;
|
||||
let wall: Wall | undefined = undefined;
|
||||
let wall: FullWall | undefined = undefined;
|
||||
let wallIndex: number = -1;
|
||||
|
||||
export default {
|
||||
@@ -29,7 +29,7 @@ export default {
|
||||
currentPlayer = str;
|
||||
},
|
||||
baseRoute,
|
||||
get game(): Game | undefined {
|
||||
get game(): FullGame | undefined {
|
||||
return game;
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -42,7 +42,7 @@ export default {
|
||||
set gameIndex(i: number) {
|
||||
gameIndex = i;
|
||||
},
|
||||
get wall(): Wall | undefined {
|
||||
get wall(): FullWall | undefined {
|
||||
return wall;
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
18
src/lib/Textfield.svelte
Normal file
18
src/lib/Textfield.svelte
Normal 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>
|
||||
@@ -1,3 +1,13 @@
|
||||
import type {
|
||||
AudioMultipleChoiceQuestion,
|
||||
AudioQuestion,
|
||||
FullWall,
|
||||
ImageMultipleChoiceQuestion,
|
||||
ImageQuestion,
|
||||
MultipleChoiceQuestion,
|
||||
SimpleQuestion
|
||||
} from "./games/games";
|
||||
|
||||
export type VisitedQuestions = number[][];
|
||||
|
||||
export type Directory = {
|
||||
@@ -23,13 +33,49 @@ export function isRessource(ressource: Ressource | Directory): ressource is Ress
|
||||
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,10 +1,21 @@
|
||||
<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";
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -13,30 +24,80 @@
|
||||
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>
|
||||
|
||||
{#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">
|
||||
<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}</div>
|
||||
{#if isWall(wall)}
|
||||
{#each 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 (onclickIds) onclickIds(category._id, question._id);
|
||||
}}
|
||||
>
|
||||
<div class="text-6xl font-thin">
|
||||
{question.points >= 0 ? question.points : "???"}
|
||||
</div>
|
||||
</div>
|
||||
{/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>
|
||||
{:else}
|
||||
<p>Wall is undefined</p>
|
||||
|
||||
@@ -1425,16 +1425,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;
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
isMultipleChoiceQuestion,
|
||||
isSimpleQuestion,
|
||||
isImageQuestion,
|
||||
type Game,
|
||||
isAudioQuestion,
|
||||
isAudioMultipleChoiceQuestion,
|
||||
isImageMultipleChoiceQuestion
|
||||
isImageMultipleChoiceQuestion,
|
||||
type FullGame
|
||||
} from "$lib/games/games";
|
||||
import ws from "$lib/websocket.svelte";
|
||||
import { page } from "$app/state";
|
||||
@@ -57,7 +57,7 @@
|
||||
|
||||
class GameManager {
|
||||
public state: GameState = $state(GameState.INIT);
|
||||
public game: Game;
|
||||
public game: FullGame;
|
||||
public players: Player[] = $state([
|
||||
{
|
||||
name: "Player 1",
|
||||
@@ -83,7 +83,7 @@
|
||||
public questionIsShowing = $state(false);
|
||||
public isBuzzed = $state(false);
|
||||
|
||||
constructor(game: Game) {
|
||||
constructor(game: FullGame) {
|
||||
this.game = game;
|
||||
}
|
||||
|
||||
|
||||
@@ -79,11 +79,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">
|
||||
|
||||
192
src/routes/editor/[gameid]/+page.svelte
Normal file
192
src/routes/editor/[gameid]/+page.svelte
Normal 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>
|
||||
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>
|
||||
Reference in New Issue
Block a user