vault backup: 2025-12-26 02:09:22
All checks were successful
Deploy Quartz site to GitHub Pages / build (push) Successful in 2m29s
All checks were successful
Deploy Quartz site to GitHub Pages / build (push) Successful in 2m29s
This commit is contained in:
13
stroma/templates/partials/complete.html
Normal file
13
stroma/templates/partials/complete.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<div style="text-align: center; padding: 2rem;">
|
||||
<div style="font-size: 3rem; margin-bottom: 1rem;">🎉</div>
|
||||
<h2 style="margin-bottom: 1rem;">Quiz Slutfört!</h2>
|
||||
<p style="color: var(--text-muted); margin-bottom: 2rem;">Bra jobbat! Du har besvarat alla tillgängliga frågor i
|
||||
detta urval.</p>
|
||||
<div style="display: flex; gap: 1rem; justify-content: center;">
|
||||
<a href="{% url 'quiz:stats' %}" class="btn btn-secondary">Se Statistik</a>
|
||||
<form action="{% url 'quiz:close_quiz' session.id %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-primary">Avsluta & Gå Tillbaka</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
226
stroma/templates/partials/matching_question.html
Normal file
226
stroma/templates/partials/matching_question.html
Normal file
@@ -0,0 +1,226 @@
|
||||
{% csrf_token %}
|
||||
<input type="hidden" id="current-question-id" value="{{ question.id }}">
|
||||
<input type="hidden" id="current-question-number" value="{{ current_number }}">
|
||||
|
||||
<div class="question-text">{{ question.text }}</div>
|
||||
|
||||
<style>
|
||||
.matching-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.matching-table th,
|
||||
.matching-table td {
|
||||
border: 2px solid var(--border);
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.matching-table .corner-cell {
|
||||
background: #f8f9fb;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.matching-table th.rotated-header {
|
||||
background: #f8f9fb;
|
||||
font-weight: 600;
|
||||
color: var(--text-main);
|
||||
height: 150px;
|
||||
white-space: nowrap;
|
||||
vertical-align: bottom;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.matching-table th.rotated-header>div {
|
||||
transform: rotate(-45deg);
|
||||
transform-origin: bottom left;
|
||||
width: 30px;
|
||||
position: relative;
|
||||
left: 15px;
|
||||
bottom: 10px;
|
||||
}
|
||||
|
||||
.matching-table .left-item {
|
||||
text-align: left;
|
||||
font-weight: 500;
|
||||
background: #f8f9fb;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.matching-table .radio-cell {
|
||||
width: 60px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.matching-table .radio-cell:hover:not(.diagonal-block) {
|
||||
background: #f0f4ff;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.matching-table .diagonal-block {
|
||||
background: #e5e7eb;
|
||||
color: #9ca3af;
|
||||
font-size: 1.5rem;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.matching-table input[type="radio"] {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.matching-table input[type="radio"]:checked {
|
||||
accent-color: var(--primary);
|
||||
}
|
||||
|
||||
.top-items-legend {
|
||||
background: #f0f4ff;
|
||||
border: 2px solid var(--primary);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.top-items-legend h4 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--primary);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.top-item-label {
|
||||
margin: 0.5rem 0;
|
||||
padding: 0.5rem;
|
||||
background: white;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="matching-question">
|
||||
<table class="matching-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="corner-cell"></th>
|
||||
{% for top_item in question.matching_data.top_items %}
|
||||
<th class="rotated-header">
|
||||
<div>{{ forloop.counter }}. {{ top_item|truncatewords:4 }}</div>
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for left_item in question.matching_data.left_items %}
|
||||
<tr>
|
||||
<td class="left-item">
|
||||
<strong>{{ forloop.counter }}.</strong> {{ left_item }}
|
||||
</td>
|
||||
{% for top_item in question.matching_data.top_items %}
|
||||
<td class="radio-cell" data-left="{{ forloop.parentloop.counter0 }}" data-top="{{ forloop.counter0 }}">
|
||||
{% if forloop.parentloop.counter0 != forloop.counter0 %}
|
||||
<input type="radio" name="match-{{ forloop.parentloop.counter0 }}" value="{{ forloop.counter0 }}"
|
||||
data-left="{{ forloop.parentloop.counter0 }}" data-top="{{ forloop.counter0 }}"
|
||||
id="radio-{{ forloop.parentloop.counter0 }}-{{ forloop.counter0 }}"
|
||||
style="pointer-events: none;">
|
||||
{% else %}
|
||||
<!-- Diagonal - no radio button -->
|
||||
<span class="diagonal-block">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
// Handle radio button changes
|
||||
const quizContent = document.getElementById('quiz-content');
|
||||
if (!quizContent) return;
|
||||
|
||||
// Handle clicks on table cells to select radio buttons
|
||||
quizContent.addEventListener('click', (e) => {
|
||||
const cell = e.target.closest('.radio-cell');
|
||||
if (!cell) return;
|
||||
|
||||
// Don't do anything for diagonal cells
|
||||
if (cell.querySelector('.diagonal-block')) return;
|
||||
|
||||
const leftIdx = cell.dataset.left;
|
||||
const topIdx = cell.dataset.top;
|
||||
|
||||
if (leftIdx !== undefined && topIdx !== undefined) {
|
||||
const radio = document.getElementById('radio-' + leftIdx + '-' + topIdx);
|
||||
if (radio) {
|
||||
radio.checked = true;
|
||||
saveMatchingAnswer();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
quizContent.addEventListener('change', (e) => {
|
||||
if (e.target.type === 'radio' && e.target.dataset.left) {
|
||||
saveMatchingAnswer();
|
||||
}
|
||||
});
|
||||
|
||||
function saveMatchingAnswer() {
|
||||
const questionId = document.getElementById('current-question-id').value;
|
||||
const selected = [];
|
||||
|
||||
// Collect all selected radio buttons
|
||||
const radios = document.querySelectorAll('input[type="radio"]:checked[data-left]');
|
||||
radios.forEach(radio => {
|
||||
const leftIdx = parseInt(radio.dataset.left);
|
||||
const topIdx = parseInt(radio.dataset.top);
|
||||
selected.push([leftIdx, topIdx]);
|
||||
});
|
||||
|
||||
// Save to localStorage
|
||||
const sessionId = SESSION_ID;
|
||||
const key = 'quiz_' + sessionId + '_answers';
|
||||
const answers = JSON.parse(localStorage.getItem(key) || '{}');
|
||||
answers[questionId] = JSON.stringify(selected); // Store as JSON string
|
||||
localStorage.setItem(key, JSON.stringify(answers));
|
||||
|
||||
if (typeof updateQuestionPills === 'function') {
|
||||
updateQuestionPills();
|
||||
}
|
||||
}
|
||||
|
||||
function restoreMatchingAnswers() {
|
||||
const questionId = document.getElementById('current-question-id')?.value;
|
||||
if (!questionId) return;
|
||||
|
||||
const sessionId = SESSION_ID;
|
||||
const key = 'quiz_' + sessionId + '_answers';
|
||||
const answers = JSON.parse(localStorage.getItem(key) || '{}');
|
||||
const saved = answers[questionId];
|
||||
|
||||
if (saved) {
|
||||
try {
|
||||
const selected = JSON.parse(saved);
|
||||
selected.forEach(pair => {
|
||||
const [leftIdx, topIdx] = pair;
|
||||
const radio = document.getElementById('radio-' + leftIdx + '-' + topIdx);
|
||||
if (radio) {
|
||||
radio.checked = true;
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error restoring matching answers:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore on load
|
||||
setTimeout(restoreMatchingAnswers, 50);
|
||||
})();
|
||||
</script>
|
||||
24
stroma/templates/partials/question.html
Normal file
24
stroma/templates/partials/question.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<div class="quiz-content">
|
||||
<h3 style="margin-bottom: 1.5rem;">{{ question.text }}</h3>
|
||||
<form hx-post="{% url 'quiz:submit_answer' session.id %}" hx-target="#quiz-container-{{ session.id }}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="question_id" value="{{ question.id }}">
|
||||
<div style="display: flex; flex-direction: column; gap: 0.75rem;">
|
||||
{% for option in question.options.all %}
|
||||
<div class="option"
|
||||
style="padding: 1rem; border: 1px solid var(--border); border-radius: 0.5rem; cursor: pointer; transition: all 0.2s;"
|
||||
onmouseover="this.style.backgroundColor='#f1f5f9'" onmouseout="this.style.backgroundColor='transparent'"
|
||||
onclick="this.querySelector('input').checked = true; this.closest('form').requestSubmit();">
|
||||
<input type="radio" name="answer" value="{{ option.letter }}"
|
||||
id="opt_{{ session.id }}_{{ question.id }}_{{ option.letter }}" style="display:none;">
|
||||
<label style="cursor: pointer; font-weight: 500; color: var(--text-main);"
|
||||
for="opt_{{ session.id }}_{{ question.id }}_{{ option.letter }}">
|
||||
<span style="display: inline-block; width: 1.5rem; color: var(--primary); font-weight: 700;">{{
|
||||
option.letter }}</span>
|
||||
{{ option.text }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
48
stroma/templates/partials/quiz_question.html
Normal file
48
stroma/templates/partials/quiz_question.html
Normal file
@@ -0,0 +1,48 @@
|
||||
{% if question.question_type == 'matching' %}
|
||||
{% include 'partials/matching_question.html' %}
|
||||
{% else %}
|
||||
{# Regular MCQ/SCQ template #}
|
||||
{% csrf_token %}
|
||||
<input type="hidden" id="current-question-id" value="{{ question.id }}">
|
||||
<input type="hidden" id="current-question-number" value="{{ current_number }}">
|
||||
|
||||
<div class="question-text">{{ question.text }}</div>
|
||||
|
||||
<div class="options-container">
|
||||
{% for option in question.options.all %}
|
||||
<div class="option-item" id="option-{{ option.letter }}" data-letter="{{ option.letter }}" style="cursor: pointer;">
|
||||
<input type="checkbox" id="checkbox-{{ option.letter }}" value="{{ option.letter }}"
|
||||
data-letter="{{ option.letter }}"
|
||||
style="margin-right: 0.5rem; width: 1.2rem; height: 1.2rem; cursor: pointer; pointer-events: auto;">
|
||||
<span class="option-letter" style="pointer-events: none;">{{ option.letter }}</span>
|
||||
<span style="pointer-events: none;">{{ option.text }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const container = document.querySelector('.options-container');
|
||||
|
||||
// Handle option item clicks (clicking anywhere on the div except checkbox)
|
||||
container?.addEventListener('click', (e) => {
|
||||
// If click was directly on checkbox, let it handle itself
|
||||
if (e.target.type === 'checkbox') return;
|
||||
|
||||
const optionItem = e.target.closest('.option-item');
|
||||
if (!optionItem) return;
|
||||
|
||||
const letter = optionItem.dataset.letter;
|
||||
if (letter) toggleOption(letter);
|
||||
});
|
||||
|
||||
// Handle checkbox changes (direct checkbox clicks or programmatic changes)
|
||||
container?.addEventListener('change', (e) => {
|
||||
if (e.target.type === 'checkbox') {
|
||||
const { letter } = e.target.dataset;
|
||||
if (letter) updateSelection(letter);
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
Reference in New Issue
Block a user