20 Commits
1.0.2 ... 2.0.0

Author SHA1 Message Date
b24e43e142 Release 2.0.0
Updated display and host
2026-01-03 00:38:52 +01:00
48074f7603 Added Editing of Questions 2026-01-02 18:00:57 +01:00
5568a5bb99 Added rename of Game, Wall and Category 2026-01-02 12:35:44 +01:00
7be5921ef6 Added Editor Game and Wall Display 2026-01-02 02:59:40 +01:00
dc2766f0ef Merge branch 'mongodb-fix' 2025-12-28 15:49:19 +01:00
7d231730a6 Updated Version 1.0.6 2025-12-28 15:40:25 +01:00
956571e470 Bumbed mongo db version 2025-12-28 15:39:20 +01:00
4405c23bee Fix: Directories cant be deleted 2025-12-28 13:11:42 +01:00
aaf09e13f5 Editor: Adjusted game link and added no games image 2025-12-28 13:02:36 +01:00
daf3f779aa Editor: added game creation and deletion 2025-12-28 12:51:43 +01:00
48bc66b89a Added deletion and renaming of files and deletion of directories 2025-12-23 17:42:33 +01:00
3a52b85dfb Added listing of files and directories 2025-12-23 12:25:24 +01:00
7349624da9 Added a RessourceManager for uploading files 2025-12-22 13:03:32 +01:00
25037f4798 Added administration and password change 2025-10-07 23:07:49 +02:00
38eee8b38c Added settings for changing password 2025-10-04 13:41:40 +02:00
9fbd6e4191 Hotfix 2025-10-03 11:58:59 +02:00
88a9778f3a Bump version to 1.0.3 2025-10-03 11:40:04 +02:00
c695d6c733 Added login and authentication 2025-10-03 11:39:20 +02:00
96388e5a50 update readme 2025-09-29 17:16:44 +02:00
888197b1c6 Readme update 2025-09-29 17:08:53 +02:00
52 changed files with 3740 additions and 462 deletions

1
.npmrc
View File

@@ -1 +1,2 @@
engine-strict=true engine-strict=true
script-shell=C:\Program Files\Git\git-bash.exe

View File

@@ -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
```

View File

@@ -4,11 +4,43 @@ services:
container_name: jeopardy container_name: jeopardy
environment: environment:
# domain:port or only domain, eg jeopardyserver.akolata.de # domain:port or only domain, eg jeopardyserver.akolata.de
- PUBLIC_JEOPARDY_SERVER=127.0.0.1:11001 PUBLIC_JEOPARDY_SERVER: localhost:11001
PUBLIC_JEOPARDY_SERVER_PROTOCOL: http
ports: ports:
- "11000:3000" - "11000:3000"
jeopardyserver: jeopardyserver:
image: jeopardyserver:latest image: jeopardyserver:latest
container_name: jeopardyserver container_name: jeopardyserver
environment:
JEOPARDYSERVER_MONGO_USERNAME: jeopardyadmin
JEOPARDYSERVER_MONGO_PASSWORD: jGpklsI9vCdixel7sDGxVBsydlzdyX8A1Zank6a12QT827PC
JEOPARDYSERVER_MONGO_URL: mongo:27017
JEOPARDY_URL: http://localhost:11000
ports: ports:
- "11001:12345" - "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
View File

@@ -0,0 +1,4 @@
docker compose down &
docker build -t jeopardy .
docker compose up -d

287
package-lock.json generated
View File

@@ -1,12 +1,16 @@
{ {
"name": "jeopardy", "name": "jeopardy",
"version": "1.0.2", "version": "2.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "jeopardy", "name": "jeopardy",
"version": "1.0.2", "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",
@@ -1275,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",
@@ -2011,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",
@@ -2052,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",
@@ -2135,6 +2179,18 @@
"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": { "node_modules/commondir": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
@@ -2150,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": {
@@ -2222,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",
@@ -2239,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",
@@ -2253,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",
@@ -2680,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",
@@ -2699,12 +2858,48 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "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",
@@ -2731,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",
@@ -2755,11 +2962,37 @@
"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": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
@@ -3252,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",
@@ -3289,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",
@@ -3765,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",

View File

@@ -1,7 +1,7 @@
{ {
"name": "jeopardy", "name": "jeopardy",
"private": true, "private": true,
"version": "1.0.2", "version": "2.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
@@ -12,7 +12,8 @@
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .", "format": "prettier --write .",
"lint": "prettier --check . && eslint .", "lint": "prettier --check . && eslint .",
"docker-build": "docker build -t jeopardy ." "docker-build": "docker build -t jeopardy .",
"docker-dev": "./docker-dev.sh"
}, },
"devDependencies": { "devDependencies": {
"@eslint/compat": "^1.2.5", "@eslint/compat": "^1.2.5",
@@ -34,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"
} }
} }

View File

@@ -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;
} }

View File

@@ -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">

View File

@@ -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}

View File

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

View File

@@ -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 {

View File

@@ -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}

View File

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

View File

@@ -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">

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

View File

@@ -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
View 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;
}
};

View File

@@ -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 class="flex items-center justify-center gap-2 text-3xl font-semibold">
<div>{category.name}</div> <div>{category.name}</div>
{#if isEditor}
<Button
onclick={() => {
catToRename = category;
newCatName = category.name;
showRenameCategory = true;
}}><i class="fa-solid fa-pen"></i></Button
>
{/if}
</div> </div>
{#each category.questions as question, queIndex} {#each category.questions as question}
<!-- svelte-ignore a11y_click_events_have_key_events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<div <div
class="card {isVisited(catIndex, queIndex) ? 'visited' : ''}" class="card {visited.includes(`${question._id}`) ? 'visited' : ''}"
role="button" role="button"
aria-pressed="false" aria-pressed="false"
tabindex="0" tabindex="0"
onclick={() => { onclick={() => {
if (onclick) onclick(catIndex, queIndex); if (onclickIds) onclickIds(category._id, question._id);
}} }}
> >
<div class="text-6xl font-thin">{question.points}</div> <div class="text-6xl font-thin">
{question.points >= 0 ? question.points : "???"}
</div>
</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;

View File

@@ -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
View 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}`;
}

