refactor: changed design to be 50ohm compatible

This commit is contained in:
Matthias Jung
2026-08-04 22:42:31 +02:00
parent 2b7d2bab21
commit e54e711cef
7 changed files with 363 additions and 536 deletions
+65 -387
View File
@@ -2,267 +2,62 @@
display: none;
}
.exam-simulator {
--exam-blue: #47abe8;
--exam-coral: #fe756c;
--exam-green: #3bb583;
--exam-ink: #20252a;
color: var(--exam-ink);
.exam-start,
.exam-summary {
max-width: 900px;
}
.exam-loading {
min-height: 45vh;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
font-size: 1.1rem;
.exam-simulator-page .fiftyohm-layout-outer-head {
width: 100%;
position: fixed;
top: 0;
z-index: 1030;
}
.exam-kicker {
margin: 0 0 0.25rem;
color: #0068a5;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.exam-start {
max-width: 1050px;
margin: 1.5rem auto 4rem;
}
.exam-choice-groups {
display: grid;
gap: 2.5rem;
margin-top: 3rem;
}
.exam-choice-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1rem;
}
.exam-choice {
min-height: 155px;
padding: 1.4rem;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
color: #fff;
text-align: left;
background: var(--exam-choice-color);
border: 3px solid var(--exam-choice-color);
border-radius: 0.65rem;
box-shadow: 0 0.35rem 1rem rgb(0 0 0 / 10%);
transition: transform 120ms ease, box-shadow 120ms ease;
}
.exam-choice:hover,
.exam-choice:focus-visible {
color: #fff;
transform: translateY(-3px);
box-shadow: 0 0.55rem 1.4rem rgb(0 0 0 / 18%);
}
.exam-choice:focus-visible {
outline: 3px solid #20252a;
outline-offset: 3px;
}
.exam-choice strong {
font-size: 2rem;
line-height: 1;
}
.exam-choice span {
margin-top: 0.55rem;
font-size: 1.05rem;
font-weight: 700;
}
.exam-choice small {
margin-top: 0.3rem;
opacity: 0.9;
}
.exam-start-note {
margin-top: 2.5rem;
padding: 1.2rem 1.4rem;
background: #eef8fd;
border-left: 5px solid #00adef;
}
.exam-session {
width: min(1560px, calc(100vw - 2rem));
position: relative;
left: 50%;
transform: translateX(-50%);
.exam-simulator-page .fiftyohm-layout-outer-content {
margin-top: calc(50px + var(--exam-header-height, 100px));
}
.exam-toolbar {
margin: 1rem 0 1.25rem;
padding: 1rem 1.25rem;
position: sticky;
top: var(--exam-header-height, 100px);
z-index: 1020;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1.5rem;
background: #fff;
border: 1px solid #c9d1d7;
border-top: 6px solid #00adef;
box-shadow: 0 0.25rem 0.8rem rgb(0 0 0 / 7%);
}
.exam-toolbar h1 {
margin: 0;
font-size: clamp(1.35rem, 2vw, 2rem);
}
.exam-progress-label {
margin-top: 0.25rem;
color: #606a73;
}
.exam-toolbar-actions {
display: flex;
align-items: center;
gap: 1rem;
}
.exam-timer {
min-width: 135px;
padding: 0.55rem 0.8rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.35rem;
color: #fff;
background: #20252a;
border-radius: 0.35rem;
font-size: 1.35rem;
font-variant-numeric: tabular-nums;
font-weight: 700;
}
.exam-timer-warning {
background: #b42d25;
animation: exam-timer-pulse 1.5s ease-in-out infinite;
}
.exam-timer-finished {
background: #5c636a;
font-size: 1rem;
}
@keyframes exam-timer-pulse {
50% { box-shadow: 0 0 0 5px rgb(254 117 108 / 30%); }
}
.exam-part-result,
.exam-overall {
margin: 0 0 1.25rem;
padding: 1rem 1.25rem;
display: flex;
align-items: center;
gap: 0.8rem;
border: 2px solid;
border-radius: 0.4rem;
}
.exam-part-result > .material-icons,
.exam-overall > .material-icons {
font-size: 2rem;
}
.result-passed {
background: #e2f5ed;
border-color: #3bb583;
}
.result-oral {
background: #fff4d5;
border-color: #e6a700;
}
.result-failed,
.result-incomplete {
background: #ffe5e3;
border-color: #fe756c;
}
.exam-paper-layout {
display: grid;
grid-template-columns: 360px minmax(0, 1fr);
grid-template-columns: minmax(0, 1fr) 300px;
align-items: start;
gap: 1.5rem;
gap: 4ex;
}
.exam-answer-sheet {
position: sticky;
top: 0.75rem;
}
.answer-sheet-paper {
padding: 1rem;
background: #fff;
border: 2px solid #20252a;
box-shadow: 0 0.35rem 1rem rgb(0 0 0 / 10%);
}
.answer-sheet-paper h2 {
margin: 0 0 0.8rem;
text-align: center;
font-size: 1.45rem;
}
.answer-sheet-part {
padding: 0.6rem 0;
display: grid;
border-top: 2px solid #20252a;
border-bottom: 2px solid #20252a;
}
.answer-sheet-part span,
.answer-sheet-part small {
font-size: 0.78rem;
}
.answer-sheet-part strong {
line-height: 1.2;
}
.answer-sheet-hint {
margin: 0.6rem 0;
font-size: 0.76rem;
line-height: 1.25;
text-align: left;
top: calc(var(--exam-header-height, 100px) + 85px);
}
.answer-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
font-size: 0.82rem;
border-collapse: collapse;
font-size: 0.78rem;
}
.answer-table th,
.answer-table td {
height: 27px;
height: 22px;
padding: 0;
border: 1px solid #20252a;
border: 1px solid #212529;
text-align: center;
}
.answer-table thead th,
.answer-table tbody th {
font-weight: 800;
}
.answer-table tr > :first-child {
width: 18%;
border-right-width: 3px;
width: 20%;
border-right-width: 2px;
}
.answer-number,
@@ -270,220 +65,103 @@
width: 100%;
height: 100%;
padding: 0;
color: #20252a;
color: #212529;
background: transparent;
border: 0;
}
.answer-number:hover,
.answer-number:focus-visible,
.answer-mark:not(:disabled):hover,
.answer-mark:not(:disabled):focus-visible {
.answer-mark:not(:disabled):hover {
background: #d2f3ff;
outline: 2px solid #0068a5;
outline-offset: -2px;
}
.answer-mark:disabled {
color: #20252a;
color: #212529;
opacity: 1;
}
.answer-mark-symbol {
display: inline-block;
min-width: 1rem;
font-size: 1.05rem;
font-weight: 900;
font-size: 1rem;
font-weight: 700;
line-height: 1;
}
.answer-mark-restored {
padding-bottom: 1px;
border-bottom: 3px double currentColor;
}
.answer-cell-correct {
background: #bfe8d6;
.answer-table td.answer-cell-correct,
.answer-table td.answer-cell-solution,
.answer-table td.answer-cell-correct .answer-mark,
.answer-table td.answer-cell-solution .answer-mark {
background-color: #3bb583;
}
.answer-cell-wrong {
background: #ffc9c5;
.answer-table td.answer-cell-wrong,
.answer-table td.answer-cell-wrong .answer-mark {
background-color: #fe756c;
}
.answer-cell-solution {
box-shadow: inset 0 0 0 2px #168a5d;
.exam-question {
scroll-margin-top: calc(var(--exam-header-height, 100px) + 85px);
}
.answer-sheet-count {
margin-top: 0.6rem;
text-align: right;
font-size: 0.8rem;
.exam-question:first-child {
margin-top: 0;
}
.exam-questions {
min-width: 0;
}
.exam-question.question {
scroll-margin-top: 1rem;
margin: 0 0 1.25rem;
padding: 1rem;
border-left: 5px solid #7c8790;
}
.exam-question .answers button {
display: block;
min-height: 48px;
border: 2px solid transparent;
.exam-questions.course .question,
.exam-questions.course .solution,
.exam-questions.course .katex-display {
width: 100%;
}
.exam-question .answers button::before {
content: none;
margin-right: 0.25em;
}
.exam-toolbar .btn-sm {
min-height: 34px;
}
.exam-toolbar .btn-secondary {
--bs-btn-color: #212529;
--bs-btn-hover-color: #212529;
--bs-btn-active-color: #212529;
--bs-btn-disabled-color: #212529;
}
.exam-question .answers button:not(:disabled):hover,
.exam-question .answers button:not(:disabled):focus-visible,
.exam-question .answers button.exam-answer-selected {
border-color: #0068a5;
background: #d2f3ff;
}
.exam-question .answers button:disabled {
color: #212529;
cursor: default;
opacity: 1;
color: #20252a;
}
.exam-question .answers button.correct {
background: #3bb583;
border-color: #168a5d;
}
.exam-question .answers button.wrong {
background: #fe756c;
border-color: #c5423a;
}
.exam-answer-label {
margin-right: 0.35rem;
}
.exam-solution-icon {
margin-right: 0.3rem;
font-size: 1.15rem;
}
.exam-submit-panel {
margin: 2rem 0 4rem;
padding: 1.5rem;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1rem;
background: #eef2f5;
border: 1px solid #c9d1d7;
}
.exam-submit-panel p {
flex-basis: 100%;
margin: 0;
}
.exam-summary {
max-width: 920px;
margin: 2rem auto 4rem;
}
.exam-result-list {
margin: 2rem 0;
display: grid;
gap: 0.75rem;
}
.exam-result-card {
padding: 0.9rem 1rem;
display: grid;
grid-template-columns: minmax(220px, 1fr) auto minmax(150px, auto) auto;
align-items: center;
gap: 1rem;
border: 1px solid #c9d1d7;
border-left: 5px solid #00adef;
}
.exam-result-card > div {
display: flex;
align-items: center;
gap: 0.75rem;
}
.exam-result-code {
width: 2.2rem;
height: 2.2rem;
display: inline-grid;
place-items: center;
color: #fff;
background: #20252a;
border-radius: 50%;
font-weight: 800;
}
.exam-score {
font-variant-numeric: tabular-nums;
}
.exam-status {
padding: 0.2rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.82rem;
font-weight: 700;
}
@media (max-width: 1050px) {
.exam-paper-layout {
grid-template-columns: 310px minmax(0, 1fr);
gap: 1rem;
}
}
@media (max-width: 800px) {
.exam-choice-grid {
grid-template-columns: 1fr;
}
.exam-choice {
min-height: 120px;
}
.exam-toolbar,
.exam-toolbar-actions {
align-items: stretch;
@media (max-width: 940px) {
.exam-toolbar {
top: var(--exam-header-height, 64px);
align-items: flex-start;
flex-direction: column;
}
.exam-timer {
align-self: flex-start;
}
.exam-paper-layout {
display: flex;
flex-direction: column;
}
.exam-answer-sheet {
width: 100%;
width: min(100%, 400px);
position: static;
order: -1;
align-self: center;
}
.answer-sheet-paper {
max-width: 430px;
margin: 0 auto;
}
.exam-result-card {
grid-template-columns: 1fr auto;
}
.exam-result-card .exam-status {
grid-column: 1;
.exam-question {
scroll-margin-top: calc(var(--exam-header-height, 64px) + 130px);
}
}
+65 -17
View File
@@ -2,7 +2,7 @@
"use strict";
var STORAGE_KEY = "50ohm-exam-simulator-v1";
var STORAGE_VERSION = 1;
var STORAGE_VERSION = 3;
var QUESTION_COUNT = 25;
var ANSWER_LABELS = ["A", "B", "C", "D"];
@@ -104,9 +104,8 @@
function renderMathSoon(VueApi) {
VueApi.nextTick(function () {
if (typeof window.renderFiftyOhmMath === "function") {
window.renderFiftyOhmMath(document.getElementById("exam-simulator"));
}
if (typeof window.renderFiftyOhmMath !== "function") return;
window.renderFiftyOhmMath(document.getElementById("exam-simulator"));
});
}
@@ -126,6 +125,8 @@
var reviewPartIndex = ref(null);
var now = ref(Date.now());
var timerId = null;
var catalogLoadHandler = null;
var headerObserver = null;
var primaryChoices = computed(function () {
return EXAM_CHOICES.filter(function (choice) { return choice.group === "primary"; });
@@ -178,7 +179,9 @@
if (failed.length === 1 && failed[0].score >= 17) return "oral";
return "failed";
});
var overallClass = computed(function () { return "result-" + overallState.value; });
var overallAlertClass = computed(function () {
return { passed: "alert-success", oral: "alert-warning", failed: "alert-danger", incomplete: "alert-danger" }[overallState.value];
});
var overallIcon = computed(function () {
return { passed: "verified", oral: "record_voice_over", failed: "cancel", incomplete: "stop_circle" }[overallState.value];
});
@@ -249,8 +252,12 @@
if (currentPart.value.status !== "running") return;
var question = currentPart.value.questions[questionIndex];
if (question.selected === answerIndex) return;
var firstAnswer = question.selected === null;
question.selected = answerIndex;
question.history.push(answerIndex);
if (firstAnswer && questionIndex < currentPart.value.questions.length - 1) {
window.setTimeout(function () { scrollToQuestion(questionIndex + 1); }, 150);
}
}
function answerMark(questionIndex, answerIndex) {
@@ -308,10 +315,16 @@
}
function startNextPart() {
if (!hasNextPart.value) return;
session.value.currentPart += 1;
activatePart(session.value.parts[session.value.currentPart]);
var nextIndex = session.value.currentPart + 1;
var nextPart = session.value.parts[nextIndex];
if (!nextPart) {
showSummary();
return;
}
session.value.currentPart = nextIndex;
reviewPartIndex.value = null;
view.value = "exam";
activatePart(nextPart);
renderMathSoon(Vue);
window.scrollTo({ top: 0, behavior: "smooth" });
}
@@ -339,7 +352,7 @@
}
function abortExam() {
if (!window.confirm("Möchtest du diese Probeprüfung abbrechen? Der gespeicherte Prüfungsstand wird gelöscht.")) return;
if (!window.confirm("Möchtest du diese Simulation abbrechen? Der gespeicherte Prüfungsstand wird gelöscht.")) return;
clearStoredSession();
session.value = null;
view.value = "exam";
@@ -366,10 +379,16 @@
return "Nicht bestanden";
}
function resultClass(part) {
if (part.score >= 19) return "result-passed";
if (part.score >= 17) return "result-oral";
return "result-failed";
function resultAlertClass(part) {
if (part.score >= 19) return "alert-success";
if (part.score >= 17) return "alert-warning";
return "alert-danger";
}
function resultBadgeClass(part) {
if (part.score >= 19) return "text-bg-success";
if (part.score >= 17) return "text-bg-warning";
return "text-bg-danger";
}
function resultIcon(part) {
@@ -390,7 +409,10 @@
var raw = window.localStorage.getItem(STORAGE_KEY);
if (!raw) return;
var stored = JSON.parse(raw);
if (!stored || stored.version !== STORAGE_VERSION || !stored.session) return;
if (!stored || stored.version !== STORAGE_VERSION || !stored.session) {
clearStoredSession();
return;
}
var choice = EXAM_CHOICES.find(function (item) { return item.id === stored.session.choiceId; });
var valid = choice && stored.session.parts.every(function (part) {
return Array.isArray(part.questions) && part.questions.length === QUESTION_COUNT && part.questions.every(function (question) {
@@ -427,7 +449,7 @@
if (session.value && currentPart.value && currentPart.value.status === "running" && seconds <= 0) submitPart(true);
});
onMounted(function () {
function loadCatalog() {
fetch("assets/exam-questions.json")
.then(function (response) {
if (!response.ok) throw new Error("Fragenkatalog nicht gefunden (HTTP " + response.status + ").");
@@ -444,11 +466,36 @@
loadError.value = error.message || "Unbekannter Ladefehler";
loading.value = false;
});
}
function updateHeaderHeight() {
var header = document.querySelector(".fiftyohm-layout-outer-head");
if (!header) return;
document.documentElement.style.setProperty("--exam-header-height", header.offsetHeight + "px");
}
onMounted(function () {
document.body.classList.add("exam-simulator-page");
updateHeaderHeight();
if (typeof window.ResizeObserver === "function") {
headerObserver = new window.ResizeObserver(updateHeaderHeight);
headerObserver.observe(document.querySelector(".fiftyohm-layout-outer-head"));
}
if (document.readyState === "loading") {
catalogLoadHandler = function () { window.setTimeout(loadCatalog, 0); };
document.addEventListener("DOMContentLoaded", catalogLoadHandler, { once: true });
} else {
loadCatalog();
}
timerId = window.setInterval(function () { now.value = Date.now(); }, 1000);
});
onBeforeUnmount(function () {
if (timerId !== null) window.clearInterval(timerId);
if (catalogLoadHandler !== null) document.removeEventListener("DOMContentLoaded", catalogLoadHandler);
if (headerObserver !== null) headerObserver.disconnect();
document.documentElement.style.removeProperty("--exam-header-height");
document.body.classList.remove("exam-simulator-page");
});
return {
@@ -469,7 +516,7 @@
answeredCount: answeredCount,
hasNextPart: hasNextPart,
isReviewing: isReviewing,
overallClass: overallClass,
overallAlertClass: overallAlertClass,
overallIcon: overallIcon,
overallTitle: overallTitle,
overallText: overallText,
@@ -490,7 +537,8 @@
newExam: newExam,
scrollToQuestion: scrollToQuestion,
resultLabel: resultLabel,
resultClass: resultClass,
resultAlertClass: resultAlertClass,
resultBadgeClass: resultBadgeClass,
resultIcon: resultIcon,
resultDescription: resultDescription
};
+1 -1
View File
@@ -5,7 +5,7 @@ function shuffleChildren(el) {
}
document.addEventListener("DOMContentLoaded", function() {
var els = document.querySelectorAll(".answers:not([data-no-shuffle])");
var els = document.querySelectorAll(".answers");
for (var i = 0; i < els.length; i++) {
shuffleChildren(els[i]);
}
+6
View File
@@ -190,6 +190,12 @@ ul.fiftyohm-navbar {
display: flex;
}
@media (min-width: 940px) {
ul.fiftyohm-navbar {
gap: 1rem;
}
}
.fiftyohm-navbar li {
list-style-type: none;
margin: 2ex 0;
+96 -2
View File
@@ -131,9 +131,102 @@ class Build:
def __build_question_slide(self, input):
return self.__build_question(input, template_file="slide/question.html")
def __build_page(self, content, course_wrapper=False, sidebar=None):
def __build_page(
self,
content,
course_wrapper=False,
sidebar=None,
page_stylesheets=None,
page_scripts=None,
):
page_template = self.env.get_template("html/page.html")
return page_template.render(content=content, course_wrapper=course_wrapper, sidebar=sidebar)
return page_template.render(
content=content,
course_wrapper=course_wrapper,
sidebar=sidebar,
page_stylesheets=page_stylesheets or [],
page_scripts=page_scripts or [],
)
@staticmethod
def __walk_exam_questions(section, category=()):
for question in section.get("questions", []):
yield category, question
for index, child in enumerate(section.get("sections", [])):
yield from Build.__walk_exam_questions(child, (*category, index))
def __build_exam_catalog(self):
"""Create the compact client-side catalog used by the exam simulator."""
with (
self.config.p_data_fragenkatalog.open(encoding="utf-8") as catalog_file,
self.config.p_data_metadata.open(encoding="utf-8") as metadata_file,
):
catalog = json.load(catalog_file)
metadata = json.load(metadata_file)
part_for_section = {1: "B", 2: "V"}
part_for_class = {"1": "N", "2": "E", "3": "A"}
questions = {}
def picture_data(picture_id):
if not picture_id:
return {"picture": "", "pictureAlt": ""}
return {
"picture": picture_id,
"pictureAlt": self.__picture_handler(picture_id) or "Bildbeschreibung noch nicht verfügbar",
}
for section_index, section in enumerate(catalog["sections"]):
for category, question in self.__walk_exam_questions(section):
if section_index == 0:
part = part_for_class.get(str(question.get("class")))
else:
part = part_for_section.get(section_index)
if part is None:
continue
question_metadata = metadata.get(question["number"], {})
question_picture = picture_data(question_metadata.get("picture_question", ""))
answers = []
for answer_letter in "abcd":
answer_picture = picture_data(question_metadata.get(f"picture_{answer_letter}", ""))
answers.append(
{
"text": question.get(f"answer_{answer_letter}", ""),
**answer_picture,
}
)
questions[question["number"]] = {
"id": question["number"],
"part": part,
"category": list(category),
"question": question["question"],
"layout": question_metadata.get("layout", "") or "default",
"answers": answers,
"hasSolution": self.__has_solution(question["number"]),
**question_picture,
}
self.config.p_build_assets.mkdir(parents=True, exist_ok=True)
with (self.config.p_build_assets / "exam-questions.json").open("w", encoding="utf-8") as file:
json.dump({"version": 1, "questions": questions}, file, ensure_ascii=False, separators=(",", ":"))
file.write("\n")
def __build_exam_simulator(self):
self.__build_exam_catalog()
exam_template = self.env.get_template("html/exam-simulator.html")
content = exam_template.render()
page = self.__build_page(
content,
page_stylesheets=["assets/exam-simulator.css"],
page_scripts=["assets/vue.global.prod.js", "assets/exam-simulator.js"],
)
legacy_path = self.config.p_build / "probeprüfung.html"
if legacy_path.exists():
legacy_path.unlink()
with (self.config.p_build / "simulation.html").open("w", encoding="utf-8") as file:
file.write(page)
def __copy_picture(self, id):
file = f"{id}.svg"
@@ -509,6 +602,7 @@ class Build:
self.__build_html_page(contents, "pruefung")
self.__build_html_page(contents, "infos")
self.__build_html_page(contents, "suche")
self.__build_exam_simulator()
def build_solutions(self):
for solution_file in self.config.p_data_solutions.glob("*.md"):
+127 -126
View File
@@ -1,118 +1,170 @@
<div id="exam-simulator" class="exam-simulator" v-cloak>
<div v-if="loading" class="exam-loading" role="status">
<div v-if="loading" class="d-flex align-items-center justify-content-center gap-3 py-5" role="status">
<div class="spinner-border text-primary" aria-hidden="true"></div>
<span>Prüfungsfragen werden geladen …</span>
</div>
<div v-else-if="loadError" class="alert alert-danger" role="alert">
<h1 class="h4">Der Prüfungssimulator konnte nicht geladen werden</h1>
<h1 class="h4">Die Prüfungssimulation konnte nicht geladen werden</h1>
<p class="mb-0" v-text="loadError"></p>
</div>
<section v-else-if="!session" class="exam-start" aria-labelledby="exam-heading">
<p class="exam-kicker">Probeprüfung</p>
<h1 id="exam-heading">Prüfungssimulator für Funkamateure</h1>
<section v-else-if="!session" class="mx-auto mb-5 exam-start" aria-labelledby="exam-heading">
<h1 id="exam-heading">Prüfungssimulation</h1>
<p class="lead">Wähle die Prüfung, die du unter möglichst realistischen Bedingungen üben möchtest.</p>
<div class="exam-choice-groups">
<div>
<h2 class="h4">Erstprüfung</h2>
<div class="exam-choice-grid exam-choice-grid-primary">
<button v-for="choice in primaryChoices" :key="choice.id" type="button" class="exam-choice"
:style="{ '--exam-choice-color': choice.color }" @click="startExam(choice)">
<strong v-text="choice.shortLabel"></strong>
<span v-text="choice.label"></span>
<small v-text="partSummary(choice.parts)"></small>
</button>
</div>
</div>
<div>
<h2 class="h4">Aufstockungsprüfung</h2>
<div class="exam-choice-grid">
<button v-for="choice in upgradeChoices" :key="choice.id" type="button" class="exam-choice"
:style="{ '--exam-choice-color': choice.color }" @click="startExam(choice)">
<strong v-text="choice.shortLabel"></strong>
<span v-text="choice.label"></span>
<small v-text="partSummary(choice.parts)"></small>
</button>
</div>
<h2 class="h4 mt-5">Erstprüfung</h2>
<div class="row row-cols-1 row-cols-md-3 g-2">
<div v-for="choice in primaryChoices" :key="choice.id" class="col">
<button type="button" class="btn w-100 h-100 py-3 text-white"
:style="{ backgroundColor: choice.color, borderColor: choice.color }" @click="startExam(choice)">
<span v-text="choice.label + ' (' + partSummary(choice.parts) + ')' "></span>
</button>
</div>
</div>
<div class="exam-start-note">
<strong>So läuft die Simulation:</strong> Jeder Prüfungsteil enthält 25 Fragen. Für B, V, N und E
stehen jeweils 45 Minuten zur Verfügung, für Technik A 60 Minuten. Ein Teil ist ab 19 Punkten bestanden.
<h2 class="h4 mt-4">Aufstockungsprüfung</h2>
<div class="row row-cols-1 row-cols-md-3 g-2">
<div v-for="choice in upgradeChoices" :key="choice.id" class="col">
<button type="button" class="btn w-100 h-100 py-3 text-white"
:style="{ backgroundColor: choice.color, borderColor: choice.color }" @click="startExam(choice)">
<span v-text="choice.label + ' (' + partSummary(choice.parts) + ')' "></span>
</button>
</div>
</div>
<div class="alert alert-primary mt-4 mb-0">
Jeder Prüfungsteil enthält 25 Fragen. Für B, V, N und E stehen jeweils 45 Minuten zur Verfügung,
für Technik A 60 Minuten. Ein Teil ist ab 19 Punkten bestanden.
</div>
</section>
<section v-else-if="view === 'summary'" class="exam-summary" aria-labelledby="summary-heading">
<p class="exam-kicker">Gesamtergebnis</p>
<h1 id="summary-heading" v-text="currentChoice.label"></h1>
<div class="exam-overall" :class="overallClass">
<section v-else-if="view === 'summary'" class="mx-auto mb-5 exam-summary" aria-labelledby="summary-heading">
<h1 id="summary-heading">Gesamtergebnis</h1>
<p class="lead" v-text="currentChoice.label"></p>
<div class="alert d-flex align-items-center gap-2" :class="overallAlertClass">
<span class="material-icons" aria-hidden="true" v-text="overallIcon"></span>
<div>
<strong v-text="overallTitle"></strong>
<p class="mb-0" v-text="overallText"></p>
<div v-text="overallText"></div>
</div>
</div>
<div class="exam-result-list">
<article v-for="(part, index) in session.parts" :key="part.code" class="exam-result-card">
<div>
<span class="exam-result-code" v-text="part.code"></span>
<strong v-text="partDefinitions[part.code].label"></strong>
</div>
<template v-if="part.status === 'evaluated'">
<span class="exam-score"><b v-text="part.score"></b>/25</span>
<span class="exam-status" :class="resultClass(part)" v-text="resultLabel(part)"></span>
<button type="button" class="btn btn-outline-dark btn-sm" @click="reviewPart(index)">Auswertung ansehen</button>
</template>
<span v-else class="text-secondary">Nicht bearbeitet</span>
</article>
<div class="table-responsive">
<table class="table align-middle">
<thead><tr><th>Teil</th><th>Ergebnis</th><th>Status</th><th></th></tr></thead>
<tbody>
<tr v-for="(part, index) in session.parts" :key="part.code">
<td><b v-text="part.code + ' – ' + partDefinitions[part.code].label"></b></td>
<template v-if="part.status === 'evaluated'">
<td><b v-text="part.score"></b>/25</td>
<td><span class="badge" :class="resultBadgeClass(part)" v-text="resultLabel(part)"></span></td>
<td><button type="button" class="btn btn-outline-dark btn-sm" @click="reviewPart(index)">Auswertung</button></td>
</template>
<template v-else>
<td colspan="3" class="text-secondary">Nicht bearbeitet</td>
</template>
</tr>
</tbody>
</table>
</div>
<button type="button" class="btn btn-primary" @click="newExam">Neue Probeprüfung</button>
<button type="button" class="btn btn-primary" @click="newExam">Neue Simulation</button>
</section>
<section v-else class="exam-session" aria-labelledby="part-heading">
<header class="exam-toolbar">
<header class="exam-toolbar bg-white border-bottom py-2 mb-3">
<div>
<p class="exam-kicker" v-text="currentChoice.label"></p>
<h1 id="part-heading" v-text="currentPartDefinition.label"></h1>
<div class="exam-progress-label">
Prüfungsteil <span v-text="displayedPartIndex + 1"></span> von <span v-text="session.parts.length"></span>
</div>
<small class="text-secondary" v-text="currentChoice.label + ' · Teil ' + (displayedPartIndex + 1) + ' von ' + session.parts.length"></small>
<h1 id="part-heading" class="h4 mb-0" v-text="currentPartDefinition.label"></h1>
</div>
<div class="exam-toolbar-actions">
<div v-if="currentPart.status === 'running'" class="exam-timer" :class="{ 'exam-timer-warning': remainingSeconds <= 300 }"
role="timer" aria-live="off">
<span class="material-icons" aria-hidden="true">timer</span>
<div class="d-flex align-items-center flex-wrap gap-2">
<a class="btn btn-secondary btn-sm d-inline-flex align-items-center" target="_blank" href="/hm"
title="Hilfsmittel herunterladen" aria-label="Hilfsmittel herunterladen">
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 640 640" fill="currentColor" aria-hidden="true">
<path d="M192 112L304 112L304 200C304 239.8 336.2 272 376 272L464 272L464 512C464 520.8 456.8 528 448 528L192 528C183.2 528 176 520.8 176 512L176 128C176 119.2 183.2 112 192 112zM352 131.9L444.1 224L376 224C362.7 224 352 213.3 352 200L352 131.9zM192 64C156.7 64 128 92.7 128 128L128 512C128 547.3 156.7 576 192 576L448 576C483.3 576 512 547.3 512 512L512 250.5C512 233.5 505.3 217.2 493.3 205.2L370.7 82.7C358.7 70.7 342.5 64 325.5 64L192 64zM248 320C234.7 320 224 330.7 224 344C224 357.3 234.7 368 248 368L392 368C405.3 368 416 357.3 416 344C416 330.7 405.3 320 392 320L248 320zM248 416C234.7 416 224 426.7 224 440C224 453.3 234.7 464 248 464L392 464C405.3 464 416 453.3 416 440C416 426.7 405.3 416 392 416L248 416z"/>
</svg>
</a>
<span v-if="currentPart.status === 'running'" class="btn btn-sm d-inline-flex align-items-center"
:class="remainingSeconds <= 300 ? 'btn-danger' : 'btn-secondary'" role="timer">
<span v-text="formattedTime"></span>
</div>
<div v-else class="exam-timer exam-timer-finished">
<span class="material-icons" aria-hidden="true">task_alt</span>
abgegeben
</div>
<button type="button" class="btn btn-outline-secondary btn-sm" @click="abortExam">Prüfung abbrechen</button>
</span>
<span v-else class="btn btn-secondary btn-sm d-inline-flex align-items-center">Abgegeben</span>
<button v-if="currentPart.status === 'evaluated' && !isReviewing && hasNextPart"
type="button" class="btn btn-primary btn-sm" @click="startNextPart">Nächsten Teil starten</button>
<button v-else-if="currentPart.status === 'evaluated' && !isReviewing"
type="button" class="btn btn-primary btn-sm" @click="showSummary">Gesamtergebnis</button>
<button v-else-if="isReviewing" type="button" class="btn btn-primary btn-sm" @click="showSummary">Gesamtergebnis</button>
<button type="button" class="btn btn-secondary btn-sm d-inline-flex align-items-center" @click="abortExam">Simulation abbrechen</button>
</div>
</header>
<div v-if="currentPart.status === 'evaluated'" class="exam-part-result" :class="resultClass(currentPart)">
<div v-if="currentPart.status === 'evaluated'" class="alert d-flex align-items-center gap-2"
:class="resultAlertClass(currentPart)">
<span class="material-icons" aria-hidden="true" v-text="resultIcon(currentPart)"></span>
<div>
<strong><span v-text="currentPart.score"></span> von 25 Punkten – <span v-text="resultLabel(currentPart)"></span></strong>
<p class="mb-0" v-text="resultDescription(currentPart)"></p>
<div v-text="resultDescription(currentPart)"></div>
</div>
</div>
<div class="exam-paper-layout">
<div class="exam-paper-layout" :key="'exam-part-' + displayedPartIndex + '-' + currentPart.code">
<main class="exam-questions course">
<template v-for="(questionState, questionIndex) in currentPart.questions" :key="questionState.id">
<article class="question exam-question" :class="'layout-' + questionData(questionState).layout"
:id="'exam-question-' + questionIndex">
<div class="question-text">
<span v-text="padNumber(questionIndex + 1) + ': '"></span><span v-html="questionData(questionState).question"></span>
</div>
<div v-if="questionData(questionState).picture" class="question-picture">
<img :src="'pictures/' + questionData(questionState).picture + '.svg'" :alt="questionData(questionState).pictureAlt">
</div>
<div class="answers">
<button v-for="(sourceIndex, answerIndex) in questionState.order" :key="answerIndex" type="button"
:disabled="currentPart.status !== 'running'" :class="questionAnswerClass(questionIndex, answerIndex)"
@click="selectAnswer(questionIndex, answerIndex)">
<span v-if="questionData(questionState).answers[sourceIndex].text"
v-html="questionData(questionState).answers[sourceIndex].text"></span>
<span v-if="questionData(questionState).answers[sourceIndex].picture" class="question-picture">
<img :src="'pictures/' + questionData(questionState).answers[sourceIndex].picture + '.svg'"
:alt="questionData(questionState).answers[sourceIndex].pictureAlt">
</span>
<span v-if="currentPart.status === 'evaluated' && sourceIndex === 0" class="result material-icons">check</span>
<span v-else-if="currentPart.status === 'evaluated' && questionState.selected === answerIndex" class="result material-icons">clear</span>
</button>
</div>
</article>
<div v-if="currentPart.status === 'evaluated' && questionData(questionState).hasSolution" class="solution">
<a class="btn btn-warning btn-sm d-inline-flex align-items-center" :href="questionState.id + '.html'" target="_blank"
:aria-label="'Lösungshinweis zu Frage ' + questionState.id">
<span class="material-icons fs-6 me-1" aria-hidden="true">lightbulb</span> Lösungsweg anzeigen
</a>
</div>
</template>
<div class="border-top pt-3 mb-5 d-flex flex-wrap gap-2 align-items-center">
<template v-if="currentPart.status === 'running'">
<span class="me-auto"><b v-text="answeredCount"></b> von 25 Fragen beantwortet.</span>
<button type="button" class="btn btn-primary" @click="requestSubmit">Prüfungsteil abgeben</button>
</template>
<template v-else-if="isReviewing">
<button type="button" class="btn btn-primary" @click="showSummary">Zurück zum Gesamtergebnis</button>
</template>
<template v-else-if="hasNextPart">
<button type="button" class="btn btn-primary" @click="startNextPart">Nächsten Prüfungsteil starten</button>
<button type="button" class="btn btn-outline-secondary" @click="finishExamEarly">Simulation hier beenden</button>
</template>
<template v-else>
<button type="button" class="btn btn-primary" @click="showSummary">Gesamtergebnis anzeigen</button>
</template>
</div>
</main>
<aside class="exam-answer-sheet" aria-label="Antwortbogen">
<div class="answer-sheet-paper">
<h2>Antwortbogen</h2>
<div class="answer-sheet-part">
<span>Prüfungsteil</span>
<strong v-text="currentPartDefinition.formLabel"></strong>
<div class="border border-dark bg-white p-2">
<h2 class="h5 text-center">Antwortbogen</h2>
<div class="border-top border-bottom border-dark py-1 mb-2">
<strong class="d-block" v-text="currentPartDefinition.formLabel"></strong>
<small><span v-text="currentPartDefinition.minutes"></span> Minuten · 25 Fragen</small>
</div>
<p class="answer-sheet-hint">Kreuze pro Frage genau eine Antwort an. Korrigierte Antworten werden wie auf dem Papierbogen mit Linien markiert.</p>
<table class="answer-table">
<thead><tr><th>Nr.</th><th v-for="label in answerLabels" :key="label" v-text="label"></th></tr></thead>
<tbody>
@@ -131,60 +183,9 @@
</tr>
</tbody>
</table>
<div class="answer-sheet-count"><b v-text="answeredCount"></b> von 25 beantwortet</div>
<small class="d-block text-end mt-1"><b v-text="answeredCount"></b> von 25 beantwortet</small>
</div>
</aside>
<main class="exam-questions course">
<article v-for="(questionState, questionIndex) in currentPart.questions" :key="questionState.id"
class="question exam-question" :class="'layout-' + questionData(questionState).layout"
:id="'exam-question-' + questionIndex">
<div class="question-text">
<span v-text="padNumber(questionIndex + 1) + ': '"></span><span v-html="questionData(questionState).question"></span>
</div>
<div v-if="questionData(questionState).picture" class="question-picture">
<img :src="'pictures/' + questionData(questionState).picture + '.svg'" :alt="questionData(questionState).pictureAlt">
</div>
<div class="answers" data-no-shuffle>
<button v-for="(sourceIndex, answerIndex) in questionState.order" :key="answerIndex" type="button"
:disabled="currentPart.status !== 'running'" :class="questionAnswerClass(questionIndex, answerIndex)"
@click="selectAnswer(questionIndex, answerIndex)">
<b class="exam-answer-label" v-text="answerLabels[answerIndex] + ':'"></b>
<span v-if="questionData(questionState).answers[sourceIndex].text"
v-html="questionData(questionState).answers[sourceIndex].text"></span>
<span v-if="questionData(questionState).answers[sourceIndex].picture" class="question-picture">
<img :src="'pictures/' + questionData(questionState).answers[sourceIndex].picture + '.svg'"
:alt="questionData(questionState).answers[sourceIndex].pictureAlt">
</span>
<span v-if="currentPart.status === 'evaluated' && sourceIndex === 0" class="result material-icons">check</span>
<span v-else-if="currentPart.status === 'evaluated' && questionState.selected === answerIndex" class="result material-icons">clear</span>
</button>
</div>
<div v-if="currentPart.status === 'evaluated' && questionData(questionState).hasSolution" class="solution">
<a class="btn btn-warning btn-sm d-inline-flex align-items-center" :href="questionState.id + '.html'" target="_blank"
:aria-label="'Lösungshinweis zu Frage ' + questionState.id">
<span class="material-icons exam-solution-icon" aria-hidden="true">lightbulb</span> Lösungsweg anzeigen
</a>
</div>
</article>
<div class="exam-submit-panel">
<template v-if="currentPart.status === 'running'">
<p><b v-text="answeredCount"></b> von 25 Fragen beantwortet.</p>
<button type="button" class="btn btn-primary btn-lg" @click="requestSubmit">Prüfungsteil abgeben</button>
</template>
<template v-else-if="isReviewing">
<button type="button" class="btn btn-primary" @click="view = 'summary'">Zurück zum Gesamtergebnis</button>
</template>
<template v-else-if="hasNextPart">
<button type="button" class="btn btn-primary btn-lg" @click="startNextPart">Nächsten Prüfungsteil starten</button>
<button type="button" class="btn btn-outline-secondary" @click="finishExamEarly">Prüfung hier beenden</button>
</template>
<template v-else>
<button type="button" class="btn btn-primary btn-lg" @click="showSummary">Gesamtergebnis anzeigen</button>
</template>
</div>
</main>
</div>
</section>
</div>
+3 -3
View File
@@ -10,7 +10,7 @@
<link rel="mask-icon" href="assets/favicon/mask-icon.svg" color="#00ADEF">
<link href="assets/bootstrap-5.2.0-beta1-dist/css/bootstrap.min.css" rel="stylesheet">
<link href="assets/antrag/darc-antrag.css" rel="stylesheet">
<link rel="stylesheet" href="assets/style-fiftyohm.css?ts=202312172012">
<link rel="stylesheet" href="assets/style-fiftyohm.css?ts=20260804">
<link rel="stylesheet" href="assets/style-course.css?ts=202312172012">
<link rel="stylesheet" href="assets/katex/katex.min.css">
{% for stylesheet in page_stylesheets|default([]) %}
@@ -90,9 +90,9 @@
</a>
</li>
<li>
<a href="probeprüfung.html">
<a href="simulation.html">
<span class="material-icons">quiz</span>
Probeprüfung
Simulation
</a>
</li>
<!-- <li>