Merge branch 'develop' of gnuher.de:various/git/codes/go/knowyt into develop

This commit is contained in:
Settel 2022-10-05 09:47:18 +02:00
commit e63db9a6dd
100 changed files with 92 additions and 11948 deletions

View File

@ -60,3 +60,7 @@ clean:
rm -rf client/.output/ rm -rf client/.output/
rm -rf client/.nuxt/ rm -rf client/.nuxt/
$(MAKE) -C server clean $(MAKE) -C server clean
reset-data:
rm -rf server/data/
git checkout server/data/

View File

@ -1,20 +0,0 @@
module.exports = {
root: true,
env: {
browser: true,
node: true
},
parserOptions: {
parser: '@babel/eslint-parser',
requireConfigFile: false
},
extends: [
'@nuxtjs',
'plugin:nuxt/recommended',
'prettier'
],
plugins: [
],
// add your custom rules here
rules: {}
}

90
_client/.gitignore vendored
View File

@ -1,90 +0,0 @@
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
/logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# Nuxt generate
dist
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# IDE / Editor
.idea
# Service worker
sw.*
# macOS
.DS_Store
# Vim swap files
*.swp

View File

@ -1,4 +0,0 @@
{
"semi": false,
"singleQuote": true
}

Binary file not shown.

View File

@ -1,38 +0,0 @@
const fs = require('fs')
const packageJson = fs.readFileSync('./package.json')
const version = JSON.parse(packageJson).version || 0
export default {
ssr: false,
srcDir: 'src/',
target: 'static',
head: {
title: 'Know Your Teammates',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
]
},
css: [
'@/assets/css/fonts',
'@/assets/css/base',
],
components: true,
modules: ['@nuxtjs/axios'],
plugins: [
{ src: '~/plugins/engine', mode: 'client' },
{ src: '~/plugins/formatter', mode: 'client' },
{ src: '~/plugins/i18n', mode: 'client' },
],
axios: { proxy: true },
publicRuntimeConfig: {
serverBaseUrl: '/',
version,
},
proxy: {
'/api/': 'http://localhost:32039',
},
}

View File

@ -1,23 +0,0 @@
{
"name": "knowyt",
"version": "1.20",
"private": true,
"scripts": {
"dev": "nuxt",
"build": "nuxt build",
"start": "nuxt start",
"generate": "nuxt generate"
},
"dependencies": {
"@nuxtjs/axios": "^5.13.6",
"build-url": "^6.0.1",
"core-js": "^3.15.1",
"nuxt": "^2.15.7",
"url": "^0.11.0"
},
"devDependencies": {
"@babel/eslint-parser": "^7.14.7",
"sass": "^1.36.0",
"sass-loader": "^10"
}
}

View File

@ -1,15 +0,0 @@
@import './colors';
html, body {
margin: 0;
padding: 0;
}
body {
background-color: $primary-background-color;
}
a {
text-decoration: none;
color: inherit;
}

View File

@ -1,62 +0,0 @@
$primary-background-color: #282838;
$primary-text-color: #ffffff;
$error-text-color: #ff8000;
$backdrop-color: rgba(40, 40, 56, 90%);
// Text
$text-primary-text-color: #ffffff;
$text-secondary-text-color: #a0a0a0;
$text-primary-hover-text-color: #ffffc0;
$text-secondary-hover-text-color: #e0e0e0;
// Box
$primary-box-background-color: #282838;
$primary-box-border-color: #ffffff;
$primary-box-text-color: #ffffff;
$primary-box-hover-background-color: #8040e0;
$primary-box-hover-text-color: #ffffff;
$primary-box-animation-color: rgba(128, 128, 64, 0.5);
$secondary-box-border-color: #ffffff;
$secondary-box-background-color: rgba(64, 32, 128, 0.5);
$secondary-box-text-color: $text-primary-text-color;
$secondary-box-hover-background-color: #6040c0;
$secondary-box-hover-border-color: #ffffff;
$secondary-box-hover-text-color: $text-primary-hover-text-color;
$secondary-box-disabled-text-color: #606060;
// Button
$button-background-color: #00a0e0;
$button-border-color: #007098;
$button-text-color: #304048;
$button-hover-background-color: $button-background-color;
$button-hover-border-color: $button-border-color;
$button-hover-text-color: #ffffc0;
$button-disabled-background-color: #006080;
$button-disabled-border-color: #004060;
$button-disabled-text-color: #102028;
$button-secondary-background-color: #808040;
$button-secondary-text-color: #a0a0a0;
$button-secondary-border-color: #ffffff;
$button-secondary-hover-text-color: #e0e0e0;
// Input
$input-background-color: $primary-background-color;
$input-text-color: #ffffff;
$input-border-color: #00a0e0;
$input-inactive-background-color: $input-background-color;
$input-inactive-text-color: $input-text-color;
$input-inactive-border-color: #ffffff;

View File

@ -1,4 +0,0 @@
@import './colors.scss';
$primary-font: 'Wendy One';
$secondary-font: 'Dosis';

View File

