A quiz with 4 questions, just one is correct. This repository is an optional module for the "lerntools". For installation and configuration, please see the documentation in https://codeberg.org/lerntools/base
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

317 lines
11 KiB

<template>
<div>
<!-- Lobby - QR-Code and wait for participants-->
<div v-if="state=='lobby'">
<page-title :title="$t('title')" :subtitle="$t('lobby-subtitle')"/>
<div class="container mt-3">
<div class="row">
<div class="col-md-6 my-4">
<div class="card shadow">
<div class="card-header bg-dark text-white">{{$t('participants-link')}}</div>
<div class="card-body text-center">
<p class="lead">{{shortUrl}}</p>
<p class="lead">{{$t('key',{key:key})}}</p>
<vue-qrcode v-if="url" :value="url" :scale="6"/>
<div>
<button v-if="participants.length>=0" v-on:click="start()" class="mr-1 btn btn-primary">{{$t('start')}}</button>
<router-link class="mr-1 btn btn-secondary" :to="{ name: 'abcd-index'}">{{$t('cancel')}}</router-link>
</div>
</div>
</div>
</div>
<div class="col-md-6 my-4">
<div class="card shadow">
<div class="card-header bg-dark text-white">{{$t('participants-list')}}</div>
<div class="card-body">
<table>
<tr v-for="part in participants">
<td>{{part.name}}</td>
<td><img src="~Main/img/delete.svg" v-on:click="deletePart(part)"></td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Show question and answer options, also statistics when state is answer-->
<div v-if="state=='ask' || state=='answer'">
<page-title :title="abcd.title+' ('+(1+pos)+'/'+abcd.questions.length+')'"/>
<audio id="long-beep" src="../audio/long-beep.mp3"></audio>
<audio id="beep" src="../audio/beep.mp3"></audio>
<div class="container mt-6">
<p class="my-3 abcd-text" v-html="md.render(abcd.questions[pos].text)"/>
<div class="row my-4" v-if="abcd.questions[pos].image">
<div class="col-6">
<div class="row mt-3" v-for="n in [0,1,2,3]" v-if="abcd.questions[pos].options[n]!='–'">
<p class="abcd-text col-2 bg-var text-white text-center rounded-left p-1 shadow" :class="{ 'abcd-wrong':abcd.questions[pos].answer!=n && state=='answer'}">
{{['A','B','C','D'][n]}}
</p>
<p class="abcd-text col-10 bg-white rounded-right p-1 pl-3 shadow" :id="'opt'+n"
:class="{ 'abcd-wrong':abcd.questions[pos].answer!=n && state=='answer'}"
v-html="md.render(abcd.questions[pos].options[n])">
</p>
<br>
</div>
<p class="abcd-text mt-3" v-if="state=='ask'">{{$t('time')}} {{remainingTime}} s</p>
</div>
<div class="col-6">
<img class="mt-3 align-middle shadow" style='width:100%' :src="abcd.questions[pos].image">
</div>
</div>
<div v-else>
<div class="row my-4" v-for="n in [0,1,2,3]" v-if="abcd.questions[pos].options[n]!='–'">
<span class="col-1"/>
<p class="abcd-text col-1 bg-var text-white text-center rounded-left p-1 shadow" :class="{ 'abcd-wrong':abcd.questions[pos].answer!=n && state=='answer'}">
{{['A','B','C','D'][n]}}
</p>
<p class="abcd-text col-10 bg-white rounded-right p-1 pl-3 shadow" :id="'opt'+n"
:class="{ 'abcd-wrong':abcd.questions[pos].answer!=n && state=='answer'}"
v-html="md.render(abcd.questions[pos].options[n])">
</p>
</div>
<p class="abcd-text mt-3" v-if="state=='ask'">{{$t('time')}} {{remainingTime}} s</p>
</div>
<p v-if="state=='answer'" class="abcd-text m-3" v-html="md.render(abcd.questions[pos].info)"/>
<div class="float-right">
<tooltip-icon size="big" v-if="state=='ask'" :tip="$t('hint-show-solution')" src="~Main/img/right-primary.svg" v-on:click="showAnswer()"/>
<tooltip-icon size="big" v-else :tip="$t('hint-score')" src="~Main/img/right-primary.svg" v-on:click="showReport()"/>
</div>
</div>
</div>
<!-- Report for all participants - scores-->
<div v-if="state=='report' || state=='finish'">
<page-title :title="abcd.title+' – '+$t('scores')"/>
<div class="container mt-6">
<span v-if="state == 'finish'">{{$t('end')}}</span>
<div class="row mt-1" v-for="score in scores">
<p class="abcd-text col-2 bg-dark text-white rounded-left p-1 text-right shadow">{{score.name}}</p>
<p class="abcd-text bg-var text-white rounded-right p-1 shadow" :style="'width:'+Math.round(80*score.points/abcd.questions.length)+'%'">{{score.points}}</p>
</div>
<div class="text-right">
<tooltip-icon v-if="state!='finish'" size="big" :tip="$t('hint-next-question')" src="~Main/img/right-primary.svg" v-on:click="ask()"/>
<tooltip-icon v-else :tip="$t('exit')" size="big" src="~Main/img/right-primary.svg" v-on:click="$router.push({name:'abcd-index'})"/>
</div>
</div>
</div>
</div>
</template>
<script>
import PageTitle from 'Main/components/PageTitle.vue';
import TooltipIcon from 'Main/components/TooltipIcon.vue';
import md from 'Main/js/markdown.js';
import color from 'Main/js/color.js';
import VueQrcode from 'vue-qrcode';
export default {
components: { PageTitle, VueQrcode, TooltipIcon},
data: function() {
return {
shortUrl:'',
url:'',
abcdInstance:{},
abcd:{},
key:'',
participants:[],
state:'lobby',
timer:{},
pos:0, //number of current question
question:{}, //displayed question
md:md,
remainingTime:0, //remaining time to answer the question
votes:[], //votes of participants from server
scores:[], //scores of the participants
}
},
mounted: function() {
//load set
var id=this.$route.params['id'];
this.$axios.get('abcd/sets/'+id).then(response=> {
if (!response.data || !response.data.questions || response.data.questions.length==0) return this.$router.push({name:'abcd-index'});
this.abcd=response.data;
//create game instance
this.$axios.post('abcd/instances',{title: this.abcd.title}).then(response=> {
this.abcdInstance=response.data;
var location=window.location.toString();
var base=location.substring(0,location.lastIndexOf('/'));
this.shortUrl=base+'/abcd';
this.key=this.abcdInstance.key
this.url=base+'/abcd/'+this.key;
//start update loop for Participants
this.timer=setInterval(this.loadPartList, 3000);
})
})
window.addEventListener('keypress', this.keypressed);
},
beforeDestroy: function() {
window.removeEventListener('keypress', this.keypressed);
window.clearInterval(this.timer);
},
updated:function() {
color.setBgVar();
},
watch: {
state: function() {
if (this.key) {
this.$axios.post('abcd/instances/'+this.key+'/state',{state:this.state, pos:this.pos});
}
}
},
methods: {
//delete paricipant (used in lobby view)
deletePart:function (part) {
this.$axios.delete('abcd/instances/'+this.key+'/participants/'+part._id).then(response=> {
this.participants.splice(this.participants.findIndex(function(i){ return i._id==part._id; }), 1);
});
},
//load list of all participants (used in lobby view)
loadPartList: function () {
this.$axios.get('abcd/instances/'+this.key+'/participants').then(response=> {
this.participants=response.data;
})
},
//start game
start: function() {
this.pos=-1;
this.ask();
},
//ask question
ask: function() {
this.pos=this.pos+1;
this.state='ask';
this.question=this.abcd.questions[this.pos];
window.clearInterval(this.timer);
this.remainingTime=this.question.time;
this.timer=window.setInterval(this.countDown, 1000);
},
//countdown during question
countDown: function () {
this.remainingTime=this.remainingTime-1;
if (this.remainingTime<10 && this.remainingTime>0) document.getElementById("beep").play();
if (this.remainingTime<=0) {
document.getElementById("long-beep").play();
this.showAnswer();
}
},
//show answer
showAnswer: function() {
this.state='answer';
window.clearInterval(this.timer);
this.$axios.get('abcd/instances/'+this.key+'/votes').then(response=> {
this.votes=response.data;
//Count votes
var stats=[0,0,0,0];
var sum=0;
for (var i=0; i<this.votes.length; i++) {
var vote=this.votes[i];
if (vote.question==this.pos) {
stats[vote.option]++;
sum++;
}
}
//Normalize to 100%
if (sum>0) {
for (var i=0; i<4; i++) {
stats[i]=Math.round(100*stats[i]/sum);
}
}
//Visualize
for (var i=0; i<4; i++) {
if (stats[i]>0) {
var background='linear-gradient(90deg, #80a0a0 '+stats[i]+'%, #000000 1%, #ffffff 1%, #ffffff '+(100-stats[i])+'%)';
document.getElementById('opt'+i).style.background=background;
}
}
})
},
//show scores report
showReport: function() {
//count results
var results={};
for (var i=0; i<this.participants.length; i++) {
results[this.participants[i].name]=0;
}
for (var i=0; i<this.votes.length; i++) {
var vote=this.votes[i];
for (var j=0; j<this.abcd.questions.length; j++) {
var question=this.abcd.questions[j];
if (vote.option==question.answer && vote.question==j) {
results[vote.participant]++;
}
}
}
//sort results descending
this.scores=[];
for (var i=this.abcd.questions.length; i>=0; i--) {
for ( var name in results) {
if (results[name]==i) this.scores.push({name:name,points:i});
}
}
//Update instance and show result
if (this.pos+1>=this.abcd.questions.length) this.state='finish';
else this.state="report";
},
keypressed: function(e) {
let cmd=String.fromCharCode(e.keyCode).toLowerCase();
if (cmd==' ') {
if (this.state=='lobby') this.start();
else if (this.state=='ask') this.showAnswer();
else if (this.state=='answer') this.showReport();
else if (this.state=='report') this.ask();
}
}
}
}
</script>
<style>
.abcd-wrong { opacity: 0.3}
.abcd-text {font-size: 1.8rem;}
</style>
<style src='../../../node_modules/katex/dist/katex.min.css'/>
<i18n>
{
"de": {
"title": "ABCD-Quiz",
"lobby-subtitle": "Bitte warten Sie, bis sich die Teilnehmer verbunden haben und drücken starten Sie dann das Quiz!",
"participants-link": "Teilnehmer-Link",
"start": "Start",
"cancel": "Abbrechen",
"participants-list": "Teilnehmer-Liste",
"time": "Zeit",
"hint-show-solution": "Lösung anzeigen",
"hint-score": "Punktestand",
"scores": "Punktestand",
"hint-next-question": "Nächste Frage",
"exit": "Verlassen",
"key":"Schlüssel: {key}",
"end":"Ende des Quiz"
},
"en": {
"title": "ABCD-Quiz",
"lobby-subtitle": "Please wait until the participants have connected and then press the quiz!",
"participants-link": "Participant Link",
"start": "Start",
"cancel": "Cancel",
"participants-list": "Subscriber List",
"time": "Time",
"hint-show-solution": "Show solution",
"hint-score": "Score",
"scores": "Scores",
"hint-next-question": "Next question",
"exit": "Exit",
"key": "Key: {key}",
"end":"End of the quiz"
}
}
</i18n>