initial code commit
This commit is contained in:
commit
2c8871cfeb
|
@ -0,0 +1,16 @@
|
|||
FS-Quiz-Tool
|
||||
============
|
||||
|
||||
Tool to train for Quizzes
|
||||
|
||||
## ToDo
|
||||
|
||||
- Global timer for quiz
|
||||
- Penalties/Bonuses
|
||||
|
||||
## Long term plans
|
||||
|
||||
- Other versioons but FSCzech:
|
||||
- FSG
|
||||
- FSA
|
||||
- FSEast
|
|
@ -0,0 +1,71 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
|
||||
<head>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="theme-color" content="#c50e1f">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<link href="style.css" rel="stylesheet" type="text/css">
|
||||
|
||||
<title>FS Quiz tool</title>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<header>
|
||||
|
||||
<h1>FS Czech Quiz Tool</h1>
|
||||
|
||||
Only works in modern browsers.
|
||||
|
||||
</header>
|
||||
|
||||
<main>
|
||||
|
||||
<form id="spreadsheet" class="view">
|
||||
|
||||
Title:<br>
|
||||
<input type="text" id="titleField" required>
|
||||
Paste your questions from a spreadsheet here:<br>
|
||||
<textarea rows="20" id="questions" required></textarea>
|
||||
<br>
|
||||
|
||||
<input type="button" value="Create Quiz" onclick="createQuiz()">
|
||||
|
||||
</form>
|
||||
|
||||
<div id="prescreen" class="view">
|
||||
<h1></h1>
|
||||
<input type="button" value="Start" onclick="startQuiz()" class="center">
|
||||
</div>
|
||||
|
||||
<div id="quiz" class="view">
|
||||
<h1></h1>
|
||||
<div id="sharing">
|
||||
<input type="button" value="Share This Quiz" onclick="shareQuiz()" class="center">
|
||||
<br><a id="shareLink"></a><br>
|
||||
</div>
|
||||
<form>
|
||||
</form>
|
||||
<input type="button" value="Submit" onclick="submitQuiz()" style="background: #008029" id="quizSubmitButton" disabled>
|
||||
<input type="button" value="Abort" onclick="abortQuiz()">
|
||||
</div>
|
||||
|
||||
<div id="postscreen" class="view">
|
||||
<h1></h1>
|
||||
<input type="button" value="Restart" onclick="startQuiz()" class="center">
|
||||
<input type="button" value="Create New Quiz" onclick="changeView('spreadsheet')" class="center">
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<footer></footer>
|
||||
|
||||
<script src="main.js"></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,413 @@
|
|||
var state = {
|
||||
style: 'FSCzech',
|
||||
id: null,
|
||||
title: null,
|
||||
questions: [],
|
||||
timer: 0,
|
||||
interval: null,
|
||||
success: 0
|
||||
}
|
||||
|
||||
|
||||
function changeView(view) {
|
||||
|
||||
for (el of document.querySelectorAll('.view'))
|
||||
el.style.display = 'none'
|
||||
|
||||
document.getElementById(view).style.display = 'block'
|
||||
|
||||
}
|
||||
|
||||
|
||||
function showLink() {
|
||||
|
||||
var link = 'https://quiz.fasttube.de/?id=' + state.id
|
||||
|
||||
var linkEl = document.getElementById('shareLink')
|
||||
linkEl.href = link
|
||||
linkEl.innerHTML = link
|
||||
|
||||
document.querySelector('#sharing input').style.display = 'none'
|
||||
|
||||
}
|
||||
|
||||
|
||||
async function shareQuiz() {
|
||||
|
||||
if (state.id) {
|
||||
console.log('Showing saved local link')
|
||||
showLink()
|
||||
return
|
||||
}
|
||||
|
||||
console.log('Saving quiz to server. QuizData:')
|
||||
|
||||
var quizData = {
|
||||
title: state.title,
|
||||
questions: state.questions
|
||||
}
|
||||
|
||||
console.log(quizData)
|
||||
console.log('Waiting for id')
|
||||
|
||||
var db = 'https://quiz.fasttube.de/db/'
|
||||
|
||||
var response = await fetch(db, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain'
|
||||
},
|
||||
body: JSON.stringify(quizData)
|
||||
})
|
||||
|
||||
if (response.ok == false) {
|
||||
alert('Something went wrong while sharing')
|
||||
return
|
||||
}
|
||||
|
||||
var responseBody = await response.text()
|
||||
console.log('Received id: ' + responseBody)
|
||||
state.id = responseBody
|
||||
|
||||
showLink()
|
||||
|
||||
}
|
||||
|
||||
|
||||
function updateTimer() {
|
||||
|
||||
var button = document.getElementById('quizSubmitButton')
|
||||
|
||||
if (state.timer > 0) {
|
||||
|
||||
button.value = 'Wait ' + state.timer + 's'
|
||||
button.disabled = true
|
||||
state.timer -= 1
|
||||
|
||||
} else {
|
||||
|
||||
button.value = 'Submit Answers'
|
||||
button.disabled = false
|
||||
clearInterval(state.interval)
|
||||
|
||||
}
|
||||
|
||||
localStorage.setItem('state', JSON.stringify(state))
|
||||
|
||||
}
|
||||
|
||||
|
||||
function startTimer(time) {
|
||||
|
||||
state.timer = time || 30
|
||||
|
||||
console.log('Setting timer to ' + state.timer + ' seconds.')
|
||||
|
||||
updateTimer()
|
||||
|
||||
state.interval = setInterval(updateTimer, 1000)
|
||||
|
||||
}
|
||||
|
||||
|
||||
function parseLine(line) {
|
||||
|
||||
// replace newline markers with newlines
|
||||
var els = line.replace(/⎊/g, '<br>').split('\t')
|
||||
|
||||
if (els.length < 4) {
|
||||
alert('Information missing. At least 4 columns needed.')
|
||||
return null
|
||||
}
|
||||
|
||||
var type = els[0]
|
||||
|
||||
// For multiple-choice, split lines into array
|
||||
var choices = els[2].split('<br>')
|
||||
|
||||
// If multiple answers are allowed, they are ampersand-separated
|
||||
var answers = (els[3]) ? els[3].split('<br>') : null
|
||||
|
||||
return {
|
||||
question: els[1] || '[No question provided]',
|
||||
type: type,
|
||||
choices: choices,
|
||||
answers: answers,
|
||||
explanation: els[4] || '[No explanation provided]',
|
||||
author: els[5] || '[No author provided]',
|
||||
picture: els[6] || null,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
function parseSpreadsheet() {
|
||||
|
||||
console.log('Parsing spreadsheet data')
|
||||
|
||||
var textEl = document.getElementById('questions')
|
||||
|
||||
var text = textEl.value.replace(/"([^"]|"")+"/g, m => m.replace(/\n/g, '⎊').replace(/""/g, '"').replace(/^"(.*)"$/, '$1'))
|
||||
|
||||
state.questions = text.split('\n').map(parseLine)
|
||||
|
||||
if (state.questions[0] == null)
|
||||
return {success: false}
|
||||
|
||||
localStorage.setItem('state', JSON.stringify(state))
|
||||
|
||||
return {success: true}
|
||||
|
||||
}
|
||||
|
||||
|
||||
function startQuiz() {
|
||||
|
||||
state.success = false
|
||||
|
||||
console.log('Starting/Resuming quiz. State:')
|
||||
console.log(state)
|
||||
|
||||
var quizForm = document.querySelector('#quiz form')
|
||||
|
||||
for (const [i, q] of state.questions.entries()) {
|
||||
|
||||
var html = ''
|
||||
|
||||
html += `<h3>Question #${i+1}</h3>`
|
||||
html += `<h4>${q.question}</h4>`
|
||||
|
||||
if (q.picture)
|
||||
html += `<img src="${q.picture}">`
|
||||
|
||||
if (q.type == 'ChooseOne' || q.type == 'ChooseAny') {
|
||||
|
||||
var choices = Array.from(q.choices.entries())
|
||||
var shuffled = choices.sort(() => Math.random() - 0.5)
|
||||
|
||||
for ([ci, c] of shuffled)
|
||||
html += (q.type == 'ChooseOne')
|
||||
? `<label><input type="radio" name="q${i}" value="${ci+1}"> <p>${c}</p></label><br>`
|
||||
: `<label><input type="checkbox" name="q${i}" value="${ci+1}"> <p>${c}</p></label><br>`
|
||||
|
||||
} else {
|
||||
|
||||
for (c of q.choices)
|
||||
html += `<label>${c}<input type="text" name="q${i}"></label>`
|
||||
|
||||
}
|
||||
|
||||
|
||||
quizForm.innerHTML += html
|
||||
|
||||
}
|
||||
|
||||
changeView('quiz')
|
||||
|
||||
if (state.timer > 0)
|
||||
startTimer(state.timer)
|
||||
else
|
||||
updateTimer()
|
||||
|
||||
console.log('Quiz started/resumed')
|
||||
|
||||
}
|
||||
|
||||
function updateTitles() {
|
||||
document.querySelector('#prescreen h1').innerHTML = state.title || ''
|
||||
document.querySelector('#quiz h1').innerHTML = state.title || ''
|
||||
}
|
||||
|
||||
function createQuiz() {
|
||||
|
||||
console.log('Creating new quiz')
|
||||
|
||||
if (parseSpreadsheet().success == false)
|
||||
return
|
||||
|
||||
console.log('Spreadsheet parsing successful')
|
||||
|
||||
state.timer = 0
|
||||
|
||||
state.title = document.getElementById('titleField').value
|
||||
updateTitles()
|
||||
|
||||
changeView('prescreen')
|
||||
|
||||
console.log('Quiz created')
|
||||
|
||||
}
|
||||
|
||||
|
||||
function endQuiz() {
|
||||
|
||||
console.log('Ending quiz')
|
||||
|
||||
localStorage.removeItem('state')
|
||||
|
||||
document.querySelector('#quiz form').innerHTML = ''
|
||||
changeView('postscreen')
|
||||
|
||||
if (state.interval)
|
||||
clearInterval(state.interval)
|
||||
|
||||
console.log('Quiz ended')
|
||||
|
||||
}
|
||||
|
||||
|
||||
function mergeKeyReducer(acc, entry) {
|
||||
|
||||
if (!acc[entry[0]])
|
||||
acc[entry[0]] = []
|
||||
|
||||
acc[entry[0]].push(entry[1])
|
||||
|
||||
return acc
|
||||
|
||||
}
|
||||
|
||||
|
||||
function submitQuiz() {
|
||||
|
||||
console.log('Submitting quiz. State:')
|
||||
console.log(state)
|
||||
|
||||
var quizForm = document.querySelector('#quiz form')
|
||||
|
||||
var data = new FormData(quizForm)
|
||||
|
||||
var responses = Array.from(data.entries()).reduce(mergeKeyReducer, {})
|
||||
|
||||
console.log(responses)
|
||||
|
||||
var correct = 0
|
||||
|
||||
for (var [key, value] of Object.entries(responses)) {
|
||||
|
||||
console.log(key + ": " + value)
|
||||
|
||||
// "q3" -> 3
|
||||
var idx = key[1]
|
||||
|
||||
if (state.questions[idx].type == 'ChooseOne' || state.questions[idx].type == 'ChooseAny') {
|
||||
state.questions[idx].answers.sort()
|
||||
value.sort()
|
||||
}
|
||||
|
||||
if (JSON.stringify(state.questions[idx].answers) == JSON.stringify(value))
|
||||
correct++
|
||||
|
||||
}
|
||||
|
||||
var text = '' + correct + '/' + state.questions.length + ' questions answered correctly.\n'
|
||||
|
||||
state.success = (correct == state.questions.length)
|
||||
|
||||
text += state.success ? 'Yay you did it!' : 'Try again!'
|
||||
|
||||
if (!state.success) {
|
||||
startTimer()
|
||||
alert(text)
|
||||
}
|
||||
|
||||
if (state.success) {
|
||||
document.querySelector('#postscreen h1').innerHTML = `Yay, we're done!<br>Everything is correct :)`
|
||||
endQuiz()
|
||||
}
|
||||
|
||||
console.log('Quiz submitted')
|
||||
|
||||
}
|
||||
|
||||
|
||||
function abortQuiz() {
|
||||
|
||||
console.log('Aborting quiz')
|
||||
|
||||
if (!confirm('You sure you want to abort this quiz? Your progress will be lost.'))
|
||||
return
|
||||
|
||||
document.querySelector('#postscreen h1').innerHTML = `Quiz cancelled.`
|
||||
|
||||
endQuiz()
|
||||
|
||||
console.log('Quiz cancelled')
|
||||
|
||||
}
|
||||
|
||||
function idFromUrl() {
|
||||
|
||||
var s = window.location.href.split('id=')
|
||||
|
||||
if (s.length <= 1)
|
||||
return null
|
||||
|
||||
return s[1].split('&')[0]
|
||||
|
||||
}
|
||||
|
||||
async function fetchQuiz(id) {
|
||||
|
||||
console.log('Fetching quiz')
|
||||
|
||||
var url = 'https://quiz.fasttube.de/db/' + id
|
||||
|
||||
var response = await fetch(url)
|
||||
|
||||
if (response.ok == false) {
|
||||
alert('Something went wrong while loading this quiz')
|
||||
return
|
||||
}
|
||||
|
||||
var json = await response.json()
|
||||
|
||||
console.log('Quiz fetched. Response:')
|
||||
console.log(json)
|
||||
|
||||
return json
|
||||
|
||||
}
|
||||
|
||||
window.onload = async function() {
|
||||
|
||||
console.log('onload')
|
||||
|
||||
var stateString = localStorage.getItem('state')
|
||||
|
||||
var urlId = idFromUrl()
|
||||
console.log('URL ID:' + urlId)
|
||||
|
||||
if (stateString) {
|
||||
|
||||
console.log('Loading local state')
|
||||
state = JSON.parse(stateString)
|
||||
|
||||
}
|
||||
|
||||
// only fetch if it's a different one
|
||||
var useUrl = (urlId && urlId != state.id)
|
||||
if (useUrl) {
|
||||
|
||||
console.log('Using remote quiz from url')
|
||||
|
||||
quiz = await fetchQuiz(urlId)
|
||||
state.title = quiz.title
|
||||
state.questions = quiz.questions
|
||||
state.id = urlId
|
||||
|
||||
changeView('prescreen')
|
||||
|
||||
}
|
||||
|
||||
updateTitles()
|
||||
|
||||
if (stateString && !useUrl)
|
||||
startQuiz()
|
||||
|
||||
if (!stateString && !useUrl)
|
||||
changeView('spreadsheet')
|
||||
|
||||
console.log('onload complete')
|
||||
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
font-family: sans-serif;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
margin: 42px auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
margin: auto;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.view {
|
||||
display: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
form {
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
textarea, input {
|
||||
margin-top: 8px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
input[type="button"] {
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
margin: 16px 0 16px 16px;
|
||||
padding: 12px 16px;
|
||||
border: none;
|
||||
background: #C50E1F;
|
||||
color: #fff;
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input[type="button"].center {
|
||||
float: none;
|
||||
}
|
||||
|
||||
input[type="button"]:disabled {
|
||||
cursor: auto;
|
||||
background: #888 !important;
|
||||
}
|
||||
|
||||
textarea, input[type="text"] {
|
||||
background: #ddd;
|
||||
margin-bottom: 16px;
|
||||
padding: .5em;
|
||||
border: 1px solid #888;
|
||||
width: 100%;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="radio"], input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
margin-top: 10px;
|
||||
float: left;
|
||||
transform: scale(1.6);
|
||||
}
|
||||
|
||||
input[type="radio"] + p, input[type="checkbox"] + p {
|
||||
margin: 0 0 0 30px;
|
||||
padding: 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
input[type="radio"]:checked + p {
|
||||
background: #aaf;
|
||||
}
|
||||
|
||||
input[type="radio"]:hover + p {
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
footer {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
#sharing a {
|
||||
font-size: 16px;
|
||||
}
|
Loading…
Reference in New Issue