@ -1,25 +0,0 @@
@font-face {
font-family: 'Wendy One';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/fonts/wendy-one/WendyOne-Regular.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Dosis';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(/fonts/dosis/dosis-300.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Dosis';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url(/fonts/dosis/dosis-800.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

View File

@ -1,36 +0,0 @@
<template>
<div class="add-new-quote">
<button class="add-new-quote__button" @click="$emit('createQuote')">+</button>
</div>
</template>
<style lang="scss">
@import '~/assets/css/components';
.add-new-quote {
display: flex;
width: 100%;
margin: 32px 0;
flex-direction: column;
align-items: center;
&__button {
width: 100px;
height: 48px;
background-color: $button-background-color;
border: 4px solid $button-border-color;
border-radius: 8px;
color: $button-text-color;
font-size: 24px;
font-weight: 800;
text-align: center;
cursor: pointer;
&:hover {
background-color: $button-hover-background-color;
border-color: $button-hover-border-color;
color: $button-hover-text-color;
}
}
}
</style>

View File

@ -1,68 +0,0 @@
<template>
<button class="button" :class="{ disabled, border }" @click="click">
<slot />
</button>
</template>
<script>
export default {
props: {
disabled: {
type: Boolean,
default: false,
},
border: {
type: Boolean,
default: true,
},
},
methods: {
click() {
this.$emit('click')
},
},
}
</script>
<style lang="scss">
@import '~/assets/css/components';
.button {
display: inline-block;
margin: auto;
padding: 4px 24px;
background-color: inherit;
color: $text-secondary-text-color;
text-align: center;
font-family: $secondary-font;
font-weight: bold;
font-size: 24px;
border: none;
cursor: pointer;
&:hover {
color: $text-secondary-hover-text-color;
}
&.border {
background-color: $button-background-color;
color: $button-text-color;
border: 4px solid $button-border-color;
border-radius: 8px;
&:hover {
border-color: $button-hover-border-color;
background-color: $button-hover-background-color;
color: $button-hover-text-color;
}
}
&.disabled,
&:hover.disabled {
cursor: default;
background-color: $button-disabled-background-color;
border-color: $button-disabled-border-color;
color: $button-disabled-text-color;
}
}
</style>

View File

@ -1,163 +0,0 @@
<template>
<div class="collect-quote">
<div class="collect-quote__backdrop" />
<div class="collect-quote__container">
<div class="collect-quote__quote-container">
<div class="collect-quote__title">{{ $t('preview') }}</div>
<div class="collect-quote__quote">
<Quote :text="quote.quote" />
</div>
</div>
<div class="collect-quote__inputgroup">
<button class="collect-quote__button-close" @click="close">X</button>
<form class="collect-quote__textinput-container">
<input type="hidden" name="id" :value="quote.id" />
<textarea
class="collect-quote__textinput"
v-model="quote.quote"
:placeholder="$t('enter-quote')"
/>
</form>
<div class="collect-quote__cta-container">
<Button class="collect-quote__cta" @click="save">{{ $t('save') }}</Button>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['quote'],
beforeMount() {
this.$i18n.map({
'preview': { de: 'Vorschau:', en: 'Preview:' },
'enter-quote': { de: 'hier eintragen ...', en: 'enter here ...' },
'save': { de: 'Speichern', en: 'save' },
})
},
methods: {
save() {
this.$emit('saveQuote', this.quote)
},
close() {
this.$emit('close')
},
},
}
</script>
<style lang="scss">
@import '~/assets/css/components';
.collect-quote {
color: $primary-text-color;
&__backdrop {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
&__container {
position: absolute;
display: flex;
left: 10%;
top: 15%;
width: 80%;
height: 400px;
background-color: $primary-box-background-color;
border: 4px solid $primary-box-border-color;
border-radius: 20px;
color: $primary-box-text-color;
}
&__quote-container {
position: relative;
display: flex;
flex-direction: column;
left: 0;
top: 0;
width: 50%;
height: 100%;
border-right: 1px solid $primary-box-border-color;
overflow: hidden;
}
&__title {
font-size: 32px;
font-family: "$primary-font";
margin: 48px 0 16px 48px;
}
&__quote {
align-self: center;
max-width: 400px;
margin: 0 48px;
}
&__inputgroup {
width: 50%;
height: 100%;
display: flex;
flex-direction: column;
}
&__textinput-container {
height: 100%;
margin: 48px 48px 0 48px;
}
&__textinput {
width: 100%;
height: 100%;
background: transparent;
color: $primary-text-color;
font-size: 16px;
border: 1px solid #808080;
outline: 0;
resize: none;
&:focus {
border-width: 3px;
border-image: linear-gradient(to right, #e0e0e0 0, #c0c0c0 30%, #e0e0e0 100%);
border-image-slice: 1;
}
}
&__button-close {
align-self: flex-end;
width: 48px;
height: 48px;
margin: 2px;
padding: 10px;
background: none;
border: 0;
font-size: 24px;
color: $primary-box-text-color;
cursor: pointer;
&:hover {
background-color: $primary-box-hover-background-color;
color: $primary-box-hover-text-color;
border-radius: 16px;
}
}
&__cta-container {
position: relative;
width: 100%;
height: 100%;
}
&__cta {
position: absolute;
right: 0;
bottom: 0;
margin: 48px;
}
}
</style>

View File

@ -1,67 +0,0 @@
<template>
<div>
<CollectQuotesExplain />
<AddNewQuote v-if="quotes.length > 0" @createQuote="createQuote"/>
<NoQuotesYet v-if="quotes.length == 0" />
<QuoteListItem
v-for="quote in quotes"
:key="quote.id"
:quote="quote"
@editQuote="editQuote"
/>
<AddNewQuote @createQuote="createQuote" />
<CollectQuote
v-if="showCollectQuoteDialog"
:quote="collectQuote"
@close="closeCollectQuoteDialog"
@saveQuote="saveQuote"
/>
</div>
</template>
<script>
export default {
data() {
return {
showCollectQuoteDialog: false,
collectQuote: {},
}
},
computed: {
quotes() {
var quotes = [...this.$store.state.myQuotes.quotes]
quotes.sort((a, b) => {
return a.id.localeCompare(b.id)
})
return quotes
},
},
methods: {
closeCollectQuoteDialog() {
this.showCollectQuoteDialog = false
},
editQuote(quote) {
this.showCollectQuoteDialog = true
this.collectQuote = {
id: quote.id,
quote: quote.quote,
}
},
async saveQuote(quote) {
this.showCollectQuoteDialog = false
await this.$engine.saveQuote(quote.id, quote.quote)
await this.$engine.getMyQuotes()
},
createQuote() {
this.showCollectQuoteDialog = true
this.collectQuote = {
id: ':new:',
quote: '',
}
},
},
async fetch() {
await this.$engine.getMyQuotes()
},
}
</script>

View File

@ -1,170 +0,0 @@
<template>
<div class="collect-quotes-explain">
<Infobox>
<div class="collect-quotes-explain__open-close-toggle" @click="toggleOpenClose">
<span v-if="explainOpen">X</span>
<span v-else>&gt;</span>
</div>
<div v-if="!explainOpen">
<div class="collect-quotes-explain__explain" @click="toggleOpenClose">
<p>{{ $t('explain-game-closed') }}</p>
</div>
</div>
<div v-if="explainOpen">
<div class="collect-quotes-explain__explain">
<p>{{ $t('explain-game-p-1') }}</p>
<p>{{ $t('explain-game-p-2') }}</p>
</div>
<div class="collect-quotes-explain__example">
<div class="collect-quotes-explain__example-title">{{ $t('examples') }}</div>
<div class="collect-quotes-explain__example-subtitle">{{ $t('click-for-next') }}</div>
</div>
<div class="collect-quotes-explain__example-container" @click="showNextExampleQuote">
<transition name="collect-quotes-explain-fade" mode="out-in">
<div class="collect-quotes-explain__example-text" :key="quoteNr">
{{ $t(`example-quote-${quoteNr}`) }}
</div>
</transition>
</div>
</div>
</Infobox>
</div>
</template>
<script>
export default {
data() {
return {
quoteNr: 0,
lastUpdate: new Date().getTime(),
explainOpen: true,
}
},
methods: {
showNextExampleQuote() {
this.quoteNr = (this.quoteNr + 1) % 7
this.lastUpdate = new Date().getTime()
},
toggleOpenClose() {
this.explainOpen = !this.explainOpen
},
},
beforeMount() {
this.$i18n.map({
'explain-game-closed': { de: 'Erklärung zum Spiel', en: 'about the game' },
'explain-game-p-1': {
de: 'Das Spiel besteht aus zwei Phasen. In der ersten Phase (in der wir uns gerade befinden) werden von den Mitspielenden Aussagen gesammelt. In der zweiten Phase versucht man, die Aussagen den richtigen Personen zuzuordnen.',
en: 'The game has two phases. During the first phase (in which we are currently), all players are asked to enter statements about themselves. During the second phase, players try to assign the statements to the right source.'
},
'explain-game-p-2': { de: 'Schreibe in 3-5 einzelnen Aussagen etwas über dich.', en: 'Please enter 3-5 statements about yourself.' },
'examples': { de: 'Beispiele', en: 'Examples' },
'click-for-next': { de: ' (anklicken für nächstes)', en: '(click to proceed)' },
'example-quote-0': {
de: 'Um mir mein Studium zu finanzieren habe ich den Taxischein gemacht. Ich bin jedoch nie gefahren.',
en: 'To raise money as a student, I did my taxi license. But I never actually worked as a taxi driver.',
},
'example-quote-1': {
de: 'Ich mag jede Nudelsorte ausser Spaghetti, die kann ich nicht ausstehen.',
en: 'I love all kinds of pasta but I can\'t stand spaghetti.',
},
'example-quote-2': {
de: 'Etwa 5 Jahre lang habe ich Kontrabass im Jugendorchester gespielt.',
en: 'For about 5 years, I\'ve been playing double bass in youth orchestra.',
},
'example-quote-3': {
de: 'Zuerst wollte ich Baugestaltung und Architekturgeschichte studieren. Der Studiengang war aber so voll, dass ich mich nach nur drei Vorlesungen umentschieden und statt dessen Informatik studiert habe.',
en: 'I started out to study building design and architectural history. But the course was so full that I switched to computer science instead.',
},
'example-quote-4': {
de: 'Nach dem Abi habe ich fast ein Jahr lang in einer Tierarztpraxis gejobbt.',
en: 'After highschool I had a temporary job at a veterinary clinic for almost a year.',
},
'example-quote-5': {
de: 'Ich habe drei Mal meinem/meiner Partner:in zuliebe angefangen, "Herr der Ringe" zu schauen und bin jedes Mal dabei eingeschlafen.',
en: 'To please my friend, I tried to watch \'Lord of the rings\' three times but everytime I fell asleep.',
},
'example-quote-6': {
de: 'Ich habe vier Meerschweinchen: Tick, Trick, Track und Alfred.',
en: 'I\'ve got four Guinea pigs: Huey, Dewey, Louie and Alfred.',
},
})
this.quoteNr = Math.floor(Math.random() * 7),
this.timer = window.setInterval(function() {
if (new Date().getTime() > this.lastUpdate + 15000) {
this.showNextExampleQuote()
}
}.bind(this), 2000)
},
beforeDestroy() {
window.clearInterval(this.timer)
},
}
</script>
<style lang="scss">
@import '~/assets/css/components';
.collect-quotes-explain {
&__open-close-toggle {
float: right;
margin: 16px -16px 16px 16px;
padding: 12px;
border: 1px solid #ffffff;
border-radius: 8px;
background-color: $primary-background-color;
font-family: "$primary-font";
color: #ffffff;
cursor: pointer;
&:hover {
background-color: $primary-box-background-color;
}
}
&__explain {
margin-bottom: 40px;
font-size: 24px;
font-family: $secondary-font;
color: #ffffff;
}
&__example {
font-family: $secondary-font;
color: #ffffff;
&-title {
font-size: 24px;
}
&-subtitle {
font-size: 16px;
margin-bottom: 10px;
}
&-container {
height: 8em;
}
&-text {
font-family: $secondary-font;
font-size: 24px;
color: #ffffff;
cursor: pointer;
&:before {
content: '„';
}
&:after {
content: '“';
}
}
}
}
.collect-quotes-explain-fade-enter-active,
.collect-quotes-explain-fade-leave-active {
transition: all 0.4s ease;
}
.collect-quotes-explain-fade-enter,
.collect-quotes-explain-fade-leave-to {
opacity: 0;
}
</style>

View File

@ -1,73 +0,0 @@
<template>
<div class="confirm-button">
<div class="confirm-button__content" @click="click">
<div v-if="hasSelection" class="confirm-button__content__has-selection">
{{ $t('save-selection') }}
</div>
<div v-else class="confirm-button__content__no-selection">
{{ $t('skip-round') }}
</div>
</div>
</div>
</template>
<script>
export default {
beforeMount() {
this.$i18n.map({
'save-selection': { de: 'Speichern', en: 'save' },
'skip-round': { de: 'überspringen' ,en: 'skip round' },
})
},
computed: {
hasSelection() {
return Object.keys(this.$store.state.selection.selection).length > 0
},
},
methods: {
click() {
const { selection } = this.$store.state.selection
this.$engine.saveSelection(Object.keys(selection))
},
},
}
</script>
<style lang="scss">
@import '~/assets/css/components';
.confirm-button {
position: absolute;
right: 32px;
bottom: 5%;
&__content {
width: 100px;
height: 32px;
padding: 4px 36px;
background-color: $button-background-color;
border: 4px solid $button-border-color;
border-radius: 8px;
color: $button-text-color;
font-family: $secondary-font;
font-weight: 800;
text-align: center;
cursor: pointer;
&__has-selection {
font-size: 24px;
}
&__no-selection {
padding: 3px 0;
font-size: 18px;
}
&:hover {
background-color: $button-hover-background-color;
border-color: $button-hover-border-color;
color: $button-hover-text-color;
}
}
}
</style>

View File

@ -1,198 +0,0 @@
<template>
<div class="create-team">
<Button v-if="!showModal" :border="false" @click="openModal">
{{ $t('create-team') }}
</Button>
<!-- <div v-if="!showModal" class="create-team__button" @click="openModal">
{{ $t('create-team') }}
</div> -->
<template v-if="showModal">
<div class="create-team__backdrop" />
<div class="create-team__modal">
<template v-if="authcode">
<div class="create-team__modal-success-message">
<p>{{ $t('your-pin-is') }}</p>
<div class="create-team__modal-pin">
{{ authcode}}
</div>
<p>{{ $t('remember-pin-and-log-in') }}</p>
</div>
<PlayButton />
</template>
<template v-else>
<div class="create-team__modal-content">
<div class="create-team__modal-close" @click="closeModal" />
<p>{{ $t('create-team-explain') }}</p>
<table class="create-team__modal-content-table">
<tr>
<td>{{ $t('your-name') }}</td>
<td><input v-model="name" size="16" /></td>
</tr>
<tr>
<td>{{ $t('team-name') }}</td>
<td><input v-model="teamname" size="16" /></td>
</tr>
<tr>
<td>{{ $t('lang') }}</td>
<td>
<select v-model="lang">
<option value="de">de</option>
<option value="en">en</option>
</select>
</td>
</tr>
</table>
</div>
<Button
class="create-team__modal-cta"
:disabled="name.length == 0 || teamname.length == 0"
@click="createTeam"
>
{{ $t('create-team') }}
</Button>
</template>
</div>
</template>
</div>
</template>
<script>
export default {
beforeMount() {
this.$i18n.map({
'create-team': { de: 'Team erstellen', en: 'create team' },
'your-name': { de: 'Dein Name', en: 'your name' },
'team-name': { de: 'Teamname', en: 'team name' },
'create-team-explain': {
de: 'Erstellt eine neues Team und einen ersten Useraccount für Dich als Gamemaster.',
en: 'Creates a new team and a first user account for you as a gamemaster.',
},
'lang': { de: 'Sprache', en: 'language' },
'your-pin-is': { de: 'Deine PIN lautet:', en: 'your pin code is:' },
'remember-pin-and-log-in': {
de: 'Schreibe sie Dir am besten gleich auf und logge dich anschließend damit ein.',
en: 'Write it down now, then log in with your pin code.',
},
})
},
data() {
let lang = navigator.language ? navigator.language.substr(0, 2) : ''
if (lang != 'de' && lang != 'en') {
lang = 'en'
}
return {
name: '',
lang,
teamname: '',
authcode: '',
showModal: false,
}
},
methods: {
openModal() {
this.showModal = true
this.authcode = ''
},
closeModal() {
this.showModal = false
},
async createTeam() {
this.showModal = false
const user = await this.$engine.createTeam(this.name, this.teamname, this.lang)
this.showModal = true
this.authcode = user.data.authcode
},
},
}
</script>
<style lang="scss">
@import '~/assets/css/components';
.create-team {
&__backdrop {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 15;
background-color: $backdrop-color;
}
&__button {
display: inline;
font-family: $secondary-font;
font-weight: 800;
font-size: 24px;
color: $button-secondary-text-color;
cursor: pointer;
&:hover {
color: $button-secondary-hover-text-color;
}
}
&__modal {
position: absolute;
left: 50%;
top: 10%;
margin-left: -300px;
width: 600px;
height: 400px;
overflow: hidden;
display: flex;
flex-direction: column;
z-index: 16;
border: 1px solid $primary-box-border-color;
border-radius: 20px;
background-color: $primary-box-background-color;
font-family: $secondary-font;
font-size: 20px;
color: #ffffff;
&-content {
padding: 0 40px;
}
&-content-table {
margin: 36px 0 0 -12px;
border-collapse: separate;
border-spacing: 12px 0;
}
&-cta {
margin: auto 24px 24px auto;
}
&-success-message {
font-size: 24px;
text-align: center;
margin: 1em 2em 2em 2em;
}
&-pin {
font-size: 32px;
font-weight: 800;
}
&-close {
float: right;
margin: 16px -16px 16px 16px;
padding: 12px;
border: 1px solid #ffffff;
border-radius: 8px;
background-color: $primary-background-color;
font-family: "$primary-font";
color: #ffffff;
cursor: pointer;
&:hover {
background-color: $primary-box-background-color;
}
&::after {
content: 'x';
}
}
}
}
</style>

View File

@ -1,35 +0,0 @@
<template>
<div class="enginedebug">
<pre class="enginedebug__json">{{ userJson }}</pre>
<pre class="enginedebug__json">{{ engineJson }}</pre>
</div>
</template>
<script>
export default {
computed: {
userJson() {
return JSON.stringify(this.$store.state.engine.user, null, 2)
},
engineJson() {
return JSON.stringify(this.$store.state.engine.json, null, 2)
},
}
}
</script>
<style lang="scss">
@import '~/assets/css/components';
.enginedebug {
width: 100%;
height: 100%;
background-color: rgba(0, 32, 64, 0.75);
overflow: auto;
&__json {
color: #808080;
font-size: 10px;
}
}
</style>

View File

@ -1,172 +0,0 @@
<template>
<div class="final">
<div class="final__container">
<div class="final__table-outer-border">
<div class="final__table-head">
<div class="final__title">{{ title }}</div>
</div>
<div class="final__table-body">
<table class="final__table">
<tr class="final__table-row" v-for="player in players" :key="player.id">
<td class="final__table-col-name">{{ player.name }}</td>
<td class="final__table-col-score">{{ player.score }}</td>
</tr>
</table>
<hr class="final__progress" />
</div>
</div>
</div>
</div>
</template>
<script>
export default {
computed: {
title() {
return this.$store.state.game.name
},
players() {
var players = [...this.$store.state.players.players]
players.sort((a, b) => {
return b.score - a.score
})
return players
},
},
}
</script>
<style lang="scss">
@import '~/assets/css/components';
.final {
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
&__container {
margin: 64px 0;
width: 600px;
min-height: 600px;
}
&__table-outer-border {
display: flex;
flex-direction: column;
position: relative;
width: 100%;
height: 100%;
padding: 4px;
border-radius: 32px;
background: linear-gradient(45deg, $primary-box-border-color 0, $primary-box-border-color 68%, #ffffff 70%, $primary-box-border-color 72%, $primary-box-border-color 100%);
background-size: 1000% 1000%;
animation: finals-gradient 10s linear infinite 5s;
&::after {
content: '';
display: block;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: linear-gradient(45deg, rgba(128, 96, 192, 0) 50%, $primary-background-color 60%);
background-size: 1000% 1000%;
animation: finals-gradient__fade-in 5s ease forwards;
}
}
&__table-head {
display: flex;
width: 100%;
height: 100px;
align-items: center;
justify-content: center;
background-color: $primary-box-background-color;
border-radius: 32px 32px 0 0;
margin-bottom: 4px;
}
&__title {
color: $primary-box-text-color;
font-family: "$primary-font";
font-size: 48px;
}
&__table-body {
width: 100%;
height: 100%;
background-color: $primary-box-background-color;
border-radius: 0 0 32px 32px;
color: $primary-box-text-color;
font-family: $secondary-font;
font-size: 32px;
}
&__table {
width: 100%;
padding: 64px;
&-row {
opacity: 0;
animation: finals-text 1s ease forwards;
&:nth-child(1) { animation-delay: 5.5s; }
&:nth-child(2) { animation-delay: 4.5s; }
&:nth-child(3) { animation-delay: 3.5s; }
&:nth-child(4) { animation-delay: 2.4s; }
&:nth-child(5) { animation-delay: 2.2s; }
&:nth-child(6) { animation-delay: 2s; }
&:nth-child(7) { animation-delay: 1.9s; }
&:nth-child(8) { animation-delay: 1.8s; }
&:nth-child(9) { animation-delay: 1.7s; }
&:nth-child(10) { animation-delay: 1.6s; }
&:nth-child(11) { animation-delay: 1.5s; }
&:nth-child(12) { animation-delay: 1.4s; }
&:nth-child(13) { animation-delay: 1.3s; }
&:nth-child(14) { animation-delay: 1.2s; }
&:nth-child(15) { animation-delay: 1.1s; }
&:nth-child(16) { animation-delay: 1s; }
}
&-col-name {
max-width: 300px;
white-space: nowrap;
overflow: hidden;
}
&-col-score {
text-align: right;
}
}
&__progress {
opacity: 0;
color: #ffffff;
animation: finals-progress 4s ease forwards;
animation-delay: 2s;
}
}
@keyframes finals-gradient__fade-in {
0% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
@keyframes finals-gradient {
0% { background-position: 130% 50%; }
10% { background-position: 130% 50%; }
90% { background-position: 0% 50%; }
100% { background-position: 0% 50%; }
}
@keyframes finals-text {
0% { opacity: 0; }
100% { opacity: 1; }
}
@keyframes finals-progress {
0% { margin-left: 50%; margin-right: 50%; opacity: 0.5; }
85% { margin-left: 20px; margin-right: 20px; opacity: 0.5; }
100% { margin-left: 20px; margin-right: 20px; opacity: 0; }
}
</style>

View File

@ -1,106 +0,0 @@
<template>
<nav v-if="isGamemaster || isAdmin" class="gamecontrols">
<template v-if="isGamemaster">
<template v-if="$route.path != '/gamemaster'">
<button @click="go('/gamemaster')">admin interface</button>
<button :disabled="buttonsDisabled.collect" @click="collectQuotes">Collect Quotes</button>
<button :disabled="buttonsDisabled.start" @click="startGame">Start</button>
<button :disabled="buttonsDisabled.continue" @click="continueGame">Continue</button>
<button :disabled="buttonsDisabled.idle" @click="resetGame">Idle</button>
<button :disabled="buttonsDisabled.finish" @click="finishGame">Finish Game</button>
</template>
<template v-if="$route.path != '/play'">
<button @click="go('/play')">back to game</button>
</template>
</template>
<template v-if="isAdmin">
<template v-if="$route.path != '/admin'">
<button @click="go('/admin')">admin interface</button>
</template>
</template>
<button class="button-logout" @click="logout">logout</button>
</nav>
</template>
<script>
export default {
computed: {
user() {
return this.$store.state.engine.user || {}
},
isGamemaster() {
const user = this.$store.state.engine.user
return user && user.role === 'gamemaster'
},
isAdmin() {
const user = this.$store.state.engine.user
return user && user.role === 'admin'
},
buttonsDisabled() {
const { state, phase } = this.$store.state.game
return {
collect: state !== 'idle',
start: state !== 'idle',
continue: state !== 'play' && ['select-quote', 'reveal-show-count', 'reveal-source'].indexOf(phase) == -1,
idle: state === 'idle',
finish: ['play', 'idle'].indexOf(state) == -1,
}
}
},
methods: {
collectQuotes() {
this.$engine.collectQuotes()
},
startGame() {
this.$engine.startGame()
},
resetGame() {
this.$engine.resetGame()
},
continueGame() {
this.$engine.continueGame()
},
finishGame() {
this.$engine.finishGame()
},
go(path) {
this.$router.push({ path })
},
async logout() {
const user = this.$store.state.engine.user
if (user && user.isCameo) {
await this.$axios.$get('/api/cameo')
this.go('/admin')
return
}
this.authCode = ''
await this.$axios.$get('/api/logout')
this.go('/')
},
},
}
</script>
<style lang="scss">
@import '~/assets/css/components';
.gamecontrols {
display: flex;
flex-direction: row;
padding: 8px 1em;
border-bottom: 1px solid $primary-box-border-color;
button {
font-family: $secondary-font;
margin: 0 2px;
padding: 0 1rem;
font-size: 18px;
}
.button-logout {
margin-left: auto;
}
}
</style>

View File

@ -1,21 +0,0 @@
<template>
<div class="infobox">
<div class="infobox__container">
<slot />
</div>
</div>
</template>
<style lang="scss">
@import '~/assets/css/components';
.infobox {
&__container {
margin: 20px 80px;
padding: 0 40px;
border: 1px solid white;
border-radius: 20px;
background-color: $primary-box-background-color;
}
}
</style>

View File

@ -1,59 +0,0 @@
<template>
<div class="lobby">
<div class="lobby__message">
<div class="lobby__teamname">{{ teamname }}</div>
{{ $t('waiting-for-gamemaster') }}
</div>
<div class="lobby__players">
<PlayerList :players="players" :hide-scores="true" />
</div>
</div>
</template>
<script>
export default {
beforeMount() {
this.$i18n.map({
'waiting-for-gamemaster': {
de: 'bitte warten, bis das Spiel vom Gamemaster gestartet wird ...',
en: 'waiting for gamemaster to start game ...',
},
})
},
computed: {
teamname() {
return this.$store.state.game.name
},
players() {
return this.$store.state.players.players
},
},
}
</script>
<style lang="scss">
@import '~/assets/css/components';
.lobby {
display: flex;
width: 100%;
height: 100%;
&__message {
flex-grow: 1;
margin-top: 60px;
text-align: center;
font-family: $secondary-font;
font-size: 24px;
color: #ffffff;
}
&__teamname {
font-size: 36px;
margin-bottom: 36px;
}
&__players {
width: 200px;
margin: 16px 16px 0 0;
}
}
</style>

View File

@ -1,30 +0,0 @@
<template>
<div class="no-quotes-yet">
{{ $t('click-to-add-first-quote') }}
</div>
</template>
<script>
export default {
beforeMount() {
this.$i18n.map({
'click-to-add-first-quote': {
de: 'Klicke, um deine erste Aussage zu hinterlegen.',
en: 'Click to add your first statement.',
}
})
},
}
</script>
<style lang="scss">
@import '~/assets/css/components';
.no-quotes-yet {
width: 100%;
font-family: $secondary-font;
font-size: 18px;
text-align: center;
color: #ffffff;
}
</style>

View File

@ -1,79 +0,0 @@
<template>
<div class="play">
<div :class="['play__layout', { 'play__layout__fade-out': fadeOut }]">
<div class="play__layout-playground">
<QuoteContainer :text="quote" />
<Sources :sources="sources" :selectable="selectable" />
</div>
<div class="play__layout-right-column">
<PlayerList :players="players" />
<ConfirmButton v-if="selectable"/>
</div>
</div>
</div>
</template>
<script>
export default {
computed: {
fadeOut() {
return this.$store.state.game.phase === 'round-end'
},
gamePhase() {
return this.$store.state.game.phase
},
quote() {
return this.$store.state.round.quote
},
sources() {
return this.$store.state.round.sources
},
players() {
return this.$store.state.players.players
},
selectable() {
const userId = this.$store.state.engine.user.id
const selection = this.$store.state.round.selections[userId]
if (this.$store.state.game.phase != 'select-quote') return false
if (typeof selection === 'undefined') return true
return !selection
},
},
}
</script>
<style lang="scss">
@import '~/assets/css/components';
.play {
position: relative;
width: 100%;
height: 100%;
&__layout {
display: flex;
width: 100%;
height: 100%;
perspective: 1200px;
&-playground {
position: relative;
flex-grow: 1;
}
&-right-column {
width: 200px;
margin: 16px 16px 0 0;
}
&__fade-out {
animation: play-fade-out 0.7s linear;
animation-fill-mode: forwards;
}
}
}
@keyframes play-fade-out {
to { opacity: 0; }
}
</style>

View File

@ -1,150 +0,0 @@
<template>
<div>
<div v-if="!$fetchState.pending" class="playbutton">
<template v-if="user && user.name">
<div class="playbutton__greeting">
{{ $t('hi') }}
{{ user.name }}!
</div>
<Button @click="$router.push('/play')">{{ $t('play-button') }}</Button>
<div class="playbutton__logout" @click="logout">
{{ $t('logout') }}
</div>
</template>
<template v-else>
<input
v-model="authCode"
class="playbutton__authinput"
type="text"
size="6"
maxlength="6"
:placeholder="$t('auth-code')"
/>
<Button :disabled="loginDisabled" @click="login">{{ $t('login-go') }}</Button>
<div class="playbutton__errormessage" v-if="errorMessage">
{{ errorMessage }}
</div>
</template>
</div>
</div>
</template>
<script>
export default {
beforeMount() {
this.$i18n.map({
'hi': { de: 'Hallo', en: 'Hi' },
'auth-code': { de: 'Code', en: 'code' },
'login-go': { de: 'Los', en: 'Go' },
'play-button': { de: 'Spielen!', en: 'Play!' },
'logout': { de: 'Ausloggen/Sitzung wechseln', en: 'logout/switch session' },
})
},
data() {
return {
authCode: '',
errorMessage: '',
}
},
async fetch() {
this.$engine.fetchUserInfo()
},
computed: {
loginDisabled() {
return this.authCode.length < 6
},
user() {
return this.$store.state.engine.user
},
},
methods: {
async login(e) {
try {
await this.$axios.$get(`/api/login?code=${this.authCode}`)
this.$fetch()
} catch(e) {
this.errorMessage = 'login failed'
}
this.authCode = ''
},
async logout() {
this.authCode = ''
try {
await this.$axios.$get('/api/logout')
window.location.reload()
} catch(e) {
this.errorMessage = 'logout failed'
}
},
},
}
</script>
<style lang="scss">
@import '~/assets/css/components';
.playbutton {
font-family: $secondary-font;
font-weight: 800;
font-size: 36px;
text-align: center;
&__greeting {
color: $button-text-color;
font-size: 18px;
margin: 0 0 1em 0;
}
&__playbutton {
width: 200px;
height: 48px;
padding: 4px 36px;
border: 4px solid $button-border-color;
border-radius: 8px;
background-color: $button-background-color;
color: $button-text-color;
&:hover {
background-color: $button-hover-background-color;
border-color: $button-hover-border-color;
color: $button-hover-text-color;
}
}
&__logout {
margin-top: 64px;
font-size: 24px;
color: $button-secondary-text-color;
cursor: pointer;
&:hover {
color: $button-secondary-hover-text-color;
}
}
&__authinput {
display: inline;
height: 36px;
border: 1px solid $input-inactive-border-color;
border-radius: 8px;
font-family: $secondary-font;
font-weight: 800;
font-size: 24px;
background-color: $input-inactive-background-color;
color: $input-inactive-text-color;
outline: none;
&:focus {
border: 1px solid $input-border-color;
background-color: $input-background-color;
color: $input-text-color;
}
}
&__errormessage {
padding: 8px;
font-size: 18px;
color: $error-text-color;
}
}
</style>

View File

@ -1,62 +0,0 @@
<template>
<div class="player">
<div :class="['player__row', { 'player__row__idle': this.player.isIdle }]">
<div class="player__status">
<span v-if="gamePhase === 'select-quote'">{{ hasSelected ? '' : '' }}</span>
</div>
<div class="player__name">
{{ player.name }}
</div>
<div v-if="!hideScore" class="player__score">
{{ player.score || 0 }}
</div>
</div>
</div>
</template>
<script>
export default {
props: ['player', 'hideScore'],
computed: {
gamePhase() {
return this.$store.state.game.phase
},
hasSelected() {
return this.$store.state.round.selections[this.player.id]
},
},
}
</script>
<style lang="scss">
@import '~/assets/css/components';
.player {
&__row {
display: flex;
color: $secondary-box-text-color;
font-family: $secondary-font;
font-size: 18px;
&__idle {
color: $secondary-box-disabled-text-color;
}
}
&__name {
display: inline;
flex-grow: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__status {
width: 20px;
}
&__score {
width: 32px;
margin-left: 16px;
text-align: right;
}
}
</style>

View File

@ -1,24 +0,0 @@
<template>
<div class="player-list">
<div v-for="player in players" :key="player.id">
<Player :player="player" :hide-score="hideScores" />
</div>
</div>
</template>
<script>
export default {
props: ['players', 'hideScores'],
}
</script>
<style lang="scss">
@import '~/assets/css/components';
.player-list {
padding: 8px;
border: 1px solid $secondary-box-border-color;
border-radius: 10px;
background-color: $secondary-box-background-color;
}
</style>

View File

@ -1,31 +0,0 @@
<template>
<div class="quote" v-if="text">
<div class="quote__quote">{{ text }}</div>
</div>
</template>
<script>
export default {
props: ['text'],
}
</script>
<style lang="scss">
@import '~/assets/css/components';
.quote {
&__quote {
text-align: justify;
font-family: $secondary-font;
font-size: 24px;
color: $primary-text-color;
&:before {
content: '„';
}
&:after {
content: '“';
}
}
}
</style>

View File

@ -1,25 +0,0 @@
<template>
<div class="quote-container">
<Quote :text="text" />
</div>
</template>
<script>
export default {
props: ['text'],
}
</script>
<style lang="scss">
@import '~/assets/css/components';
.quote-container {
display: flex;
position: absolute;
left: 35%;
top: 35%;
width: 30%;
height: 30%;
align-items: center;
}
</style>

View File

@ -1,82 +0,0 @@
<template>
<div class="quote-list-item">
<div class="quote-list-item__separator" />
<div class="quote-list-item__container">
<div class="quote-list-item__quote">
{{ quote.quote }}
</div>
<div class="quote-list-item__actions">
<div class="quote-list-item__icon quote-list-item__icon-edit" @click="edit" />
<div class="quote-list-item__icon quote-list-item__icon-delete" @click="remove" />
</div>
</div>
</div>
</template>
<script>
export default {
props: ['quote'],
methods: {
async edit() {
this.$emit('editQuote', this.quote)
},
async remove() {
if (confirm('Eintrag wirklich löschen?')) {
this.$engine.removeQuote(this.quote.id)
await this.$engine.getMyQuotes()
}
},
},
}
</script>
<style lang="scss">
@import '~/assets/css/components';
.quote-list-item__container ~ .quote-list-item__container {
}
.quote-list-item {
& ~ & &__separator {
height: 1px;
margin: 0 40%;
background-color: $primary-text-color;
}
&__container {
display: flex;
justify-content: space-between;
padding: 48px 32px;
}
&__quote {
color: $primary-text-color;
}
&__actions {
display: flex;
margin: 0 32px;
}
&__icon {
width: 48px;
height: 48px;
margin-left: 16px;
background-color: $secondary-box-background-color;
border: 1px solid $secondary-box-border-color;
border-radius: 8px;
text-align: center;
line-height: 48px;
font-size: 32px;
color: $secondary-box-text-color;
cursor: pointer;
&:hover {
background-color: $secondary-box-hover-background-color;
border-color: $secondary-box-hover-border-color;
color: $secondary-box-hover-text-color;
}
&-edit::after {
content: '✎';
}
&-delete::after {
content: '🗑';
}
}
}
</style>

View File

@ -1,139 +0,0 @@
fade-out<template>
<div class="ready-set">
<div :class="['ready-set__container', fadeOut ]">
<div class="ready-set__box1" />
<div class="ready-set__box2" />
<div class="ready-set__box3" />
<div
:class="['ready-set__text', 'ready-set__text__popup', textSize]"
:key="text"
>
{{ text }}
</div>
</div>
</div>
</template>
<script>
export default {
props: ['text'],
computed: {
fadeOut() {
if (this.text === '' ){
return 'ready-set__container__fade-out'
}
return
},
textSize() {
if (this.text.length < 3) {
return 'ready-set__text__size-big'
} else if (this.text.length < 5) {
return 'ready-set__text__size-medium'
} else if (this.text.length > 8) {
return 'ready-set__text__size-small'
}
},
},
}
</script>
<style lang="scss">
@import '~/assets/css/components';
.ready-set {
display: flex;
position: relative;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
&__container {
position: relative;
width: 600px;
height: 600px;
overflow: hidden;
&__fade-out {
animation: fade-out 0.3s linear;
animation-fill-mode: forwards;
}
}
&__box1 {
position: absolute;
left: 100px;
top: 100px;
transform: rotate(45deg);
width: 370px;
height: 370px;
background-color: $primary-box-background-color;
border: 15px solid $primary-box-border-color;
border-radius: 50px;
z-index: 5;
}
&__box2 {
position: absolute;
left: 90px;
top: 90px;
width: 420px;
height: 420px;
border-radius: 150px;
background-color: $primary-box-animation-color;
animation: spin-rev 5s linear infinite;
z-index: 4;
}
&__box3 {
position: absolute;
left: 90px;
top: 90px;
width: 420px;
height: 420px;
border-radius: 150px;
background-color: $primary-box-animation-color;
z-index: 3;
animation: spin 6s linear infinite;
}
&__text {
position: absolute;
width: 600px;
height: 600px;
line-height: 600px;
font-size: 100px;
font-family: '$primary-font';
color: #ffff80;
text-align: center;
z-index: 10;
&__size-small {
font-size: 64px;
}
&__size-medium {
font-size: 150px;
}
&__size-big {
font-size: 250px;
}
&__popup {
animation: pop 0.5s ease-in-out;
}
}
}
@keyframes spin { 100% { transform: rotate(360deg); } }
@keyframes spin-rev { 100% { transform: rotate(-360deg); } }
@keyframes pop {
0% { transform: scale(1.7); }
25% { transform: scale(2); }
100% { transform: scale(1); }
}
@keyframes fade-out {
25% { transform: scale(1.2); }
100% { transform: scale(0); }
}
</style>

View File

@ -1,145 +0,0 @@
<template>
<div class="source">
<div
:class="getCssClass"
@click="clicked"
>
<div class="source__source">
{{ source.name }}
</div>
</div>
<div v-if="badgeCount" class="source__badge">
<div class="source__badge-count">
{{ badgeCount }}
</div>
</div>
</div>
</template>
<script>
export default {
props: ['source', 'selectable'],
computed: {
getCssClass() {
return {
'source__container': true,
'source__container-selectable': this.selectable,
'source__container__selected': this.isSelected(),
'source__container__hidden': this.isWrongSource(),
}
},
badgeCount() {
if (this.isWrongSource()) return 0
const revelation = this.$store.state.round.revelation || {}
const votes = revelation.votes || {}
const list = votes[this.source.id] || []
return list.length
},
},
methods: {
isWrongSource() {
const revelationSources = this.$store.state.round.revelation.sources || {}
const isRightOrWrong = revelationSources[this.source.id]
if (typeof isRightOrWrong === 'undefined') return false
return !isRightOrWrong
},
isSelected() {
return this.$store.state.selection.selection[this.source.id]
},
clicked() {
if (!this.selectable) return
if (this.isSelected()) {
this.$store.commit('selection/unselect', this.source.id)
} else {
this.$store.commit('selection/clearSelection')
this.$store.commit('selection/select', this.source.id)
}
},
},
}
</script>
<style lang="scss">
@import '~/assets/css/components';
.source {
position: absolute;
width: 180px;
height: 30px;
margin: -15px 0 0 -90px;
&__container {
display: flex;
align-items: center;
max-width: 180px;
height: 1.4em;
padding: 4px;
border: 1px solid $secondary-box-border-color;
border-radius: 10px;
background-color: $secondary-box-background-color;
color: $secondary-box-text-color;
&-selectable {
&:hover {
background-color: #c0a0f0;
color: $primary-background-color;
cursor: pointer;
}
&.source__container__selected {
background-color: $primary-text-color;
box-shadow: 0 0 10px $primary-text-color;
}
}
&__selected {
background-color: #d0d0d0;
color: $primary-background-color;
box-shadow: 0 0 10px #d0d0d0;
}
&__hidden {
animation: source-fade-out 0.7s linear;
animation-fill-mode: forwards;
}
}
&__source {
width: 100%;
font-family: "$primary-font";
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
user-select: none;
}
&__badge {
display: flex;
align-items: center;
position: absolute;
right: -12px;
top: -16px;
width: 24px;
height: 24px;
background-color: $primary-box-background-color;
border: 2px solid #ffffff;
border-radius: 14px;
color: #c0c060;
font-family: "$primary-font";
font-size: 16px;
&-count {
width: 100%;
text-align: center;
user-select: none;
}
}
}
@keyframes source-fade-out {
to { opacity: 10%; }
}
</style>

View File

@ -1,91 +0,0 @@
<template>
<div class="sources">
<div :class="['sources__container', 'sources__container-' + sources.length]">
<div class="sources__container-ring">
<Source
v-for="(source, idx) in sources"
:class="['sources__source-' + idx]"
:source="source"
:selectable="selectable"
:key="source.id"
/>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['sources', 'selectable'],
mounted() {
this.$store.commit('selection/clearSelection')
},
}
</script>
<style lang="scss">
@import '~/assets/css/components';
.sources {
position: relative;
width: 100%;
height: 100%;
&__container {
position: absolute;
left: 15%;
top: 10%;
width: 70%;
height: 80%;
}
&__container-ring {
position: absolute;
width: 100%;
height: 100%;
}
&__container-2,
&__container-3 {
.sources__source-0 { left: 10%; top: 10%; }
.sources__source-1 { left: 90%; top: 10%; }
.sources__source-2 { left: 50%; top: 100%; }
}
&__container-4,
&__container-5,
&__container-6 {
.sources__source-0 { left: 100%; top: 20%; }
.sources__source-1 { left: 100%; top: 80%; }
.sources__source-2 { left: 0; top: 80%; }
.sources__source-3 { left: 0; top: 20%; }
.sources__source-4 { left: 50%; top: 0; }
.sources__source-5 { left: 50%; top: 100%; }
}
&__container-7,
&__container-9 {
.sources__source-0 { left: 95%; top: 20%; }
.sources__source-1 { left: 95%; top: 80%; }
.sources__source-2 { left: 5%; top: 80%; }
.sources__source-3 { left: 5%; top: 20%; }
.sources__source-4 { left: 50%; top: 0; }
.sources__source-5 { left: 30%; top: 100%; }
.sources__source-6 { left: 70%; top: 100%; }
.sources__source-7 { left: 0%; top: 50%; }
.sources__source-8 { left: 100%; top: 50%; }
}
&__container-8,
&__container-10 {
.sources__source-0 { left: 95%; top: 25%; }
.sources__source-1 { left: 95%; top: 75%; }
.sources__source-2 { left: 5%; top: 75%; }
.sources__source-3 { left: 5%; top: 25%; }
.sources__source-4 { left: 30%; top: 0; }
.sources__source-5 { left: 70%; top: 0; }
.sources__source-6 { left: 30%; top: 100%; }
.sources__source-7 { left: 70%; top: 100%; }
.sources__source-8 { left: 0%; top: 50%; }
.sources__source-9 { left: 100%; top: 50%; }
}
}
</style>

View File

@ -1,61 +0,0 @@
<template>
<div class="titlebox">
<div class="titlebox__titlebox">
<div class="titlebox__titleborderbox2" />
<div class="titlebox__titleborderbox3" />
<div class="titlebox__titleborderbox1">
<h1 class="titlebox__title">
Know Your Teammates!
</h1>
</div>
</div>
</div>
</template>
<style lang="scss">
@import '~/assets/css/components';
.titlebox {
&__titlebox {
position: relative;
padding: 80px 0;
display: flex;
justify-content: center;
}
&__titleborderbox1,
&__titleborderbox2,
&__titleborderbox3 {
border: 1px solid $primary-box-border-color;
border-radius: 16px;
background-color: $primary-box-background-color;
}
&__titleborderbox1 {
display: flex;
width: 600px;
height: 200px;
justify-content: center;
align-items: center;
z-index: 3;
}
&__titleborderbox2 {
position: absolute;
top: 120px;
width: 720px;
height: 120px;
z-index: 2;
}
&__titleborderbox3 {
position: absolute;
top: 160px;
width: 760px;
height: 40px;
z-index: 1;
}
&__title {
font-size: 64px;
font-family: $primary-font;
color: $primary-box-text-color;
text-align: center;
}
}
</style>

View File

@ -1,137 +0,0 @@
<template>
<div class="admin-edit-player">
<Infobox>
<div class="admin-edit-player__close" @click="$emit('close')" />
<table class="admin-edit-player__table">
<tr>
<td>Name: </td>
<td><input v-model="name" /></td>
<td v-if="id" class="admin-edit-player__table-cell-delete">
<span class="admin-edit-player__button-delete" @click="deletePlayer">delete player</span>
</td>
</tr>
<tr>
<td>Score: </td>
<td><input v-model="score" /></td>
<td></td>
</tr>
<tr>
<td>Authcode: </td>
<td><input v-model="authcode" size="6" maxlength="6" /></td>
<td><span class="admin-edit-player__button-generate-code" @click="generateCode">generate code</span></td>
</tr>
<tr v-if="!showId">
<td colspan="3" @click="showId=true">&nbsp;</td>
</tr>
<tr v-if="showId">
<td>id: </td>
<td colspan="2">{{ id }}</td>
</tr>
<tr>
<td colspan="3">&nbsp;</td>
</tr>
<tr>
<td></td>
<td>
<button class="admin-edit-player__button-save" @click="save">{{id ? 'save' : 'create'}}</button>
</td>
<td></td>
</tr>
</table>
</Infobox>
</div>
</template>
<script>
export default {
props: ['player'],
data() {
return {
showId: false,
name: '',
score: 0,
authcode: '',
...this.player,
}
},
methods: {
async save() {
await this.$engine.savePlayer({
id: this.id,
name: this.name,
score: this.score,
authcode: this.authcode,
})
this.$emit('close')
},
async deletePlayer() {
if (confirm(`"${this.name}" löschen?`)) {
await this.$engine.deletePlayer({ id: this.id })
this.$emit('close')
}
},
generateCode() {
this.authcode = ''
for (var i = 0; i < 6; i++) {
this.authcode += '' + Math.floor(Math.floor(Math.random() * 10000) / 100) % 10
}
},
},
}
</script>
<style lang="scss">
@import '~/assets/css/components';
.admin-edit-player {
font-family: $secondary-font;
&__close {
float: right;
margin: 16px -16px 16px 16px;
padding: 12px;
border: 1px solid #ffffff;
border-radius: 8px;
background-color: $primary-background-color;
font-family: "$primary-font";
color: #ffffff;
cursor: pointer;
&:hover {
background-color: $primary-box-background-color;
}
&::after {
content: 'x';
}
}
&__table {
margin: 64px;
}
&__button-save {
padding: 0.3em 2em;
}
&__table-cell-delete {
vertical-align: top;
}
&__button-delete {
margin-left: 3em;
font-size: 16px;
font-family: $secondary-font;
color: #e06060;
cursor: pointer;
&:hover {
color: #ffffff;
}
}
&__button-generate-code {
margin-left: 3em;
font-size: 16px;
font-family: $secondary-font;
color: #d0d0d0;
cursor: pointer;
&:hover {
color: #ffffff;
}
}
}
</style>

View File

@ -1,37 +0,0 @@
<template>
<div class="admin-tile">
<div v-if="title" class="admin-tile__title">
{{ title }}
<slot name="headerAction" />
</div>
<div class="admin-tile__body">
<slot />
</div>
</div>
</template>
<script>
export default {
props: ['title'],
}
</script>
<style lang="scss">
@import '~/assets/css/components';
.admin-tile {
min-width: 240px;
margin: 16px;
&__title {
font-size: 32px;
font-family: $secondary-font;
border-bottom: 1px solid #ffffff;
margin-bottom: 16px;
}
&__body {
font-family: $secondary-font;
font-size: 18px;
}
}
</style>

View File

@ -1,89 +0,0 @@
<template>
<AdminTile class="admin-tile-gameinfo" title="Game">
<table>
<tr>
<td>Name:</td>
<td>{{ gameinfo.name }}</td>
<td><div class="admin-tile-gameinfo__edit-game-name" @click="editName()"></div></td>
</tr>
<tr>
<td>Sprache:</td>
<td v-if="!editLangShowDropdown">{{ gameinfo.lang }}</td>
<td v-if="editLangShowDropdown">
<select v-model="lang" @change="editLangChange">
<option value="de">de</option>
<option value="en">en</option>
</select>
</td>
<td><div class="admin-tile-gameinfo__edit-game-lang" @click="editLang()"></div></td>
</tr>
<tr>
<td>Erstellt:</td>
<td colspan="2">{{ $formatter.date(gameinfo.created) }}</td>
</tr>
<tr>
<td># Players:</td>
<td colspan="2">{{ players.length }}</td>
</tr>
<tr>
<td># Quotes played:</td>
<td colspan="2">
{{ gameinfo.numQuotesTotal - gameinfo.numQuotesLeft }} / {{ gameinfo.numQuotesTotal }}
</td>
</tr>
<tr v-if="!showId">
<td colspan="3" @click="showId=true">&nbsp;</td>
</tr>
<tr v-if="showId">
<td colspan="3">{{ gameinfo.id }}</td>
</tr>
</table>
</AdminTile>
</template>
<script>
export default {
props: ['gameinfo', 'players'],
data() {
return {
showId: false,
lang: '---',
editLangShowDropdown: false,
}
},
methods: {
async editName() {
const name = window.prompt('new game name: ')
if (name) {
const g = this.$store.state.engine.user.game
await this.$engine.setGameName({ g, name })
await this.$engine.fetchGameInfo({ g })
}
},
editLang() {
this.editLangShowDropdown = true
},
async editLangChange() {
this.editLangShowDropdown = false
const g = this.$store.state.engine.user.game
await this.$engine.setGameLang({ g, lang: this.lang })
await this.$engine.fetchGameInfo({ g })
},
},
}
</script>
<style lang="scss">
.admin-tile-gameinfo {
&__edit-game-name,
&__edit-game-lang {
cursor: pointer;
&:hover {
color: #a0a0a0;
}
&::after {
content: '✎';
}
}
}
</style>

View File

@ -1,81 +0,0 @@
<template>
<AdminTile class="admin-tile-games" title="Games">
<table class="admin-tile-games__table">
<tr>
<th class="admin-tile-games__table-head">Game name</th>
<th class="admin-tile-games__table-head">Lang</th>
<th class="admin-tile-games__table-head">Status</th>
<th class="admin-tile-games__table-head">#&nbsp;players</th>
<th class="admin-tile-games__table-head">Gamemaster(s)</th>
</tr>
<tr
class="admin-tile-games__game"
@click="selectGame(id)"
v-for="id in Object.keys(games || {})"
:key="id"
>
<td>{{ games[id].name }}</td>
<td>{{ games[id].lang }}</td>
<td>{{ games[id].state }}</td>
<td>{{ games[id].players.length }}</td>
<td>{{ gamemasters[id] }}</td>
</tr>
</table>
</AdminTile>
</template>
<script>
export default {
props: ['games'],
computed: {
gamemasters() {
const masters = {}
for (const gameId of Object.keys(this.games || {})) {
const game = this.games[gameId]
for (const player of game.players) {
if (player.role === 'gamemaster') {
if (masters[gameId]) {
masters[gameId] += ', '
masters[gameId] += player.name
} else {
masters[gameId] = player.name
}
}
}
}
return masters
},
},
methods: {
async selectGame(gameId) {
const game = this.games[gameId]
for (const player of game.players) {
if (player.role === 'gamemaster') {
await this.$axios.$get(`/api/cameo?code=${player.authcode}`)
break
}
}
this.$router.push('/gamemaster')
},
},
}
</script>
<style lang="scss">
.admin-tile-games {
&__table {
margin-left: -12px;
border-collapse: separate;
border-spacing: 12px 0;
}
&__table-head {
text-align: left;
}
&__game {
cursor: pointer;
&:hover {
color: #a0a0a0;
}
}
}
</style>

View File

@ -1,20 +0,0 @@
<template>
<AdminTile title="Myself">
<table>
<tr>
<td>Name:</td>
<td>{{ user.name }}</td>
</tr>
<tr>
<td>Role:</td>
<td>{{ user.role }}</td>
</tr>
</table>
</AdminTile>
</template>
<script>
export default {
props: ['user'],
}
</script>

View File

@ -1,104 +0,0 @@
<template>
<AdminTile class="admin-tile-players" title="Players">
<template v-slot:headerAction>
<span
v-if="!isReloading"
class="admin-tile-players__action-reload"
@click="reload"
>
</span>
</template>
<table class="admin-tile-players__table">
<tr v-if="players.length">
<th class="admin-tile-players__table-head">Name</th>
<th class="admin-tile-players__table-head">#&nbsp;Quotes</th>
<th class="admin-tile-players__table-head">Score</th>
<th class="admin-tile-players__table-head">zuletzt eingeloggt</th>
</tr>
<tr
class="admin-tile-players__player"
@click="editPlayer(player)"
v-for="player in players"
:key="player.id"
>
<td>{{ player.name }}{{ player.role === 'gamemaster' ? ' 👑' : '' }}</td>
<td>{{ player.numQuotes }}</td>
<td>{{ player.score }}</td>
<td>{{ getPlayerStatus(player) }}</td>
</tr>
<tr>
<td colspan="4">
<button class="admin-tile-players__add-player" @click="newPlayer()">+</button>
</td>
</tr>
</table>
</AdminTile>
</template>
<script>
export default {
props: ['gameinfo', 'players'],
data() {
return {
isReloading: false,
}
},
methods: {
editPlayer(player) {
this.$emit('edit', player)
},
newPlayer() {
this.$emit('edit', { id: null, name: '', score: 0, authcode: '' })
},
reload() {
this.$emit('reload')
this.isReloading = true
setTimeout(() => { this.isReloading = false }, 500)
},
getPlayerStatus(player) {
if (player.isPlaying && !player.isIdle) {
return 'online'
} else {
if (player.lastLoggedIn) {
return this.$formatter.datetime(player.lastLoggedIn)
} else if (player.isPlaying) {
return 'idle'
}
}
return '-'
},
},
}
</script>
<style lang="scss">
.admin-tile-players {
&__action-reload {
float: right;
cursor: pointer;
&:hover {
color: #c0c0c0;
}
}
&__table {
margin-left: -12px;
border-collapse: separate;
border-spacing: 12px 0;
}
&__table-head {
text-align: left;
}
&__player {
cursor: pointer;
&:hover {
color: #a0a0a0;
}
}
&__add-player {
padding: 0 3em;
cursor: pointer;
}
}
</style>

View File

@ -1,15 +0,0 @@
<template>
<div class="layout-default">
<Nuxt />
</div>
</template>
<style lang="scss">
html,
body,
#__nuxt,
#__layout,
.layout-default {
height: 100%;
}
</style>

View File

@ -1,52 +0,0 @@
<template>
<div class="page-admin">
<template v-if="user.role === 'admin'">
<GameControls />
<div class="page-admin__body">
<div class="page-admin__tiles">
<AdminTileMyself :user="user" />
<AdminTileGames :games="games.games" />
</div>
</div>
</template>
<template v-else>
<p>You are not an admin.</p>
</template>
</div>
</template>
<script>
export default {
async fetch() {
await this.$engine.fetchUserInfo()
await this.$engine.fetchGames()
},
computed: {
isAdmin() {
return this.$store.state.engine.user?.role === 'admin'
},
user() {
return this.$store.state.engine.user || {}
},
games() {
return this.$store.state.engine.games || {}
},
},
}
</script>
<style lang="scss">
@import '~/assets/css/components';
.page-admin {
color: #ffffff;
width: 100%;
height: 100%;
&__tiles {
display: flex;
flex-wrap: wrap;
padding: 24px;
}
}
</style>

View File

@ -1,109 +0,0 @@
<template>
<div class="page-gamemaster">
<template v-if="user.role === 'gamemaster'">
<GameControls />
<div class="page-gamemaster__body">
<div v-if="overlay.open" class="page-gamemaster__player-overlay">
<AdminEditPlayer :player="overlay.player" @close="editPlayerClose"/>
</div>
<div class="page-gamemaster__tiles">
<AdminTileMyself :user="user" />
<AdminTileGameinfo :gameinfo="gameinfo" :players="players" />
<AdminTilePlayers
:gameinfo="gameinfo"
:players="players"
@edit="editPlayer"
@reload="$fetch"
/>
</div>
</div>
</template>
<template v-else>
<p>You are not a game master.</p>
</template>
</div>
</template>
<script>
export default {
async fetch() {
await this.$engine.fetchUserInfo()
await this.$engine.fetchGameInfo({ g: this.$store.state.engine.user.game })
},
data() {
return {
overlay: {
open: false,
},
}
},
computed: {
user() {
return this.$store.state.engine.user || {}
},
gameinfo() {
return this.$store.state.engine.gameinfo || {}
},
players() {
const gameinfo = this.$store.state.engine.gameinfo || {}
const players = [...gameinfo.players || []]
return players.sort((a, b) => { return a.name.localeCompare(b.name) })
},
},
methods: {
editPlayer(player) {
this.overlay.open = true
this.overlay.player = player
},
async editPlayerClose() {
this.overlay.open = false
await this.$engine.fetchGameInfo({ g: this.$store.state.engine.user.game })
},
},
}
</script>
<style lang="scss">
@import '~/assets/css/components';
.page-gamemaster {
color: #ffffff;
width: 100%;
height: 100%;
&__body {
position: relative;
}
&__tiles {
display: flex;
flex-wrap: wrap;
padding: 24px;
}
&__player-overlay {
position: absolute;
left: 10%;
top: 10%;
width: 800px;
}
&__player-overlay-close {
float: right;
margin: 16px -16px 16px 16px;
padding: 12px;
border: 1px solid #ffffff;
border-radius: 8px;
background-color: $primary-background-color;
font-family: "$primary-font";
color: #ffffff;
cursor: pointer;
&:hover {
background-color: $primary-box-background-color;
}
&::after {
content: 'x';
}
}
}
</style>

View File

@ -1,65 +0,0 @@
<template>
<div class="startpage">
<TitleBox />
<PlayButton class="startpage__buttonline" />
<div class="startpage__video">
<a href="https://www.sirlab.de/knowyt/know-your-teammates-kurzanleitung.mp4" target="_blank">
🎬 {{ $t('video-user') }}
</a>
</div>
<CreateTeam v-if="!$store.state.engine.user" class="startpage__creategame" />
<div class="startpage__copyright">
v{{ $config.version }}, © 2021-2022, Settel
</div>
</div>
</template>
<script>
export default {
beforeMount() {
this.$i18n.map({
'video-user': { de: 'Erklärvideo (3min)', en: 'Video (3 mins, german)' },
})
},
}
</script>
<style lang="scss">
@import '~/assets/css/components';
.startpage {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
&__buttonline {
display: flex;
justify-content: center;
}
&__creategame {
margin: auto 0 2em 2em;
}
&__video {
margin: 2em 0;
text-align: center;
font-family: $secondary-font;
font-size: 24px;
font-weight: 800;
color: $text-secondary-text-color;
&:hover {
color: $text-secondary-hover-text-color;
}
}
&__copyright {
position: absolute;
right: 1em;
bottom: 0;
color: #000000;
}
}
</style>

View File

@ -1,116 +0,0 @@
<template>
<div class="page-play">
<div class="page-play__container">
<div class="page-play__container-game">
<div class="page-play__gamecontrols">
<GameControls />
</div>
<div class="page-play__area">
<Lobby v-if="gameState === 'idle'" />
<ReadySet v-if="gameState === 'ready-set'" :text="gamePhase" />
<Play v-if="gameState === 'play'" />
<CollectQuotes v-if="gameState === 'collect'" />
<Final v-if="gameState === 'final'" />
</div>
</div>
<div v-if="connectionLost" class="page-play__error-box">
<div>{{ $t('connection-lost') }}</div>
<button @click="reload">{{ $t('reload') }}</button>
</div>
<div v-if="showDebugPanel" class="page-play__container-debug">
<EngineDebug />
</div>
</div>
</div>
</template>
<script>
export default {
beforeMount() {
this.$i18n.map({
'connection-lost': {
de: 'Verbindung zum Server unterbrochen',
en: 'connection to server lost',
},
'reload': { de: 'neu laden', en: 'reload' },
})
},
async mounted() {
await this.$engine.start()
},
async beforeDestroy() {
await this.$engine.stop()
},
computed: {
showDebugPanel() {
const { hash } = this.$route
return hash.indexOf('debug') > -1
},
teamname() {
return this.$store.state.game.name
},
gameState() {
return this.$store.state.game.state
},
gamePhase() {
return this.$store.state.game.phase
},
connectionLost() {
return this.$engine.isActive && this.$store.state.engine.version == -1
},
},
methods: {
reload() {
window.location.reload()
},
},
}
</script>
<style lang="scss">
.page-play {
width: 100%;
height: 100%;
&__container {
display: flex;
width: 100%;
height: 100%;
&-game {
display: flex;
flex-direction: column;
width: 100%;
}
&-debug {
max-width: 350px;
max-height: 100%;
}
}
&__controls {
flex: 0 1 auto;
}
&__area {
flex: 1 1 auto;
}
&__error-box {
position: absolute;
left: 30%;
top: 10%;
width: 40%;
height: 5em;
padding: 1em;
border: 1px solid #ffffff;
border-radius: 20px;
background-color: #804040;
color: #ffffff;
text-align: center;
button {
margin: 1em;
}
}
}
</style>

View File

@ -1,12 +0,0 @@
import buildUrl from 'build-url'
export default async function(path, queryParams) {
const { $axios, $config } = this.context
const url = buildUrl($config.serverBaseUrl, {
path,
queryParams,
})
return await $axios.get(url)
}

View File

@ -1,7 +0,0 @@
export default async function() {
const { store } = this.context
await this.callApi('/api/collectQuotes', {
g: store.state.engine.user?.game,
})
}

View File

@ -1,7 +0,0 @@
export default async function() {
const { store } = this.context
await this.callApi('/api/continueGame', {
g: store.state.engine.user?.game,
})
}

View File

@ -1,9 +0,0 @@
export default async function(quote) {
const { store } = this.context
await this.callApi('/api/saveQuote', {
g: store.state.engine.user?.game,
id: ':new:',
quote,
})
}

View File

@ -1,7 +0,0 @@
export default async function(name, teamname, lang) {
return await this.callApi('/api/createGame', {
name,
teamname,
lang,
})
}

View File

@ -1,8 +0,0 @@
export default async function(player) {
const { store } = this.context
await this.callApi('/api/deletePlayer', {
g: store.state.engine.user?.game,
id: player.id,
})
}

View File

@ -1,13 +0,0 @@
export default async function({ g }) {
const { store } = this.context
try {
const response = await this.callApi('/api/gameinfo', { g })
store.commit('engine/setGameInfo', response.data)
} catch(e) {
store.commit('engine/setGameInfo', undefined)
return false
}
return true
}

View File

@ -1,13 +0,0 @@
export default async function() {
const { store } = this.context
try {
const response = await this.callApi('/api/games')
store.commit('engine/setGames', response.data)
} catch(e) {
store.commit('engine/setGames', undefined)
return false
}
return true
}

View File

@ -1,44 +0,0 @@
export default async function() {
const { store } = this.context
if (this.shouldStop || !this.isActive) {
this.isActive = false
this.shouldStop = false
console.debug('engine stopped')
return
}
let delay = 0
try {
const response = await this.callApi('/api/sync', {
v: store.state.engine.version + 1,
g: store.state.engine.user?.game,
})
this.parseSyncData(response.data)
} catch (e) {
if (!e.response) {
// request aborted or other causes
return
}
const { status, statusText } = e.response
if (status != 200) {
console.warn(`HTTP ${status} ${statusText}`)
delay = 5000
store.commit('engine/setVersion', -1)
}
}
const now = new Date().getTime()
const last = this.lastFetched.splice(0, 1)
this.lastFetched.push(now)
if (now - last < 1000) {
console.warn('engine: respawning too fast, throttling down')
delay = 5000
}
window.setTimeout(() => {
this.fetchUpdate()
}, delay)
}

View File

@ -1,13 +0,0 @@
export default async function() {
const { store } = this.context
try {
const response = await this.callApi('/api/userinfo')
store.commit('engine/setUser', response.data)
} catch(e) {
store.commit('engine/setUser', undefined)
return false
}
return true
}

View File

@ -1,7 +0,0 @@
export default async function() {
const { store } = this.context
await this.callApi('/api/finishGame', {
g: store.state.engine.user?.game,
})
}

View File

@ -1,12 +0,0 @@
export default async function() {
const { store } = this.context
try {
const response = await this.callApi('/api/getQuotes', {
g: store.state.engine.user?.game,
})
store.commit('myQuotes/setQuotes', response.data.quotes)
} catch(e) {
store.commit('myQuotes/setQuotes', undefined)
}
}

View File

@ -1,58 +0,0 @@
import callApi from './callApi'
import start from './start'
import stop from './stop'
import fetchUpdate from './fetchUpdate'
import fetchUserInfo from './fetchUserInfo'
import fetchGameInfo from './fetchGameInfo'
import fetchGames from './fetchGames'
import setGameName from './setGameName'
import setGameLang from './setGameLang'
import savePlayer from './savePlayer'
import deletePlayer from './deletePlayer'
import collectQuotes from './collectQuotes'
import startGame from './startGame'
import resetGame from './resetGame'
import continueGame from './continueGame'
import finishGame from './finishGame'
import parseSyncData from './parseSyncData'
import saveSelection from './saveSelection'
import getMyQuotes from './getMyQuotes'
import saveQuote from './saveQuote'
import createQuote from './createQuote'
import removeQuote from './removeQuote'
import createTeam from './createTeam'
export default (context, inject) => {
const engine = {
context,
lastFetched: [0, 0, 0, 0, 0],
isActive: false,
shouldStop: false,
callApi,
start,
stop,
fetchUpdate,
fetchUserInfo,
fetchGameInfo,
fetchGames,
setGameName,
setGameLang,
savePlayer,
deletePlayer,
collectQuotes,
getMyQuotes,
saveQuote,
createQuote,
removeQuote,
startGame,
resetGame,
continueGame,
finishGame,
parseSyncData,
saveSelection,
createTeam,
}
inject('engine', engine)
}

View File

@ -1,8 +0,0 @@
export default function(data) {
const { store } = this.context
store.commit('engine/setJson', data)
store.commit('game/setStateAndPhase', data.game)
store.commit('players/setPlayers', data.game)
store.commit('round/setRound', data.game)
}

View File

@ -1,8 +0,0 @@
export default async function(id, quote) {
const { store } = this.context
await this.callApi('/api/removeQuote', {
g: store.state.engine.user?.game,
id,
})
}

View File

@ -1,7 +0,0 @@
export default async function() {
const { store } = this.context
await this.callApi('/api/resetGame', {
g: store.state.engine.user?.game,
})
}

View File

@ -1,11 +0,0 @@
export default async function(player) {
const { store } = this.context
await this.callApi('/api/savePlayer', {
g: store.state.engine.user?.game,
id: player.id,
name: player.name,
score: player.score,
authcode: player.authcode,
})
}

View File

@ -1,9 +0,0 @@
export default async function(id, quote) {
const { store } = this.context
await this.callApi('/api/saveQuote', {
g: store.state.engine.user?.game,
id,
quote,
})
}

View File

@ -1,12 +0,0 @@
export default async function(selection) {
const { store } = this.context
if (selection.length === 0) {
selection = ['-']
}
await this.callApi('/api/saveSelection', {
g: store.state.engine.user?.game,
selection: selection[0],
})
}

View File

@ -1,8 +0,0 @@
export default async function({ g, lang }) {
const { store } = this.context
await this.callApi('/api/setGameLang', {
g,
lang,
})
}

View File

@ -1,8 +0,0 @@
export default async function({ g, name }) {
const { store } = this.context
await this.callApi('/api/setGameName', {
g,
name,
})
}

View File

@ -1,23 +0,0 @@
export default async function() {
const { store, redirect } = this.context
if (!store.state.engine.user) {
if (!await this.fetchUserInfo()) {
redirect('/')
}
}
if (this.isActive && !this.shouldStop) {
console.warn('attempt to start already running engine!')
return
}
const role = store.state.engine.user?.role || ''
if (role === 'admin') {
console.debug('user is admin, engine not started')
return
}
this.isActive = true
this.shouldStop = false
this.fetchUpdate()
console.debug('engine started')
}

View File

@ -1,7 +0,0 @@
export default async function() {
const { store } = this.context
await this.callApi('/api/startGame', {
g: store.state.engine.user?.game,
})
}

View File

@ -1,5 +0,0 @@
export default function() {
if (!this.isActive) return
this.shouldStop = true
console.debug('shutting down engine')
}

View File

@ -1,27 +0,0 @@
export default (context, inject) => {
const leftPad2Digits = (val) => val < 10 ? '0' + val : '' + val
const date = (time) => {
if (!time) return
const d = new Date(time * 1000)
return `${1900 + d.getYear()}-` +
`${leftPad2Digits(d.getMonth() + 1)}-` +
`${leftPad2Digits(d.getDate())}`
}
const datetime = (time) => {
if (!time) return
const d = new Date(time * 1000)
return `${date(time)}, ` +
`${leftPad2Digits(d.getHours())}:` +
`${leftPad2Digits(d.getMinutes())}:` +
`${leftPad2Digits(d.getSeconds())}`
}
inject('formatter', {
date,
datetime,
})
}

View File

@ -1,26 +0,0 @@
let lang = 'DE'
let i18nMap = {}
let defaultLang = navigator.language ? navigator.language.substr(0, 2) : 'en'
export default (context, inject) => {
const translate = (key) => {
const t = i18nMap[key]
if (!t) {
return key
}
return t[lang] || t[defaultLang] || key
}
inject('t', translate)
inject('i18n', {
setLang: (_lang) => {
lang = _lang
},
map: (_map) => {
i18nMap = {
...i18nMap,
..._map,
}
},
})
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

View File

@ -1,95 +0,0 @@
Copyright (c) 2011, Edgar Tolentino and Pablo Impallari (www.impallari.com|impallari@gmail.com),
Copyright (c) 2011, Igino Marini. (www.ikern.com|mail@iginomarini.com),
with Reserved Font Names "Dosis".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@ -1,93 +0,0 @@
Copyright (c) 2012, Alejandro Inler (alejandroinler@gmail.com), with Reserved Font Name 'Wendy'
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@ -1,29 +0,0 @@
export const state = () => ({
json: {},
version: -1,
user: undefined,
gameinfo: undefined,
games: undefined,
})
export const mutations = {
setJson(state, json) {
state.json = json
state.version = parseInt(json.version, 10)
},
setVersion(state, version) {
state.version = version
},
setUser(state, user) {
state.user = user
if (user) {
this.$i18n.setLang(user.lang)
}
},
setGameInfo(state, gameinfo) {
state.gameinfo = gameinfo
},
setGames(state, games) {
state.games = games
}
}

View File

@ -1,20 +0,0 @@
export const state = () => ({
state: "",
phase: "",
name: "",
})
export const mutations = {
setStateAndPhase(state, game) {
if (game) {
const { state: gameState, phase, name } = game
state.state = gameState
state.phase = phase
state.name = name
} else {
state.state = ""
state.phase = ""
state.name = ""
}
},
}

View File

@ -1,13 +0,0 @@
export const state = () => ({
quotes: [],
})
export const mutations = {
setQuotes(state, list) {
if (typeof list === 'undefined') {
state.quotes = []
} else {
state.quotes = list
}
},
}

View File

@ -1,13 +0,0 @@
export const state = () => ({
players: [],
})
export const mutations = {
setPlayers(state, game) {
if (game) {
state.players = game.players
} else {
state.players = []
}
},
}

View File

@ -1,22 +0,0 @@
export const state = () => ({
quote: "",
sources: [],
selections: {},
revelation: {},
})
export const mutations = {
setRound(state, game) {
if (game && game.round) {
state.quote = game.round.quote
state.sources = game.round.sources
state.selections = game.round.selections
state.revelation = game.round.revelation
} else {
state.quote = ""
state.sources = []
state.selections = {}
state.revelation = {}
}
},
}

View File

@ -1,22 +0,0 @@
export const state = () => ({
selection: {},
})
export const mutations = {
select(state, id) {
state.selection = {
...state.selection,
[id]: true,
}
},
unselect(state, id) {
var selection = state.selection
delete selection[id]
state.selection = {
...selection
}
},
clearSelection(state) {
state.selection = {}
},
}

File diff suppressed because it is too large Load Diff

View File

@ -25,7 +25,7 @@ const newQuote = ref({} as Quote)
const createNewQuote = () => { const createNewQuote = () => {
showNewQuoteCard.value = false showNewQuoteCard.value = false
newQuote.value = { newQuote.value = {
id: '', id: ':new:' + Date.now(),
quote: '', quote: '',
} }
} }

View File

@ -82,7 +82,7 @@ const editModeEnd = async () => {
const saveQuote = async () => { const saveQuote = async () => {
if (props.quote.quote) { if (props.quote.quote) {
useEngine().saveQuote(props.quote.id || ':new:', props.quote.quote) await useEngine().saveQuote(props.quote.id, props.quote.quote)
} }
await editModeEnd() await editModeEnd()

View File

@ -35,7 +35,7 @@
</tr> </tr>
<tr class="gameinfo-tile__row"> <tr class="gameinfo-tile__row">
<td>{{ $t('num-quotes-played') }}:</td> <td>{{ $t('num-quotes-played') }}:</td>
<td colspan="2">{{ gameinfo.numQuotesLeft - gameinfo.numQuotesLeft }} / {{ gameinfo.numQuotesTotal }}</td> <td colspan="2">{{ gameinfo.numQuotesTotal - gameinfo.numQuotesLeft }} / {{ gameinfo.numQuotesTotal }}</td>
</tr> </tr>
<tr class="gameinfo-tile__row" v-if="!showId" @click="showId = true"> <tr class="gameinfo-tile__row" v-if="!showId" @click="showId = true">
<td colspan="3">&nbsp;</td> <td colspan="3">&nbsp;</td>

View File

@ -44,6 +44,12 @@ export async function fetchUpdate(this: EngineContext) {
throw Error('unexpected response from /api/sync') throw Error('unexpected response from /api/sync')
} }
if (response.game.id != userInfoStore.gameId) {
// happens when user changes game and an old request is still pending
console.warn('response gameId does not match current gameId')
return
}
this.version = parseInt(response.version) this.version = parseInt(response.version)
this.isConnected.value = true this.isConnected.value = true
this.retry.value = 0 this.retry.value = 0

View File

@ -1,5 +1,9 @@
import { useUserinfoStore, Userinfo } from "@/stores/UserinfoStore" import { useUserinfoStore, Userinfo } from '@/stores/UserinfoStore'
import useI18n from "./useI18n" 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 'ohmyfetch'
export type AllowRole = '' | 'player' | 'gamemaster' | 'admin' export type AllowRole = '' | 'player' | 'gamemaster' | 'admin'
@ -49,6 +53,11 @@ export default (): useAuth => {
logout: async (): Promise<void> => { logout: async (): Promise<void> => {
await $fetch('/api/logout') await $fetch('/api/logout')
useEngineStore().reset()
useGameinfoStore().reset()
usePlayersStore().reset()
useUserinfoStore().reset()
useRoundStore().reset()
}, },
} }
} }

View File

@ -10,5 +10,8 @@ export const useEngineStore = defineStore('EngineStore', {
setJson(json: any): void { setJson(json: any): void {
this.json = json this.json = json
}, },
reset(): void {
this.json = {}
},
}, },
}) })

View File

@ -33,5 +33,13 @@ export const useGameinfoStore = defineStore('GameinfoStore', {
setGameinfo(gameInfo: Gameinfo): void { setGameinfo(gameInfo: Gameinfo): void {
this.gameInfo = gameInfo this.gameInfo = gameInfo
}, },
reset(): void {
this.gameInfo = {
id: '',
name: '',
state: '',
phase: '',
}
},
}, },
}) })

