Add service worker for push notifications, create calendar layout, and implement WLAN QR code page

- Implemented a service worker (sw.js) to handle push notifications with dynamic options and notification click events.
- Created a calendar layout in test.html with a grid system for displaying events across days and times.
- Developed a visually engaging WLAN QR code page (wlan.html) with animated backgrounds, particle effects, and tips for connecting to the network.
This commit is contained in:
2026-02-22 00:50:22 +01:00
parent 6b96cd2012
commit 038910e9f0
26 changed files with 32980 additions and 5 deletions

418
public/ausbildung_quiz.html Normal file
View File

@@ -0,0 +1,418 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Quiz: Ausbildung</title>
<script src="/lib/pocketbase.umd.js"></script>
<script defer src="https://analytics.fsae41.de/script.js" data-website-id="257da02e-d678-47b6-b036-e3bdabaf1405"></script>
<style>
:root {
--bg: #f7fafc;
--card: #ffffff;
--accent: #2563eb;
--muted: #6b7280;
--correct: #16a34a;
--wrong: #ef4444
}
body {
font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
background: var(--bg);
padding: 18px
}
.card {
background: var(--card);
border-radius: 12px;
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.08);
padding: 18px;
max-width: 760px
}
.q-head {
display: flex;
gap: 12px;
align-items: flex-start
}
.q-id {
display: none
}
h2 {
margin: 0 0 8px 0;
font-size: 18px
}
p.meta {
margin: 0 0 14px 0;
color: var(--muted)
}
p.intro {
margin-bottom: 12px;
color: var(--muted)
}
ul.answers {
list-style: none;
padding: 0;
margin: 0;
display: grid;
gap: 10px
}
ul.answers li {
border: 1px solid #e6e9ef;
border-radius: 10px;
padding: 12px;
cursor: pointer;
transition: all .12s
}
ul.answers li.selected {
border-color: var(--accent);
background: rgba(37, 99, 235, 0.06)
}
ul.answers li:hover {
transform: translateY(-2px)
}
ul.answers li.correct {
border-color: var(--correct);
background: rgba(16, 185, 129, 0.06)
}
ul.answers li.incorrect {
border-color: var(--wrong);
background: rgba(239, 68, 68, 0.06)
}
.badge-container {
display: flex;
justify-content: flex-end;
margin-bottom: 4px
}
.badge {
font-size: 12px;
padding: 4px 8px;
border-radius: 999px;
background: #f1f5f9;
color: var(--muted)
}
.controls {
display: flex;
justify-content: space-between;
margin-top: 12px;
flex-wrap: wrap
}
.control-right {
display: flex;
gap: 8px
}
.btn {
padding: 8px 12px;
border-radius: 8px;
border: 0;
cursor: pointer
}
.btn.primary {
background: var(--accent);
color: #fff
}
.btn.ghost {
background: transparent;
border: 1px solid #e6e9ef
}
.btn.danger {
background: var(--wrong);
color: #fff
}
.hint {
margin-top: 10px;
font-size: 13px;
color: var(--muted)
}
#report-box {
display: none;
margin-top: 12px
}
#report-box textarea {
width: 100%;
min-height: 80px;
border: 1px solid #e6e9ef;
border-radius: 8px;
padding: 8px;
font-family: inherit
}
#report-box button {
margin-top: 8px
}
.dropdown-container {
margin-bottom: 12px
}
select#question-select {
padding: 6px 10px;
border-radius: 6px;
border: 1px solid #e6e9ef;
font-family: inherit
}
</style>
</head>
<body>
<div id="quiz-card" class="card" role="region" aria-label="Ausbildungsfrage">
<div class="dropdown-container">
<label for="question-select">Frage auswählen ID: </label>
<select id="question-select"></select>
</div>
<p class="intro" id="q-textIntro">Als Ausbilder setzen Sie zur Anleitung der Auszubildenden auch die
Vier-Stufen-Methode ein.</p>
<div class="q-head">
<div class="q-id" id="q-id">ID 344</div>
<div style="flex:1">
<h2 id="q-text">Welches der folgenden Merkmale trifft auf die erste Stufe zu?</h2>
<p class="meta" id="q-category">Kategorie: 3</p>
<div class="badge-container">
<p class="badge" id="q-type">1 Antwort richtig</p>
</div>
</div>
</div>
<p class="intro" id="answersIntro">Die Auszubildenden sollen…</p>
<ul id="answers" class="answers" role="list">
<!-- Antworten werden hier gerendert -->
</ul>
<div class="controls">
<button class="btn danger" id="report-error">Fehler melden</button>
<button class="btn" id="last-q">Zurück</button>
<div class="control-right">
<button class="btn" id="next-q">Weiter</button>
<button class="btn ghost" id="clear-selection">Auswahl zurücksetzen</button>
<button class="btn primary" id="show-correct">Korrekte Antwort anzeigen</button>
</div>
</div>
<div id="report-box" style="display: none;">
<textarea id="report-text" placeholder="Bitte beschreiben Sie den Fehler..."></textarea>
<button class="btn primary" id="send-report">Absenden</button>
</div>
</div>
<script>
const state = { item: null, selected: [], allQuestions: [] };
function populateDropdown() {
const select = document.getElementById('question-select');
select.innerHTML = '';
state.allQuestions.forEach(q => {
const option = document.createElement('option');
option.value = q.id;
option.textContent = `${q.id}`;
option.selected = (state.item && state.item.id === q.id);
select.appendChild(option);
});
select.addEventListener('change', () => {
const selectedId = select.value;
const question = state.allQuestions.find(q => q.id == selectedId);
if (question) window.updateQuiz(question);
});
}
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
function render(data) {
state.item = data;
state.selected = [];
document.getElementById('q-id').textContent = 'ID ' + (data.id ?? '');
document.getElementById('q-text').textContent = data.text ?? '';
document.getElementById('q-textIntro').innerHTML = data.textIntro || '&nbsp;';
if (data.textIntro) {
document.getElementById('q-textIntro').style.display = 'block';
} else {
document.getElementById('q-textIntro').style.display = 'none';
}
document.getElementById('q-category').textContent = 'Kategorie: ' + (data.category ?? '');
document.getElementById('answersIntro').innerHTML = data.answersIntro || '&nbsp;';
if (data.answersIntro) {
document.getElementById('answersIntro').style.display = 'block';
} else {
document.getElementById('answersIntro').style.display = 'none';
}
const correctCount = (data.answers || []).filter(a => a.correct).length;
document.getElementById('q-type').textContent = `${correctCount} Antwort${correctCount !== 1 ? 'en' : ''} richtig`;
const answersEl = document.getElementById('answers');
answersEl.innerHTML = '';
let answers = [...(data.answers || [])];
//answers = shuffle(answers);
answers.forEach((a, i) => {
const li = document.createElement('li');
li.setAttribute('role', 'button');
li.tabIndex = 0;
li.dataset.index = i;
li.dataset.answerId = a.id;
li.innerHTML = `<div>${a.text}</div>`;
li.addEventListener('click', () => toggleAnswer(i));
li.addEventListener('keydown', (e) => { if (e.key === "Enter" || e.key === " ") toggleAnswer(i) });
answersEl.appendChild(li);
});
const lis = document.querySelectorAll('#answers li');
lis.forEach(li => { li.classList.remove('incorrect', 'correct', 'selected'); li.style.boxShadow = 'none' });
state.item.shuffledAnswers = answers;
}
function toggleAnswer(index) {
const li = document.querySelectorAll('#answers li')[index];
if (state.selected.includes(index)) {
state.selected = state.selected.filter(i => i !== index);
li.classList.remove('selected');
} else {
state.selected.push(index);
li.classList.add('selected');
}
}
window.updateQuiz = function (data) {
if (data && data.answers) {
data.answers = data.answers.map(a => ({ id: a.id, text: a.text, correct: !!a.correct }));
}
render(data);
};
window.showCorrect = function () {
if (!state.item) return;
const lis = document.querySelectorAll('#answers li');
lis.forEach((li, i) => {
const ans = state.item.answers[i];
if (ans && ans.correct) {
li.classList.add('correct');
if (state.selected.includes(i)) li.classList.add('selected');
} else if (state.selected.includes(i)) {
li.classList.add('incorrect');
}
});
};
window.setCorrect = function ({ index = null, answerId = null } = {}) {
if (!state.item) return;
const answers = state.item.answers;
if (index == null && answerId == null) return;
answers.forEach((a, i) => a.correct = ((index != null && i === index) || (answerId != null && a.id === answerId)));
render(state.item);
};
document.getElementById('show-correct').addEventListener('click', () => window.showCorrect());
document.getElementById('clear-selection').addEventListener('click', () => {
state.selected = [];
const lis = document.querySelectorAll('#answers li');
lis.forEach(li => { li.classList.remove('incorrect', 'correct', 'selected'); li.style.boxShadow = 'none' });
});
const nextbt = document.getElementById("next-q");
const lastbt = document.getElementById("last-q");
nextbt.addEventListener("click", () => {
const select = document.getElementById('question-select');
// Nur weitergehen, wenn es noch ein nächstes Element gibt
if (select.selectedIndex < select.options.length - 1) {
select.selectedIndex++;
const selectedId = select.value;
const question = state.allQuestions.find(q => q.id == selectedId);
if (question) window.updateQuiz(question);
}
})
lastbt.addEventListener("click", () => {
const select = document.getElementById('question-select');
if (select.selectedIndex > 0) {
select.selectedIndex--;
const selectedId = select.value;
const question = state.allQuestions.find(q => q.id == selectedId);
if (question) window.updateQuiz(question);
}
})
const reportBtn = document.getElementById('report-error');
const reportBox = document.getElementById('report-box');
const sendReport = document.getElementById('send-report');
reportBtn.addEventListener('click', () => {
reportBox.style.display = reportBox.style.display === 'none' ? 'block' : 'none';
});
sendReport.addEventListener('click', async () => {
const text = document.getElementById('report-text').value.trim();
if (text) {
console.log('Fehlerbericht gesendet:', text);
alert('Vielen Dank für Ihre Rückmeldung!');
const data = {
"question": state.item.id,
"text": text
};
const record = await pb.collection('ADA_report').create(data);
document.getElementById('report-text').value = '';
reportBox.style.display = 'none';
} else {
alert('Bitte geben Sie eine Fehlerbeschreibung ein.');
}
});
// Beispiel-Initialisierung mit mehreren Fragen
state.allQuestions = [];
let pb = new PocketBase();
(async () => {
const records = await pb.collection('ADA_question').getFullList();
console.log(records);
records.forEach(r => {
const item = r;
state.allQuestions.push(item);
});
let r = Math.floor(Math.random() * state.allQuestions.length);
window.updateQuiz(state.allQuestions[r]);
populateDropdown();
})();
</script>
</body>
</html>