View File

@@ -14,7 +14,7 @@ let socket: WebSocket | undefined;
const connectAsHost = () => { const connectAsHost = () => {
if (socket !== undefined) return; if (socket !== undefined) return;
socket = new WebSocket( socket = new WebSocket(
`${location.protocol === "https:" ? "wss" : "ws"}://${env.PUBLIC_JEOPARDY_SERVER ?? "127.0.0.1:12345"}` `${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);
@@ -25,7 +25,7 @@ const connectAsHost = () => {
const connectAsDisplay = () => { const connectAsDisplay = () => {
if (socket !== undefined) return; if (socket !== undefined) return;
socket = new WebSocket( socket = new WebSocket(
`${location.protocol === "https:" ? "wss" : "ws"}://${env.PUBLIC_JEOPARDY_SERVER ?? "127.0.0.1:12345"}` `${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);
@@ -43,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());
}; };
} }

View File

@@ -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}

View File

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

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

View File

@@ -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">

View File

@@ -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} />

View File

@@ -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,9 +75,21 @@
} }
} 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="mt-4 flex grow flex-col">
<div class="mb-4 flex justify-between text-4xl"> <div class="mb-4 flex justify-between text-4xl">
<div>{category.name}</div> <div>{category.name}</div>
<div> <div>
@@ -135,25 +104,42 @@
{:else if isMultipleChoiceQuestion(question)} {:else if isMultipleChoiceQuestion(question)}
<MultipleChoiceQuestionComponent {question} {showAnswer} {showQuestion} /> <MultipleChoiceQuestionComponent {question} {showAnswer} {showQuestion} />
{:else if isImageQuestion(question)} {:else if isImageQuestion(question)}
<ImageQuestionComponent {question} {showAnswer} {showQuestion} {isBuzzed} /> <ImageQuestionComponent
{question}
{showAnswer}
{showQuestion}
{isBuzzed}
isLegacy={false}
/>
{:else if isImageMultipleChoiceQuestion(question)} {:else if isImageMultipleChoiceQuestion(question)}
<ImageMultipleChoiceQuestionComponent <ImageMultipleChoiceQuestionComponent
{question} {question}
{showAnswer} {showAnswer}
{showQuestion} {showQuestion}
{isBuzzed} {isBuzzed}
isLegacy={false}
/> />
{:else if isAudioQuestion(question)} {:else if isAudioQuestion(question)}
<AudioQuestionComponent {question} {showAnswer} {showQuestion} showPlayer={false} /> <AudioQuestionComponent
{question}
{showAnswer}
{showQuestion}
showPlayer={false}
isLegacy={false}
/>
{:else if isAudioMultipleChoiceQuestion(question)} {:else if isAudioMultipleChoiceQuestion(question)}
<AudioMultipleChoiceQuestionComponent <AudioMultipleChoiceQuestionComponent
{question} {question}
{showAnswer} {showAnswer}
{showQuestion} {showQuestion}
showPlayer={false} showPlayer={false}
isLegacy={false}
/> />
{:else} {:else}
<p>Type of question unknown</p> <p>Type of question unknown</p>
{/if} {/if}
</div> </div>
</div> </div>
{:else}
Loading...
{/if}