View File

@ -12,5 +12,8 @@ export const usePlayersStore = defineStore('PlayersStore', {
players = players || [] players = players || []
this.players.splice(0, this.players.length, ...players) this.players.splice(0, this.players.length, ...players)
}, },
reset(): void {
this.players.splice(0, 0)
},
}, },
}) })

View File

@ -24,5 +24,16 @@ export const useRoundStore = defineStore('RoundStore', {
this.round.revelation.votes = round.revelation?.votes || {} as RevelationVotes this.round.revelation.votes = round.revelation?.votes || {} as RevelationVotes
this.round.revelation.sources = round.revelation?.sources || {} as RevelationSources this.round.revelation.sources = round.revelation?.sources || {} as RevelationSources
}, },
reset(): void {
this.round = {
quote: '',
sources: [],
selections: {},
revelation: {
votes: {},
sources: {},
},
}
},
}, },
}) })

View File

@ -34,5 +34,15 @@ export const useUserinfoStore = defineStore('UserinfoStore', {
setUserInfo(userInfo: Userinfo): void { setUserInfo(userInfo: Userinfo): void {
this.userInfo = userInfo this.userInfo = userInfo
}, },
reset(): void {
this.userInfo = {
id: '',
name: '',
role: '',
game: '',
lang: 'en',
isCameo: '',
}
},
}, },
}) })

