Compare commits
7 Commits
1.0.6
...
dc2766f0ef
| Author | SHA1 | Date | |
|---|---|---|---|
| dc2766f0ef | |||
| 4405c23bee | |||
| aaf09e13f5 | |||
| daf3f779aa | |||
| 48bc66b89a | |||
| 3a52b85dfb | |||
| 7349624da9 |
1
.npmrc
1
.npmrc
@@ -1 +1,2 @@
|
|||||||
engine-strict=true
|
engine-strict=true
|
||||||
|
script-shell=C:\Program Files\Git\git-bash.exe
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ services:
|
|||||||
JEOPARDY_URL: http://localhost:11000
|
JEOPARDY_URL: http://localhost:11000
|
||||||
ports:
|
ports:
|
||||||
- "11001:12345"
|
- "11001:12345"
|
||||||
|
volumes:
|
||||||
|
- jeopardyserver_data_volume:/data
|
||||||
mongo:
|
mongo:
|
||||||
image: mongo:8.0.17
|
image: mongo:8.0.17
|
||||||
restart: always
|
restart: always
|
||||||
@@ -41,3 +43,4 @@ services:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mongodb_data_volume:
|
mongodb_data_volume:
|
||||||
|
jeopardyserver_data_volume:
|
||||||
|
|||||||
4
docker-dev.sh
Normal file
4
docker-dev.sh
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
docker compose down &
|
||||||
|
docker build -t jeopardy .
|
||||||
|
docker compose up -d
|
||||||
@@ -12,7 +12,8 @@
|
|||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"lint": "prettier --check . && eslint .",
|
"lint": "prettier --check . && eslint .",
|
||||||
"docker-build": "docker build -t jeopardy ."
|
"docker-build": "docker build -t jeopardy .",
|
||||||
|
"docker-dev": "./docker-dev.sh"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.2.5",
|
"@eslint/compat": "^1.2.5",
|
||||||
|
|||||||
@@ -9,7 +9,10 @@
|
|||||||
cancelFn?: () => void;
|
cancelFn?: () => void;
|
||||||
okFn: () => Promise<boolean>;
|
okFn: () => Promise<boolean>;
|
||||||
oncloseFn?: () => void;
|
oncloseFn?: () => void;
|
||||||
|
okButtonText?: string;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
|
width?: string;
|
||||||
|
height?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -19,7 +22,10 @@
|
|||||||
cancelFn,
|
cancelFn,
|
||||||
okFn,
|
okFn,
|
||||||
oncloseFn,
|
oncloseFn,
|
||||||
actionButtons
|
actionButtons,
|
||||||
|
okButtonText = "Ok",
|
||||||
|
width,
|
||||||
|
height
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let dialog: HTMLDialogElement | undefined = $state(); // HTMLDialogElement
|
let dialog: HTMLDialogElement | undefined = $state(); // HTMLDialogElement
|
||||||
@@ -40,10 +46,15 @@
|
|||||||
if (e.target === dialog) dialog.close();
|
if (e.target === dialog) dialog.close();
|
||||||
}}
|
}}
|
||||||
class="rounded-md"
|
class="rounded-md"
|
||||||
|
style:width
|
||||||
|
style:height
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-4 p-4">
|
<div class="flex h-full flex-col gap-4 p-4">
|
||||||
{@render header?.()}
|
{@render header?.()}
|
||||||
{@render children?.()}
|
<div class="grow overflow-y-auto">
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
<!-- <div class="grow"></div> -->
|
||||||
<!-- svelte-ignore a11y_autofocus -->
|
<!-- svelte-ignore a11y_autofocus -->
|
||||||
<div class="flex justify-end gap-4">
|
<div class="flex justify-end gap-4">
|
||||||
{@render actionButtons?.()}
|
{@render actionButtons?.()}
|
||||||
@@ -62,7 +73,7 @@
|
|||||||
dialog?.close();
|
dialog?.close();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
class="btn min-w-[64px]">Ok</button
|
class="btn min-w-[64px]">{okButtonText}</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
452
src/lib/RessourceManager.svelte
Normal file
452
src/lib/RessourceManager.svelte
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { env } from "$env/dynamic/public";
|
||||||
|
import axios from "axios";
|
||||||
|
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;
|
||||||
|
ok?: (res: Ressource) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { show = $bindable(false), ok = (res) => {} }: Props = $props();
|
||||||
|
|
||||||
|
let file: File | null = null;
|
||||||
|
let path: string = $state("/");
|
||||||
|
|
||||||
|
let fetchingRessources = $state(false);
|
||||||
|
let ressources: (Ressource | Directory)[] = $state([]);
|
||||||
|
let selectedRessource: Ressource | undefined = $state();
|
||||||
|
|
||||||
|
let error = $state("");
|
||||||
|
|
||||||
|
let showNewDir = $state(false);
|
||||||
|
let newDirName = $state("");
|
||||||
|
let showDeleteDir = $state(false);
|
||||||
|
let dirToDelete: Directory | undefined = $state();
|
||||||
|
|
||||||
|
let showDeleteRessource = $state(false);
|
||||||
|
let resToDelete: Ressource | undefined = $state();
|
||||||
|
|
||||||
|
let showRenameFile = $state(false);
|
||||||
|
let fileToRename: Ressource | undefined = $state();
|
||||||
|
let newFileName = $state("");
|
||||||
|
|
||||||
|
function handleFileChange(event: Event) {
|
||||||
|
const target = event.target as HTMLInputElement | null;
|
||||||
|
if (target?.files?.[0]) {
|
||||||
|
file = target.files[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadData(event: SubmitEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (file === null) return;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("path", path);
|
||||||
|
formData.append("file", file);
|
||||||
|
|
||||||
|
axios
|
||||||
|
.post(
|
||||||
|
`${env.PUBLIC_JEOPARDY_SERVER_PROTOCOL}://${env.PUBLIC_JEOPARDY_SERVER}/upload`,
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data"
|
||||||
|
},
|
||||||
|
onUploadProgress: (event) => {
|
||||||
|
console.log(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
fetchDirectory();
|
||||||
|
} else {
|
||||||
|
alert("Failed with status: " + response.status);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
alert(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteRessource(res: Ressource | Directory): Promise<boolean> {
|
||||||
|
if (isRessource(res)) {
|
||||||
|
return axios
|
||||||
|
.delete(url("/cdn/" + res.user + "/" + res.filename), {
|
||||||
|
withCredentials: true
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
fetchDirectory();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
alert("Something went wrong: " + response.status);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
} else if (isDir(res)) {
|
||||||
|
showDeleteDir = true;
|
||||||
|
dirToDelete = res;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteRessourceCancel() {
|
||||||
|
resToDelete = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteDir() {
|
||||||
|
if (dirToDelete === undefined) return false;
|
||||||
|
return axios
|
||||||
|
.delete(url("/directory"), {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
data: { path: path + (path === "/" ? "" : "/") + dirToDelete.name },
|
||||||
|
withCredentials: true
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
fetchDirectory();
|
||||||
|
dirToDelete = undefined;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
alert("Failed to delete directory: " + response.status);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteDirCancel() {
|
||||||
|
dirToDelete = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchDirectory() {
|
||||||
|
fetchingRessources = true;
|
||||||
|
return axios
|
||||||
|
.post(
|
||||||
|
url("/directory"),
|
||||||
|
{ path },
|
||||||
|
{
|
||||||
|
withCredentials: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
ressources = response.data;
|
||||||
|
ressources.sort((a, b) => {
|
||||||
|
if (isDir(a) && !isDir(b)) return -1;
|
||||||
|
if (!isDir(a) && isDir(b)) return 1;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
if (path !== "/") {
|
||||||
|
ressources.unshift({
|
||||||
|
isDir: true,
|
||||||
|
name: ".."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
fetchingRessources = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addDirectoryOk(): Promise<boolean> {
|
||||||
|
error = "";
|
||||||
|
if (newDirName.length <= 0) {
|
||||||
|
error = "Gib einen Namen für den Ordner ein";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return axios
|
||||||
|
.put(url("/directory"), { name: newDirName, path }, { withCredentials: true })
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
fetchDirectory();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
error = "Etwas ist schief gelaufen";
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDirectoryCancel() {
|
||||||
|
error = "";
|
||||||
|
newDirName = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function ressourceClicked(res: Ressource | Directory) {
|
||||||
|
if (isRessource(res)) {
|
||||||
|
selectedRessource = res;
|
||||||
|
} else if (isDir(res)) {
|
||||||
|
if (res.name === "..") {
|
||||||
|
let breadcumbs = path.split("/");
|
||||||
|
breadcumbs.pop();
|
||||||
|
path = breadcumbs.join("/");
|
||||||
|
if (path.length <= 0) path = "/";
|
||||||
|
} else {
|
||||||
|
if (!path.endsWith("/")) {
|
||||||
|
path += "/";
|
||||||
|
}
|
||||||
|
path += res.name;
|
||||||
|
}
|
||||||
|
fetchDirectory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renameRessource(res: Ressource) {
|
||||||
|
fileToRename = res;
|
||||||
|
newFileName = res.name;
|
||||||
|
showRenameFile = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renameFile() {
|
||||||
|
if (fileToRename === undefined) return false;
|
||||||
|
return axios
|
||||||
|
.put(
|
||||||
|
url("/cdn/" + fileToRename.user + "/" + fileToRename.filename),
|
||||||
|
{
|
||||||
|
name: newFileName
|
||||||
|
},
|
||||||
|
{ withCredentials: true }
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
fetchDirectory();
|
||||||
|
fileToRename = undefined;
|
||||||
|
newFileName = "";
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
alert("Failed to rename File: " + response.status);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renameFileCancel() {
|
||||||
|
fileToRename = undefined;
|
||||||
|
newFileName = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeRessourceManager() {
|
||||||
|
selectedRessource = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (show) {
|
||||||
|
fetchDirectory();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
bind:showModal={show}
|
||||||
|
okFn={async () => {
|
||||||
|
if (selectedRessource) ok({ ...selectedRessource });
|
||||||
|
return true;
|
||||||
|
}}
|
||||||
|
oncloseFn={closeRessourceManager}
|
||||||
|
okButtonText={selectedRessource !== undefined ? selectedRessource.name : "Ok"}
|
||||||
|
width="90%"
|
||||||
|
height="90%"
|
||||||
|
>
|
||||||
|
{#snippet header()}
|
||||||
|
<h2 class="text-3xl">Ressourcenmanager - {path}</h2>
|
||||||
|
{/snippet}
|
||||||
|
<div class="h-full">
|
||||||
|
{#if fetchingRessources}
|
||||||
|
Loading...
|
||||||
|
{:else if ressources.length > 0}
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
{#each ressources as ressource}
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div
|
||||||
|
class="flex grow cursor-pointer items-center justify-between rounded-sm border-1 border-solid p-1 hover:bg-gray-200"
|
||||||
|
onclick={() => ressourceClicked(ressource)}
|
||||||
|
class:bg-green-200={isRessource(ressource) &&
|
||||||
|
ressource._id === selectedRessource?._id}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{#if isDir(ressource)}
|
||||||
|
<i class="fa-solid fa-folder"></i>
|
||||||
|
{ressource.name}
|
||||||
|
{:else}
|
||||||
|
{#if ressource.mimetype.includes("image")}
|
||||||
|
<i class="fa-solid fa-image"></i>
|
||||||
|
{:else if ressource.mimetype.includes("audio")}
|
||||||
|
<i class="fa-solid fa-music"></i>
|
||||||
|
{:else}
|
||||||
|
<i class="fa-solid fa-file"></i>
|
||||||
|
{/if}
|
||||||
|
{ressource.name}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if !(isDir(ressource) && ressource.name === "..")}
|
||||||
|
<div class="flex gap-4">
|
||||||
|
{#if isRessource(ressource)}
|
||||||
|
<!-- svelte-ignore a11y_consider_explicit_label -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn border-black"
|
||||||
|
onclick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
renameRessource(ressource);
|
||||||
|
}}><i class="fa-solid fa-pencil"></i></button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
<!-- svelte-ignore a11y_consider_explicit_label -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn border-red-600 text-red-600"
|
||||||
|
onclick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (isRessource(ressource)) {
|
||||||
|
showDeleteRessource = true;
|
||||||
|
resToDelete = ressource;
|
||||||
|
} else if (isDir(ressource)) {
|
||||||
|
dirToDelete = ressource;
|
||||||
|
showDeleteDir = true;
|
||||||
|
}
|
||||||
|
}}><i class="fa-solid fa-trash"></i></button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex h-full w-full grow-2 flex-col items-center justify-center">
|
||||||
|
<div class="text-[128px] text-gray-300"><i class="fa-solid fa-database"></i></div>
|
||||||
|
<div class="text-[24px] text-gray-500 select-none">
|
||||||
|
Noch keine Ressourcen vorhanden
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#snippet actionButtons()}
|
||||||
|
<form
|
||||||
|
action={`${env.PUBLIC_JEOPARDY_SERVER_PROTOCOL}://${env.PUBLIC_JEOPARDY_SERVER}/upload`}
|
||||||
|
enctype="multipart/form-data"
|
||||||
|
method="post"
|
||||||
|
onsubmit={uploadData}
|
||||||
|
>
|
||||||
|
<input class="btn" type="file" name="file" onchange={handleFileChange} />
|
||||||
|
<input type="submit" value="Hochladen" class="btn" />
|
||||||
|
</form>
|
||||||
|
<button class="btn" type="button" onclick={() => (showNewDir = true)}>Neuer Ordner</button>
|
||||||
|
{/snippet}
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal bind:showModal={showNewDir} okFn={addDirectoryOk} cancelFn={addDirectoryCancel}>
|
||||||
|
{#snippet header()}
|
||||||
|
<h2 class="text-3xl">Neuer Ordner</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={newDirName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{#if error.length > 0}
|
||||||
|
<div class="text-red-700">{error}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
bind:showModal={showDeleteRessource}
|
||||||
|
okFn={async () => {
|
||||||
|
if (resToDelete === undefined) return false;
|
||||||
|
return deleteRessource(resToDelete);
|
||||||
|
}}
|
||||||
|
cancelFn={deleteRessourceCancel}
|
||||||
|
>
|
||||||
|
{#snippet header()}
|
||||||
|
<h2 class="text-3xl">Ressource löschen</h2>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
Soll die Ressource {resToDelete?.name} wirklich gelöscht werden?
|
||||||
|
</div>
|
||||||
|
{#if error.length > 0}
|
||||||
|
<div class="text-red-700">{error}</div>
|
||||||
|
{/if}
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal bind:showModal={showDeleteDir} okFn={deleteDir} cancelFn={deleteDirCancel}>
|
||||||
|
{#snippet header()}
|
||||||
|
<h2 class="text-3xl">Ordner löschen</h2>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
Soll der Ordner {dirToDelete?.name} wirklich gelöscht werden? Alle darin enthaltenen Daten gehen
|
||||||
|
verloren.
|
||||||
|
</div>
|
||||||
|
{#if error.length > 0}
|
||||||
|
<div class="text-red-700">{error}</div>
|
||||||
|
{/if}
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal bind:showModal={showRenameFile} okFn={renameFile} cancelFn={renameFileCancel}>
|
||||||
|
{#snippet header()}
|
||||||
|
<h2 class="text-3xl">Datei umbenennen</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={newFileName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{#if error.length > 0}
|
||||||
|
<div class="text-red-700">{error}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.borders {
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 5px;
|
||||||
|
display: flex;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1 +1,35 @@
|
|||||||
export type VisitedQuestions = number[][];
|
export type VisitedQuestions = number[][];
|
||||||
|
|
||||||
|
export type Directory = {
|
||||||
|
name: string;
|
||||||
|
isDir: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Ressource = {
|
||||||
|
_id: string;
|
||||||
|
fullpath: string;
|
||||||
|
path: string;
|
||||||
|
user: string;
|
||||||
|
mimetype: string;
|
||||||
|
name: string;
|
||||||
|
filename: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isDir(dir: Directory | Ressource): dir is Directory {
|
||||||
|
return (dir as Directory).isDir === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isRessource(ressource: Ressource | Directory): ressource is Ressource {
|
||||||
|
return (ressource as Directory).isDir === undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GameId = string;
|
||||||
|
|
||||||
|
export type Game = {
|
||||||
|
name: string;
|
||||||
|
owner: string;
|
||||||
|
_id: GameId;
|
||||||
|
walls: WallId[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WallId = string;
|
||||||
|
|||||||
5
src/lib/util.ts
Normal file
5
src/lib/util.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { env } from "$env/dynamic/public";
|
||||||
|
|
||||||
|
export function url(path: string) {
|
||||||
|
return `${env.PUBLIC_JEOPARDY_SERVER_PROTOCOL}://${env.PUBLIC_JEOPARDY_SERVER}${path.startsWith("/") ? "" : "/"}${path}`;
|
||||||
|
}
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { env } from "$env/dynamic/public";
|
import { env } from "$env/dynamic/public";
|
||||||
|
import RessourceManager from "$lib/RessourceManager.svelte";
|
||||||
import UserSvelte from "$lib/User.svelte";
|
import UserSvelte from "$lib/User.svelte";
|
||||||
import websocket, { SocketConnectionType } from "$lib/websocket.svelte";
|
import websocket, { SocketConnectionType } from "$lib/websocket.svelte";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
|
let showRessourceManager = $state(false);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (websocket.connectionType === SocketConnectionType.HOST) {
|
if (websocket.connectionType === SocketConnectionType.HOST) {
|
||||||
console.log(`Type: ${websocket.connectionType}. Redirecting to /connected/games`);
|
console.log(`Type: ${websocket.connectionType}. Redirecting to /connected/games`);
|
||||||
@@ -53,11 +56,12 @@
|
|||||||
{#if UserSvelte.role === "admin"}
|
{#if UserSvelte.role === "admin"}
|
||||||
<button type="button" class="btn" onclick={() => goto("/admin")}>Administration</button>
|
<button type="button" class="btn" onclick={() => goto("/admin")}>Administration</button>
|
||||||
{/if}
|
{/if}
|
||||||
<button type="button" class="btn" onclick={() => goto("/settings")}>Einstellungen</button>
|
<button type="button" class="btn" onclick={() => (showRessourceManager = true)}
|
||||||
<button type="button" class="btn" onclick={logout}>Logout</button>
|
>Ressourcen</button
|
||||||
<button type="button" class="btn" onclick={logoutFromAllDevices}
|
|
||||||
>Logout von allen Geräten</button
|
|
||||||
>
|
>
|
||||||
|
<button type="button" class="btn" onclick={() => goto("/editor")}>Editor</button>
|
||||||
|
<button type="button" class="btn" onclick={() => goto("/settings")}>Einstellungen</button>
|
||||||
|
<button type="button" class="btn" onclick={logoutFromAllDevices}>Logout</button>
|
||||||
<div class="btn profile ps-2 pe-2">
|
<div class="btn profile ps-2 pe-2">
|
||||||
<i class="fa-regular fa-user"></i>
|
<i class="fa-regular fa-user"></i>
|
||||||
{UserSvelte.username}
|
{UserSvelte.username}
|
||||||
@@ -74,6 +78,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<RessourceManager
|
||||||
|
bind:show={showRessourceManager}
|
||||||
|
ok={(res) => {
|
||||||
|
console.log(res);
|
||||||
|
}}
|
||||||
|
></RessourceManager>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.profile {
|
.profile {
|
||||||
border-color: gray;
|
border-color: gray;
|
||||||
|
|||||||
162
src/routes/editor/+page.svelte
Normal file
162
src/routes/editor/+page.svelte
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
|
import Modal from "$lib/Modal.svelte";
|
||||||
|
import type { Game } from "$lib/Types";
|
||||||
|
import { url } from "$lib/util";
|
||||||
|
import axios from "axios";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
let games: Game[] = $state([]);
|
||||||
|
|
||||||
|
let error = $state("");
|
||||||
|
|
||||||
|
let showNewGame = $state(false);
|
||||||
|
let newGameName = $state("");
|
||||||
|
|
||||||
|
let showDeleteGame = $state(false);
|
||||||
|
let gameToDelete: Game | undefined = $state();
|
||||||
|
|
||||||
|
async function addNewGame() {
|
||||||
|
if (!newGameName) return false;
|
||||||
|
return axios
|
||||||
|
.post(url("game"), { name: newGameName }, { withCredentials: true })
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
fetchGames();
|
||||||
|
newGameName = "";
|
||||||
|
return true;
|
||||||
|
} else return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNewGameCancel() {
|
||||||
|
newGameName = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteGame() {
|
||||||
|
if (gameToDelete === undefined) return false;
|
||||||
|
return axios
|
||||||
|
.delete(url("/game/" + gameToDelete._id), { withCredentials: true })
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
fetchGames();
|
||||||
|
gameToDelete = undefined;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.error("Failed to delete Game: " + response.status);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteGameCancel() {
|
||||||
|
gameToDelete = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
<div class="flex h-full flex-col">
|
||||||
|
<div class="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>
|
||||||
|
{#if games.length > 0}
|
||||||
|
<div class="flex flex-col space-y-4 overflow-y-auto">
|
||||||
|
{#each games as game}
|
||||||
|
<!-- 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"
|
||||||
|
onclick={() => {
|
||||||
|
goto(`/editor/${game._id}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{game.name}
|
||||||
|
</div>
|
||||||
|
<!-- svelte-ignore a11y_consider_explicit_label -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn border-red-600 text-red-600"
|
||||||
|
onclick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
gameToDelete = game;
|
||||||
|
showDeleteGame = true;
|
||||||
|
}}><i class="fa-solid fa-trash"></i></button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex h-full w-full grow-2 flex-col items-center justify-center">
|
||||||
|
<div class="text-[128px] text-gray-300"><i class="fa-solid fa-database"></i></div>
|
||||||
|
<div class="text-[24px] text-gray-500 select-none">Noch keine Spiele vorhanden</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal bind:showModal={showNewGame} okFn={addNewGame} cancelFn={addNewGameCancel}>
|
||||||
|
{#snippet header()}
|
||||||
|
<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}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{#if error.length > 0}
|
||||||
|
<div class="text-red-700">{error}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal bind:showModal={showDeleteGame} okFn={deleteGame} cancelFn={deleteGameCancel}>
|
||||||
|
{#snippet header()}
|
||||||
|
<h2 class="text-3xl">Spiel löschen</h2>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<div>Soll das Spiel {gameToDelete?.name} wirklich gelöscht werden?</div>
|
||||||
|
{#if error.length > 0}
|
||||||
|
<div class="text-red-700">{error}</div>
|
||||||
|
{/if}
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.borders {
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 5px;
|
||||||
|
display: flex;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user