View File

@@ -1,7 +0,0 @@
import games from '$lib/games/games';
export function load() {
return {
games
};
}

View File

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

View File

@@ -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;
}

View File

@@ -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,14 @@
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 { interface SaveData {
players: Player[]; players: Player[];
@@ -57,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",
@@ -74,37 +77,42 @@
]); ]);
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 { save(): void {
if (!this.game) return;
const saveData = { const saveData = {
players: this.players, players: this.players,
currentPlayer: this.currentPlayer, currentPlayer: this.currentPlayer,
currentWall: this.currentWall, currentWall: this.currentWallIndex,
visitedQuestions: this.visitedQuestions visitedQuestions: this.visitedQuestions
}; };
localStorage.setItem(`saveGame-${this.game.name}`, JSON.stringify(saveData)); localStorage.setItem(`saveGame-${this.game._id}`, JSON.stringify(saveData));
} }
load(): void { load(): void {
const saveDataString = localStorage.getItem(`saveGame-${this.game.name}`); if (!this.game) return;
const saveDataString = localStorage.getItem(`saveGame-${this.game._id}`);
if (saveDataString === null) return; if (saveDataString === null) return;
try { try {
const saveData: SaveData = JSON.parse(saveDataString); const saveData: SaveData = JSON.parse(saveDataString);
this.players = saveData.players; this.players = saveData.players;
this.currentPlayer = saveData.currentPlayer; this.currentPlayer = saveData.currentPlayer;
this.currentWall = saveData.currentWall; this.currentWallIndex = saveData.currentWall;
this.visitedQuestions = saveData.visitedQuestions; this.visitedQuestions = saveData.visitedQuestions;
console.log(saveData); console.log(saveData);
} catch (e) { } catch (e) {
@@ -113,10 +121,14 @@
} }
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;
fetchWall(this.game.walls[this.currentWallIndex]).then((wall) => {
this.currentWall = wall;
this.sendStart(); this.sendStart();
this.sendCurrentState(); this.sendCurrentState();
});
} }
sendStart(): void { sendStart(): void {
@@ -136,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();
} }
@@ -154,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;
return fetchQuestion(this.currentQuestionId);
})
.then((question) => {
this.currentQuestion = question;
this.sendCurrentQuestion();
this.state = GameState.SHOW_QUESTION; this.state = GameState.SHOW_QUESTION;
});
} }
addPlayer() { addPlayer() {
@@ -236,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();
} }
@@ -267,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();
@@ -286,28 +304,21 @@
} }
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 = [];
fetchWall(this.game.walls[this.currentWallIndex]).then((wall) => {
this.currentWall = wall;
this.sendWall(); this.sendWall();
this.state = GameState.CHOOSING_QUESTION; this.state = GameState.CHOOSING_QUESTION;
});
} }
} }
@@ -324,32 +335,27 @@
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() { let gameManager = new GameManager();
return this.wall.categories[this.currentCategory];
}
get question() { onMount(() => {
return this.category.questions[this.currentQuestion]; fetchGame(`${page.params.game}`).then((game) => {
} gameManager.game = game;
} });
});
let { data } = $props();
let gameManager = new GameManager(data);
</script> </script>
<div class="flex h-full flex-col"> {#if gameManager.game}
<div class="flex h-full flex-col">
<h1 class="ms-4 text-7xl font-bold">{gameManager.game.name}</h1> <h1 class="ms-4 text-7xl font-bold">{gameManager.game.name}</h1>
{#if gameManager.state === GameState.INIT} {#if gameManager.state === GameState.INIT}
<div class="p-4"> <div class="p-4">
<div class="flex items-center"> <div class="flex items-center">
<h2 class="grow pb-4 text-5xl">Spieler</h2> <h2 class="grow pb-4 text-5xl">Spieler</h2>
<button class="btn me-4" onclick={() => gameManager.load()}>Load SaveGame</button> <button class="btn me-4" onclick={() => gameManager.load()}
>Load SaveGame</button
>
<button <button
class="btn" class="btn"
disabled={!startDisabled} disabled={!startDisabled}
@@ -366,7 +372,9 @@
</div> </div>
{/each} {/each}
</div> </div>
<button class="btn" onclick={() => gameManager.addPlayer()}>Spieler hinzufügen</button> <button class="btn" onclick={() => gameManager.addPlayer()}
>Spieler hinzufügen</button
>
</div> </div>
{:else} {:else}
<div class="flex grow"> <div class="flex grow">
@@ -379,53 +387,57 @@
{#if gameManager.state === GameState.SHOW_QUESTION} {#if gameManager.state === GameState.SHOW_QUESTION}
<div class="flex grow flex-col"> <div class="flex grow flex-col">
<div class="m-4 flex justify-between text-4xl"> <div class="m-4 flex justify-between text-4xl">
<div>{gameManager.category.name}</div> <div>{gameManager.currentCategory?.name}</div>
<div> <div>
{gameManager.question.points} Punkte {gameManager.currentQuestion?.points} Punkte
</div> </div>
</div> </div>
<div class="flex grow ps-4 pe-4"> <div class="flex grow ps-4 pe-4">
{#if gameManager.question === undefined} {#if gameManager.currentQuestion === undefined}
<p>Question is undefined</p> <p>Question is undefined</p>
{:else if isSimpleQuestion(gameManager.question)} {:else if isSimpleQuestion(gameManager.currentQuestion)}
<SimpleQuestionComponent <SimpleQuestionComponent
question={gameManager.question} question={gameManager.currentQuestion}
showAnswer={true} showAnswer={true}
showQuestion={true} showQuestion={true}
/> />
{:else if isMultipleChoiceQuestion(gameManager.question)} {:else if isMultipleChoiceQuestion(gameManager.currentQuestion)}
<MultipleChoiceQuestionComponent <MultipleChoiceQuestionComponent
question={gameManager.question} question={gameManager.currentQuestion}
showAnswer={true} showAnswer={true}
showQuestion={true} showQuestion={true}
/> />
{:else if isImageQuestion(gameManager.question)} {:else if isImageQuestion(gameManager.currentQuestion)}
<ImageQuestionComponent <ImageQuestionComponent
question={gameManager.question} question={gameManager.currentQuestion}
showAnswer={true} showAnswer={true}
showQuestion={true} showQuestion={true}
isBuzzed={false} isBuzzed={false}
isLegacy={false}
/> />
{:else if isImageMultipleChoiceQuestion(gameManager.question)} {:else if isImageMultipleChoiceQuestion(gameManager.currentQuestion)}
<ImageMultipleChoiceQuestionComponent <ImageMultipleChoiceQuestionComponent
question={gameManager.question} question={gameManager.currentQuestion}
showAnswer={true} showAnswer={true}
showQuestion={true} showQuestion={true}
isBuzzed={false} isBuzzed={false}
isLegacy={false}
/> />
{:else if isAudioQuestion(gameManager.question)} {:else if isAudioQuestion(gameManager.currentQuestion)}
<AudioQuestionComponent <AudioQuestionComponent
question={gameManager.question} question={gameManager.currentQuestion}
showAnswer={true} showAnswer={true}
showPlayer={true} showPlayer={true}
showQuestion={true} showQuestion={true}
isLegacy={false}
/> />
{:else if isAudioMultipleChoiceQuestion(gameManager.question)} {:else if isAudioMultipleChoiceQuestion(gameManager.currentQuestion)}
<AudioMultipleChoiceQuestionComponent <AudioMultipleChoiceQuestionComponent
question={gameManager.question} question={gameManager.currentQuestion}
showAnswer={true} showAnswer={true}
showPlayer={true} showPlayer={true}
showQuestion={true} showQuestion={true}
isLegacy={false}
/> />
{:else} {:else}
<p>Type of question unknown</p> <p>Type of question unknown</p>
@@ -477,16 +489,21 @@
</div> </div>
</div> </div>
{:else if gameManager.state === GameState.END} {:else if gameManager.state === GameState.END}
<div class="flex grow items-center justify-center text-7xl"><div>ENDE</div></div> <div class="flex grow items-center justify-center text-7xl">
<div>ENDE</div>
</div>
{:else} {:else}
<div class="grow ps-4 pe-4"> <div class="grow ps-4 pe-4">
<Wall <Wall
wall={gameManager.game.walls[gameManager.currentWall]} wall={gameManager.currentWall}
onclick={(cat, que) => gameManager.tileClicked(cat, que)} onclickIds={(cat, que) => gameManager.tileClicked(cat, que)}
visited={gameManager.visitedQuestions} visited={gameManager.visitedQuestions}
/> />
</div> </div>
{/if} {/if}
</div> </div>
{/if} {/if}
</div> </div>
{:else}
Loading Game...
{/if}

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

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

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

View File

@@ -0,0 +1,15 @@
<script lang="ts">
import type { SimpleQuestion } from "$lib/games/games";
import Textfield from "$lib/Textfield.svelte";
interface Props {
question: SimpleQuestion;
}
let { question }: Props = $props();
</script>
<div>
<Textfield bind:value={question.data.question}></Textfield>
<Textfield bind:value={question.data.answer}></Textfield>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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}

View File

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

View File

@@ -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
}
};
}

View 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}`;
});
}

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

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

View File

@@ -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()]
}); });