const defaultState = { style: 'FSCzech', id: null, title: null, questions: [], success: 0, submitTimer: 0, submitInterval: null, totalTimer: 0, totalInterval: null, } var state = defaultState function clearState() { state = JSON.parse(JSON.stringify(defaultState)) } function changeView(view) { for (el of document.querySelectorAll('.view')) el.style.display = 'none' document.getElementById(view).style.display = 'block' } function showLink() { var link = location.origin + '/?id=' + state.id var linkEl = document.getElementById('shareLink') linkEl.href = link linkEl.innerHTML = link document.querySelector('#sharing input').style.display = 'none' } function removeLink() { var linkEl = document.getElementById('shareLink') linkEl.href = '' linkEl.innerHTML = '' document.querySelector('#sharing input').style.display = 'inline-block' } 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 = location.origin + '/db' var response = await fetch(db, { method: 'POST', 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 updateTotalTimer() { var timerEls = document.querySelectorAll('.totaltimer') var minutes = Math.floor(state.totalTimer / 60) var seconds = state.totalTimer % 60 var text = minutes + ':' + ('0' + seconds).slice(-2) for (timerEl of timerEls) timerEl.innerHTML = text state.totalTimer++ localStorage.setItem('state', JSON.stringify(state)) } function startTotalTimer() { console.log('Setting totalTimer to ' + state.totalTimer + ' seconds.') updateTotalTimer() state.totalInterval = setInterval(updateTotalTimer, 1000) } function updateSubmitTimer() { var button = document.getElementById('quizSubmitButton') if (state.submitTimer > 0) { button.value = 'Wait ' + state.submitTimer + 's' button.disabled = true state.submitTimer -= 1 } else { button.value = 'Submit Answers' button.disabled = false clearInterval(state.submitInterval) } } function startSubmitTimer(time) { state.submitTimer = time || 30 console.log('Setting submitTimer to ' + state.submitTimer + ' seconds.') updateSubmitTimer() state.submitInterval = setInterval(updateSubmitTimer, 1000) } function parseLine(line) { // replace newline markers with html newlines var els = line.replace(/⎊/g, '
').split('\t') var q = {} if (els[0] != '') { if ( els[0] == 'ChooseOne' || els[0] == 'ChooseAny' || els[0] == 'Text') { q.type = els[0] } else { alert('Invalid type given on one line') return null } } else { alert('No type given on one line') return null } if (els[1]) { q.question = els[1] } else { alert('No question given on one line') return null } // Split choices into array // Choices for text questions are labels // No choices (empty string) are okay for text questions // Also filter out empty lines because some people can't use spreadsheets if (els[2] || (q.type == 'Text' && els[2] == '')) { q.choices = els[2].split('
').filter(el => el != "") } else { alert('No choices given at question "' + q.question + '"') return null } // If multiple answers are allowed, they are newline-separated // Also filter out empty lines because some people can't use spreadsheets if (els[3] && els[3] != '') { q.answers = els[3].split('
').filter(el => el != "") } else { alert('No answers given at question "' + q.question + '"') return null } // Optional parameters q.explanation = els[4] || '[No explanation provided]' q.author = els[5] || '[No author provided]' q.picture = els[6] || null return q } function parseSpreadsheet() { console.log('Parsing spreadsheet data') var textEl = document.getElementById('questions') /* MAGIC: * 1. Find quoted multiline cells ("ab") * 2. Replace all newlines inside with a special char so the cell won't be * split later ("ab") * 3. Replace escaped quotes ("a ""b"" c") with single ones ("a "b" c") * 4. Remove the outer quotes and tabs (ab) * 5. Replace all tabs inside with spaces * 6. Re-add outer tabs (ab) */ var text = textEl.value.replace(/\t"([^"\n]|"")+\n([^"]|"")*"/g, m => m.replace(/\n/g, '⎊') .replace(/""/g, '"') .replace(/^\t"(.*)"$/, '$1') .replace(/\t/g, ' ') .replace(/^(.*)$/g, '\t$1') ) state.questions = text.split('\n').map(parseLine) if (state.questions.some(el => el == null)) { alert('Erroneous data. Please check and re-enter.') return {success: false} } localStorage.setItem('state', JSON.stringify(state)) return {success: true} } function renderQuiz() { var quizForm = document.querySelector('#quiz form') quizForm.innerHTML = '' for (const [i, q] of state.questions.entries()) { var html = '' html += `

Question #${i+1}

` html += `

${q.question}

` if (q.picture) html += `` 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') ? `
` : `
` } else if (q.type == 'Text') { // If choices were given, use them as labels, otherwise use generic label for ([ai, a] of q.answers.entries()) html += `` } quizForm.innerHTML += html } } function startQuiz() { state.success = false console.log('Starting/Resuming quiz. State:') console.log(state) renderQuiz() changeView('quiz') if (state.submitTimer > 0) startSubmitTimer(state.submitTimer) else updateSubmitTimer() startTotalTimer() console.log('Quiz started/resumed') } function reStartQuiz() { console.log('Restarting quiz'); state.totalTimer = 0 state.submitTimer = 0 state.success = 0 startQuiz() } function updateTitles() { document.querySelector('#prescreen h1').innerHTML = state.title || '' document.querySelector('#quiz h1').innerHTML = state.title || '' } function createQuiz() { console.log('Creating new quiz') state.title = document.getElementById('titleField').value if (state.title == '') { alert('Please enter a title') return } clearState() removeLink() if (parseSpreadsheet().success == false) { console.log('Quiz creation failed.') return } console.log('Spreadsheet parsing successful') 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.submitInterval) clearInterval(state.submitInterval) clearInterval(state.totalInterval) 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.slice(1) if (state.questions[idx].type == 'ChooseOne' || state.questions[idx].type == 'ChooseAny') { state.questions[idx].answers.sort() value.sort() } var correctAnswer = JSON.stringify(state.questions[idx].answers) var givenAnswer = JSON.stringify(value) console.log('Comparing: ' + givenAnswer + ' == ' + correctAnswer + '?: ' + (givenAnswer == correctAnswer)) if (givenAnswer == correctAnswer) 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) { renderQuiz() startSubmitTimer() alert(text) } if (state.success) { document.querySelector('#postscreen h1').innerHTML = `Yay, we're done!
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.search.split('id=') if (s.length <= 1) return null return s[1].split('&')[0] } async function fetchQuiz(id) { console.log('Fetching quiz') var url = location.origin + '/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 } const memes = [ 'GE-SUND-BRUN-NEN-CENTER!', 'Deine Mudda! Berlin!', 'Eine Runde Kicker?', 'Hulkdrian!', 'Jetz\' bin i\' wieda doa', 'Mmmmh Carbonstaub', '#würthshausfranz', '#berlinerluft', 'FaSTTUBe, not FastCOCUE', 'Yes, we CAN', 'Ist in der Cloud.', 'Ist im Wiki.', 'Ich liiebe Teamcenter <3', 'Podio kann alles!', 'Let\'s build 3 fucking racecars!', 'Der Fahrstuhl ist kaputt', 'Frau Ipta reißt euch den Kopf ab!', 'Max, Max, Max, Max, Max, Max, MaxMax!', 'Julian, Anwärter, Firewall', 'Diese Webseite ist geerdet.', 'Wer AMS sagt muss auch BMS sagen', 'Ist der Kabelbinder in der BOM?', '*Fistbump*', 'Nividia!', 'Ihr schafft das! :)', 'Klotzen, nicht kleckern!', '*revving noises*', 'Resistance is futile', 'Jan schweißt das noch', 'Would Claude approve of this?', 'Im CAD hat\'s noch gepasst', '¯\\_(ツ)_/¯', 'AMK Brudi', 'Mmmhh cones', ] window.onload = async function() { console.log('onload') var browserWarning = document.getElementById('browserwarning') browserWarning.style.display = (() => 'none')() 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() document.getElementById('meme').innerHTML = memes[Math.floor(Math.random()*memes.length)] if (stateString && !useUrl) startQuiz() if (!stateString && !useUrl) changeView('spreadsheet') console.log('onload complete') }