Added rename of Game, Wall and Category

This commit is contained in:
2026-01-02 12:35:44 +01:00
parent 7be5921ef6
commit 5568a5bb99
5 changed files with 284 additions and 60 deletions

49
src/lib/Button.svelte Normal file
View File

@@ -0,0 +1,49 @@
<script lang="ts">
import type { Snippet } from "svelte";
interface Props {
onclick?: (
event: MouseEvent & {
currentTarget: EventTarget & HTMLButtonElement;
}
) => void;
children: Snippet;
class?: string;
}
let { onclick, children, class: classes }: Props = $props();
</script>
<!-- svelte-ignore a11y_consider_explicit_label -->
<button
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>

View File

@@ -1,12 +1,28 @@
<script lang="ts"> <script lang="ts">
interface Props { interface Props {
value: string; value: string;
label?: string;
} }
let { value = $bindable() }: Props = $props(); let { value = $bindable(), label }: Props = $props();
const id = crypto.randomUUID();
</script> </script>
<input type="text" name="textfield" class="borders mt-2 mb-2 w-full" bind:value /> <div>
{#if label}
<label for="textfield-{id}" class="">{label}</label>
{/if}
<div>
<input
type="text"
name="textfield"
id="textfield-{id}"
class="borders mt-2 mb-2 w-full"
bind:value
/>
</div>
</div>
<style> <style>
.borders { .borders {

View File

@@ -11,43 +11,90 @@
import { url } from "./util"; import { url } from "./util";
import { onMount } from "svelte"; import { onMount } from "svelte";
import type { FullWall } from "./games/games"; import type { FullWall } from "./games/games";
import Button from "./Button.svelte";
import Modal from "./Modal.svelte";
import Textfield from "./Textfield.svelte";
interface Props { interface Props {
wall: Wall | FullWall | undefined; wall: Wall | FullWall | undefined;
onclick?: (catIndex: number, questionIndex: number) => unknown; onclick?: (catIndex: number, questionIndex: number) => unknown;
onclickIds?: (catId: CategoryId, questionId: QuestionId) => unknown; onclickIds?: (catId: CategoryId, questionId: QuestionId) => unknown;
visited: VisitedQuestions; visited: VisitedQuestions;
[key: string]: unknown; isEditor?: boolean;
} }
function isVisited(catIndex: number, queIndex: number): boolean { function isVisited(catIndex: number, queIndex: number): boolean {
return visited[catIndex] && visited[catIndex].includes(queIndex); return visited[catIndex] && visited[catIndex].includes(queIndex);
} }
let { wall, onclick, onclickIds, visited }: Props = $props(); let { wall, onclick, onclickIds, visited, isEditor = false }: Props = $props();
let categories: Category[] = $state([]); let categories: Category[] = $state([]);
async function fetchCategories(wall: Wall) { let showRenameCategory = $state(false);
let cats: Promise<Category>[] = []; let catToRename: Category | undefined = $state();
for (const catId of wall.categories) { let newCatName = $state("");
cats.push( let error = $state("");
axios
.get(url(`/category?id=${catId}`), { withCredentials: true }) async function fetchCategories(wall: Wall | FullWall | undefined) {
.then((response) => { if (wall && isWall(wall)) {
if (response.status === 200) { let cats: Promise<Category>[] = [];
return response.data; for (const catId of wall.categories) {
} else throw "Failed to fetch: " + response.status; 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;
});
} }
return Promise.all(cats).then((cats) => {
categories = cats;
});
} }
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(() => { onMount(() => {
if (wall && isWall(wall)) fetchCategories(wall); fetchCategories(wall);
}); });
</script> </script>
@@ -55,8 +102,17 @@
<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">
{#if isWall(wall)} {#if isWall(wall)}
{#each categories as category, catIndex} {#each categories as category, catIndex}
<div class="flex items-center justify-center text-3xl font-semibold"> <div class="flex items-center justify-center gap-2 text-3xl font-semibold">
<div>{category.name}</div> <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> </div>
{#each category.questions as question, queIndex} {#each category.questions as question, queIndex}
<!-- svelte-ignore a11y_click_events_have_key_events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
@@ -103,6 +159,18 @@
<p>Wall is undefined</p> <p>Wall is undefined</p>
{/if} {/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> <style>
.visited { .visited {
background-color: gray; background-color: gray;

View File

@@ -1,6 +1,8 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import Button from "$lib/Button.svelte";
import Modal from "$lib/Modal.svelte"; import Modal from "$lib/Modal.svelte";
import Textfield from "$lib/Textfield.svelte";
import type { Game } from "$lib/Types"; import type { Game } from "$lib/Types";
import { url } from "$lib/util"; import { url } from "$lib/util";
import axios from "axios"; import axios from "axios";
@@ -11,7 +13,9 @@
let error = $state(""); let error = $state("");
let showNewGame = $state(false); let showNewGame = $state(false);
let showRenameGame = $state(false);
let newGameName = $state(""); let newGameName = $state("");
let gameToRename: Game | undefined = $state();
let showDeleteGame = $state(false); let showDeleteGame = $state(false);
let gameToDelete: Game | undefined = $state(); let gameToDelete: Game | undefined = $state();
@@ -19,7 +23,7 @@
async function addNewGame() { async function addNewGame() {
if (!newGameName) return false; if (!newGameName) return false;
return axios return axios
.post(url("game"), { name: newGameName }, { withCredentials: true }) .post(url("/game"), { name: newGameName }, { withCredentials: true })
.then((response) => { .then((response) => {
if (response.status === 200) { if (response.status === 200) {
fetchGames(); fetchGames();
@@ -33,6 +37,39 @@
newGameName = ""; 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() { async function deleteGame() {
if (gameToDelete === undefined) return false; if (gameToDelete === undefined) return false;
return axios return axios
@@ -59,7 +96,7 @@
function fetchGames() { function fetchGames() {
axios axios
.get(url("games"), { withCredentials: true }) .get(url("/games"), { withCredentials: true })
.then((response) => { .then((response) => {
if (response.status === 200) { if (response.status === 200) {
games = response.data; games = response.data;
@@ -93,7 +130,7 @@
<!-- svelte-ignore a11y_click_events_have_key_events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div <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={() => { onclick={() => {
goto(`/editor/${game._id}`); goto(`/editor/${game._id}`);
}} }}
@@ -101,15 +138,22 @@
<div> <div>
{game.name} {game.name}
</div> </div>
<!-- svelte-ignore a11y_consider_explicit_label --> <div class="grow"></div>
<button <Button
type="button" onclick={(event) => {
class="btn border-red-600 text-red-600" 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) => { onclick={(event) => {
event.stopPropagation(); event.stopPropagation();
gameToDelete = game; gameToDelete = game;
showDeleteGame = true; showDeleteGame = true;
}}><i class="fa-solid fa-trash"></i></button }}><i class="fa-solid fa-trash"></i></Button
> >
</div> </div>
{/each} {/each}
@@ -127,16 +171,19 @@
<h2 class="text-3xl">Neues Spiel</h2> <h2 class="text-3xl">Neues Spiel</h2>
{/snippet} {/snippet}
<div> <div>
<label for="directory" class="">Name</label> <Textfield bind:value={newGameName} label="Name"></Textfield>
<div> {#if error.length > 0}
<input <div class="text-red-700">{error}</div>
type="text" {/if}
name="directory" </div>
id="directory" </Modal>
class="borders mt-2 mb-2 w-full"
bind:value={newGameName} <Modal bind:showModal={showRenameGame} okFn={renameGame} cancelFn={renameGameCancel}>
/> {#snippet header()}
</div> <h2 class="text-3xl">Spiel umbenennen</h2>
{/snippet}
<div>
<Textfield bind:value={newGameName} label="Name"></Textfield>
{#if error.length > 0} {#if error.length > 0}
<div class="text-red-700">{error}</div> <div class="text-red-700">{error}</div>
{/if} {/if}
@@ -153,12 +200,3 @@
<div class="text-red-700">{error}</div> <div class="text-red-700">{error}</div>
{/if} {/if}
</Modal> </Modal>
<style>
.borders {
border: 1px solid black;
border-radius: 5px;
display: flex;
padding: 2px;
}
</style>

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { page } from "$app/state"; import { page } from "$app/state";
import Button from "$lib/Button.svelte";
import Modal from "$lib/Modal.svelte"; import Modal from "$lib/Modal.svelte";
import Textfield from "$lib/Textfield.svelte"; import Textfield from "$lib/Textfield.svelte";
import type { Game, Wall as WallType } from "$lib/Types"; import type { Game, Wall as WallType } from "$lib/Types";
@@ -15,7 +16,9 @@
let selectedWall: WallType | undefined = $state(); let selectedWall: WallType | undefined = $state();
let showNewWall = $state(false); let showNewWall = $state(false);
let showRenameWall = $state(false);
let newWallName = $state(""); let newWallName = $state("");
let walltoRename: WallType | undefined = $state();
let showDeleteWall = $state(false); let showDeleteWall = $state(false);
let wallToDelete: WallType | undefined = $state(); let wallToDelete: WallType | undefined = $state();
@@ -79,6 +82,39 @@
newWallName = ""; 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() { async function deleteWall() {
if (!wallToDelete) return false; if (!wallToDelete) return false;
if (wallToDelete._id === selectedWall?._id) selectedWall = undefined; if (wallToDelete._id === selectedWall?._id) selectedWall = undefined;
@@ -116,15 +152,15 @@
<!-- Sidebar --> <!-- Sidebar -->
<div class="flex h-full max-w-[600px] min-w-[400px] flex-col gap-4 border-r-1"> <div class="flex h-full max-w-[600px] min-w-[400px] flex-col gap-4 border-r-1">
<div> <div>
<button class="btn ms-4 me-4" type="button" onclick={() => (showNewWall = true)} <Button class="ms-4 me-4" onclick={() => (showNewWall = true)}
>Wand hinzufügen</button >Wand hinzufügen</Button
> >
</div> </div>
{#each walls as wall} {#each walls as wall}
<!-- svelte-ignore a11y_click_events_have_key_events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div <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"
class:bg-emerald-200={selectedWall?._id === wall._id} class:bg-emerald-200={selectedWall?._id === wall._id}
onclick={() => { onclick={() => {
selectedWall = wall; selectedWall = wall;
@@ -133,15 +169,22 @@
<div> <div>
{wall.name} {wall.name}
</div> </div>
<!-- svelte-ignore a11y_consider_explicit_label --> <div class="grow"></div>
<button <Button
type="button" onclick={(event) => {
class="btn border-red-600 text-red-600" 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) => { onclick={(event) => {
event.stopPropagation(); event.stopPropagation();
wallToDelete = wall; wallToDelete = wall;
showDeleteWall = true; showDeleteWall = true;
}}><i class="fa-solid fa-trash"></i></button }}><i class="fa-solid fa-trash"></i></Button
> >
</div> </div>
{/each} {/each}
@@ -150,6 +193,7 @@
{#if selectedWall} {#if selectedWall}
<div class="ms-4 me-4 grow"> <div class="ms-4 me-4 grow">
<Wall <Wall
isEditor
wall={selectedWall} wall={selectedWall}
visited={[]} visited={[]}
onclickIds={(catId, queId) => { onclickIds={(catId, queId) => {
@@ -170,10 +214,19 @@
<h2 class="text-3xl">Neue Wand</h2> <h2 class="text-3xl">Neue Wand</h2>
{/snippet} {/snippet}
<div> <div>
<label for="directory" class="">Name</label> <Textfield bind:value={newWallName} label="Name"></Textfield>
<div> {#if error.length > 0}
<Textfield bind:value={newWallName}></Textfield> <div class="text-red-700">{error}</div>
</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} {#if error.length > 0}
<div class="text-red-700">{error}</div> <div class="text-red-700">{error}</div>
{/if} {/if}