Compare commits
26 Commits
Ich_gehe_z
...
2.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| b24e43e142 | |||
| 48074f7603 | |||
| 5568a5bb99 | |||
| 7be5921ef6 | |||
| dc2766f0ef | |||
| 7d231730a6 | |||
| 956571e470 | |||
| 4405c23bee | |||
| aaf09e13f5 | |||
| daf3f779aa | |||
| 48bc66b89a | |||
| 3a52b85dfb | |||
| 7349624da9 | |||
| 25037f4798 | |||
| 38eee8b38c | |||
| 9fbd6e4191 | |||
| 88a9778f3a | |||
| c695d6c733 | |||
| 96388e5a50 | |||
| 888197b1c6 | |||
| 29b2ef1b17 | |||
| b26af58b51 | |||
| f10ccd0d16 | |||
| f6926b2230 | |||
| bb78d7f5af | |||
| e6f16ca8cd |
17
.dockerignore
Normal file
17
.dockerignore
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
|
README.md
|
||||||
|
.npmrc
|
||||||
|
.prettierrc
|
||||||
|
.eslintrc.cjs
|
||||||
|
.graphqlrc
|
||||||
|
.editorconfig
|
||||||
|
.vscode
|
||||||
|
node_modules
|
||||||
|
build
|
||||||
|
package
|
||||||
|
**/.env
|
||||||
|
.env*
|
||||||
1
.npmrc
1
.npmrc
@@ -1 +1,2 @@
|
|||||||
engine-strict=true
|
engine-strict=true
|
||||||
|
script-shell=C:\Program Files\Git\git-bash.exe
|
||||||
|
|||||||
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
FROM node:24-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
RUN npm prune --production
|
||||||
|
|
||||||
|
FROM node:24-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /app/build build/
|
||||||
|
COPY --from=builder /app/node_modules node_modules/
|
||||||
|
COPY package.json .
|
||||||
|
EXPOSE 3000
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV PUBLIC_JEOPARDY_SERVER=127.0.0.1:12345
|
||||||
|
CMD [ "node", "build" ]
|
||||||
48
README.md
48
README.md
@@ -1,38 +1,34 @@
|
|||||||
# sv
|
# Jeopardy
|
||||||
|
|
||||||
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
Ein Jeopardy-Style Projekt
|
||||||
|
|
||||||
## Creating a project
|
|
||||||
|
|
||||||
If you're seeing this, you've probably already done this step. Congrats!
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# create a new project in the current directory
|
|
||||||
npx sv create
|
|
||||||
|
|
||||||
# create a new project in my-app
|
|
||||||
npx sv create my-app
|
|
||||||
```
|
|
||||||
|
|
||||||
## Developing
|
## Developing
|
||||||
|
|
||||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
Zum entwickeln am besten `docker compose` nutzen. Wichtig hierbei ist, dass man das JeopardyServer Projekt auch gebaut haben muss.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm run dev
|
npm run docker-build
|
||||||
|
|
||||||
# or start the server and open the app in a new browser tab
|
docker compose up -d
|
||||||
npm run dev -- --open
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building
|
Eventuell muss das `docker-compose.yml` und das `.env.local` angepasst werden, sollte aber eigentlich alles so stimmen.
|
||||||
|
|
||||||
To create a production version of your app:
|
Ansonsten kann man auch mit `npm run dev` entwickeln.
|
||||||
|
|
||||||
```sh
|
## Build Production
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
You can preview the production build with `npm run preview`.
|
1. Versionsnummer in `package.json` updaten
|
||||||
|
2. commit erstellen und mit Versionsnummer taggen
|
||||||
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
3. push des commits **und der tags**
|
||||||
|
4. Auf Server connecten
|
||||||
|
```sh
|
||||||
|
sudo su
|
||||||
|
cd /opt/jeopardy/Jeopardy
|
||||||
|
git fetch --tags
|
||||||
|
git checkout <versionsnummer>
|
||||||
|
docker build -t jeopardy .
|
||||||
|
docker tag jeopardy:latest jeopardy:<versionsnummer>
|
||||||
|
cd ..
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|||||||
46
docker-compose.yml
Normal file
46
docker-compose.yml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
services:
|
||||||
|
jeopardy:
|
||||||
|
image: jeopardy:latest
|
||||||
|
container_name: jeopardy
|
||||||
|
environment:
|
||||||
|
# domain:port or only domain, eg jeopardyserver.akolata.de
|
||||||
|
PUBLIC_JEOPARDY_SERVER: localhost:11001
|
||||||
|
PUBLIC_JEOPARDY_SERVER_PROTOCOL: http
|
||||||
|
ports:
|
||||||
|
- "11000:3000"
|
||||||
|
jeopardyserver:
|
||||||
|
image: jeopardyserver:latest
|
||||||
|
container_name: jeopardyserver
|
||||||
|
environment:
|
||||||
|
JEOPARDYSERVER_MONGO_USERNAME: jeopardyadmin
|
||||||
|
JEOPARDYSERVER_MONGO_PASSWORD: jGpklsI9vCdixel7sDGxVBsydlzdyX8A1Zank6a12QT827PC
|
||||||
|
JEOPARDYSERVER_MONGO_URL: mongo:27017
|
||||||
|
JEOPARDY_URL: http://localhost:11000
|
||||||
|
ports:
|
||||||
|
- "11001:12345"
|
||||||
|
volumes:
|
||||||
|
- jeopardyserver_data_volume:/data
|
||||||
|
mongo:
|
||||||
|
image: mongo:8.0.17
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
MONGO_INITDB_ROOT_USERNAME: jeopardyadmin
|
||||||
|
MONGO_INITDB_ROOT_PASSWORD: jGpklsI9vCdixel7sDGxVBsydlzdyX8A1Zank6a12QT827PC
|
||||||
|
ports:
|
||||||
|
- 11002:27017
|
||||||
|
volumes:
|
||||||
|
- mongodb_data_volume:/data/db
|
||||||
|
mongo-express:
|
||||||
|
image: mongo-express
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 11003:8081
|
||||||
|
environment:
|
||||||
|
ME_CONFIG_MONGODB_URL: mongodb://jeopardyadmin:jGpklsI9vCdixel7sDGxVBsydlzdyX8A1Zank6a12QT827PC@mongo:27017/
|
||||||
|
ME_CONFIG_BASICAUTH_ENABLED: true
|
||||||
|
ME_CONFIG_BASICAUTH_USERNAME: Trushy
|
||||||
|
ME_CONFIG_BASICAUTH_PASSWORD: IXvtRcbrXy7wX4VKfmwKzRdRnHwbMlDpLm4ETKk9jgzJoylhakUCpcRWN3xVbAuM
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
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
|
||||||
515
package-lock.json
generated
515
package-lock.json
generated
@@ -1,16 +1,20 @@
|
|||||||
{
|
{
|
||||||
"name": "jeopardy",
|
"name": "jeopardy",
|
||||||
"version": "1.0.1",
|
"version": "2.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "jeopardy",
|
"name": "jeopardy",
|
||||||
"version": "1.0.1",
|
"version": "2.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.12.2",
|
||||||
|
"cookie": "^1.0.2"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.2.5",
|
"@eslint/compat": "^1.2.5",
|
||||||
"@eslint/js": "^9.18.0",
|
"@eslint/js": "^9.18.0",
|
||||||
"@sveltejs/adapter-auto": "^6.0.0",
|
"@sveltejs/adapter-node": "^5.3.2",
|
||||||
"@sveltejs/kit": "^2.22.0",
|
"@sveltejs/kit": "^2.22.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
@@ -817,6 +821,112 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@rollup/plugin-commonjs": {
|
||||||
|
"version": "28.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.6.tgz",
|
||||||
|
"integrity": "sha512-XSQB1K7FUU5QP+3lOQmVCE3I0FcbbNvmNT4VJSj93iUjayaARrTQeoRdiYQoftAJBLrR9t2agwAd3ekaTgHNlw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@rollup/pluginutils": "^5.0.1",
|
||||||
|
"commondir": "^1.0.1",
|
||||||
|
"estree-walker": "^2.0.2",
|
||||||
|
"fdir": "^6.2.0",
|
||||||
|
"is-reference": "1.2.1",
|
||||||
|
"magic-string": "^0.30.3",
|
||||||
|
"picomatch": "^4.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.0.0 || 14 >= 14.17"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"rollup": "^2.68.0||^3.0.0||^4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"rollup": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/plugin-commonjs/node_modules/is-reference": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/plugin-json": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@rollup/pluginutils": "^5.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"rollup": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/plugin-node-resolve": {
|
||||||
|
"version": "16.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.1.tgz",
|
||||||
|
"integrity": "sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@rollup/pluginutils": "^5.0.1",
|
||||||
|
"@types/resolve": "1.20.2",
|
||||||
|
"deepmerge": "^4.2.2",
|
||||||
|
"is-module": "^1.0.0",
|
||||||
|
"resolve": "^1.22.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"rollup": "^2.78.0||^3.0.0||^4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"rollup": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/pluginutils": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "^1.0.0",
|
||||||
|
"estree-walker": "^2.0.2",
|
||||||
|
"picomatch": "^4.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"rollup": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.47.1",
|
"version": "4.47.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.47.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.47.1.tgz",
|
||||||
@@ -1114,14 +1224,20 @@
|
|||||||
"acorn": "^8.9.0"
|
"acorn": "^8.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sveltejs/adapter-auto": {
|
"node_modules/@sveltejs/adapter-node": {
|
||||||
"version": "6.1.0",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.3.2.tgz",
|
||||||
"integrity": "sha512-shOuLI5D2s+0zTv2ab5M5PqfknXqWbKi+0UwB9yLTRIdzsK1R93JOO8jNhIYSHdW+IYXIYnLniu+JZqXs7h9Wg==",
|
"integrity": "sha512-nBJSipMb1KLjnAM7uzb+YpnA1VWKb+WdR+0mXEnXI6K1A3XYWbjkcjnW20ubg07sicK8XaGY/FAX3PItw39qBQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@rollup/plugin-commonjs": "^28.0.1",
|
||||||
|
"@rollup/plugin-json": "^6.1.0",
|
||||||
|
"@rollup/plugin-node-resolve": "^16.0.0",
|
||||||
|
"rollup": "^4.9.5"
|
||||||
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@sveltejs/kit": "^2.0.0"
|
"@sveltejs/kit": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sveltejs/kit": {
|
"node_modules/@sveltejs/kit": {
|
||||||
@@ -1163,6 +1279,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@sveltejs/kit/node_modules/cookie": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@sveltejs/vite-plugin-svelte": {
|
"node_modules/@sveltejs/vite-plugin-svelte": {
|
||||||
"version": "6.1.3",
|
"version": "6.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.1.3.tgz",
|
||||||
@@ -1561,6 +1687,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/resolve": {
|
||||||
|
"version": "1.20.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||||
|
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.40.0",
|
"version": "8.40.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz",
|
||||||
@@ -1892,6 +2025,23 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.12.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
|
||||||
|
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.6",
|
||||||
|
"form-data": "^4.0.4",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/axobject-query": {
|
"node_modules/axobject-query": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||||
@@ -1933,6 +2083,19 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/callsites": {
|
"node_modules/callsites": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||||
@@ -2016,6 +2179,25 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/commondir": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -2024,13 +2206,12 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/cookie": {
|
"node_modules/cookie": {
|
||||||
"version": "0.6.0",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
@@ -2096,6 +2277,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||||
@@ -2113,6 +2303,20 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.18.3",
|
"version": "5.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
||||||
@@ -2127,6 +2331,51 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-object-atoms": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-set-tostringtag": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.6",
|
||||||
|
"has-tostringtag": "^1.0.2",
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.9",
|
"version": "0.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz",
|
||||||
@@ -2394,6 +2643,13 @@
|
|||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/estree-walker": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/esutils": {
|
"node_modules/esutils": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||||
@@ -2547,6 +2803,42 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||||
|
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
@@ -2562,6 +2854,52 @@
|
|||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.1",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/glob-parent": {
|
"node_modules/glob-parent": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
@@ -2588,6 +2926,18 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/graceful-fs": {
|
"node_modules/graceful-fs": {
|
||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
@@ -2612,6 +2962,45 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-tostringtag": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"has-symbols": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
@@ -2649,6 +3038,22 @@
|
|||||||
"node": ">=0.8.19"
|
"node": ">=0.8.19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-core-module": {
|
||||||
|
"version": "2.16.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
||||||
|
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-extglob": {
|
"node_modules/is-extglob": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
@@ -2672,6 +3077,13 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-module": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/is-number": {
|
"node_modules/is-number": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||||
@@ -3073,6 +3485,15 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/merge2": {
|
"node_modules/merge2": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
@@ -3110,6 +3531,27 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
@@ -3298,6 +3740,13 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/path-parse": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -3579,6 +4028,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
@@ -3624,6 +4079,27 @@
|
|||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/resolve": {
|
||||||
|
"version": "1.22.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
||||||
|
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-core-module": "^2.16.0",
|
||||||
|
"path-parse": "^1.0.7",
|
||||||
|
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"resolve": "bin/resolve"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/resolve-from": {
|
"node_modules/resolve-from": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||||
@@ -3816,6 +4292,19 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/supports-preserve-symlinks-flag": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/svelte": {
|
"node_modules/svelte": {
|
||||||
"version": "5.38.2",
|
"version": "5.38.2",
|
||||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.38.2.tgz",
|
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.38.2.tgz",
|
||||||
|
|||||||
12
package.json
12
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "jeopardy",
|
"name": "jeopardy",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.1",
|
"version": "2.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
@@ -11,12 +11,14 @@
|
|||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"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-dev": "./docker-dev.sh"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.2.5",
|
"@eslint/compat": "^1.2.5",
|
||||||
"@eslint/js": "^9.18.0",
|
"@eslint/js": "^9.18.0",
|
||||||
"@sveltejs/adapter-auto": "^6.0.0",
|
"@sveltejs/adapter-node": "^5.3.2",
|
||||||
"@sveltejs/kit": "^2.22.0",
|
"@sveltejs/kit": "^2.22.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
@@ -33,5 +35,9 @@
|
|||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"typescript-eslint": "^8.20.0",
|
"typescript-eslint": "^8.20.0",
|
||||||
"vite": "^7.0.4"
|
"vite": "^7.0.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.12.2",
|
||||||
|
"cookie": "^1.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/app.css
11
src/app.css
@@ -17,6 +17,17 @@
|
|||||||
background-color: rgba(0, 0, 0, 0.1);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
.inputField {
|
.inputField {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"
|
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"
|
||||||
/>
|
/>
|
||||||
|
<script src="https://kit.fontawesome.com/4115dc7344.js" crossorigin="anonymous"></script>
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover" class="size-full">
|
<body data-sveltekit-preload-data="hover" class="size-full">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import AudioPlayerComponent from "./AudioPlayerComponent.svelte";
|
import AudioPlayerComponent from "./AudioPlayerComponent.svelte";
|
||||||
import type { AudioMultipleChoiceQuestion } from "./games/games";
|
import type { AudioMultipleChoiceQuestion } from "./games/games";
|
||||||
|
import { url } from "./util";
|
||||||
|
|
||||||
const path = "/sounds/";
|
const path = "/sounds/";
|
||||||
|
|
||||||
@@ -9,10 +10,19 @@
|
|||||||
showAnswer: boolean;
|
showAnswer: boolean;
|
||||||
showQuestion: boolean;
|
showQuestion: boolean;
|
||||||
showPlayer: boolean;
|
showPlayer: boolean;
|
||||||
|
isLegacy?: boolean;
|
||||||
|
randomize?: boolean;
|
||||||
[key: string]: unknown;
|
[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[]) {
|
function shuffle<T>(array: T[]) {
|
||||||
let currentIndex = array.length;
|
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];
|
let _choices = $derived.by(() => {
|
||||||
shuffle(_choices);
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="mb-4 flex grow flex-col items-center text-6xl">
|
<div class="mb-4 flex grow flex-col items-center text-6xl">
|
||||||
@@ -42,7 +66,16 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#if showPlayer}
|
{#if showPlayer}
|
||||||
<div class="flex w-full flex-col justify-center">
|
<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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if showQuestion}
|
{#if showQuestion}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import AudioPlayerComponent from "./AudioPlayerComponent.svelte";
|
import AudioPlayerComponent from "./AudioPlayerComponent.svelte";
|
||||||
import type { AudioQuestion } from "./games/games";
|
import type { AudioQuestion } from "./games/games";
|
||||||
|
import { url } from "./util";
|
||||||
|
|
||||||
const path = "/sounds/";
|
const path = "/sounds/";
|
||||||
|
|
||||||
@@ -9,10 +10,22 @@
|
|||||||
showAnswer: boolean;
|
showAnswer: boolean;
|
||||||
showQuestion: boolean;
|
showQuestion: boolean;
|
||||||
showPlayer: boolean;
|
showPlayer: boolean;
|
||||||
|
isLegacy?: boolean;
|
||||||
[key: string]: unknown;
|
[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>
|
</script>
|
||||||
|
|
||||||
<div class="mb-4 flex grow flex-col items-center text-6xl">
|
<div class="mb-4 flex grow flex-col items-center text-6xl">
|
||||||
@@ -23,7 +36,16 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#if showPlayer}
|
{#if showPlayer}
|
||||||
<div class="flex w-full flex-col justify-center">
|
<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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if showAnswer}
|
{#if showAnswer}
|
||||||
|
|||||||
26
src/lib/Auth.svelte.ts
Normal file
26
src/lib/Auth.svelte.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { env } from "$env/dynamic/public";
|
||||||
|
import UserSvelte, { type UserObj } from "./User.svelte";
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
|
|
||||||
|
export async function isAuthenticated() {
|
||||||
|
if (location.pathname.startsWith("/login")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return axios
|
||||||
|
.get(`${env.PUBLIC_JEOPARDY_SERVER_PROTOCOL}://${env.PUBLIC_JEOPARDY_SERVER}/auth`, {
|
||||||
|
withCredentials: true
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (res.status === 200) {
|
||||||
|
UserSvelte.user = res.data as UserObj;
|
||||||
|
console.log(UserSvelte.id, UserSvelte.username, UserSvelte.role);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
goto("/login");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
goto("/login");
|
||||||
|
});
|
||||||
|
}
|
||||||
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 {
|
interface Player {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -8,10 +8,9 @@ interface Player {
|
|||||||
const baseRoute = "/connected/display/running";
|
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
|
let game: Game | undefined = $state();
|
||||||
let game: Game | undefined = undefined;
|
|
||||||
let gameIndex: number = -1;
|
let gameIndex: number = -1;
|
||||||
let wall: Wall | undefined = undefined;
|
let wall: Wall | undefined = $state();
|
||||||
let wallIndex: number = -1;
|
let wallIndex: number = -1;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ImageMultipleChoiceQuestion } from "./games/games";
|
import type { ImageMultipleChoiceQuestion } from "./games/games";
|
||||||
|
import { url } from "./util";
|
||||||
|
|
||||||
const path = "/images/";
|
const path = "/images/";
|
||||||
|
|
||||||
@@ -8,6 +9,8 @@
|
|||||||
showAnswer: boolean;
|
showAnswer: boolean;
|
||||||
showQuestion: boolean;
|
showQuestion: boolean;
|
||||||
isBuzzed: boolean;
|
isBuzzed: boolean;
|
||||||
|
isLegacy?: boolean;
|
||||||
|
randomize?: boolean;
|
||||||
[key: string]: unknown;
|
[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];
|
let _choices = $derived.by(() => {
|
||||||
shuffle(_choices);
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="mb-4 flex w-full grow flex-col items-center gap-2 text-6xl">
|
<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 class="text-center">{question.data.question}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container grow-6">
|
<div class="container grow-6">
|
||||||
<img
|
{#if imagePath}
|
||||||
src={path + question.data.image}
|
<img src={imagePath} alt={imagePath} class={isBuzzed ? "blurry" : ""} />
|
||||||
alt={path + question.data.image}
|
{:else}
|
||||||
class={isBuzzed ? "blurry" : ""}
|
<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>
|
||||||
<div class="flex w-full grow-1 flex-wrap items-center justify-around gap-2">
|
<div class="flex w-full grow-1 flex-wrap items-center justify-around gap-2">
|
||||||
{#each _choices as choice}
|
{#each _choices as choice}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ImageQuestion } from "./games/games";
|
import type { ImageQuestion } from "./games/games";
|
||||||
|
import { isRessource } from "./Types";
|
||||||
|
import { url } from "./util";
|
||||||
|
|
||||||
const path = "/images/";
|
const path = "/images/";
|
||||||
|
|
||||||
@@ -8,10 +10,22 @@
|
|||||||
showAnswer: boolean;
|
showAnswer: boolean;
|
||||||
showQuestion: boolean;
|
showQuestion: boolean;
|
||||||
isBuzzed: boolean;
|
isBuzzed: boolean;
|
||||||
|
isLegacy?: boolean;
|
||||||
[key: string]: unknown;
|
[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>
|
</script>
|
||||||
|
|
||||||
<div class="mb-4 flex w-full grow flex-col items-center gap-2 text-6xl">
|
<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 class="text-center">{question.data.question}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container grow-6">
|
<div class="container grow-6">
|
||||||
<img
|
{#if imagePath}
|
||||||
src={path + question.data.image}
|
<img src={imagePath} alt={imagePath} class={isBuzzed ? "blurry" : ""} />
|
||||||
alt={path + question.data.image}
|
{:else}
|
||||||
class={isBuzzed ? "blurry" : ""}
|
<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>
|
||||||
{/if}
|
{/if}
|
||||||
{#if showAnswer}
|
{#if showAnswer}
|
||||||
|
|||||||
96
src/lib/Modal.svelte
Normal file
96
src/lib/Modal.svelte
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
showModal: boolean;
|
||||||
|
header: Snippet;
|
||||||
|
children: Snippet;
|
||||||
|
actionButtons?: Snippet;
|
||||||
|
cancelFn?: () => void;
|
||||||
|
okFn: () => Promise<boolean>;
|
||||||
|
oncloseFn?: () => void;
|
||||||
|
okButtonText?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
width?: string;
|
||||||
|
height?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
showModal = $bindable(),
|
||||||
|
header,
|
||||||
|
children,
|
||||||
|
cancelFn,
|
||||||
|
okFn,
|
||||||
|
oncloseFn,
|
||||||
|
actionButtons,
|
||||||
|
okButtonText = "Ok",
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
let dialog: HTMLDialogElement | undefined = $state(); // HTMLDialogElement
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (showModal) dialog?.showModal();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions -->
|
||||||
|
<dialog
|
||||||
|
bind:this={dialog}
|
||||||
|
onclose={() => {
|
||||||
|
showModal = false;
|
||||||
|
if (oncloseFn) oncloseFn();
|
||||||
|
}}
|
||||||
|
onclick={(e) => {
|
||||||
|
if (e.target === dialog) dialog.close();
|
||||||
|
}}
|
||||||
|
class="rounded-md"
|
||||||
|
style:width
|
||||||
|
style:height
|
||||||
|
>
|
||||||
|
<div class="flex h-full flex-col gap-4 p-4">
|
||||||
|
{@render header?.()}
|
||||||
|
<div class="grow overflow-y-auto">
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
|
<!-- <div class="grow"></div> -->
|
||||||
|
<!-- svelte-ignore a11y_autofocus -->
|
||||||
|
<div class="flex justify-end gap-4">
|
||||||
|
{@render actionButtons?.()}
|
||||||
|
<button
|
||||||
|
autofocus
|
||||||
|
onclick={() => {
|
||||||
|
dialog?.close();
|
||||||
|
if (cancelFn) cancelFn();
|
||||||
|
}}
|
||||||
|
class="btn">Abbrechen</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
autofocus
|
||||||
|
onclick={async () => {
|
||||||
|
if (await okFn()) {
|
||||||
|
dialog?.close();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
class="btn min-w-[64px]">{okButtonText}</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
dialog {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
/* Move it back 50% relative to self */
|
||||||
|
-webkit-transform: translateX(-50%) translateY(-50%);
|
||||||
|
-moz-transform: translateX(-50%) translateY(-50%);
|
||||||
|
-ms-transform: translateX(-50%) translateY(-50%);
|
||||||
|
transform: translateX(-50%) translateY(-50%);
|
||||||
|
}
|
||||||
|
dialog::backdrop {
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
question: MultipleChoiceQuestion;
|
question: MultipleChoiceQuestion;
|
||||||
showAnswer: boolean;
|
showAnswer: boolean;
|
||||||
showQuestion: boolean;
|
showQuestion: boolean;
|
||||||
|
randomize?: boolean;
|
||||||
[key: string]: unknown;
|
[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];
|
let _choices = $derived.by(() => {
|
||||||
shuffle(_choices);
|
let c = [...question.data.choices];
|
||||||
|
if (randomize) shuffle(c);
|
||||||
|
return c;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mb-4 flex grow flex-col items-center text-6xl">
|
<div class="mb-4 flex grow flex-col items-center text-6xl">
|
||||||
|
|||||||
451
src/lib/RessourceManager.svelte
Normal file
451
src/lib/RessourceManager.svelte
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
<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";
|
||||||
|
|
||||||
|
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>
|
||||||
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 +1,84 @@
|
|||||||
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;
|
||||||
|
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 | string | null
|
||||||
|
): ressource is Ressource {
|
||||||
|
if (ressource === null) return false;
|
||||||
|
return (ressource as Directory).isDir === undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type _id = string;
|
||||||
|
|
||||||
|
export type GameId = _id;
|
||||||
|
|
||||||
|
export type Game = {
|
||||||
|
name: string;
|
||||||
|
owner: _id;
|
||||||
|
_id: GameId;
|
||||||
|
walls: WallId[];
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
35
src/lib/User.svelte.ts
Normal file
35
src/lib/User.svelte.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
let username: string = $state("");
|
||||||
|
let role: string = $state("");
|
||||||
|
let id: string = $state("");
|
||||||
|
|
||||||
|
export type UserObj = {
|
||||||
|
username: string;
|
||||||
|
role: string;
|
||||||
|
_id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
get username(): string {
|
||||||
|
return username;
|
||||||
|
},
|
||||||
|
set username(uname: string) {
|
||||||
|
username = uname;
|
||||||
|
},
|
||||||
|
get role(): string {
|
||||||
|
return role;
|
||||||
|
},
|
||||||
|
set role(newrole: string) {
|
||||||
|
role = newrole;
|
||||||
|
},
|
||||||
|
get id(): string {
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
set id(newid: string) {
|
||||||
|
id = newid;
|
||||||
|
},
|
||||||
|
set user(userobj: UserObj) {
|
||||||
|
username = userobj.username;
|
||||||
|
role = userobj.role;
|
||||||
|
id = userobj._id;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,47 +1,145 @@
|
|||||||
<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";
|
||||||
|
import Button from "./Button.svelte";
|
||||||
|
import Modal from "./Modal.svelte";
|
||||||
|
import Textfield from "./Textfield.svelte";
|
||||||
|
import { fetchCategory } from "../routes/editor/fetchers";
|
||||||
|
|
||||||
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;
|
isEditor?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isVisited(catIndex: number, queIndex: number): boolean {
|
let { wall, onclick, onclickIds, visited, isEditor = false }: Props = $props();
|
||||||
return visited[catIndex] && visited[catIndex].includes(queIndex);
|
|
||||||
|
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>
|
</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 gap-2 text-3xl font-semibold">
|
||||||
</div>
|
<div>{category.name}</div>
|
||||||
{#each category.questions as question, queIndex}
|
{#if isEditor}
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<Button
|
||||||
<div
|
onclick={() => {
|
||||||
class="card {isVisited(catIndex, queIndex) ? 'visited' : ''}"
|
catToRename = category;
|
||||||
role="button"
|
newCatName = category.name;
|
||||||
aria-pressed="false"
|
showRenameCategory = true;
|
||||||
tabindex="0"
|
}}><i class="fa-solid fa-pen"></i></Button
|
||||||
onclick={() => {
|
>
|
||||||
if (onclick) onclick(catIndex, queIndex);
|
{/if}
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="text-6xl font-thin">{question.points}</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{#each category.questions as question}
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
|
<div
|
||||||
|
class="card {visited.includes(`${question._id}`) ? '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}
|
||||||
|
Legacy Not Supported
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<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;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { Ressource } from "$lib/Types";
|
||||||
|
|
||||||
const games: Games = [
|
const games: Games = [
|
||||||
{
|
{
|
||||||
name: "LAN Party",
|
name: "LAN Party",
|
||||||
@@ -1331,8 +1333,10 @@ export type QuestionType =
|
|||||||
| "AUDIO_MULTIPLE_CHOICE";
|
| "AUDIO_MULTIPLE_CHOICE";
|
||||||
|
|
||||||
export type Question = {
|
export type Question = {
|
||||||
|
_id?: string;
|
||||||
points: number;
|
points: number;
|
||||||
type: QuestionType;
|
type: QuestionType;
|
||||||
|
owner?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SimpleQuestion = Question & {
|
export type SimpleQuestion = Question & {
|
||||||
@@ -1354,11 +1358,13 @@ export type MultipleChoiceQuestion = Question & {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type QuestionRessource = string | Ressource | null;
|
||||||
|
|
||||||
export type ImageQuestion = Question & {
|
export type ImageQuestion = Question & {
|
||||||
type: "IMAGE";
|
type: "IMAGE";
|
||||||
data: {
|
data: {
|
||||||
question: string;
|
question: string;
|
||||||
image: string;
|
image: QuestionRessource;
|
||||||
answer: string;
|
answer: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1367,7 +1373,7 @@ export type ImageMultipleChoiceQuestion = Question & {
|
|||||||
type: "IMAGE_MULTIPLE_CHOICE";
|
type: "IMAGE_MULTIPLE_CHOICE";
|
||||||
data: {
|
data: {
|
||||||
question: string;
|
question: string;
|
||||||
image: string;
|
image: QuestionRessource;
|
||||||
choices: string[];
|
choices: string[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1376,7 +1382,7 @@ export type AudioQuestion = Question & {
|
|||||||
type: "AUDIO";
|
type: "AUDIO";
|
||||||
data: {
|
data: {
|
||||||
question: string;
|
question: string;
|
||||||
audio: string;
|
audio: QuestionRessource;
|
||||||
answer: string;
|
answer: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1385,7 +1391,7 @@ export type AudioMultipleChoiceQuestion = Question & {
|
|||||||
type: "AUDIO_MULTIPLE_CHOICE";
|
type: "AUDIO_MULTIPLE_CHOICE";
|
||||||
data: {
|
data: {
|
||||||
question: string;
|
question: string;
|
||||||
audio: string;
|
audio: QuestionRessource;
|
||||||
choices: string[];
|
choices: string[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1425,16 +1431,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;
|
||||||
|
|||||||
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,3 +1,5 @@
|
|||||||
|
import { env } from "$env/dynamic/public";
|
||||||
|
|
||||||
export enum SocketConnectionType {
|
export enum SocketConnectionType {
|
||||||
NONE = "NONE",
|
NONE = "NONE",
|
||||||
HOST = "HOST",
|
HOST = "HOST",
|
||||||
@@ -11,7 +13,9 @@ let socket: WebSocket | undefined;
|
|||||||
|
|
||||||
const connectAsHost = () => {
|
const connectAsHost = () => {
|
||||||
if (socket !== undefined) return;
|
if (socket !== undefined) return;
|
||||||
socket = new WebSocket("ws://127.0.0.1:12345");
|
socket = new WebSocket(
|
||||||
|
`${location.protocol === "https:" ? "wss" : "ws"}://${env.PUBLIC_JEOPARDY_SERVER ?? "127.0.0.1:12345"}/websocket`
|
||||||
|
);
|
||||||
socket.addEventListener("open", onOpen(SocketConnectionType.HOST));
|
socket.addEventListener("open", onOpen(SocketConnectionType.HOST));
|
||||||
socket.addEventListener("message", onFirstMessage);
|
socket.addEventListener("message", onFirstMessage);
|
||||||
socket.addEventListener("close", onClose);
|
socket.addEventListener("close", onClose);
|
||||||
@@ -20,7 +24,9 @@ const connectAsHost = () => {
|
|||||||
|
|
||||||
const connectAsDisplay = () => {
|
const connectAsDisplay = () => {
|
||||||
if (socket !== undefined) return;
|
if (socket !== undefined) return;
|
||||||
socket = new WebSocket("ws://127.0.0.1:12345");
|
socket = new WebSocket(
|
||||||
|
`${location.protocol === "https:" ? "wss" : "ws"}://${env.PUBLIC_JEOPARDY_SERVER ?? "127.0.0.1:12345"}/websocket`
|
||||||
|
);
|
||||||
socket.addEventListener("open", onOpen(SocketConnectionType.DISPLAY));
|
socket.addEventListener("open", onOpen(SocketConnectionType.DISPLAY));
|
||||||
socket.addEventListener("message", onFirstMessage);
|
socket.addEventListener("message", onFirstMessage);
|
||||||
};
|
};
|
||||||
@@ -37,10 +43,12 @@ const sendMessage = (obj: unknown) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function onOpen(type: SocketConnectionType) {
|
function onOpen(type: SocketConnectionType) {
|
||||||
return (event: Event) => {
|
return async (event: Event) => {
|
||||||
console.log("Connection established");
|
console.log("Connection established");
|
||||||
console.log(event);
|
console.log(event);
|
||||||
if (socket === undefined) return;
|
if (socket === undefined) return;
|
||||||
|
// somehow beeing to fast so have to wait some time
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
socket.send(type.toString());
|
socket.send(type.toString());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,28 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "../app.css";
|
import "../app.css";
|
||||||
import favicon from "$lib/assets/favicon.svg";
|
import favicon from "$lib/assets/favicon.svg";
|
||||||
|
import { afterNavigate } from "$app/navigation";
|
||||||
|
import { isAuthenticated } from "$lib/Auth.svelte";
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
||||||
|
let renderit = $state(false);
|
||||||
|
|
||||||
|
afterNavigate(() => {
|
||||||
|
isAuthenticated()
|
||||||
|
.then(() => {
|
||||||
|
renderit = true;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
renderit = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<link rel="icon" href={favicon} />
|
<link rel="icon" href={favicon} />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{@render children?.()}
|
{#if renderit}
|
||||||
|
{@render children?.()}
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
|
import { env } from "$env/dynamic/public";
|
||||||
|
import RessourceManager from "$lib/RessourceManager.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";
|
||||||
|
|
||||||
|
let showRessourceManager = $state(false);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (websocket.connectionType === SocketConnectionType.HOST) {
|
if (websocket.connectionType === SocketConnectionType.HOST) {
|
||||||
@@ -12,17 +18,80 @@
|
|||||||
goto("/connected/display");
|
goto("/connected/display");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
cookieStore
|
||||||
|
.delete("jeopardytoken")
|
||||||
|
.then(() => {
|
||||||
|
goto("/login");
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
alert("Logout fehlgeschlagen!");
|
||||||
|
goto("/login");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logoutFromAllDevices() {
|
||||||
|
axios
|
||||||
|
.post(
|
||||||
|
`${env.PUBLIC_JEOPARDY_SERVER_PROTOCOL}://${env.PUBLIC_JEOPARDY_SERVER}/user/logout`,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
withCredentials: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
logout();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
alert("Logout fehlgeschlagen!");
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-full flex-col">
|
<div class="flex h-full flex-col">
|
||||||
<h1 class="m-4 mb-8 text-7xl font-bold">Jeopardy</h1>
|
<div class="ms-4 me-4 flex items-center gap-4">
|
||||||
|
<h1 class="text-7xl font-bold">Jeopardy</h1>
|
||||||
|
<div class="grow"></div>
|
||||||
|
{#if UserSvelte.role === "admin"}
|
||||||
|
<button type="button" class="btn" onclick={() => goto("/admin")}>Administration</button>
|
||||||
|
{/if}
|
||||||
|
<button type="button" class="btn" onclick={() => (showRessourceManager = true)}
|
||||||
|
>Ressourcen</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">
|
||||||
|
<i class="fa-regular fa-user"></i>
|
||||||
|
{UserSvelte.username}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex h-full grow items-center justify-around p-4">
|
<div class="flex h-full grow items-center justify-around p-4">
|
||||||
<button class="btn m-2 h-1/2 w-1/2 text-5xl" onclick={websocket.connectAsHost}
|
<button class="btn m-2 h-1/2 w-1/2 text-5xl" onclick={websocket.connectAsHost}
|
||||||
>Connect as Host</button
|
>Spiel hosten</button
|
||||||
>
|
>
|
||||||
<button class="btn m-2 h-1/2 w-1/2 text-5xl" onclick={websocket.connectAsDisplay}
|
<button class="btn m-2 h-1/2 w-1/2 text-5xl" onclick={websocket.connectAsDisplay}
|
||||||
>Connect as Display</button
|
>Spiel darstellen</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<RessourceManager
|
||||||
|
bind:show={showRessourceManager}
|
||||||
|
ok={(res) => {
|
||||||
|
console.log(res);
|
||||||
|
}}
|
||||||
|
></RessourceManager>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.profile {
|
||||||
|
border-color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile:hover {
|
||||||
|
background-color: unset !important;
|
||||||
|
cursor: unset !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
350
src/routes/admin/+page.svelte
Normal file
350
src/routes/admin/+page.svelte
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { afterNavigate, goto } from "$app/navigation";
|
||||||
|
import type { UserObj } from "$lib/User.svelte";
|
||||||
|
import { env } from "$env/dynamic/public";
|
||||||
|
import axios from "axios";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import Modal from "$lib/Modal.svelte";
|
||||||
|
import UserSvelte from "$lib/User.svelte";
|
||||||
|
|
||||||
|
let users: UserObj[] = $state([]);
|
||||||
|
let roles: string[] = $state([]);
|
||||||
|
|
||||||
|
let showAddUser = $state(false);
|
||||||
|
let addUserName = $state("");
|
||||||
|
|
||||||
|
let showDeleteUser = $state(false);
|
||||||
|
let showResetPassword = $state(false);
|
||||||
|
let showChangeRole = $state(false);
|
||||||
|
let selectedUser: UserObj | undefined = $state(undefined);
|
||||||
|
let roleToChange: string = $state("");
|
||||||
|
|
||||||
|
let error = $state("");
|
||||||
|
|
||||||
|
async function loadUsers() {
|
||||||
|
console.log("loading users");
|
||||||
|
return axios
|
||||||
|
.get(
|
||||||
|
`${env.PUBLIC_JEOPARDY_SERVER_PROTOCOL}://${env.PUBLIC_JEOPARDY_SERVER}/admin/user/list`,
|
||||||
|
{
|
||||||
|
withCredentials: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
users = response.data;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRoles() {
|
||||||
|
console.log("loading roles");
|
||||||
|
return axios
|
||||||
|
.get(
|
||||||
|
`${env.PUBLIC_JEOPARDY_SERVER_PROTOCOL}://${env.PUBLIC_JEOPARDY_SERVER}/admin/roles`,
|
||||||
|
{
|
||||||
|
withCredentials: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
roles = response.data;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
afterNavigate(() => {
|
||||||
|
if (UserSvelte.role !== "admin") goto("/");
|
||||||
|
});
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (UserSvelte.role !== "admin") goto("/");
|
||||||
|
loadRoles().then(loadUsers);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function addUserOk(): Promise<boolean> {
|
||||||
|
error = "";
|
||||||
|
if (addUserName.length <= 0) {
|
||||||
|
error = "Gib einen Nutzernamen ein";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return axios
|
||||||
|
.put(
|
||||||
|
`${env.PUBLIC_JEOPARDY_SERVER_PROTOCOL}://${env.PUBLIC_JEOPARDY_SERVER}/admin/user`,
|
||||||
|
{
|
||||||
|
username: addUserName
|
||||||
|
},
|
||||||
|
{
|
||||||
|
withCredentials: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
alert(`Passwort: ${response.data.password}`);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
error = "Etwas ist schief gelaufen";
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteUserOk(): Promise<boolean> {
|
||||||
|
error = "";
|
||||||
|
|
||||||
|
if (selectedUser === undefined) {
|
||||||
|
error = "Etwas ist schief gelaufen. Kein Nutzer zum Löschen definiert.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return axios
|
||||||
|
.delete(
|
||||||
|
`${env.PUBLIC_JEOPARDY_SERVER_PROTOCOL}://${env.PUBLIC_JEOPARDY_SERVER}/admin/user`,
|
||||||
|
{
|
||||||
|
withCredentials: true,
|
||||||
|
data: {
|
||||||
|
userid: selectedUser._id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
error = "Nutzer konnte nicht gelöscht werden";
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetPasswordOk(): Promise<boolean> {
|
||||||
|
error = "";
|
||||||
|
|
||||||
|
if (selectedUser === undefined) {
|
||||||
|
error = "Etwas ist schief gelaufen. Kein Nutzer zum Löschen definiert.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return axios
|
||||||
|
.post(
|
||||||
|
`${env.PUBLIC_JEOPARDY_SERVER_PROTOCOL}://${env.PUBLIC_JEOPARDY_SERVER}/admin/user/resetpw`,
|
||||||
|
{
|
||||||
|
userid: selectedUser._id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
withCredentials: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
alert(`Passwort: ${response.data.password}`);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
error = "Etwas ist schief gelaufen";
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changeRoleOk(): Promise<boolean> {
|
||||||
|
error = "";
|
||||||
|
|
||||||
|
if (selectedUser === undefined) {
|
||||||
|
error = "Etwas ist schief gelaufen. Kein Nutzer zum Löschen definiert.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roleToChange === selectedUser.role) {
|
||||||
|
error = `${selectedUser.username} hat bereits die Rolle ${roleToChange}. Wähle eine andere Rolle aus.`;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return axios
|
||||||
|
.post(
|
||||||
|
`${env.PUBLIC_JEOPARDY_SERVER_PROTOCOL}://${env.PUBLIC_JEOPARDY_SERVER}/admin/user/changerole`,
|
||||||
|
{
|
||||||
|
userid: selectedUser._id,
|
||||||
|
role: roleToChange
|
||||||
|
},
|
||||||
|
{
|
||||||
|
withCredentials: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
error = "Etwas ist schief gelaufen";
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addUserCancel() {
|
||||||
|
error = "";
|
||||||
|
addUserName = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectedUserCancel() {
|
||||||
|
error = "";
|
||||||
|
selectedUser = undefined;
|
||||||
|
roleToChange = "";
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-4 p-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<h2 class="text-4xl font-bold">Administration</h2>
|
||||||
|
<div class="grow"></div>
|
||||||
|
<button type="button" class="btn" onclick={() => goto("/")}>Zurück</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
{#each users as user (user._id)}
|
||||||
|
<div class="flex justify-between gap-4 rounded-md border-1 p-2">
|
||||||
|
<div class="shrink-0 grow-0 text-center align-middle">{user._id}</div>
|
||||||
|
<div>{user.username}</div>
|
||||||
|
<div>{user.role}</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<!-- svelte-ignore a11y_consider_explicit_label -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn"
|
||||||
|
disabled={user._id === UserSvelte.id}
|
||||||
|
onclick={() => {
|
||||||
|
selectedUser = user;
|
||||||
|
roleToChange = selectedUser.role;
|
||||||
|
showChangeRole = true;
|
||||||
|
}}><i class="fa-solid fa-user"></i> Rolle ändern</button
|
||||||
|
>
|
||||||
|
<!-- svelte-ignore a11y_consider_explicit_label -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn"
|
||||||
|
disabled={user._id === UserSvelte.id}
|
||||||
|
onclick={() => {
|
||||||
|
selectedUser = user;
|
||||||
|
showResetPassword = true;
|
||||||
|
}}
|
||||||
|
><i class="fa-solid fa-arrow-rotate-right"></i> Passwort zurücksetzen</button
|
||||||
|
>
|
||||||
|
<!-- svelte-ignore a11y_consider_explicit_label -->
|
||||||
|
<button
|
||||||
|
disabled={user._id === UserSvelte.id}
|
||||||
|
type="button"
|
||||||
|
class="btn border-red-600 text-red-600"
|
||||||
|
onclick={() => {
|
||||||
|
selectedUser = user;
|
||||||
|
showDeleteUser = true;
|
||||||
|
}}><i class="fa-solid fa-trash"></i></button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn" onclick={() => (showAddUser = true)}>Nutzer hinzufügen</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal bind:showModal={showAddUser} okFn={addUserOk} cancelFn={addUserCancel} oncloseFn={loadUsers}>
|
||||||
|
{#snippet header()}
|
||||||
|
<h2 class="text-3xl">Nutzer hinzufügen</h2>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="username" class="">Name</label>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
id="username"
|
||||||
|
class="borders mt-2 mb-2 w-full"
|
||||||
|
bind:value={addUserName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{#if error.length > 0}
|
||||||
|
<div class="text-red-700">{error}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
bind:showModal={showDeleteUser}
|
||||||
|
okFn={deleteUserOk}
|
||||||
|
cancelFn={selectedUserCancel}
|
||||||
|
oncloseFn={loadUsers}
|
||||||
|
>
|
||||||
|
{#snippet header()}
|
||||||
|
<h2 class="text-3xl">Nutzer löschen</h2>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<div>Soll Nutzer {selectedUser?.username} wirklich gelöscht werden?</div>
|
||||||
|
{#if error.length > 0}
|
||||||
|
<div class="text-red-700">{error}</div>
|
||||||
|
{/if}
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
bind:showModal={showResetPassword}
|
||||||
|
okFn={resetPasswordOk}
|
||||||
|
cancelFn={selectedUserCancel}
|
||||||
|
oncloseFn={loadUsers}
|
||||||
|
>
|
||||||
|
{#snippet header()}
|
||||||
|
<h2 class="text-3xl">Passwort zurücksetzen</h2>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
Soll das Passwort von Nutzer {selectedUser?.username} wirklich zurückgesetzt werden?
|
||||||
|
</div>
|
||||||
|
{#if error.length > 0}
|
||||||
|
<div class="text-red-700">{error}</div>
|
||||||
|
{/if}
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
bind:showModal={showChangeRole}
|
||||||
|
okFn={changeRoleOk}
|
||||||
|
cancelFn={selectedUserCancel}
|
||||||
|
oncloseFn={loadUsers}
|
||||||
|
>
|
||||||
|
{#snippet header()}
|
||||||
|
<h2 class="text-3xl">Rolle von {selectedUser?.username} ändern</h2>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{#if selectedUser}
|
||||||
|
<select name="roles" id="role-select" class="btn w-full" bind:value={roleToChange}>
|
||||||
|
{#each roles as role, index (index)}
|
||||||
|
<option value={role}>{role}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{/if}
|
||||||
|
</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>
|
||||||
@@ -2,29 +2,17 @@
|
|||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
||||||
import { error } from "@sveltejs/kit";
|
import { error } from "@sveltejs/kit";
|
||||||
import games from "$lib/games/games";
|
// import games from "$lib/games/games";
|
||||||
import { page } from "$app/state";
|
import { page } from "$app/state";
|
||||||
import DisplayStateSvelte from "$lib/DisplayState.svelte";
|
import DisplayStateSvelte from "$lib/DisplayState.svelte";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { fetchGame } from "../../../../editor/fetchers";
|
||||||
|
|
||||||
console.log("game:", page.params.game);
|
onMount(() => {
|
||||||
|
fetchGame(`${page.params.game}`).then((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) {
|
|
||||||
DisplayStateSvelte.game = game;
|
DisplayStateSvelte.game = game;
|
||||||
DisplayStateSvelte.gameIndex = gameIndex;
|
});
|
||||||
} else {
|
});
|
||||||
error(404);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex grow flex-col pr-4 pl-4">
|
<div class="flex grow flex-col pr-4 pl-4">
|
||||||
|
|||||||
@@ -6,27 +6,11 @@
|
|||||||
import ws from "$lib/websocket.svelte";
|
import ws from "$lib/websocket.svelte";
|
||||||
import { MessageType } from "$lib/MessageType";
|
import { MessageType } from "$lib/MessageType";
|
||||||
import type { VisitedQuestions } from "$lib/Types";
|
import type { VisitedQuestions } from "$lib/Types";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { fetchWall } from "../../../../../editor/fetchers";
|
||||||
|
|
||||||
console.log("wall:", page.params.wall);
|
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([]);
|
let visited: VisitedQuestions = $state([]);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -40,6 +24,12 @@
|
|||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
fetchWall(`${page.params.wall}`).then((wall) => {
|
||||||
|
DisplayStateSvelte.wall = wall;
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Wall wall={DisplayStateSvelte.wall} {visited} />
|
<Wall wall={DisplayStateSvelte.wall} {visited} />
|
||||||
|
|||||||
@@ -9,65 +9,22 @@
|
|||||||
isImageMultipleChoiceQuestion,
|
isImageMultipleChoiceQuestion,
|
||||||
isImageQuestion,
|
isImageQuestion,
|
||||||
isMultipleChoiceQuestion,
|
isMultipleChoiceQuestion,
|
||||||
isSimpleQuestion
|
isSimpleQuestion,
|
||||||
|
type Question
|
||||||
} from "$lib/games/games";
|
} from "$lib/games/games";
|
||||||
import ws from "$lib/websocket.svelte";
|
import ws from "$lib/websocket.svelte";
|
||||||
import { MessageType } from "$lib/MessageType";
|
import { MessageType } from "$lib/MessageType";
|
||||||
import { untrack } from "svelte";
|
import { onMount, untrack } from "svelte";
|
||||||
import MultipleChoiceQuestionComponent from "$lib/MultipleChoiceQuestionComponent.svelte";
|
import MultipleChoiceQuestionComponent from "$lib/MultipleChoiceQuestionComponent.svelte";
|
||||||
import ImageQuestionComponent from "$lib/ImageQuestionComponent.svelte";
|
import ImageQuestionComponent from "$lib/ImageQuestionComponent.svelte";
|
||||||
import AudioQuestionComponent from "$lib/AudioQuestionComponent.svelte";
|
import AudioQuestionComponent from "$lib/AudioQuestionComponent.svelte";
|
||||||
import AudioMultipleChoiceQuestionComponent from "$lib/AudioMultipleChoiceQuestionComponent.svelte";
|
import AudioMultipleChoiceQuestionComponent from "$lib/AudioMultipleChoiceQuestionComponent.svelte";
|
||||||
import ImageMultipleChoiceQuestionComponent from "$lib/ImageMultipleChoiceQuestionComponent.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 category: Category | undefined = $state();
|
||||||
|
let question: GeneralQuestion | undefined = $state();
|
||||||
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 showAnswer = $state(false);
|
let showAnswer = $state(false);
|
||||||
let showQuestion = $state(false);
|
let showQuestion = $state(false);
|
||||||
@@ -118,42 +75,71 @@
|
|||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
fetchCategory(`${page.params.category}`)
|
||||||
|
.then((cat) => {
|
||||||
|
category = cat;
|
||||||
|
return fetchQuestion(`${page.params.question}`);
|
||||||
|
})
|
||||||
|
.then((que) => {
|
||||||
|
question = que;
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mt-4 flex grow flex-col">
|
{#if category && question}
|
||||||
<div class="mb-4 flex justify-between text-4xl">
|
<div class="mt-4 flex grow flex-col">
|
||||||
<div>{category.name}</div>
|
<div class="mb-4 flex justify-between text-4xl">
|
||||||
<div>
|
<div>{category.name}</div>
|
||||||
{question.points} Punkte
|
<div>
|
||||||
|
{question.points} Punkte
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex grow flex-col">
|
||||||
|
{#if question === undefined}
|
||||||
|
<p>Question is undefined</p>
|
||||||
|
{:else if isSimpleQuestion(question)}
|
||||||
|
<SimpleQuestionComponent {question} {showAnswer} {showQuestion} />
|
||||||
|
{:else if isMultipleChoiceQuestion(question)}
|
||||||
|
<MultipleChoiceQuestionComponent {question} {showAnswer} {showQuestion} />
|
||||||
|
{:else if isImageQuestion(question)}
|
||||||
|
<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}
|
||||||
|
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>
|
</div>
|
||||||
<div class="flex grow flex-col">
|
{:else}
|
||||||
{#if question === undefined}
|
Loading...
|
||||||
<p>Question is undefined</p>
|
{/if}
|
||||||
{:else if isSimpleQuestion(question)}
|
|
||||||
<SimpleQuestionComponent {question} {showAnswer} {showQuestion} />
|
|
||||||
{:else if isMultipleChoiceQuestion(question)}
|
|
||||||
<MultipleChoiceQuestionComponent {question} {showAnswer} {showQuestion} />
|
|
||||||
{:else if isImageQuestion(question)}
|
|
||||||
<ImageQuestionComponent {question} {showAnswer} {showQuestion} {isBuzzed} />
|
|
||||||
{:else if isImageMultipleChoiceQuestion(question)}
|
|
||||||
<ImageMultipleChoiceQuestionComponent
|
|
||||||
{question}
|
|
||||||
{showAnswer}
|
|
||||||
{showQuestion}
|
|
||||||
{isBuzzed}
|
|
||||||
/>
|
|
||||||
{:else if isAudioQuestion(question)}
|
|
||||||
<AudioQuestionComponent {question} {showAnswer} {showQuestion} showPlayer={false} />
|
|
||||||
{:else if isAudioMultipleChoiceQuestion(question)}
|
|
||||||
<AudioMultipleChoiceQuestionComponent
|
|
||||||
{question}
|
|
||||||
{showAnswer}
|
|
||||||
{showQuestion}
|
|
||||||
showPlayer={false}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<p>Type of question unknown</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import games from '$lib/games/games';
|
|
||||||
|
|
||||||
export function load() {
|
|
||||||
return {
|
|
||||||
games
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,39 @@
|
|||||||
<script lang="ts">
|
<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>
|
</script>
|
||||||
|
|
||||||
<h1 class="m-4 mb-8 text-7xl font-bold">Games</h1>
|
<h1 class="m-4 mb-8 text-7xl font-bold">Games</h1>
|
||||||
|
|
||||||
<div class="flex flex-col space-y-4">
|
<div class="flex flex-col space-y-4">
|
||||||
{#each data.games as game, i}
|
{#each games as game}
|
||||||
<a
|
<a
|
||||||
class="ms-4 me-4 rounded-xl border-2 p-4 hover:cursor-pointer hover:bg-emerald-200"
|
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}
|
{/each}
|
||||||
</div>
|
</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,
|
isMultipleChoiceQuestion,
|
||||||
isSimpleQuestion,
|
isSimpleQuestion,
|
||||||
isImageQuestion,
|
isImageQuestion,
|
||||||
type Game,
|
|
||||||
isAudioQuestion,
|
isAudioQuestion,
|
||||||
isAudioMultipleChoiceQuestion,
|
isAudioMultipleChoiceQuestion,
|
||||||
isImageMultipleChoiceQuestion
|
isImageMultipleChoiceQuestion,
|
||||||
|
type FullGame,
|
||||||
|
type Question
|
||||||
} 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";
|
||||||
@@ -18,12 +19,21 @@
|
|||||||
import Scoreboard from "$lib/Scoreboard.svelte";
|
import Scoreboard from "$lib/Scoreboard.svelte";
|
||||||
import SimpleQuestionComponent from "$lib/SimpleQuestionComponent.svelte";
|
import SimpleQuestionComponent from "$lib/SimpleQuestionComponent.svelte";
|
||||||
import PlusMinusButton from "$lib/PlusMinusButton.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 MultipleChoiceQuestionComponent from "$lib/MultipleChoiceQuestionComponent.svelte";
|
||||||
import ImageQuestionComponent from "$lib/ImageQuestionComponent.svelte";
|
import ImageQuestionComponent from "$lib/ImageQuestionComponent.svelte";
|
||||||
import AudioQuestionComponent from "$lib/AudioQuestionComponent.svelte";
|
import AudioQuestionComponent from "$lib/AudioQuestionComponent.svelte";
|
||||||
import AudioMultipleChoiceQuestionComponent from "$lib/AudioMultipleChoiceQuestionComponent.svelte";
|
import AudioMultipleChoiceQuestionComponent from "$lib/AudioMultipleChoiceQuestionComponent.svelte";
|
||||||
import ImageMultipleChoiceQuestionComponent from "$lib/ImageMultipleChoiceQuestionComponent.svelte";
|
import ImageMultipleChoiceQuestionComponent from "$lib/ImageMultipleChoiceQuestionComponent.svelte";
|
||||||
|
import { fetchCategory, fetchGame, fetchQuestion, fetchWall } from "../../../editor/fetchers";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
interface SaveData {
|
||||||
|
players: Player[];
|
||||||
|
currentPlayer: number;
|
||||||
|
currentWall: number;
|
||||||
|
visitedQuestions: VisitedQuestions;
|
||||||
|
}
|
||||||
|
|
||||||
let startDisabled = $state(true);
|
let startDisabled = $state(true);
|
||||||
|
|
||||||
@@ -50,7 +60,7 @@
|
|||||||
|
|
||||||
class GameManager {
|
class GameManager {
|
||||||
public state: GameState = $state(GameState.INIT);
|
public state: GameState = $state(GameState.INIT);
|
||||||
public game: Game;
|
public game: Game | undefined = $state();
|
||||||
public players: Player[] = $state([
|
public players: Player[] = $state([
|
||||||
{
|
{
|
||||||
name: "Player 1",
|
name: "Player 1",
|
||||||
@@ -67,24 +77,58 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
public currentPlayer = $state(0);
|
public currentPlayer = $state(0);
|
||||||
public currentWall = $state(0);
|
public currentWallIndex = $state(0);
|
||||||
|
public currentWall: WallType | undefined = $state();
|
||||||
public visitedQuestions: VisitedQuestions = $state([]);
|
public visitedQuestions: VisitedQuestions = $state([]);
|
||||||
public currentCategory = $state(0);
|
public currentCategoryId = $state("");
|
||||||
public currentQuestion = $state(0);
|
public currentCategory: Category | undefined = $state();
|
||||||
|
public currentQuestionId = $state("");
|
||||||
|
public currentQuestion: Question | undefined = $state();
|
||||||
|
|
||||||
public answerIsShowing = $state(false);
|
public answerIsShowing = $state(false);
|
||||||
public questionIsShowing = $state(false);
|
public questionIsShowing = $state(false);
|
||||||
public isBuzzed = $state(false);
|
public isBuzzed = $state(false);
|
||||||
|
|
||||||
constructor(game: Game) {
|
setGame(game: Game) {
|
||||||
this.game = game;
|
this.game = game;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
save(): void {
|
||||||
|
if (!this.game) return;
|
||||||
|
const saveData = {
|
||||||
|
players: this.players,
|
||||||
|
currentPlayer: this.currentPlayer,
|
||||||
|
currentWall: this.currentWallIndex,
|
||||||
|
visitedQuestions: this.visitedQuestions
|
||||||
|
};
|
||||||
|
localStorage.setItem(`saveGame-${this.game._id}`, JSON.stringify(saveData));
|
||||||
|
}
|
||||||
|
|
||||||
|
load(): void {
|
||||||
|
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.currentWallIndex = saveData.currentWall;
|
||||||
|
this.visitedQuestions = saveData.visitedQuestions;
|
||||||
|
console.log(saveData);
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
startGame(): void {
|
startGame(): void {
|
||||||
|
if (!this.game) return;
|
||||||
this.currentPlayer = Math.floor(Math.random() * this.players.length);
|
this.currentPlayer = Math.floor(Math.random() * this.players.length);
|
||||||
this.state = GameState.CHOOSING_QUESTION;
|
this.state = GameState.CHOOSING_QUESTION;
|
||||||
this.sendStart();
|
fetchWall(this.game.walls[this.currentWallIndex]).then((wall) => {
|
||||||
this.sendCurrentState();
|
this.currentWall = wall;
|
||||||
|
this.sendStart();
|
||||||
|
this.sendCurrentState();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
sendStart(): void {
|
sendStart(): void {
|
||||||
@@ -104,7 +148,7 @@
|
|||||||
sendWall(): void {
|
sendWall(): void {
|
||||||
ws.sendMessage({
|
ws.sendMessage({
|
||||||
type: MessageType.GOTO,
|
type: MessageType.GOTO,
|
||||||
route: `/${page.params.game}/${this.currentWall}`
|
route: `/${page.params.game}/${this.currentWall?._id}`
|
||||||
});
|
});
|
||||||
this.sendVisited();
|
this.sendVisited();
|
||||||
}
|
}
|
||||||
@@ -122,30 +166,39 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendCurrentQuestion(): void {
|
sendCurrentQuestion(): void {
|
||||||
|
if (!this.game) return;
|
||||||
ws.sendMessage({
|
ws.sendMessage({
|
||||||
type: MessageType.GOTO,
|
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 {
|
sendEnd(): void {
|
||||||
ws.sendMessage({
|
ws.sendMessage({
|
||||||
type: MessageType.GOTO,
|
type: MessageType.GOTO,
|
||||||
route: `/${page.params.game}/${this.currentWall}/end`
|
route: `/${page.params.game}/${this.currentWall?._id}/end`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
tileClicked(categoryIndex: number, questionIndex: number) {
|
tileClicked(catId: _id, queId: _id) {
|
||||||
console.log("Cat", categoryIndex, "Que", questionIndex);
|
console.log("Cat", catId, "Que", queId);
|
||||||
console.log(gameManager.wall.categories[categoryIndex]?.questions[questionIndex]);
|
|
||||||
|
|
||||||
this.currentCategory = categoryIndex;
|
this.currentCategoryId = catId;
|
||||||
this.currentQuestion = questionIndex;
|
this.currentQuestionId = queId;
|
||||||
this.answerIsShowing = false;
|
this.answerIsShowing = false;
|
||||||
|
|
||||||
this.sendCurrentQuestion();
|
fetchCategory(this.currentCategoryId)
|
||||||
|
.then((category) => {
|
||||||
|
this.currentCategory = category;
|
||||||
|
|
||||||
this.state = GameState.SHOW_QUESTION;
|
return fetchQuestion(this.currentQuestionId);
|
||||||
|
})
|
||||||
|
.then((question) => {
|
||||||
|
this.currentQuestion = question;
|
||||||
|
|
||||||
|
this.sendCurrentQuestion();
|
||||||
|
this.state = GameState.SHOW_QUESTION;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
addPlayer() {
|
addPlayer() {
|
||||||
@@ -204,20 +257,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
plus(player: Player) {
|
plus(player: Player) {
|
||||||
if (!this.answerIsShowing) return;
|
if (!this.answerIsShowing || !this.currentQuestion) return;
|
||||||
if (player.name === this.currentPlayerToName()) {
|
if (player.name === this.currentPlayerToName()) {
|
||||||
player.points += this.question.points * 2;
|
player.points += this.currentQuestion.points * 2;
|
||||||
} else {
|
} else {
|
||||||
player.points += this.question.points;
|
player.points += this.currentQuestion.points;
|
||||||
}
|
}
|
||||||
this.sendPlayers();
|
this.sendPlayers();
|
||||||
}
|
}
|
||||||
|
|
||||||
minus(player: Player) {
|
minus(player: Player) {
|
||||||
|
if (!this.currentQuestion) return;
|
||||||
if (player.name === this.currentPlayerToName()) {
|
if (player.name === this.currentPlayerToName()) {
|
||||||
player.points -= this.question.points * 2;
|
player.points -= this.currentQuestion.points * 2;
|
||||||
} else {
|
} else {
|
||||||
player.points -= this.question.points;
|
player.points -= this.currentQuestion.points;
|
||||||
}
|
}
|
||||||
this.sendPlayers();
|
this.sendPlayers();
|
||||||
}
|
}
|
||||||
@@ -235,12 +289,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
finishQuestion(): void {
|
finishQuestion(): void {
|
||||||
if (this.visitedQuestions[this.currentCategory] === undefined) {
|
if (!this.visitedQuestions.includes(this.currentQuestionId)) {
|
||||||
this.visitedQuestions[this.currentCategory] = [this.currentQuestion];
|
this.visitedQuestions.push(this.currentQuestionId);
|
||||||
} else if (
|
|
||||||
!this.visitedQuestions[this.currentCategory].includes(this.currentQuestion)
|
|
||||||
) {
|
|
||||||
this.visitedQuestions[this.currentCategory].push(this.currentQuestion);
|
|
||||||
}
|
}
|
||||||
this.setupGoingBack();
|
this.setupGoingBack();
|
||||||
this.nextPlayer();
|
this.nextPlayer();
|
||||||
@@ -250,31 +300,25 @@
|
|||||||
this.sendWall();
|
this.sendWall();
|
||||||
this.state = GameState.CHOOSING_QUESTION;
|
this.state = GameState.CHOOSING_QUESTION;
|
||||||
}
|
}
|
||||||
|
this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
wallIsDone(): boolean {
|
wallIsDone(): boolean {
|
||||||
let visitedNum = 0;
|
return this.visitedQuestions.length >= 25;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
goToNextWall(): void {
|
goToNextWall(): void {
|
||||||
if (this.currentWall + 1 >= this.game.walls.length) {
|
if (!this.game) return;
|
||||||
|
if (this.currentWallIndex + 1 >= this.game.walls.length) {
|
||||||
this.goToEndScreen();
|
this.goToEndScreen();
|
||||||
} else {
|
} else {
|
||||||
this.currentWall += 1;
|
this.currentWallIndex += 1;
|
||||||
this.visitedQuestions = [];
|
this.visitedQuestions = [];
|
||||||
this.sendWall();
|
fetchWall(this.game.walls[this.currentWallIndex]).then((wall) => {
|
||||||
this.state = GameState.CHOOSING_QUESTION;
|
this.currentWall = wall;
|
||||||
|
this.sendWall();
|
||||||
|
this.state = GameState.CHOOSING_QUESTION;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,168 +335,175 @@
|
|||||||
currentPlayerToName(): string {
|
currentPlayerToName(): string {
|
||||||
return this.players[this.currentPlayer].name;
|
return this.players[this.currentPlayer].name;
|
||||||
}
|
}
|
||||||
|
|
||||||
get wall() {
|
|
||||||
return this.game.walls[this.currentWall];
|
|
||||||
}
|
|
||||||
|
|
||||||
get category() {
|
|
||||||
return this.wall.categories[this.currentCategory];
|
|
||||||
}
|
|
||||||
|
|
||||||
get question() {
|
|
||||||
return this.category.questions[this.currentQuestion];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let { data } = $props();
|
let gameManager = new GameManager();
|
||||||
|
|
||||||
let gameManager = new GameManager(data);
|
onMount(() => {
|
||||||
|
fetchGame(`${page.params.game}`).then((game) => {
|
||||||
|
gameManager.game = game;
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-full flex-col">
|
{#if gameManager.game}
|
||||||
<h1 class="ms-4 text-7xl font-bold">{gameManager.game.name}</h1>
|
<div class="flex h-full flex-col">
|
||||||
{#if gameManager.state === GameState.INIT}
|
<h1 class="ms-4 text-7xl font-bold">{gameManager.game.name}</h1>
|
||||||
<div class="p-4">
|
{#if gameManager.state === GameState.INIT}
|
||||||
<div class="flex items-center">
|
<div class="p-4">
|
||||||
<h2 class="grow pb-4 text-5xl">Spieler</h2>
|
<div class="flex items-center">
|
||||||
<button
|
<h2 class="grow pb-4 text-5xl">Spieler</h2>
|
||||||
class="btn"
|
<button class="btn me-4" onclick={() => gameManager.load()}
|
||||||
disabled={!startDisabled}
|
>Load SaveGame</button
|
||||||
onclick={() => gameManager.startGame()}>Start</button
|
>
|
||||||
|
<button
|
||||||
|
class="btn"
|
||||||
|
disabled={!startDisabled}
|
||||||
|
onclick={() => gameManager.startGame()}>Start</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col space-y-2 pb-4">
|
||||||
|
{#each gameManager.players as player, i}
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input class="inputField grow" type="text" bind:value={player.name} />
|
||||||
|
<button class="btn" onclick={() => gameManager.removePlayer(i)}
|
||||||
|
>Löschen</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<button class="btn" onclick={() => gameManager.addPlayer()}
|
||||||
|
>Spieler hinzufügen</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col space-y-2 pb-4">
|
{:else}
|
||||||
{#each gameManager.players as player, i}
|
<div class="flex grow">
|
||||||
<div class="flex items-center">
|
<Scoreboard
|
||||||
<input class="inputField grow" type="text" bind:value={player.name} />
|
players={gameManager.players}
|
||||||
<button class="btn" onclick={() => gameManager.removePlayer(i)}
|
currentPlayer={gameManager.currentPlayerToName()}
|
||||||
>Löschen</button
|
editable={true}
|
||||||
>
|
onReload={() => gameManager.sendPlayers()}
|
||||||
</div>
|
/>
|
||||||
{/each}
|
{#if gameManager.state === GameState.SHOW_QUESTION}
|
||||||
</div>
|
<div class="flex grow flex-col">
|
||||||
<button class="btn" onclick={() => gameManager.addPlayer()}>Spieler hinzufügen</button>
|
<div class="m-4 flex justify-between text-4xl">
|
||||||
</div>
|
<div>{gameManager.currentCategory?.name}</div>
|
||||||
{:else}
|
<div>
|
||||||
<div class="flex grow">
|
{gameManager.currentQuestion?.points} Punkte
|
||||||
<Scoreboard
|
</div>
|
||||||
players={gameManager.players}
|
</div>
|
||||||
currentPlayer={gameManager.currentPlayerToName()}
|
<div class="flex grow ps-4 pe-4">
|
||||||
editable={true}
|
{#if gameManager.currentQuestion === undefined}
|
||||||
onReload={() => gameManager.sendPlayers()}
|
<p>Question is undefined</p>
|
||||||
/>
|
{:else if isSimpleQuestion(gameManager.currentQuestion)}
|
||||||
{#if gameManager.state === GameState.SHOW_QUESTION}
|
<SimpleQuestionComponent
|
||||||
<div class="flex grow flex-col">
|
question={gameManager.currentQuestion}
|
||||||
<div class="m-4 flex justify-between text-4xl">
|
showAnswer={true}
|
||||||
<div>{gameManager.category.name}</div>
|
showQuestion={true}
|
||||||
<div>
|
/>
|
||||||
{gameManager.question.points} Punkte
|
{:else if isMultipleChoiceQuestion(gameManager.currentQuestion)}
|
||||||
|
<MultipleChoiceQuestionComponent
|
||||||
|
question={gameManager.currentQuestion}
|
||||||
|
showAnswer={true}
|
||||||
|
showQuestion={true}
|
||||||
|
/>
|
||||||
|
{:else if isImageQuestion(gameManager.currentQuestion)}
|
||||||
|
<ImageQuestionComponent
|
||||||
|
question={gameManager.currentQuestion}
|
||||||
|
showAnswer={true}
|
||||||
|
showQuestion={true}
|
||||||
|
isBuzzed={false}
|
||||||
|
isLegacy={false}
|
||||||
|
/>
|
||||||
|
{:else if isImageMultipleChoiceQuestion(gameManager.currentQuestion)}
|
||||||
|
<ImageMultipleChoiceQuestionComponent
|
||||||
|
question={gameManager.currentQuestion}
|
||||||
|
showAnswer={true}
|
||||||
|
showQuestion={true}
|
||||||
|
isBuzzed={false}
|
||||||
|
isLegacy={false}
|
||||||
|
/>
|
||||||
|
{:else if isAudioQuestion(gameManager.currentQuestion)}
|
||||||
|
<AudioQuestionComponent
|
||||||
|
question={gameManager.currentQuestion}
|
||||||
|
showAnswer={true}
|
||||||
|
showPlayer={true}
|
||||||
|
showQuestion={true}
|
||||||
|
isLegacy={false}
|
||||||
|
/>
|
||||||
|
{:else if isAudioMultipleChoiceQuestion(gameManager.currentQuestion)}
|
||||||
|
<AudioMultipleChoiceQuestionComponent
|
||||||
|
question={gameManager.currentQuestion}
|
||||||
|
showAnswer={true}
|
||||||
|
showPlayer={true}
|
||||||
|
showQuestion={true}
|
||||||
|
isLegacy={false}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<p>Type of question unknown</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="m-4 flex flex-wrap items-center gap-4">
|
||||||
|
<button class="btn" onclick={() => gameManager.goBack()}>Zurück</button>
|
||||||
|
{#if gameManager.questionIsShowing}
|
||||||
|
<button class="btn" onclick={() => gameManager.hideQuestion()}
|
||||||
|
>Frage verstecken</button
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<button class="btn" onclick={() => gameManager.showQuestion()}
|
||||||
|
>Frage aufdecken</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
{#if gameManager.answerIsShowing}
|
||||||
|
<button class="btn" onclick={() => gameManager.hideAnswer()}
|
||||||
|
>Antwort verstecken</button
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<button class="btn" onclick={() => gameManager.showAnswer()}
|
||||||
|
>Antwort aufdecken</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
{#if gameManager.isBuzzed}
|
||||||
|
<button class="btn" onclick={() => gameManager.buzzerReleased()}
|
||||||
|
>Entbuzzern</button
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<button class="btn" onclick={() => gameManager.buzzerPressed()}
|
||||||
|
>Buzzern</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
{#each gameManager.players as player}
|
||||||
|
<PlusMinusButton
|
||||||
|
label={player.name}
|
||||||
|
plus={() => gameManager.plus(player)}
|
||||||
|
minus={() => gameManager.minus(player)}
|
||||||
|
showPlus={gameManager.answerIsShowing}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
<div class="grow"></div>
|
||||||
|
{#if gameManager.answerIsShowing}
|
||||||
|
<button class="btn" onclick={() => gameManager.finishQuestion()}
|
||||||
|
>Abschließen</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex grow ps-4 pe-4">
|
{:else if gameManager.state === GameState.END}
|
||||||
{#if gameManager.question === undefined}
|
<div class="flex grow items-center justify-center text-7xl">
|
||||||
<p>Question is undefined</p>
|
<div>ENDE</div>
|
||||||
{:else if isSimpleQuestion(gameManager.question)}
|
|
||||||
<SimpleQuestionComponent
|
|
||||||
question={gameManager.question}
|
|
||||||
showAnswer={true}
|
|
||||||
showQuestion={true}
|
|
||||||
/>
|
|
||||||
{:else if isMultipleChoiceQuestion(gameManager.question)}
|
|
||||||
<MultipleChoiceQuestionComponent
|
|
||||||
question={gameManager.question}
|
|
||||||
showAnswer={true}
|
|
||||||
showQuestion={true}
|
|
||||||
/>
|
|
||||||
{:else if isImageQuestion(gameManager.question)}
|
|
||||||
<ImageQuestionComponent
|
|
||||||
question={gameManager.question}
|
|
||||||
showAnswer={true}
|
|
||||||
showQuestion={true}
|
|
||||||
isBuzzed={false}
|
|
||||||
/>
|
|
||||||
{:else if isImageMultipleChoiceQuestion(gameManager.question)}
|
|
||||||
<ImageMultipleChoiceQuestionComponent
|
|
||||||
question={gameManager.question}
|
|
||||||
showAnswer={true}
|
|
||||||
showQuestion={true}
|
|
||||||
isBuzzed={false}
|
|
||||||
/>
|
|
||||||
{:else if isAudioQuestion(gameManager.question)}
|
|
||||||
<AudioQuestionComponent
|
|
||||||
question={gameManager.question}
|
|
||||||
showAnswer={true}
|
|
||||||
showPlayer={true}
|
|
||||||
showQuestion={true}
|
|
||||||
/>
|
|
||||||
{:else if isAudioMultipleChoiceQuestion(gameManager.question)}
|
|
||||||
<AudioMultipleChoiceQuestionComponent
|
|
||||||
question={gameManager.question}
|
|
||||||
showAnswer={true}
|
|
||||||
showPlayer={true}
|
|
||||||
showQuestion={true}
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<p>Type of question unknown</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="m-4 flex flex-wrap items-center gap-4">
|
{:else}
|
||||||
<button class="btn" onclick={() => gameManager.goBack()}>Zurück</button>
|
<div class="grow ps-4 pe-4">
|
||||||
{#if gameManager.questionIsShowing}
|
<Wall
|
||||||
<button class="btn" onclick={() => gameManager.hideQuestion()}
|
wall={gameManager.currentWall}
|
||||||
>Frage verstecken</button
|
onclickIds={(cat, que) => gameManager.tileClicked(cat, que)}
|
||||||
>
|
visited={gameManager.visitedQuestions}
|
||||||
{:else}
|
/>
|
||||||
<button class="btn" onclick={() => gameManager.showQuestion()}
|
|
||||||
>Frage aufdecken</button
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
{#if gameManager.answerIsShowing}
|
|
||||||
<button class="btn" onclick={() => gameManager.hideAnswer()}
|
|
||||||
>Antwort verstecken</button
|
|
||||||
>
|
|
||||||
{:else}
|
|
||||||
<button class="btn" onclick={() => gameManager.showAnswer()}
|
|
||||||
>Antwort aufdecken</button
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
{#if gameManager.isBuzzed}
|
|
||||||
<button class="btn" onclick={() => gameManager.buzzerReleased()}
|
|
||||||
>Entbuzzern</button
|
|
||||||
>
|
|
||||||
{:else}
|
|
||||||
<button class="btn" onclick={() => gameManager.buzzerPressed()}
|
|
||||||
>Buzzern</button
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
{#each gameManager.players as player}
|
|
||||||
<PlusMinusButton
|
|
||||||
label={player.name}
|
|
||||||
plus={() => gameManager.plus(player)}
|
|
||||||
minus={() => gameManager.minus(player)}
|
|
||||||
showPlus={gameManager.answerIsShowing}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
<div class="grow"></div>
|
|
||||||
{#if gameManager.answerIsShowing}
|
|
||||||
<button class="btn" onclick={() => gameManager.finishQuestion()}
|
|
||||||
>Abschließen</button
|
|
||||||
>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{:else if gameManager.state === GameState.END}
|
</div>
|
||||||
<div class="flex grow items-center justify-center text-7xl"><div>ENDE</div></div>
|
{/if}
|
||||||
{:else}
|
</div>
|
||||||
<div class="grow ps-4 pe-4">
|
{:else}
|
||||||
<Wall
|
Loading Game...
|
||||||
wall={gameManager.game.walls[gameManager.currentWall]}
|
{/if}
|
||||||
onclick={(cat, que) => gameManager.tileClicked(cat, que)}
|
|
||||||
visited={gameManager.visitedQuestions}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|||||||
202
src/routes/editor/+page.svelte
Normal file
202
src/routes/editor/+page.svelte
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<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";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
let games: Game[] = $state([]);
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
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 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
|
||||||
|
.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="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">
|
||||||
|
{#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 gap-2 rounded-xl border-2 p-2 hover:cursor-pointer hover:bg-emerald-200"
|
||||||
|
onclick={() => {
|
||||||
|
goto(`/editor/${game._id}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{game.name}
|
||||||
|
</div>
|
||||||
|
<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
|
||||||
|
>
|
||||||
|
</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>
|
||||||
|
<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}
|
||||||
|
</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>
|
||||||
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}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
69
src/routes/login/+page.svelte
Normal file
69
src/routes/login/+page.svelte
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
|
import { env } from "$env/dynamic/public";
|
||||||
|
import UserState, { type UserObj } from "$lib/User.svelte";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
let username = $state("");
|
||||||
|
let password = $state("");
|
||||||
|
let error = $state("");
|
||||||
|
|
||||||
|
async function login() {
|
||||||
|
axios
|
||||||
|
.post(
|
||||||
|
`${env.PUBLIC_JEOPARDY_SERVER_PROTOCOL}://${env.PUBLIC_JEOPARDY_SERVER}/auth/login`,
|
||||||
|
{
|
||||||
|
username: username,
|
||||||
|
password: password
|
||||||
|
},
|
||||||
|
{ withCredentials: true }
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
UserState.user = response.data as UserObj;
|
||||||
|
console.log(UserState.id, UserState.username, UserState.role);
|
||||||
|
goto("/");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
error = "Login fehlgeschlagen";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex h-full w-full items-center justify-center">
|
||||||
|
<div class="borders flex-col items-center justify-center">
|
||||||
|
<div>
|
||||||
|
<label for="username" class="ms-4">Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
id="username"
|
||||||
|
class="borders ms-4 me-4 mt-2 mb-4"
|
||||||
|
bind:value={username}
|
||||||
|
/>
|
||||||
|
<label for="password" class="ms-4">Passwort</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
id="password"
|
||||||
|
class="borders ms-4 me-4 mt-2 mb-4"
|
||||||
|
bind:value={password}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn mb-2 w-fit ps-4 pe-4" onclick={login}>Login</button>
|
||||||
|
{#if error.length > 0}
|
||||||
|
<div class="text-red-700">{error}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.borders {
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 5px;
|
||||||
|
display: flex;
|
||||||
|
font-size: larger;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
92
src/routes/settings/+page.svelte
Normal file
92
src/routes/settings/+page.svelte
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
|
import { env } from "$env/dynamic/public";
|
||||||
|
import UserState, { type UserObj } from "$lib/User.svelte";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
let oldpassword = $state("");
|
||||||
|
let newpassword = $state("");
|
||||||
|
let reppassword = $state("");
|
||||||
|
let error = $state("");
|
||||||
|
|
||||||
|
async function changePassword() {
|
||||||
|
if (newpassword !== reppassword) {
|
||||||
|
error = "Passwörter stimmen nicht überein.";
|
||||||
|
} else {
|
||||||
|
error = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
axios
|
||||||
|
.post(
|
||||||
|
`${env.PUBLIC_JEOPARDY_SERVER_PROTOCOL}://${env.PUBLIC_JEOPARDY_SERVER}/user/changepw`,
|
||||||
|
{
|
||||||
|
old: oldpassword,
|
||||||
|
new: newpassword
|
||||||
|
},
|
||||||
|
{ withCredentials: true }
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
goto("/login");
|
||||||
|
} else {
|
||||||
|
error = "Passwort ändern fehlgeschlagen";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
error = "Passwort ändern fehlgeschlagen";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-4 p-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<h2 class="text-4xl font-bold">Einstellungen</h2>
|
||||||
|
<div class="grow"></div>
|
||||||
|
<button type="button" class="btn" onclick={() => goto("/")}>Zurück</button>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-md border-1 p-4">
|
||||||
|
<h4 class="font-bold">Passwort ändern</h4>
|
||||||
|
<div>
|
||||||
|
<label for="oldpassword" class="">Altes Passwort</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="oldpassword"
|
||||||
|
id="oldpassword"
|
||||||
|
class="borders me-4 mt-2 mb-4"
|
||||||
|
bind:value={oldpassword}
|
||||||
|
/>
|
||||||
|
<label for="newpassword" class="">Neues Passwort</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="newpassword"
|
||||||
|
id="newpassword"
|
||||||
|
class="borders me-4 mt-2 mb-4"
|
||||||
|
bind:value={newpassword}
|
||||||
|
/>
|
||||||
|
<label for="reppassword" class="">Neues Passwort wiederholen</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="reppassword"
|
||||||
|
id="reppassword"
|
||||||
|
class="borders me-4 mt-2 mb-4"
|
||||||
|
bind:value={reppassword}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn mb-2 w-fit ps-4 pe-4" onclick={changePassword}
|
||||||
|
>Passwort ändern</button
|
||||||
|
>
|
||||||
|
{#if error.length > 0}
|
||||||
|
<div class="text-red-700">{error}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.borders {
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 5px;
|
||||||
|
display: flex;
|
||||||
|
font-size: larger;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,18 +1,16 @@
|
|||||||
import adapter from '@sveltejs/adapter-auto';
|
import adapter from "@sveltejs/adapter-node";
|
||||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
// Consult https://svelte.dev/docs/kit/integrations
|
// Consult https://svelte.dev/docs/kit/integrations
|
||||||
// for more information about preprocessors
|
// for more information about preprocessors
|
||||||
preprocess: vitePreprocess(),
|
preprocess: vitePreprocess(),
|
||||||
|
|
||||||
kit: {
|
kit: {
|
||||||
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
||||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
adapter: adapter()
|
||||||
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
}
|
||||||
adapter: adapter()
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import tailwindcss from '@tailwindcss/vite';
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import { sveltekit } from '@sveltejs/kit/vite';
|
import { sveltekit } from "@sveltejs/kit/vite";
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [tailwindcss(), sveltekit()]
|
plugins: [sveltekit(), tailwindcss()]
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user