Compare commits
80 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
1243dec6b9 | ||
|
a7602eed6e | ||
|
ae2f0407fa | ||
|
c60976a06a | ||
|
ad58cbc577 | ||
|
40cdfe42be | ||
|
6f69ca91af | ||
|
113a5fb16e | ||
|
a0c8e9592f | ||
|
512edbc62b | ||
|
2bcb322488 | ||
|
3a19c5b3f7 | ||
|
bde47843bf | ||
|
33bd7e8bab | ||
|
bcc446ed16 | ||
|
0f0b2ede64 | ||
|
a06e50b704 | ||
|
ca609fad94 | ||
|
ca4dddec2e | ||
|
a5257506da | ||
|
8375a3b5bd | ||
|
bad7e137ad | ||
|
a8ba49bda4 | ||
|
163b1bbfc5 | ||
|
e25f803c8e | ||
|
7ed8c73112 | ||
|
1d235c1583 | ||
|
96c04835bc | ||
|
0b7db9297c | ||
|
3b86b04de2 | ||
|
e6208e9ec2 | ||
|
0e6fef049f | ||
|
b5191039ee | ||
|
5d2d33ebe3 | ||
|
967cf68a7f | ||
|
24c93623dc | ||
|
fee9caff08 | ||
|
c86dc32335 | ||
|
915c78dd58 | ||
|
6787f1c3f8 | ||
|
4cd1bf6984 | ||
|
959ba20d98 | ||
|
695d007fd7 | ||
|
0d255c1ee5 | ||
|
9ba74f5b2f | ||
|
b359d30d8c | ||
|
0f805962a4 | ||
|
7e4c344ec1 | ||
|
7de4f1cad8 | ||
|
fcfed00d5b | ||
|
e4114837c4 | ||
|
66fa59ea2e | ||
|
3577a9291a | ||
|
af89f6c9fb | ||
|
555c26448a | ||
|
ef486010af | ||
|
e726b8f990 | ||
|
9d4c6916a6 | ||
|
0cd78c5af9 | ||
|
a78ed70f05 | ||
|
846f137b7a | ||
|
c871418d07 | ||
|
78b4947730 | ||
|
dd653c39f4 | ||
|
3f8df8b538 | ||
|
415c42cefd | ||
|
0367fa6987 | ||
|
1876484590 | ||
|
21a24632b2 | ||
|
059035e4fd | ||
|
6b9394a50a | ||
|
0d2ae30b24 | ||
|
7cd0639708 | ||
|
e5bfd05e71 | ||
|
cfd16e8e6c | ||
|
4967d6bdf1 | ||
|
37198001a3 | ||
|
ea7caa322b | ||
|
1dcd26ffcf | ||
|
a243deb79e |
@ -1,18 +1,23 @@
|
||||
pipeline:
|
||||
when:
|
||||
branch: prod
|
||||
|
||||
steps:
|
||||
frontend:
|
||||
image: node:16-alpine
|
||||
image: node:18-alpine
|
||||
commands:
|
||||
- ( cd client && yarn install )
|
||||
- ( cd client/ && yarn generate )
|
||||
- yarn global add pnpm
|
||||
- ( cd client && pnpm run setup )
|
||||
- ( cd client/ && pnpm run generate )
|
||||
- mkdir -p client/.output/public/
|
||||
|
||||
backend:
|
||||
image: golang:1.18-alpine
|
||||
image: golang:1.20-alpine
|
||||
commands:
|
||||
- apk add go-bindata
|
||||
- apk add make
|
||||
- make -C server setup
|
||||
- make -C server build
|
||||
- mkdir -p build/files/data
|
||||
- cp server/knowyt build/files/
|
||||
|
||||
build-publish:
|
||||
@ -28,7 +33,7 @@ pipeline:
|
||||
tags: latest
|
||||
|
||||
notify:
|
||||
image: plugins/matrix
|
||||
image: thegeeklab/drone-matrix
|
||||
settings:
|
||||
homeserver: https://matrix.bw-messenger.de/
|
||||
roomid: wHCOKvEHLUmsNybNwh:matrix.bw-messenger.de
|
||||
@ -36,5 +41,3 @@ pipeline:
|
||||
from_secret: matrix.bw-messenger.de.username
|
||||
password:
|
||||
from_secret: matrix.bw-messenger.de.password
|
||||
|
||||
branches: prod
|
||||
|
48
Makefile
48
Makefile
@ -1,7 +1,8 @@
|
||||
TMUX_SESSION=knowyt
|
||||
VERSION=$(shell grep version client/package.json | cut -d\" -f4)
|
||||
CONTAINER=$(shell which podman || which docker)
|
||||
|
||||
.PHONY: info run-all run-server run-client run-tmux build podman clean
|
||||
.PHONY: info setup run-all run-server run-client run-tmux build clean
|
||||
|
||||
info:
|
||||
@echo available targets:
|
||||
@ -9,7 +10,16 @@ info:
|
||||
|
||||
setup:
|
||||
@echo "I checking for tools"
|
||||
@for binary in go tmux node yarn npx podman; do \
|
||||
@echo -n " container: " ; \
|
||||
if ! [ -x "$(CONTAINER)" ]; then \
|
||||
echo "neither podman nor docker found" ;\
|
||||
exit 1 ;\
|
||||
else \
|
||||
echo "$(CONTAINER)" ;\
|
||||
fi
|
||||
@for binary in go go-bindata tmux node pnpm npx \
|
||||
pexec inotifyloop inotifywait \
|
||||
newuidmap slirp4netns; do \
|
||||
echo -n " $$binary: " ; \
|
||||
if ! which "$$binary"; then \
|
||||
echo "not found" ;\
|
||||
@ -17,11 +27,11 @@ setup:
|
||||
fi ;\
|
||||
done
|
||||
@echo "I installing client dependencies"
|
||||
( cd client && yarn install )
|
||||
@echo "I installing server dependencies"
|
||||
$(MAKE) -C server setup
|
||||
( cd client && pnpm run setup )
|
||||
@echo "I create client output directory"
|
||||
mkdir -p client/.output/public/
|
||||
@echo "I installing server dependencies"
|
||||
$(MAKE) -C server setup
|
||||
|
||||
run-all:
|
||||
pexec -R -c -e TARGET \
|
||||
@ -39,32 +49,36 @@ run-tmux:
|
||||
tmux attach-session -t "$(TMUX_SESSION)"
|
||||
|
||||
run-client:
|
||||
(cd client/ && yarn dev)
|
||||
(cd client/ && pnpm dev)
|
||||
|
||||
run-server:
|
||||
$(MAKE) -C server run-loop
|
||||
|
||||
build:
|
||||
echo $(VERSION)
|
||||
(cd client/ && yarn generate)
|
||||
(cd client/ && pnpm run generate)
|
||||
$(MAKE) -C server build
|
||||
$(MAKE) podman-build
|
||||
$(MAKE) podman-save
|
||||
$(MAKE) container-build
|
||||
|
||||
podman-build:
|
||||
container-build:
|
||||
mkdir -p build/files/data
|
||||
cp server/knowyt build/files/
|
||||
podman build --tag knowyt:$(VERSION) .
|
||||
$(CONTAINER) build --tag knowyt:$(VERSION) .
|
||||
|
||||
podman-save:
|
||||
container-save:
|
||||
rm -f build/knowyt-$(VERSION).tar
|
||||
podman save knowyt:$(VERSION) -o build/knowyt-$(VERSION).tar
|
||||
$(CONTAINER) save knowyt:$(VERSION) -o build/knowyt-$(VERSION).tar
|
||||
ls -lh build/knowyt-$(VERSION).tar
|
||||
|
||||
podman-run:
|
||||
podman run --rm -it -p 32039:32039 -v $$(pwd)/server/data/:/data --name knowyt knowyt:$(VERSION)
|
||||
container-run:
|
||||
$(CONTAINER) run --rm -it -p 8080:32039 -v $$(pwd)/server/data/:/data --name knowyt knowyt:$(VERSION)
|
||||
|
||||
podman-stop:
|
||||
podman stop knowyt
|
||||
container-stop:
|
||||
$(CONTAINER) stop knowyt
|
||||
|
||||
container-publish:
|
||||
$(CONTAINER) push knowyt:$(VERSION) docker.io/settel/knowyt:$(VERSION)
|
||||
$(CONTAINER) push knowyt:$(VERSION) docker.io/settel/knowyt:latest
|
||||
|
||||
clean:
|
||||
rm -rf client/.output/
|
||||
|
29
README.md
29
README.md
@ -1,22 +1,45 @@
|
||||
# Know Your Teammates
|
||||
|
||||
Know Your Teammates is a free team building game for 3-20 players that can be played in a browser.
|
||||
|
||||
It has two phases: during collection phase, the players are asked to enter 3-5 facts about themselves, eg. hobbies, books, movies they enjoy, places they have visited or other fun facts. In play phase, one fact is presented to all players and they have to guess who wrote it. The real fun is in the discussion following it :-)
|
||||
|
||||
[Watch video (3:30mins)](https://www.sirlab.de/knowyt/know-your-teammates.mp4)
|
||||
|
||||
|
||||
See website https://www.sirlab.de/linux/games/knowyt/
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
install dependencies and check for missing tools
|
||||
|
||||
```
|
||||
make setup
|
||||
```
|
||||
|
||||
|
||||
## run (development mode)
|
||||
|
||||
start client and server in a split terminal window
|
||||
|
||||
```
|
||||
make run-tmux
|
||||
```
|
||||
|
||||
This will start frontend (port 3000) and backend (port 32039) as two separate services. Hot reload is active, changes will take effect immediately.
|
||||
|
||||
## build podman/docker image
|
||||
Point your browser at [http://localhost:3000/](http://localhost:3000/) to start playing.
|
||||
|
||||
|
||||
|
||||
## build and run podman/docker image
|
||||
|
||||
```
|
||||
make setup # only needed on first run
|
||||
|
||||
make build
|
||||
make container-run
|
||||
```
|
||||
|
||||
|
||||
@ -27,4 +50,6 @@ AGPLv3 (GNU Affero General Public License)
|
||||
|
||||
## Author
|
||||
|
||||
© 2021-2022 Achim Settelmeier <knowyt@m1.sirlab.de>
|
||||
© 2021-2024 Achim Settelmeier <knowyt@m1.sirlab.de>
|
||||
|
||||
https://www.sirlab.de/
|
||||
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"authcode": "646162",
|
||||
"name": "Settel (Admin)",
|
||||
"role": "admin"
|
||||
}
|
@ -1,25 +1,35 @@
|
||||
{
|
||||
"name": "knowyt",
|
||||
"version": "2.14",
|
||||
"version": "3.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "tsc-strict",
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview"
|
||||
"setup": "pnpm install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nuxt": "^3.0.0",
|
||||
"sass": "^1.56.1",
|
||||
"sass-loader": "^13.2.0",
|
||||
"typescript-strict-plugin": "^2.1.0"
|
||||
"@types/node": "^18.19.17",
|
||||
"http-proxy": "^1.18.1",
|
||||
"nuxt": "^3.10.2",
|
||||
"sass": "^1.71.0",
|
||||
"sass-loader": "^13.3.3",
|
||||
"typescript-strict-plugin": "^2.3.0",
|
||||
"webpack": "^5.90.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pinia/nuxt": "^0.4.4",
|
||||
"build-url": "^6.0.1",
|
||||
"nuxt-icons": "^3.0.0",
|
||||
"typescript": "^4.9.3",
|
||||
"vue-contenteditable": "^4.1.0"
|
||||
"@pinia/nuxt": "^0.4.11",
|
||||
"@vue/reactivity": "^3.4.19",
|
||||
"@vue/runtime-core": "^3.4.19",
|
||||
"@vue/runtime-dom": "^3.4.19",
|
||||
"@vue/shared": "^3.4.19",
|
||||
"nuxt-icons": "^3.2.1",
|
||||
"ofetch": "^1.3.3",
|
||||
"query-string": "^8.2.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vue": "^3.4.19",
|
||||
"vue-contenteditable": "^4.1.0",
|
||||
"vue-router": "^4.2.5"
|
||||
}
|
||||
}
|
||||
|
6715
client/pnpm-lock.yaml
generated
Normal file
6715
client/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
49
client/src/assets/icons/checkmark.svg
Normal file
49
client/src/assets/icons/checkmark.svg
Normal file
@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="128"
|
||||
height="100"
|
||||
viewBox="0 0 128 100"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
sodipodi:docname="checkmark.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px"
|
||||
showgrid="false"
|
||||
inkscape:zoom="2.3786088"
|
||||
inkscape:cx="10.720552"
|
||||
inkscape:cy="21.441105"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1181"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer1"
|
||||
showborder="true" />
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<path
|
||||
style="fill:#4bc417;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 93.253283,6.1273107 C 100.05266,14.282038 108.74264,17.439899 117.91216,22.197816 104.79242,27.143529 52.350943,76.132207 44.577742,96.156471 42.587301,88.868507 16.14146,63.021362 10.575889,61.270758 c 6.682863,-3.009693 20.41554,-9.619427 23.978573,-15.275893 0.758717,5.579065 9.653081,21.558694 9.653081,21.558694 8.596139,-4.086698 48.882074,-51.182042 49.04574,-61.4262483 z"
|
||||
id="path790"
|
||||
sodipodi:nodetypes="ccccccc" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
@ -27,6 +27,7 @@ const createNewQuote = () => {
|
||||
newQuote.value = {
|
||||
id: ':new:' + Date.now(),
|
||||
quote: '',
|
||||
isPlayed: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
33
client/src/components/CopyrightNotice.vue
Normal file
33
client/src/components/CopyrightNotice.vue
Normal file
@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="copyright-notice__version" @click="openInfoModal">
|
||||
v{{ config.public.version }}, © 2021-2024, Settel
|
||||
</div>
|
||||
<InfoModal v-if="showInfoModal" @close="closeInfoModal" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRuntimeConfig } from '#app'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
const showInfoModal = ref(false)
|
||||
const openInfoModal = () => { showInfoModal.value = true }
|
||||
const closeInfoModal = () => { showInfoModal.value = false }
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.copyright-notice {
|
||||
&__version {
|
||||
position: absolute;
|
||||
right: 1em;
|
||||
bottom: 0;
|
||||
color: #606060;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #c0c0c0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -37,7 +37,12 @@ const emit = defineEmits(['close'])
|
||||
border-radius: 8px;
|
||||
color: $dialog-box-text-color;
|
||||
z-index: 12;
|
||||
|
||||
@media (max-width: $phone-max-width) {
|
||||
min-width: unset;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
|
@ -11,9 +11,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from '#app'
|
||||
import { ref, Ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import useAuth from '@/composables/useAuth';
|
||||
import useI18n from '@/composables/useI18n';
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
const { $t } = useI18n({
|
||||
'login': { en: 'log in', de: 'login'},
|
||||
|
@ -13,7 +13,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Player } from '@/composables/engine.d';
|
||||
import type { Player } from '@/composables/engine.d';
|
||||
|
||||
defineProps<{
|
||||
player: Player,
|
||||
|
@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<div :class="cssClasses">
|
||||
<div v-if="quote.isPlayed" class="quote-card__marker__is-played">
|
||||
<nuxt-icon name="checkmark" filled />
|
||||
</div>
|
||||
<div v-if="editable && !isEditMode" class="quote-card__action-buttons">
|
||||
<QuoteCardActionButton @click="editQuote" icon="edit" />
|
||||
<QuoteCardActionButton v-if="!quote.isPlayed" @click="editQuote" icon="edit" />
|
||||
<QuoteCardActionButton @click="deleteQuote" icon="trash" />
|
||||
</div>
|
||||
<div class="quote-card__text-container">
|
||||
@ -180,5 +183,18 @@ const keydown = async (ev: KeyboardEvent) => {
|
||||
top: -24px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__marker__is-played {
|
||||
position: absolute;
|
||||
left: -28px;
|
||||
top: -40px;
|
||||
width: 128px;
|
||||
height: 100px;
|
||||
|
||||
.nuxt-icon svg {
|
||||
width: unset;
|
||||
height: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,32 +1,37 @@
|
||||
<template>
|
||||
<AdminInfoTile title="Games">
|
||||
<table class="gameinfos-tile__table">
|
||||
<tr class="gameinfos-tile__row">
|
||||
<th class="gameinfos-tile__table-head">{{ $t('team-name')}}</th>
|
||||
<th class="gameinfos-tile__table-head">{{ $t('lang')}}</th>
|
||||
<th class="gameinfos-tile__table-head">{{ $t('state')}}</th>
|
||||
<th class="gameinfos-tile__table-head">{{ $t('num-players')}}</th>
|
||||
<th class="gameinfos-tile__table-head">{{ $t('gamemasters')}}</th>
|
||||
</tr>
|
||||
<tr
|
||||
:class="{ 'gameinfos-tile__row': true, 'gameinfos-tile__row__disabled': game.state == 'disabled' }"
|
||||
v-for="game in games"
|
||||
:key="game.id"
|
||||
@click="selectGame(game)"
|
||||
>
|
||||
<div>
|
||||
<AdminInfoTile v-if="gamesCount > 0" title="Games">
|
||||
<table class="gameinfos-tile__table">
|
||||
<tr class="gameinfos-tile__row">
|
||||
<th class="gameinfos-tile__table-head">{{ $t('team-name')}}</th>
|
||||
<th class="gameinfos-tile__table-head">{{ $t('lang')}}</th>
|
||||
<th class="gameinfos-tile__table-head">{{ $t('state')}}</th>
|
||||
<th class="gameinfos-tile__table-head">{{ $t('num-players')}}</th>
|
||||
<th class="gameinfos-tile__table-head">{{ $t('gamemasters')}}</th>
|
||||
</tr>
|
||||
<tr
|
||||
:class="{ 'gameinfos-tile__row': true, 'gameinfos-tile__row__disabled': game.state == 'disabled' }"
|
||||
v-for="game in games"
|
||||
:key="game.id"
|
||||
@click="selectGame(game)"
|
||||
>
|
||||
<td class="gameinfos-tile__cell">{{ game.name }}</td>
|
||||
<td class="gameinfos-tile__cell">{{ game.lang }}</td>
|
||||
<td class="gameinfos-tile__cell">{{ game.state }}</td>
|
||||
<td class="gameinfos-tile__cell">{{ game.players.length }}</td>
|
||||
<td class="gameinfos-tile__cell">{{ getGamemastersFromGame(game).join(', ') }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</AdminInfoTile>
|
||||
</tr>
|
||||
</table>
|
||||
</AdminInfoTile>
|
||||
<AdminInfoTile v-if="gamesCount == 0" :title="$t('no-games-1')">
|
||||
<p>{{ $t('no-games-2') }}</p>
|
||||
</AdminInfoTile>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { navigateTo } from "#app"
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import useI18n from '@/composables/useI18n'
|
||||
import useEngine from '@/composables/useEngine'
|
||||
import type { GameInfo } from '@/composables/engine.d'
|
||||
@ -37,11 +42,16 @@ const { $t } = useI18n({
|
||||
state: { en: 'State', de: 'Status' },
|
||||
'num-players': { en: '# Players', de: '# Spieler' },
|
||||
gamemasters: { en: 'Gamemaster(s)', de: 'Gamemaster(s)' },
|
||||
'no-games-1': { en: 'The list of teams is empty', de: 'Die Liste der Teams ist noch leer'},
|
||||
'no-games-2': { en: 'Log out and click the "create team" button on the start page.', de: 'Logge dich aus und klicke auf der Startseite auf "Team erstellen".'},
|
||||
})
|
||||
|
||||
const { fetchGameInfos, cameo } = useEngine()
|
||||
|
||||
const games = ref(await fetchGameInfos())
|
||||
const gamesCount = computed(() => {
|
||||
return Object.keys(games.value).length
|
||||
})
|
||||
const getGamemastersFromGame = (game: GameInfo) => game.players.filter((player) => player.role === 'gamemaster').map((player) => player.name)
|
||||
|
||||
const selectGame = async (game: GameInfo): Promise<void> => {
|
||||
|
@ -30,7 +30,7 @@ const emit = defineEmits(['icon-top-click', 'icon-bottom-click'])
|
||||
&__container {
|
||||
position: relative;
|
||||
min-width: 300px;
|
||||
margin: 40px;
|
||||
margin: 16px;
|
||||
padding: 16px 30px;
|
||||
background-color: $admin-tile-background-color;
|
||||
border: $admin-tile-border;
|
||||
|
@ -22,8 +22,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { PlayerEdit } from '@/composables/engine.d'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import type { PlayerEdit } from '@/composables/engine.d'
|
||||
import type { Button } from '@/components/admin/PlayerModal'
|
||||
|
||||
const props = defineProps<{
|
||||
|
@ -3,10 +3,10 @@
|
||||
<AdminInfoTile title="Players" icon-top="reload" @icon-top-click="reload" icon-bottom="add" @icon-bottom-click="addPlayer">
|
||||
<table class="players-tile__table">
|
||||
<tr>
|
||||
<th class="players-tile__table-head">{{ $t('name') }}:</th>
|
||||
<th class="players-tile__table-head">{{ $t('num-quotes') }}</th>
|
||||
<th class="players-tile__table-head">{{ $t('score') }}</th>
|
||||
<th class="players-tile__table-head">{{ $t('last-logged-in') }}</th>
|
||||
<th class="players-tile__table-head players-tile__cell">{{ $t('name') }}:</th>
|
||||
<th class="players-tile__table-head players-tile__cell">{{ $t('num-quotes') }}</th>
|
||||
<th class="players-tile__table-head players-tile__cell">{{ $t('score') }}</th>
|
||||
<th class="players-tile__table-head players-tile__cell">{{ $t('last-logged-in') }}</th>
|
||||
</tr>
|
||||
<tr v-for="player in players" class="players-tile__row" :key="player.id" @click="editPlayer(player)">
|
||||
<td class="players-tile__cell">
|
||||
@ -15,7 +15,7 @@
|
||||
<nuxt-icon name="crown" filled />
|
||||
</div>
|
||||
</td>
|
||||
<td class="players-tile__cell">{{ player.numQuotes }}</td>
|
||||
<td class="players-tile__cell">{{ player.numQuotesPlayed }} / {{ player.numQuotes }}</td>
|
||||
<td class="players-tile__cell">{{ player.score }}</td>
|
||||
<td class="players-tile__cell">{{ !player.isIdle ? 'online' : player.lastLoggedIn === 0 ? '-' : datetime(player.lastLoggedIn) }}</td>
|
||||
</tr>
|
||||
@ -42,7 +42,7 @@ const emit = defineEmits(['update'])
|
||||
|
||||
const { $t } = useI18n({
|
||||
name: { en: 'Name', de: 'Name' },
|
||||
'num-quotes': { en: '# quotes', de: '# Quotes' },
|
||||
'num-quotes': { en: '# quotes played', de: '# Aussagen gespielt' },
|
||||
'score': { en: 'Score', de: 'Score' },
|
||||
'last-logged-in': { en: 'last logged in', de: 'zuletzt eingeloggt' },
|
||||
})
|
||||
@ -135,7 +135,7 @@ const playerDialogSubmit = async (action: ButtonAction): Promise<void> => {
|
||||
}
|
||||
|
||||
&__cell {
|
||||
padding-right: 8px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
&__is-gamemaster {
|
||||
|
2
client/src/composables/engine.d.ts
vendored
2
client/src/composables/engine.d.ts
vendored
@ -18,12 +18,14 @@ export type PlayerInfo = PlayerEdit & {
|
||||
lastLoggedIn: number
|
||||
isPlaying: boolean
|
||||
numQuotes: number
|
||||
numQuotesPlayed: number
|
||||
role: Role
|
||||
}
|
||||
|
||||
export type Quote = {
|
||||
id: string
|
||||
quote: string
|
||||
isPlayed: boolean
|
||||
}
|
||||
|
||||
export type Quotes = Array<Quote>
|
||||
|
@ -1,14 +1,13 @@
|
||||
import buildUrl from 'build-url'
|
||||
import queryString from 'query-string'
|
||||
|
||||
export type QueryParams = {
|
||||
[name: string]: string
|
||||
}
|
||||
|
||||
export async function callApi(path: string, queryParams?: QueryParams) {
|
||||
const url = buildUrl('/', {
|
||||
path,
|
||||
queryParams,
|
||||
})
|
||||
const url = path + (
|
||||
queryParams ? '?' + queryString.stringify(queryParams) : ''
|
||||
)
|
||||
|
||||
return await $fetch(url)
|
||||
}
|
||||
|
@ -3,9 +3,9 @@ import { useUserinfoStore } from "@/stores/UserinfoStore"
|
||||
import { useGameinfoStore } from "@/stores/GameinfoStore"
|
||||
import { useRoundStore } from "@/stores/RoundStore"
|
||||
import { usePlayersStore } from "@/stores/PlayersStore"
|
||||
import { EngineContext } from '@/composables/useEngine'
|
||||
import useAlert from '@/composables/useAlert'
|
||||
import useI18n from '@/composables/useI18n'
|
||||
import type { EngineContext } from '@/composables/useEngine'
|
||||
|
||||
type EngineResponse = {
|
||||
version: string,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { EngineContext } from '@/composables/useEngine'
|
||||
import { useUserinfoStore } from "@/stores/UserinfoStore"
|
||||
import type { EngineContext } from '@/composables/useEngine'
|
||||
import type { GameInfo, GameInfos, PlayerEdit, Lang, CreateGameStatus } from '@/composables/engine.d'
|
||||
|
||||
export async function fetchGameInfo(this: EngineContext): Promise<GameInfo> {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { EngineContext } from '@/composables/useEngine'
|
||||
import { useUserinfoStore } from "@/stores/UserinfoStore"
|
||||
import type { EngineContext } from '@/composables/useEngine'
|
||||
|
||||
export async function collectQuotes(this: EngineContext): Promise<void> {
|
||||
const userInfoStore = useUserinfoStore()
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useUserinfoStore } from "@/stores/UserinfoStore"
|
||||
import { EngineContext } from '@/composables/useEngine'
|
||||
import type { EngineContext } from '@/composables/useEngine'
|
||||
|
||||
export async function saveSelection(this: EngineContext, selection: string): Promise<void> {
|
||||
const userInfoStore = useUserinfoStore()
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { Ref, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import type { Quotes } from '@/composables/engine.d'
|
||||
import { useUserinfoStore } from "@/stores/UserinfoStore"
|
||||
import { EngineContext } from '@/composables/useEngine'
|
||||
import type { Ref } from 'vue'
|
||||
import type { EngineContext } from '@/composables/useEngine'
|
||||
|
||||
type QuotesResponse = {
|
||||
quotes: Quotes
|
||||
|
7
client/src/composables/engine/setupApp.ts
Normal file
7
client/src/composables/engine/setupApp.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { EngineContext } from '@/composables/useEngine'
|
||||
|
||||
export async function setupApp(this: EngineContext, authcode: string): Promise<void> {
|
||||
await this.callApi('/api/setupApp', {
|
||||
authcode,
|
||||
})
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { useUserinfoStore } from "@/stores/UserinfoStore"
|
||||
import { EngineContext } from '@/composables/useEngine'
|
||||
import useAlert from '@/composables/useAlert'
|
||||
import type { EngineContext } from '@/composables/useEngine'
|
||||
|
||||
export function start(this: EngineContext): void {
|
||||
if (this.isActive && !this.shouldStop) {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Ref, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
export type AlertMessages = Array<string>
|
||||
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { useUserinfoStore, Userinfo } from '@/stores/UserinfoStore'
|
||||
import { useUserinfoStore } from '@/stores/UserinfoStore'
|
||||
import { useEngineStore } from '@/stores/EngineStore'
|
||||
import { useGameinfoStore } from '@/stores/GameinfoStore'
|
||||
import { usePlayersStore } from '@/stores/PlayersStore'
|
||||
import { useRoundStore } from '@/stores/RoundStore'
|
||||
import useI18n from './useI18n'
|
||||
import { $fetch } from 'ohmyfetch'
|
||||
import { $fetch } from 'ofetch'
|
||||
|
||||
export type AllowRole = '' | 'player' | 'gamemaster' | 'admin'
|
||||
import type { Userinfo } from '@/stores/UserinfoStore'
|
||||
export type AllowRole = '' | 'player' | 'gamemaster' | 'admin' | 'setup'
|
||||
export type AllowRoles = Array<AllowRole>
|
||||
|
||||
export interface useAuth {
|
||||
@ -25,8 +26,17 @@ export default (): useAuth => {
|
||||
user.setUserInfo(userInfo)
|
||||
useI18n({}).setLang(userInfo.lang)
|
||||
if (allowRoles.indexOf(userInfo.role) >= 0 ) {
|
||||
// user is authenticated and authorized, let the user in
|
||||
return
|
||||
}
|
||||
|
||||
// game is not initialized yet, needs setup
|
||||
if (userInfo.role === 'setup') {
|
||||
document.location.pathname = '/setup'
|
||||
return
|
||||
}
|
||||
|
||||
// user is authenticated but not authorized for this page
|
||||
if (user.isAdmin) {
|
||||
document.location.pathname = '/admin'
|
||||
// can't use navigateTo() for it fails with DOMException if two consecutive redirects happen (at least in docker container)
|
||||
@ -35,6 +45,7 @@ export default (): useAuth => {
|
||||
document.location.pathname = '/play'
|
||||
}
|
||||
} catch (e) {
|
||||
// user is not authenticated
|
||||
if (allowRoles.indexOf('') == -1 ) {
|
||||
document.location.pathname = '/'
|
||||
}
|
||||
|
@ -1,11 +1,14 @@
|
||||
import { Ref, ref } from 'vue'
|
||||
import { callApi, QueryParams } from '@/composables/engine/callApi'
|
||||
import { ref } from 'vue'
|
||||
import { callApi } from '@/composables/engine/callApi'
|
||||
import { start, stop } from '@/composables/engine/startStop'
|
||||
import { setupApp } from '@/composables/engine/setupApp'
|
||||
import { fetchUpdate } from '@/composables/engine/fetchUpdate'
|
||||
import { loadQuotes, getQuotesRef, deleteQuote, saveQuote } from '@/composables/engine/quotes'
|
||||
import { fetchGameInfo, fetchGameInfos, setGameLang, setGameName, savePlayer, removePlayer, createGame, cameo, logoutCameo } from '@/composables/engine/gameManagement'
|
||||
import { collectQuotes, startGame, continueGame, resetGame, finishGame, disableGame, removeGame } from '@/composables/engine/gameState'
|
||||
import { saveSelection } from '@/composables/engine/play'
|
||||
import type { Ref } from 'vue'
|
||||
import type { QueryParams } from '@/composables/engine/callApi'
|
||||
import type { Quotes, GameInfo, GameInfos, PlayerEdit, Lang, CreateGameStatus } from '@/composables/engine.d'
|
||||
|
||||
export interface EngineContext {
|
||||
@ -48,6 +51,7 @@ export interface useEngine {
|
||||
createGame: (name: string, teamname: string, lang: Lang) => Promise<CreateGameStatus>
|
||||
cameo: (authcode: string) => Promise<void>
|
||||
logoutCameo: () => Promise<void>
|
||||
setupApp: (authcode: string) => Promise<void>
|
||||
}
|
||||
|
||||
export default (): useEngine => {
|
||||
@ -92,5 +96,6 @@ export default (): useEngine => {
|
||||
createGame: (name: string, teamname: string, lang: Lang) => createGame.apply(context, [name, teamname, lang]),
|
||||
cameo: (authcode: string) => cameo.apply(context,[authcode]),
|
||||
logoutCameo: () => logoutCameo.apply(context),
|
||||
setupApp: (authcode) => setupApp.apply(context, [authcode]),
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ await useAuth().authenticateAndLoadUserInfo(['admin'])
|
||||
|
||||
&__tiles {
|
||||
display: flex;
|
||||
margin: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -42,6 +42,7 @@ updateGameinfo()
|
||||
|
||||
&__tiles {
|
||||
display: flex;
|
||||
margin: 24px;
|
||||
}
|
||||
|
||||
&__tiles-spacer {
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-index__page">
|
||||
<TitleBox />
|
||||
<div class="page-index__action-box">
|
||||
<div class="page-index__space" />
|
||||
@ -12,34 +12,38 @@
|
||||
</div>
|
||||
<div class="page-index__space" />
|
||||
</div>
|
||||
<div class="page-index__copyright-notice" @click="openInfoModal">
|
||||
v{{ config.version }}, © 2021-2022, Settel
|
||||
<div class="page-index__about">
|
||||
<a
|
||||
href="https://www.sirlab.de/linux/games/knowyt/"
|
||||
target="_blank" rel="noopener"
|
||||
>
|
||||
<Button :border="false">
|
||||
<div class="page-index__about-text">{{ $t('about') }}</div>
|
||||
</Button>
|
||||
|
||||
</a>
|
||||
</div>
|
||||
<InfoModal v-if="showInfoModal" @close="closeInfoModal" />
|
||||
<CopyrightNotice />
|
||||
<CreateTeamDialog v-if="showCreateTeamDialog" @close="closeCreateTeamDialog" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRuntimeConfig, navigateTo } from '#app'
|
||||
import { ref } from 'vue'
|
||||
import useAuth from '@/composables/useAuth'
|
||||
import useI18n from '@/composables/useI18n'
|
||||
import CopyrightNotice from '../components/CopyrightNotice.vue';
|
||||
|
||||
const { $t } = useI18n({
|
||||
'create-team': { en: 'Create Team ...', de: 'Team erstellen ...' },
|
||||
'about': { en: 'about the game', de: 'Über das Spiel' },
|
||||
})
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
await useAuth().authenticateAndLoadUserInfo([''])
|
||||
|
||||
const showCreateTeamDialog = ref(false)
|
||||
const showInfoModal = ref(false)
|
||||
const createTeam = () => { showCreateTeamDialog.value = true }
|
||||
const closeCreateTeamDialog = () => { showCreateTeamDialog.value = false }
|
||||
const openInfoModal = () => { showInfoModal.value = true }
|
||||
const closeInfoModal = () => { showInfoModal.value = false }
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@ -53,6 +57,11 @@ body {
|
||||
}
|
||||
|
||||
.page-index {
|
||||
&__page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__action-box {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
@ -93,16 +102,9 @@ body {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&__copyright-notice {
|
||||
position: absolute;
|
||||
right: 1em;
|
||||
bottom: 0;
|
||||
color: #606060;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #c0c0c0;
|
||||
}
|
||||
&__about {
|
||||
margin: 48px 0;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
</style>
|
111
client/src/pages/setup.vue
Normal file
111
client/src/pages/setup.vue
Normal file
@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div>
|
||||
<TitleBox />
|
||||
<div class="page-setup__action-box">
|
||||
<div class="page-setup__description">
|
||||
<p>{{ $t('description-1') }}</p>
|
||||
<p>{{ $t('description-2') }}</p>
|
||||
</div>
|
||||
<div class="page-setup__button">
|
||||
<Button @click="openModal">{{ $t('create admin user') }}</Button>
|
||||
|
||||
<ModalDialog v-if="showAdminAccountCreatedDialog" :no-close-button="true" @close="showAdminAccountCreatedDialog = false">
|
||||
<div class="page-setup__auth-message">{{ $t('pin') }}</div>
|
||||
<div class="page-setup__pin">{{ authcode }}</div>
|
||||
<div class="page-setup__auth-message">{{ $t('remember-and-log-in') }}</div>
|
||||
<template v-slot:footer>
|
||||
<div class="page-setup__cta">
|
||||
<Button @click="createAdminAccount" :disabled="saveInProgress">{{ $t('complete-setup') }}</Button>
|
||||
</div>
|
||||
</template>
|
||||
</ModalDialog>
|
||||
</div>
|
||||
</div>
|
||||
<CopyrightNotice />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import useAuth from '@/composables/useAuth'
|
||||
import useI18n from '@/composables/useI18n'
|
||||
import useEngine from '@/composables/useEngine'
|
||||
|
||||
const { $t } = useI18n({
|
||||
'create admin user': { en: 'create admin user', de: 'Admin-Benutzer anlegen' },
|
||||
'description-1': { en: 'Congratulation!', de: 'Herzlichen Glückwunsch!' },
|
||||
'description-2': { en: 'You\'ve successfully installed Know Your Teammates.', de: 'Know Your Teammates wurde erfolgreich installiert.' },
|
||||
'pin': { en: 'Your pin code is:', de: 'Deine PIN lautet:' },
|
||||
'remember-and-log-in': { en: 'Write it down now.', de: 'Schreibe sie Dir jetzt auf.' },
|
||||
'complete-setup': { en: 'Complete setup', de: 'Setup abschließen' },
|
||||
})
|
||||
|
||||
await useAuth().authenticateAndLoadUserInfo(['setup'])
|
||||
|
||||
const showAdminAccountCreatedDialog = ref(false)
|
||||
const saveInProgress = ref(false)
|
||||
const authcode = ref('000000')
|
||||
const openModal = () => {
|
||||
authcode.value = ''
|
||||
for (var i = 0; i < 6; i++) {
|
||||
authcode.value += '' + Math.floor(Math.floor(Math.random() * 10000) / 100) % 10
|
||||
}
|
||||
showAdminAccountCreatedDialog.value = true
|
||||
saveInProgress.value = false
|
||||
}
|
||||
|
||||
const createAdminAccount = async () => {
|
||||
saveInProgress.value = true
|
||||
await useEngine().setupApp(authcode.value)
|
||||
saveInProgress.value = false
|
||||
showAdminAccountCreatedDialog.value = false
|
||||
document.location.pathname = "/"
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~/assets/css/components';
|
||||
|
||||
.page-setup {
|
||||
&__action-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 340px;
|
||||
margin: 0 auto;
|
||||
|
||||
@media (max-width: $phone-max-width) {
|
||||
margin: 32px 0 0 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: 24px;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__auth-message {
|
||||
font-family: $font-secondary;
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
margin: 32px;
|
||||
}
|
||||
|
||||
&__pin {
|
||||
font-family: $font-secondary;
|
||||
font-size: 32px;
|
||||
font-weight: 800;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__button {
|
||||
display: flex;
|
||||
margin: 32px auto;
|
||||
}
|
||||
|
||||
&__cta {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,5 +1,5 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { Players } from '@/composables/engine.d';
|
||||
import type { Players } from '@/composables/engine.d';
|
||||
|
||||
export const usePlayersStore = defineStore('PlayersStore', {
|
||||
state: () => {
|
||||
|
@ -3,7 +3,7 @@ import { defineStore } from 'pinia'
|
||||
export type Userinfo = {
|
||||
id: string
|
||||
name: string
|
||||
role: '' | 'player' | 'gamemaster' | 'admin'
|
||||
role: '' | 'player' | 'gamemaster' | 'admin' | 'setup'
|
||||
game: string
|
||||
lang: 'de' | 'en'
|
||||
isCameo: string
|
||||
|
4457
client/yarn.lock
4457
client/yarn.lock
File diff suppressed because it is too large
Load Diff
2
server/.gitignore
vendored
2
server/.gitignore
vendored
@ -1,2 +1,2 @@
|
||||
knowyt
|
||||
data/games/*/state.json
|
||||
data/
|
||||
|
@ -7,6 +7,7 @@ setup:
|
||||
|
||||
build:
|
||||
-mkdir -p ../client/.output/public/
|
||||
-mkdir data/
|
||||
$(MAKE) generate
|
||||
cd src/ && CGO_ENABLED=0 go build -o ../knowyt knowyt.go
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
{"name":"Massive Demo","lang":"de","created":1651603106}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "12.2",
|
||||
"source": "9c5a22d3-1e82-4bad-95a4-c9efb169ede0"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "1.1",
|
||||
"source": "4b1c22b8-6fa1-4c9d-98d7-cbf498035074"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "11.2",
|
||||
"source": "cede08c4-768a-4792-b92f-8df162a07307"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "4.1",
|
||||
"source": "b190ebb1-86c0-4308-a6b6-bf34237c10f8"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "Admin #1",
|
||||
"source": "f30802dc-1c18-4169-99fe-04d1d8e7bd9e"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "7.2",
|
||||
"source": "3945635e-c65a-4fb7-a46c-675ec53abebe"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "16.1",
|
||||
"source": "49295e5b-0d0a-44ea-9bff-e74acaaa6dcf"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "3.2",
|
||||
"source": "4fa78612-accd-491d-93e6-cca251ac0e5a"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "5.1",
|
||||
"source": "3c60f533-676b-4464-8542-1f3e6fc49d13"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "Admin #2",
|
||||
"source": "f30802dc-1c18-4169-99fe-04d1d8e7bd9e"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "2.2",
|
||||
"source": "da6fdb50-8773-40ce-889f-c6f565ca35e3"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "6.1",
|
||||
"source": "38f508e0-b808-4d28-be5b-d2f5cb54fd69"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "4.2",
|
||||
"source": "b190ebb1-86c0-4308-a6b6-bf34237c10f8"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "13.1",
|
||||
"source": "233dd18b-cb0d-4f77-b0ee-248dc7fec65d"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "9.2",
|
||||
"source": "f99ee1de-af5c-4d7e-a1f4-622ad0cd40ac"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "12.1",
|
||||
"source": "9c5a22d3-1e82-4bad-95a4-c9efb169ede0"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "5.2",
|
||||
"source": "3c60f533-676b-4464-8542-1f3e6fc49d13"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "9.1",
|
||||
"source": "f99ee1de-af5c-4d7e-a1f4-622ad0cd40ac"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "8.2",
|
||||
"source": "81cf1907-8566-4b03-a433-3f6ea9bf8c85"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "15.1",
|
||||
"source": "4706f51a-c014-4e0a-99f7-866e98b19986"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "1.2",
|
||||
"source": "4b1c22b8-6fa1-4c9d-98d7-cbf498035074"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "11.1",
|
||||
"source": "cede08c4-768a-4792-b92f-8df162a07307"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "10.2",
|
||||
"source": "8f688b3b-6e2f-4bf2-bdfa-03762f2c7b72"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "6.2",
|
||||
"source": "38f508e0-b808-4d28-be5b-d2f5cb54fd69"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "13.2",
|
||||
"source": "233dd18b-cb0d-4f77-b0ee-248dc7fec65d"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "8.1",
|
||||
"source": "81cf1907-8566-4b03-a433-3f6ea9bf8c85"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "10.1",
|
||||
"source": "8f688b3b-6e2f-4bf2-bdfa-03762f2c7b72"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "16.2",
|
||||
"source": "49295e5b-0d0a-44ea-9bff-e74acaaa6dcf"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "14.2",
|
||||
"source": "51c4c7cb-8382-4bef-ad2d-c457c5af12f2"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "2.1",
|
||||
"source": "da6fdb50-8773-40ce-889f-c6f565ca35e3"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "14.1",
|
||||
"source": "51c4c7cb-8382-4bef-ad2d-c457c5af12f2"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "3.1",
|
||||
"source": "4fa78612-accd-491d-93e6-cca251ac0e5a"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "7.1",
|
||||
"source": "3945635e-c65a-4fb7-a46c-675ec53abebe"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "15.2",
|
||||
"source": "4706f51a-c014-4e0a-99f7-866e98b19986"
|
||||
}
|
@ -1 +0,0 @@
|
||||
{"name":"Bumsquatsch","lang":"de","created":1670881903}
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"quote": "Extra Bumsquatsch vom Oberadmin",
|
||||
"source": "6ef8620b-2b5e-4749-821e-b0722b8d8117",
|
||||
"created": 1670882004
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"quote": "Bumsquatsch #2",
|
||||
"source": "c5e0cbf4-1556-4488-93de-9367f84e5ce8",
|
||||
"created": 1670881947
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"quote": "Bumsquatsch #1",
|
||||
"source": "c5e0cbf4-1556-4488-93de-9367f84e5ce8",
|
||||
"created": 1670881943
|
||||
}
|
@ -1 +0,0 @@
|
||||
{"name":"Team Hogwards","lang":"en","created":1650831066}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "I'm friends with spiders.",
|
||||
"source": "23c93faa-ac5b-4e37-bf32-4276aba682bb"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "My first magic wand - an exclusive model from designer Thoronus Karpes — was a gift from my father.",
|
||||
"source": "15c03b7c-729f-4202-a880-3bcc7214dad9"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "My stupid brothers frequently test their latest invention on me.",
|
||||
"source": "bff68447-513c-4fa3-9224-fc59d83da81a"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "Since first grade, I've been best in all subjects and courses without a single miss.",
|
||||
"source": "99e1aa2e-6e37-43c7-809c-28f093e2ae81"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "My private collection has 4031 different potions.",
|
||||
"source": "35ee06ca-82c7-4f23-9bb9-bd0943848b07"
|
||||
}
|
@ -1 +0,0 @@
|
||||
{"name":"Team Hogwards","lang":"de","created":1649841591}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "Ich bin seit der ersten Klasse in allen Fächern Klassenbeste:r.",
|
||||
"source": "de69fa89-2b55-4c78-8718-9084458ceada"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "Meine Privatsammlung umfasst 4031 verschiedene Zaubertränke.",
|
||||
"source": "c885c77f-bbc8-4547-a3b4-baac5a58e76e"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "Meinen ersten Zauberstab — ein exklusives Modell von Edeldesigner Thoronus Karpes — hat mir mein Vater geschenkt.",
|
||||
"source": "cf230e60-9e1e-4158-93eb-ca184e15a6af"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "Ich bin mit Spinnen befreundet.",
|
||||
"source": "47ff4d04-b403-468f-b152-72226762e373"
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"quote": "Meine blöden Brüder testen ständig ihre neusten Erfindungen an mir.",
|
||||
"source": "fae6e837-d603-436d-ba0c-df6a850350c0"
|
||||
}
|
@ -1 +0,0 @@
|
||||
{"authcode":"","name":"Draco","role":"player","game":"663576f0-1378-496b-a970-578bdcb222af","created":1650831166,"lastLoggedIn":0}
|
@ -1 +0,0 @@
|
||||
{"authcode":"805088","name":"Player #13","role":"player","game":"64efba47-87dc-4c19-851c-aa68c9f0e2c1","created":1651603645,"lastLoggedIn":1651603989}
|
@ -1 +0,0 @@
|
||||
{"authcode":"","name":"Hagrid","role":"player","game":"663576f0-1378-496b-a970-578bdcb222af","created":1650831162,"lastLoggedIn":0}
|
@ -1 +0,0 @@
|
||||
{"authcode":"","name":"Snape","role":"player","game":"663576f0-1378-496b-a970-578bdcb222af","created":1650831158,"lastLoggedIn":0}
|
@ -1 +0,0 @@
|
||||
{"authcode":"406234","name":"Player #06","role":"player","game":"64efba47-87dc-4c19-851c-aa68c9f0e2c1","created":1651603311,"lastLoggedIn":1651603989}
|
@ -1 +0,0 @@
|
||||
{"authcode":"098577","name":"Player #07","role":"player","game":"64efba47-87dc-4c19-851c-aa68c9f0e2c1","created":1651603340,"lastLoggedIn":1651603989}
|
@ -1 +0,0 @@
|
||||
{"authcode":"044843","name":"Player #05","role":"player","game":"64efba47-87dc-4c19-851c-aa68c9f0e2c1","created":1651603278,"lastLoggedIn":1651603989}
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"authcode": "646162",
|
||||
"name": "Settel (Admin)",
|
||||
"role": "admin"
|
||||
}
|
@ -1 +0,0 @@
|
||||
{"authcode":"606242","name":"Player #15","role":"player","game":"64efba47-87dc-4c19-851c-aa68c9f0e2c1","created":1651603679,"lastLoggedIn":1651603989}
|
@ -1 +0,0 @@
|
||||
{"authcode":"","name":"Hagrid","role":"player","game":"e24444aa-8a18-48aa-a36d-8f84620726f8","created":1649841804}
|
@ -1 +0,0 @@
|
||||
{"authcode":"920510","name":"Player #16","role":"player","game":"64efba47-87dc-4c19-851c-aa68c9f0e2c1","created":1651603703,"lastLoggedIn":1651603989}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user