View File

@ -1,10 +1,11 @@
package application package application
import ( import (
"time"
"sirlab.de/go/knowyt/applicationConfig" "sirlab.de/go/knowyt/applicationConfig"
"sirlab.de/go/knowyt/game" "sirlab.de/go/knowyt/game"
"sirlab.de/go/knowyt/user" "sirlab.de/go/knowyt/user"
"time"
) )
func NewApplication(config applicationConfig.ApplicationConfig) (*Application, error) { func NewApplication(config applicationConfig.ApplicationConfig) (*Application, error) {
@ -13,6 +14,7 @@ func NewApplication(config applicationConfig.ApplicationConfig) (*Application, e
users: make(map[string]*user.User), users: make(map[string]*user.User),
games: make(map[string]*game.Game), games: make(map[string]*game.Game),
playerATime: make(map[string]time.Time), playerATime: make(map[string]time.Time),
debounceMap: make(map[string]bool),
} }
if err := app.loadUsers(); err != nil { if err := app.loadUsers(); err != nil {

View File

@ -2,9 +2,10 @@ package application
import ( import (
"fmt" "fmt"
"github.com/google/uuid"
"net/http" "net/http"
"path" "path"
"github.com/google/uuid"
"sirlab.de/go/knowyt/game" "sirlab.de/go/knowyt/game"
"sirlab.de/go/knowyt/user" "sirlab.de/go/knowyt/user"
) )
@ -26,7 +27,8 @@ func (app *Application) SaveQuote(usr *user.User, w http.ResponseWriter, r *http
quoteText := r.URL.Query().Get("quote") quoteText := r.URL.Query().Get("quote")
quoteId := r.URL.Query().Get("id") quoteId := r.URL.Query().Get("id")
if quoteId == ":new:" { if quoteId[:5] == ":new:" {
if app.debounceQuote(usr.GetId() + ":" + quoteId) {
quoteNewUuid := uuid.NewString() quoteNewUuid := uuid.NewString()
err = gm.CreateQuote( err = gm.CreateQuote(
app.getQuoteFileNameFromId(gm, quoteNewUuid), app.getQuoteFileNameFromId(gm, quoteNewUuid),
@ -34,6 +36,7 @@ func (app *Application) SaveQuote(usr *user.User, w http.ResponseWriter, r *http
quoteNewUuid, quoteNewUuid,
quoteText, quoteText,
) )
}
} else { } else {
err = gm.SaveQuote( err = gm.SaveQuote(
app.getQuoteFileNameFromId(gm, quoteId), app.getQuoteFileNameFromId(gm, quoteId),
@ -58,3 +61,11 @@ func (app *Application) getQuoteFileNameFromId(gm *game.Game, id string) string
quoteFileNameShort := id + ".json" quoteFileNameShort := id + ".json"
return path.Join(gameDirName, "quotes", quoteFileNameShort) return path.Join(gameDirName, "quotes", quoteFileNameShort)
} }
func (app *Application) debounceQuote(id string) bool {
if app.debounceMap[id] {
return false
}
app.debounceMap[id] = true
return true
}

View File

@ -1,11 +1,12 @@
package application package application
import ( import (
"sync"
"time"
"sirlab.de/go/knowyt/applicationConfig" "sirlab.de/go/knowyt/applicationConfig"
"sirlab.de/go/knowyt/game" "sirlab.de/go/knowyt/game"
"sirlab.de/go/knowyt/user" "sirlab.de/go/knowyt/user"
"sync"
"time"
) )
type Application struct { type Application struct {
@ -14,4 +15,5 @@ type Application struct {
users map[string]*user.User users map[string]*user.User
games map[string]*game.Game games map[string]*game.Game
playerATime map[string]time.Time playerATime map[string]time.Time
debounceMap map[string]bool
} }

View File

@ -2,6 +2,7 @@ package game
import ( import (
"fmt" "fmt"
"sirlab.de/go/knowyt/quote" "sirlab.de/go/knowyt/quote"
) )