172 lines
5.2 KiB
HTML
172 lines
5.2 KiB
HTML
<!-- player.html -->
|
||
{% extends 'base.html' %}
|
||
{% block content %}
|
||
<style>
|
||
.option.selected {
|
||
background-color:rgb(110, 168, 255); /* bootstrap “primary” bg */
|
||
color: black;
|
||
}
|
||
.option.disabled {
|
||
background-color:rgb(148, 148, 148); /* bootstrap “secondary” bg */
|
||
color: black;
|
||
}
|
||
.option.correct {
|
||
border: 5px solid #198754 !important; /* bootstrap “success” green */
|
||
}
|
||
</style>
|
||
<div class="container" id="player-app">
|
||
<!-- Join View -->
|
||
<div id="view-join" {% if state == 'join' %} {% else %}style="display:none;"{% endif %}>
|
||
<h1 class="mb-4">Dem Quiz Beitreten</h1>
|
||
{% if not error %}
|
||
<form id="joinForm" action="{{ url_for('do_join') }}" method="post">
|
||
<div class="input-group mb-3">
|
||
<input name="name" id="nameInput" type="text"
|
||
class="form-control" placeholder="Deinen Name eingeben..." />
|
||
<button type="submit" class="btn btn-success">Beitreten</button>
|
||
</div>
|
||
</form>
|
||
{% else %}
|
||
<!-- show backend-passed error if present -->
|
||
<div id="joinStatus" class="mt-2">
|
||
<div class="alert alert-danger" role="alert">{{ error }}</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- Wait View -->
|
||
<div id="view-wait" {% if state == 'waiting' %} {% else %}style="display:none;"{% endif %}>
|
||
<div class="alert alert-info mt-4" role="alert">
|
||
Bitte warten bis das Spiel beginnt.
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Options View -->
|
||
<div id="view-question" {% if state == 'question' %} {% else %}style="display:none;"{% endif %}>
|
||
<div id="options" class="d-grid gap-2"></div>
|
||
</div>
|
||
|
||
<!-- End View -->
|
||
<div id="view-end" style="display:none;">
|
||
<div class="alert alert-success mt-4 text-center">
|
||
<h2>Spiel beendet!</h2>
|
||
<p>Du hast <strong><span id="playerScore">0</span> Punkte</strong> erzielt und Platz <strong><span id="playerRank">–</span></strong> belegt.</p>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<script>
|
||
const socket = io();
|
||
|
||
// Restore state on load
|
||
socket.on('connect', () => {
|
||
socket.emit('rejoin_game');
|
||
});
|
||
|
||
// Sync initial or reconnect state
|
||
socket.on('sync_state', data => {
|
||
if (data.state === 'waiting') {
|
||
$('#view-join, #view-question').hide();
|
||
$('#view-wait').show();
|
||
}
|
||
else if (data.state === 'question') {
|
||
$('#view-join, #view-wait').hide();
|
||
$('#view-question').show();
|
||
renderOptions(data.options, data.answered);
|
||
}
|
||
else if (data.state === 'end') {
|
||
$('#view-join, #view-wait, #view-question').hide();
|
||
$('#view-end').show();
|
||
$('#playerScore').text(data.score);
|
||
$('#playerRank').text(data.placement);
|
||
}
|
||
});
|
||
|
||
// Host clicked “Start Game”
|
||
socket.on('game_started', () => {
|
||
$('#view-join, #view-wait').hide();
|
||
$('#view-question').show();
|
||
});
|
||
|
||
// A new question just dropped
|
||
socket.on('new_question', data => {
|
||
$('#view-join, #view-wait').hide();
|
||
$('#view-question').show();
|
||
renderOptions(data.options, false);
|
||
});
|
||
|
||
|
||
// when the right answer is revealed
|
||
socket.on('answer_revealed', data => {
|
||
$('.option').each(function() {
|
||
const $opt = $(this);
|
||
if ($opt.text() === data.correct) {
|
||
// highlight as correct with green outline
|
||
$opt
|
||
.addClass('correct')
|
||
.removeClass('btn-outline-primary disabled')
|
||
.prop('disabled', true);
|
||
} else {
|
||
// any un-clicked options become permanently disabled
|
||
if (!$opt.hasClass('selected')) {
|
||
$opt
|
||
.addClass('disabled')
|
||
.removeClass('btn-outline-primary btn-primary')
|
||
.prop('disabled', true);
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
|
||
// Handle answer clicks
|
||
$(document).on('click', '.option', function() {
|
||
const $all = $('.option');
|
||
const $me = $(this);
|
||
const answer = $me.text();
|
||
|
||
// emit once
|
||
socket.emit('submit_answer', { answer });
|
||
|
||
// style the clicked one as selected
|
||
$me
|
||
.addClass('selected')
|
||
.removeClass('btn-outline-primary')
|
||
.prop('disabled', true);
|
||
|
||
// style all the others as disabled
|
||
$all.not($me)
|
||
.addClass('disabled')
|
||
.removeClass('btn-outline-primary btn-primary')
|
||
.prop('disabled', true);
|
||
});
|
||
|
||
// When re-rendering options (e.g. on sync or new_question), clear any classes:
|
||
function renderOptions(options, alreadyAnswered) {
|
||
const $opts = $('#options').empty();
|
||
options.forEach(opt => {
|
||
const btn = $(`<button class="btn option btn-outline-primary btn-lg mb-2">${opt}</button>`);
|
||
if (alreadyAnswered) {
|
||
btn.addClass('disabled')
|
||
.removeClass('btn-outline-primary')
|
||
.prop('disabled', true);
|
||
}
|
||
$opts.append(btn);
|
||
});
|
||
}
|
||
|
||
// Handle end-of-game for players
|
||
socket.on('game_over', data => {
|
||
if (data.placement !== undefined) {
|
||
// hide everything else
|
||
$('#view-join, #view-wait, #view-question').hide();
|
||
// show final screen
|
||
$('#view-end').show();
|
||
// fill in score & rank
|
||
$('#playerScore').text(data.score);
|
||
$('#playerRank').text(data.placement);
|
||
}
|
||
})
|
||
</script>
|
||
{% endblock %} |