feat: add contextual help panels to all backoffice pages

Add a 50/50 or 66/33 split layout to every backoffice page with Dutch
instructions explaining how to use Tijd voor de Test. Content covers the
overall workflow, quiz finalize/activate flow, and both candidate
participation methods (own device vs. shared laptop).

Help text lives in dedicated partials under templates/backoffice/help/ and
is loaded via Twig include(), keeping page templates clean. All strings use
a separate 'instructions' translation domain (instructions+intl-icu.nl.xliff)
isolated from the main messages domain.

Also updates 'Finalize'-related Dutch translations to use 'Afronden' and
adds tooltips to the finalize/unfinalize buttons.
This commit is contained in:
2026-07-04 23:10:12 +02:00
parent f6988f4d77
commit 482ca8be7e
32 changed files with 514 additions and 74 deletions
+10
View File
@@ -0,0 +1,10 @@
<h6>{{ 'Getting started'|trans({}, 'instructions') }}</h6>
<p>{{ 'Each season groups one play round with all its quizzes and candidates. The season code is the link candidates use to start a quiz — this link only works when there is an active quiz.'|trans({}, 'instructions') }}</p>
<h6>{{ 'How it works'|trans({}, 'instructions') }}</h6>
<ol>
<li><strong>{{ 'Create a season and add candidates'|trans({}, 'instructions') }}</strong></li>
<li><strong>{{ 'Add a quiz via XLSX or the question bank'|trans({}, 'instructions') }}</strong></li>
<li>{{ 'Finalize and activate the quiz'|trans({}, 'instructions') }}</li>
<li>{{ 'Let candidates take the quiz (own device or shared laptop)'|trans({}, 'instructions') }}</li>
<li>{{ 'View results and start the elimination'|trans({}, 'instructions') }}</li>
</ol>
@@ -0,0 +1,3 @@
<h6>{{ 'Prepare elimination'|trans({}, 'instructions') }}</h6>
<p>{{ 'Choose a colour for each candidate: green means safe, red means eliminated.'|trans({}, 'instructions') }}</p>
<p>{{ 'Use Save and start to play the elimination immediately, or save first and start later via the Results tab.'|trans({}, 'instructions') }}</p>
@@ -0,0 +1,8 @@
<h6>{{ 'Upload quiz via XLSX'|trans({}, 'instructions') }}</h6>
<p>{{ 'Upload an XLSX file with the questions for this quiz. After uploading you can review, check and finalize the quiz.'|trans({}, 'instructions') }}</p>
<h6>{{ 'Expected format'|trans({}, 'instructions') }}</h6>
<ul>
<li>{{ 'First column: question text'|trans({}, 'instructions') }}</li>
<li>{{ 'Next columns: answer options'|trans({}, 'instructions') }}</li>
<li>{{ 'Mark the correct answer in the spreadsheet'|trans({}, 'instructions') }}</li>
</ul>
@@ -0,0 +1,3 @@
<h6>{{ 'Create blank quiz'|trans({}, 'instructions') }}</h6>
<p>{{ 'Create an empty quiz and add questions from the question bank. Useful if you reuse questions or have prepared them in the bank in advance.'|trans({}, 'instructions') }}</p>
<p>{{ 'After creating, open the quiz, add questions via the Overview tab and finalize the quiz before activating it.'|trans({}, 'instructions') }}</p>
@@ -0,0 +1,3 @@
<h6>{{ 'Fill in answers'|trans({}, 'instructions') }}</h6>
<p>{{ 'Use this overview to manually assign answers — for example in case of technical problems or if someone took the quiz on paper.'|trans({}, 'instructions') }}</p>
<p>{{ 'Navigate between questions using the Previous and Next buttons. Tick the given answer per candidate and save.'|trans({}, 'instructions') }}</p>
@@ -0,0 +1,6 @@
<h6>{{ 'Let candidates take the quiz'|trans({}, 'instructions') }}</h6>
<p>{{ 'Candidates can take the quiz in two ways:'|trans({}, 'instructions') }}</p>
<p>{{ 'Option 1 — own device: Share the season code. Each candidate visits the site on their phone, enters their own name and starts the quiz.'|trans({}, 'instructions') }}</p>
<p>{{ 'Option 2 — shared laptop: Open the name entry page in advance on a laptop. Each candidate types their name and starts. After finishing, the next candidate can do the same.'|trans({}, 'instructions') }}</p>
<h6>{{ 'Status'|trans({}, 'instructions') }}</h6>
<p>{{ 'Deactivate a candidate if they should not take this quiz, for example after being eliminated earlier. Deactivation is per quiz and does not affect other quizzes.'|trans({}, 'instructions') }}</p>
@@ -0,0 +1,5 @@
<h6>{{ 'Overview & finalize'|trans({}, 'instructions') }}</h6>
<p>{{ 'Questions with a red marker in the list have an error. Fix these before finalizing.'|trans({}, 'instructions') }}</p>
<p><strong>{{ 'Finalize'|trans }}</strong> — {{ 'Finalize locks the quiz for editing and makes it ready for candidates. You can then activate it.'|trans({}, 'instructions') }}</p>
<p><strong>{{ 'Make active'|trans }}</strong> — {{ 'Activate makes the quiz available to candidates. Only one quiz can be active at a time — only activate the next quiz when everyone has completed the current one.'|trans({}, 'instructions') }}</p>
<p><strong>{{ 'Clear Quiz...'|trans }}</strong> — {{ 'Clear quiz removes all given answers and undoes finalization, so you can edit and run the quiz again.'|trans({}, 'instructions') }}</p>
@@ -0,0 +1,4 @@
<h6>{{ 'Add question'|trans({}, 'instructions') }}</h6>
<p>{{ 'Enter the question and add at least two answer options. Mark exactly one answer as correct.'|trans({}, 'instructions') }}</p>
<p>{{ 'Use labels to organise questions in the question bank, for example by episode or theme.'|trans({}, 'instructions') }}</p>
<p>{{ 'Mark a question as reusable if it may appear in multiple quizzes — otherwise a question can only be linked to one quiz.'|trans({}, 'instructions') }}</p>
@@ -0,0 +1,5 @@
<h6>{{ 'Results'|trans({}, 'instructions') }}</h6>
<p>{{ 'The table shows the final result per candidate sorted by score. Red rows are the candidates with the lowest score who are at risk of elimination.'|trans({}, 'instructions') }}</p>
<p><strong>{{ 'Corrections'|trans }}</strong> — {{ 'Corrections are added for bonus points or joker deductions (half points are possible).'|trans({}, 'instructions') }}</p>
<p><strong>{{ 'Penalty'|trans }}</strong> — {{ 'Penalty is a time penalty in seconds and is taken into account when scores are tied.'|trans({}, 'instructions') }}</p>
<p>{{ 'Via Prepare elimination you set the screen colours manually and start the elimination sequence.'|trans({}, 'instructions') }}</p>
@@ -0,0 +1,3 @@
<h6>{{ 'New season'|trans({}, 'instructions') }}</h6>
<p>{{ 'A season groups all quizzes and candidates for one play round. Give the season a recognisable name — the season code is generated automatically.'|trans({}, 'instructions') }}</p>
<p>{{ 'After creating, add candidates and quizzes from the season page.'|trans({}, 'instructions') }}</p>
@@ -0,0 +1,3 @@
<h6>{{ 'Add candidates'|trans({}, 'instructions') }}</h6>
<p>{{ 'Enter one name per line. These are the players participating in this season.'|trans({}, 'instructions') }}</p>
<p>{{ 'You can always add more candidates later via the Candidates tab. Use the same spelling of names you use in the game.'|trans({}, 'instructions') }}</p>
@@ -0,0 +1,3 @@
<h6>{{ 'Candidates'|trans({}, 'instructions') }}</h6>
<p>{{ 'These are the players of this season. Add all participants before starting the first quiz — candidates are automatically linked to new quizzes.'|trans({}, 'instructions') }}</p>
<p>{{ 'Names are entered freely; use the same spelling you use in the game.'|trans({}, 'instructions') }}</p>
@@ -0,0 +1,3 @@
<h6>{{ 'Question bank'|trans({}, 'instructions') }}</h6>
<p>{{ "The question bank is a library of questions that can be linked to multiple quizzes. Mark a question as reusable if it may appear in multiple quizzes (e.g. 'Who is de Mol?')."|trans({}, 'instructions') }}</p>
<p>{{ 'After editing a bank question, quizzes that already contain it are not updated automatically — use the sync button next to a quiz to push the latest version.'|trans({}, 'instructions') }}</p>
@@ -0,0 +1,3 @@
<h6>{{ 'Season settings'|trans({}, 'instructions') }}</h6>
<p>{{ 'Adjust the name and other settings for this season here.'|trans({}, 'instructions') }}</p>
<p>{{ 'The season code is fixed after creation and determines the URL candidates use to log in.'|trans({}, 'instructions') }}</p>
@@ -0,0 +1,11 @@
<h6>{{ 'Managing quizzes'|trans({}, 'instructions') }}</h6>
<p>{{ 'Add a quiz from an XLSX file or create a blank one and fill it via the question bank. Then open the quiz to review and finalize it.'|trans({}, 'instructions') }}</p>
<p>{{ 'A quiz must be finalized before it can be activated. Only one quiz can be active at a time — this is the quiz candidates can currently take.'|trans({}, 'instructions') }}</p>
<h6>{{ 'Order of operations'|trans({}, 'instructions') }}</h6>
<ol>
<li>{{ 'Create the quiz (XLSX or blank)'|trans({}, 'instructions') }}</li>
<li>{{ 'Review questions and finalize the quiz'|trans({}, 'instructions') }}</li>
<li>{{ 'Activate the quiz'|trans({}, 'instructions') }}</li>
<li>{{ 'Let candidates take the quiz'|trans({}, 'instructions') }}</li>
<li>{{ 'View results and prepare the elimination'|trans({}, 'instructions') }}</li>
</ol>
+55 -48
View File
@@ -11,53 +11,60 @@
{% endblock %}
{% block body %}
<div class="d-flex flex-row align-items-center mb-3">
<h2 class="mb-0 pe-2">
{{ is_granted('ROLE_ADMIN') ? 'All Seasons'|trans : 'Your Seasons'|trans }}
</h2>
<a class="link" href="{{ path('tvdt_backoffice_season_add') }}">
{{ 'Add'|trans }}
</a>
</div>
{% if seasons %}
<table class="table table-hover mb-3">
<thead>
<tr>
{% if is_granted('ROLE_ADMIN') %}
<th scope="col">{{ 'Owner(s)'|trans }}</th>
{% endif %}
<th scope="col">{{ 'Name'|trans }}</th>
<th scope="col">{{ 'Active Quiz'|trans }}</th>
<th scope="col">{{ 'Season Code'|trans }}</th>
<th scope="col">{{ 'Manage'|trans }}</th>
</tr>
</thead>
<tbody>
{% for season in seasons %}
<tr class="align-middle">
{% if is_granted('ROLE_ADMIN') %}
<td>{{ season.owners|map(o => o.email)|join(', ') }}</td>
{% endif %}
<td>{{ season.name }}</td>
<td>
{% if season.activeQuiz %}
{{ season.activeQuiz.name }}
{% else %}
{{ 'No active quiz'|trans }}
<div class="row">
<div class="col-md-8 col-12">
<div class="d-flex flex-row align-items-center mb-3">
<h2 class="mb-0 pe-2">
{{ is_granted('ROLE_ADMIN') ? 'All Seasons'|trans : 'Your Seasons'|trans }}
</h2>
<a class="link" href="{{ path('tvdt_backoffice_season_add') }}">
{{ 'Add'|trans }}
</a>
</div>
{% if seasons %}
<table class="table table-hover mb-3">
<thead>
<tr>
{% if is_granted('ROLE_ADMIN') %}
<th scope="col">{{ 'Owner(s)'|trans }}</th>
{% endif %}
</td>
<td>
<a {% if season.activeQuiz %}href="{{ path('tvdt_quiz_enter_name', {seasonCode: season.seasonCode}) }}"
{% else %}class="disabled" {% endif %}>{{ season.seasonCode }}</a>
</td>
<td>
<a href="{{ path('tvdt_backoffice_season', {seasonCode: season.seasonCode}) }}">{{ 'Manage'|trans }}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
{{ 'You have no seasons yet.'|trans }}
{% endif %}
<th scope="col">{{ 'Name'|trans }}</th>
<th scope="col">{{ 'Active Quiz'|trans }}</th>
<th scope="col">{{ 'Season Code'|trans }}</th>
<th scope="col">{{ 'Manage'|trans }}</th>
</tr>
</thead>
<tbody>
{% for season in seasons %}
<tr class="align-middle">
{% if is_granted('ROLE_ADMIN') %}
<td>{{ season.owners|map(o => o.email)|join(', ') }}</td>
{% endif %}
<td>{{ season.name }}</td>
<td>
{% if season.activeQuiz %}
{{ season.activeQuiz.name }}
{% else %}
{{ 'No active quiz'|trans }}
{% endif %}
</td>
<td>
<a {% if season.activeQuiz %}href="{{ path('tvdt_quiz_enter_name', {seasonCode: season.seasonCode}) }}"
{% else %}class="disabled" {% endif %}>{{ season.seasonCode }}</a>
</td>
<td>
<a href="{{ path('tvdt_backoffice_season', {seasonCode: season.seasonCode}) }}">{{ 'Manage'|trans }}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
{{ 'You have no seasons yet.'|trans }}
{% endif %}
</div>
<div class="col-md-4 col-12">
{{ include('backoffice/help/index.html.twig') }}
</div>
</div>
{% endblock %}
@@ -40,7 +40,7 @@
</form>
</div>
<div class="col-12 col-md-6">
<p class="mb-3">{{ 'Help text for preparing elimination'|trans }}</p>
{{ include('backoffice/help/prepare_elimination.html.twig') }}
</div>
</div>
{% endblock %}
@@ -51,5 +51,8 @@
{{ form_end(form) }}
</div>
<div class="col-md-6 col-12">
{{ include('backoffice/help/quiz_question_bank_form.html.twig') }}
</div>
</div>
{% endblock body %}
@@ -1,3 +1,5 @@
<div class="row">
<div class="col-xl-9 col-12">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
{% set questions = quiz.questions %}
@@ -64,3 +66,8 @@
</table>
<button type="submit" class="btn btn-primary">{{ 'Save'|trans }}</button>
</form>
</div>
<div class="col-xl-3 col-12">
{{ include('backoffice/help/quiz_answer_mapping.html.twig') }}
</div>
</div>
@@ -1,3 +1,5 @@
<div class="row">
<div class="col-md-8 col-12">
<h4 class="mb-3">{{ 'Candidates'|trans }}</h4>
<table class="table table-hover mb-3">
<thead>
@@ -50,3 +52,8 @@
{% endfor %}
</tbody>
</table>
</div>
<div class="col-md-4 col-12">
{{ include('backoffice/help/quiz_candidates.html.twig') }}
</div>
</div>
@@ -22,7 +22,8 @@
</div>
{% endmacro %}
<div data-controller="bo--quiz">
<div class="row">
<div class="col-md-8 col-12" data-controller="bo--quiz">
<h4 class="mb-3">
{{ 'Quick actions'|trans }}
{% if quiz.isFinalized %}
@@ -56,14 +57,18 @@
{% if not quiz.isFinalized %}
<form action="{{ path('tvdt_backoffice_quiz_finalize', {quiz: quiz.id}) }}" method="POST">
<input type="hidden" name="_token" value="{{ csrf_token('finalize_quiz') }}">
<button type="submit" class="btn btn-success rounded-0">
<button type="submit" class="btn btn-success rounded-0"
data-bs-toggle="tooltip"
title="{{ 'Locks the quiz so it can no longer be edited and makes it ready for candidates to take.'|trans }}">
{{ 'Finalize'|trans }}
</button>
</form>
{% elseif not quiz.hasStartedCandidates and quiz is not same as (season.activeQuiz) %}
<form action="{{ path('tvdt_backoffice_quiz_unfinalize', {quiz: quiz.id}) }}" method="POST">
<input type="hidden" name="_token" value="{{ csrf_token('unfinalize_quiz') }}">
<button type="submit" class="btn btn-outline-success rounded-0">
<button type="submit" class="btn btn-outline-success rounded-0"
data-bs-toggle="tooltip"
title="{{ 'Re-opens the quiz for editing. Candidates will no longer be able to take the quiz until it is finalized again.'|trans }}">
{{ 'Undo finalization'|trans }}
</button>
</form>
@@ -138,3 +143,7 @@
csrf_token('delete_quiz'),
) }}
</div>
<div class="col-md-4 col-12">
{{ include('backoffice/help/quiz_overview.html.twig') }}
</div>
</div>
@@ -1,3 +1,5 @@
<div class="row">
<div class="col-md-8 col-12">
<h4 class="mb-3">{{ 'Score'|trans }}</h4>
<div class="btn-toolbar mb-3" role="toolbar">
<div class="btn-group me-2">
@@ -69,3 +71,8 @@
{% endfor %}
</tbody>
</table>
</div>
<div class="col-md-4 col-12">
{{ include('backoffice/help/quiz_result.html.twig') }}
</div>
</div>
+1 -3
View File
@@ -21,9 +21,7 @@
{{ form_end(form) }}
</div>
<div class="col-md-6 col-12">
<p class="mb-3">
{{ 'Help text for adding a quiz'|trans }}
</p>
{{ include('backoffice/help/quiz_add.html.twig') }}
</div>
</div>
{% endblock %}
@@ -20,7 +20,7 @@
{{ form_end(form) }}
</div>
<div class="col-md-6 col-12">
<p class="mb-3">{{ 'Create an empty quiz and add questions from the question bank.'|trans }}</p>
{{ include('backoffice/help/quiz_add_blank.html.twig') }}
</div>
</div>
{% endblock %}
@@ -12,4 +12,7 @@
{% endfor %}
</ul>
</div>
<div class="col-md-6 col-12">
{{ include('backoffice/help/season_candidates.html.twig') }}
</div>
</div>
@@ -1,5 +1,12 @@
<div class="mb-3">
<a class="btn btn-sm btn-outline-primary" href="{{ path('tvdt_backoffice_question_bank_new', {seasonCode: season.seasonCode}) }}">{{ 'Add question'|trans }}</a>
<div class="row mb-4">
<div class="col-md-8 col-12">
<div class="mb-3">
<a class="btn btn-sm btn-outline-primary" href="{{ path('tvdt_backoffice_question_bank_new', {seasonCode: season.seasonCode}) }}">{{ 'Add question'|trans }}</a>
</div>
</div>
<div class="col-md-4 col-12">
{{ include('backoffice/help/season_question_bank.html.twig') }}
</div>
</div>
<div class="d-flex align-items-center flex-wrap gap-2 mb-3">
@@ -2,4 +2,7 @@
<div class="col-md-6 col-12">
{{ form(form) }}
</div>
<div class="col-md-6 col-12">
{{ include('backoffice/help/season_settings.html.twig') }}
</div>
</div>
@@ -20,4 +20,7 @@
{% endfor %}
</div>
</div>
<div class="col-md-6 col-12">
{{ include('backoffice/help/season_tests.html.twig') }}
</div>
</div>
+1 -3
View File
@@ -19,9 +19,7 @@
{{ form_end(form) }}
</div>
<div class="col-md-6 col-12">
<p class="mb-3">
{{ 'Help text for creating a season'|trans }}
</p>
{{ include('backoffice/help/season_add.html.twig') }}
</div>
</div>
{% endblock %}
@@ -20,9 +20,7 @@
{{ form_end(form) }}
</div>
<div class="col-md-6 col-12">
<p class="mb-3">
{{ 'Help text for adding candidates'|trans }}
</p>
{{ include('backoffice/help/season_add_candidates.html.twig') }}
</div>
</div>
{% endblock %}