Compare commits

...

4 commits

Author SHA1 Message Date
5802a00e8e Fix results
All checks were successful
Deploy to Pages / deploy (push) Successful in 9s
Test / test (push) Successful in 9s
Build and Publish Docker Image / build-and-push (push) Successful in 25s
2026-06-06 20:42:09 +01:00
5f898194f5 Remove dead code 2026-06-06 20:34:56 +01:00
6b586f6e07 Integrate plausible 2026-06-06 20:34:34 +01:00
eb807a63a6 Add plausible 2026-06-06 20:33:53 +01:00
14 changed files with 196 additions and 195 deletions

View file

@ -7,4 +7,5 @@ backend/package-lock.json
# Vendored / generated assets # Vendored / generated assets
frontend/js/jquery.min.js frontend/js/jquery.min.js
frontend/js/plausible.js
*.min.js *.min.js

View file

@ -8,6 +8,7 @@
"name": "fizika-admin-backend", "name": "fizika-admin-backend",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@plausible-analytics/tracker": "^0.4.5",
"cors": "^2.8.6", "cors": "^2.8.6",
"express": "^5.2.1", "express": "^5.2.1",
"helmet": "^8.2.0", "helmet": "^8.2.0",
@ -20,6 +21,12 @@
"node": ">=18.0.0" "node": ">=18.0.0"
} }
}, },
"node_modules/@plausible-analytics/tracker": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/@plausible-analytics/tracker/-/tracker-0.4.5.tgz",
"integrity": "sha512-6BfAGejXY+YA3Cw6LYT2Zpn4hTxDtPQAawFsYUsQCOg78wIS5C4deAGXTfJffa5VleMWITv5lpJ/EYuQBl1tPA==",
"license": "MIT"
},
"node_modules/accepts": { "node_modules/accepts": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",

View file

@ -9,6 +9,7 @@
"test": "node --test" "test": "node --test"
}, },
"dependencies": { "dependencies": {
"@plausible-analytics/tracker": "^0.4.5",
"express": "^5.2.1", "express": "^5.2.1",
"multer": "^2.1.1", "multer": "^2.1.1",
"cors": "^2.8.6", "cors": "^2.8.6",

View file

@ -1,11 +1,5 @@
const API_BASE = window.location.origin; const API_BASE = window.location.origin;
window.plausible =
window.plausible ||
function () {
(window.plausible.q = window.plausible.q || []).push(arguments);
};
// Escape user-supplied text before interpolating it into HTML, to prevent XSS. // Escape user-supplied text before interpolating it into HTML, to prevent XSS.
function escapeHtml(value) { function escapeHtml(value) {
return String(value).replace( return String(value).replace(

View file

@ -4,12 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Fizika Admin - Kérdések és képek kezelése</title> <title>Fizika Admin - Kérdések és képek kezelése</title>
<script <script type="module" src="stats.js"></script>
defer
data-domain="fizika.schmelczer.dev"
data-api="https://stats.schmelczer.dev/status"
src="https://stats.schmelczer.dev/js/script.file-downloads.hash.outbound-links.js"
></script>
<style> <style>
* { * {
margin: 0; margin: 0;

12
backend/public/stats.js Normal file
View file

@ -0,0 +1,12 @@
import { init } from "/vendor/plausible.js";
// Plausible analytics via the official npm tracker (@plausible-analytics/tracker).
// server.js serves /vendor/plausible.js straight from node_modules.
init({
domain: "fizika.schmelczer.dev",
endpoint: "https://stats.schmelczer.dev/status",
autoCapturePageviews: true,
outboundLinks: true,
fileDownloads: true,
hashBasedRouting: true,
});

View file

@ -37,6 +37,14 @@ app.use(
app.use(express.json({ limit: "100mb" })); app.use(express.json({ limit: "100mb" }));
app.use(express.static("public")); app.use(express.static("public"));
// Serve the official Plausible tracker (npm: @plausible-analytics/tracker) from node_modules.
const plausibleTrackerPath =
require.resolve("@plausible-analytics/tracker/plausible.js");
app.get("/vendor/plausible.js", (req, res) => {
res.sendFile(plausibleTrackerPath);
});
// File paths // File paths
const DATA_PATH = const DATA_PATH =
process.env.DATA_PATH || path.join(__dirname, "../frontend/fizika.json"); process.env.DATA_PATH || path.join(__dirname, "../frontend/fizika.json");

View file

@ -56,19 +56,7 @@
<script src="js/jquery.min.js"></script> <script src="js/jquery.min.js"></script>
<script src="js/load.js"></script> <script src="js/load.js"></script>
<script src="js/fizika.js"></script> <script src="js/fizika.js"></script>
<script <script type="module" src="js/stat.js"></script>
defer
data-domain="fizika.schmelczer.dev"
data-api="https://stats.schmelczer.dev/status"
src="https://stats.schmelczer.dev/js/script.file-downloads.hash.outbound-links.js"
></script>
<script>
window.plausible =
window.plausible ||
function () {
(window.plausible.q = window.plausible.q || []).push(arguments);
};
</script>
</head> </head>
<body onresize="aspect()"> <body onresize="aspect()">
<div id="titlebox"> <div id="titlebox">

View file

@ -1,7 +1,6 @@
var isSearch = false; var isSearch = false;
var totalPoints = 0; var totalPoints = 0;
var currentPoints = 0; var loadedQuestions = [];
var correctAnswersGiven = 0;
var timer = 0; var timer = 0;
var startTimer = 0; var startTimer = 0;
var reviewMode = 0; var reviewMode = 0;
@ -17,7 +16,7 @@ function aspect() {
async function ajaxLoad(type) { async function ajaxLoad(type) {
reviewMode = 0; reviewMode = 0;
totalPoints = 0; totalPoints = 0;
currentPoints = 0; loadedQuestions = [];
$("#state").html(""); $("#state").html("");
$("#state2").html(""); $("#state2").html("");
$("#percentage").html(""); $("#percentage").html("");
@ -92,6 +91,8 @@ async function ajaxLoad(type) {
) { ) {
$("#megoldas").show(); $("#megoldas").show();
$("#state").html("Feladatok sikeresen letöltve!"); $("#state").html("Feladatok sikeresen letöltve!");
startTimer = 1;
timer = 0;
} }
} catch (error) { } catch (error) {
$("#loadingGif").hide(); $("#loadingGif").hide();
@ -116,66 +117,73 @@ async function ajaxLoad(type) {
} }
} }
function showCorrect(id, correctAns) { // Colour a single question green/red depending on whether the user selected
teszt(id, correctAns); // its correct answer, and report whether they got it right.
eval( function gradeQuestion(question) {
"$('" + var div = "#feladat" + question.id;
"#label" + var selected = "#form" + question.id + " #rad" + question.correct;
id + var isCorrect = $(selected).is(":checked");
".rad" + $(div).animate({ backgroundColor: isCorrect ? "#C6FF8C" : "#FF808C" }, 1100);
correctAns + return isCorrect;
"').css('background-color', '#C6FF8C');", }
// Grade every question currently on screen and return the score as a string
// percentage (e.g. "66.67"). Grading the whole known set in one pass replaces
// the old per-question click handlers, which accumulated across loads and
// could save bogus results.
function scoreLoadedQuestions() {
var correctAnswersGiven = 0;
loadedQuestions.forEach(function (question) {
if (gradeQuestion(question)) correctAnswersGiven++;
});
var percentage = (correctAnswersGiven / totalPoints) * 100;
percentage = Math.round(percentage * 100) / 100;
return percentage.toFixed(2);
}
// "Kiértékelés": grade the answers and, unless we are already reviewing, save
// the result once.
function evaluate() {
if (!loadedQuestions.length) return;
var percentage = scoreLoadedQuestions();
$("#percentage").html("Eredmény: " + percentage + "%");
$("#state").html("Válaszok leellenőrizve!");
if (isLocal && !reviewMode) {
saveResult(percentage);
reviewMode = 1;
}
}
// "Helyes megoldások": reveal the correct answers. This is a review action, so
// we enter review mode *before* grading to guarantee it can never overwrite a
// previously saved result.
function showCorrect() {
if (!loadedQuestions.length) return;
reviewMode = 1;
var percentage = scoreLoadedQuestions();
loadedQuestions.forEach(function (question) {
$("#label" + question.id + ".rad" + question.correct).css(
"background-color",
"#C6FF8C",
); );
});
$("#percentage").html("Eredmény: " + percentage + "%");
$("#state").html("Helyes válaszok bejelölve!"); $("#state").html("Helyes válaszok bejelölve!");
$("#state2").show();
$("#state2").html( $("#state2").html(
"(Ellenőrző mód, az itteni eredményeid nem kerülnek elmentésre, a módból való kilépéshez tölts be egy új tesztsort!)", "(Ellenőrző mód, az itteni eredményeid nem kerülnek elmentésre, a módból való kilépéshez tölts be egy új tesztsort!)",
); );
} }
function teszt(id, correctAns) { function saveResult(percentage) {
var div = "#feladat" + id;
var correct = "#form" + id + " #rad" + correctAns;
var isCorrect = $(correct).is(":checked");
currentPoints++;
if (isCorrect) {
$(div).animate({ backgroundColor: "#C6FF8C" }, 1100);
correctAnswersGiven++;
} else {
$(div).animate({ backgroundColor: "#FF808C" }, 1100);
}
if (currentPoints >= totalPoints) {
var percentage = (correctAnswersGiven / totalPoints) * 100;
percentage = Math.round(percentage * 100) / 100;
percentage = percentage.toFixed(2);
$("#percentage").html("Eredmény: " + percentage + "%");
$("#state").html("Válaszok leellenőrizve!");
if (isLocal && !reviewMode) {
var datum = new Date(); var datum = new Date();
var ido = Math.round(timer / 60); var ido = Math.round(timer / 60);
eval( localStorage["teszt" + numberOfPreviousTests] = percentage;
"localStorage.teszt" + localStorage["teszt" + numberOfPreviousTests + "date"] =
numberOfPreviousTests + datum.toLocaleDateString();
" = '" + localStorage["teszt" + numberOfPreviousTests + "time"] = ido;
percentage + localStorage["teszt" + numberOfPreviousTests + "total"] = totalPoints;
"'",
);
eval(
"localStorage.teszt" +
numberOfPreviousTests +
"date = '" +
datum.toLocaleDateString() +
"'",
);
eval(
"localStorage.teszt" + numberOfPreviousTests + "time = '" + ido + "'",
);
eval(
"localStorage.teszt" +
numberOfPreviousTests +
"total = '" +
totalPoints +
"'",
);
startTimer = 0; startTimer = 0;
timer = 0; timer = 0;
$("#state2").show(); $("#state2").show();
@ -183,20 +191,6 @@ function teszt(id, correctAns) {
"Eredményed mentésre került! Ellenőrző módba belépve az eredményeid nem kerülnek tárolásra. A módból való kilépéshez tölts be egy új tesztsort!", "Eredményed mentésre került! Ellenőrző módba belépve az eredményeid nem kerülnek tárolásra. A módból való kilépéshez tölts be egy új tesztsort!",
); );
eredmeny(); eredmeny();
reviewMode = 1;
}
currentPoints = 0;
correctAnswersGiven = 0;
}
}
function scrollTo(where) {
$("html, body").animate(
{
scrollTop: $(where).offset().top - 100,
},
1000,
);
} }
function howMany() { function howMany() {
@ -266,28 +260,13 @@ setInterval(function () {
$(document).ready(function () { $(document).ready(function () {
eredmeny(); eredmeny();
// Initialize year dropdown with dynamic years from question data // Populate the year dropdown, then the month dropdown for the default "all".
if (typeof initializeYearDropdown === "function") { initializeYearDropdown().then(() => initializeMonthDropdown("all/"));
initializeYearDropdown().then(() => {
// Initialize month dropdown for default "all" year selection
if (typeof initializeMonthDropdown === "function") {
initializeMonthDropdown("all/");
}
});
}
$(window).on("mousewheel", function () { $(window).on("mousewheel", function () {
$("body").stop(); $("body").stop();
}); });
$(document).ajaxError(function (event, jqxhr, settings) {
if (!settings.secondExec) {
settings.secondExec = true;
setTimeout(function () {
$.ajax(settings);
}, 500);
}
});
aspect(); aspect();
$("#bfooldal").click(function () { $("#bfooldal").click(function () {
@ -333,36 +312,7 @@ $(document).ready(function () {
isSearch = false; isSearch = false;
}); });
$("#evszam").change(function () { $("#evszam").change(function () {
const selectedYear = $("#evszam").val(); initializeMonthDropdown($("#evszam").val());
// Initialize month dropdown dynamically based on selected year
if (typeof initializeMonthDropdown === "function") {
initializeMonthDropdown(selectedYear);
} else {
// Fallback to original logic if dynamic function not available
if (selectedYear == "2006/") {
$(".f2006").show();
$(".f2016").hide();
$(".f").hide();
$(".fnem17").hide();
} else if (selectedYear == "2016/") {
$(".f2006").hide();
$(".f2016").show();
$(".f").show();
$(".fnem17").show();
} else if (selectedYear == "2017/") {
$(".f2006").hide();
$(".f2016").hide();
$(".f").show();
$(".fnem17").hide();
} else {
$(".f2006").hide();
$(".f2016").hide();
$(".f").show();
$(".fnem17").show();
}
}
$("#honap").val("all"); $("#honap").val("all");
}); });
@ -391,6 +341,14 @@ $(document).ready(function () {
$("#load").click(function (event) { $("#load").click(function (event) {
ajaxLoad(isSearch ? 1 : 3); ajaxLoad(isSearch ? 1 : 3);
}); });
$("#ans").click(function (event) {
event.preventDefault();
evaluate();
});
$("#cAns").click(function (event) {
event.preventDefault();
showCorrect();
});
$(".scroll").click(function () { $(".scroll").click(function () {
$("body").animate( $("body").animate(
{ {

View file

@ -72,13 +72,24 @@ const loadQuestions = async (
); );
} }
resultHtml = "";
currentQuestions = currentQuestions.slice(0, questionCount); currentQuestions = currentQuestions.slice(0, questionCount);
// Remember the exact set being shown. The two action buttons ("Kiértékelés"
// and "Helyes megoldások") are bound once in fizika.js and grade this whole
// set in a single pass. Previously each question injected its own #ans/#cAns
// click handler; those were never removed, so they piled up across loads and
// ended up grading stale (already-removed) questions, saving bogus 0%
// results over the user's history.
loadedQuestions = currentQuestions.map(({ id, correct }) => ({
id: Number(id),
correct: Number(correct),
}));
totalPoints = loadedQuestions.length;
let resultHtml = "";
currentQuestions.forEach( currentQuestions.forEach(
({ id, source, description, a, b, c, d, correct, image }, i) => { ({ id, source, description, a, b, c, d, image }, i) => {
const qid = Number(id); const qid = Number(id);
const correctAnswer = Number(correct);
const safeImage = image ? encodeURIComponent(image) : ""; const safeImage = image ? encodeURIComponent(image) : "";
resultHtml += ` resultHtml += `
<div class="feladat card" id="feladat${qid}"> <div class="feladat card" id="feladat${qid}">
@ -104,31 +115,15 @@ const loadQuestions = async (
: "" : ""
} }
</form> </form>
<script type="text/javascript">
$(document).ready(function(){
totalPoints++;
$("#ans").click(function(event){
event.preventDefault();
teszt(${qid}, ${correctAnswer});
});
$("#cAns").click(function(event){
event.preventDefault();
showCorrect(${qid}, ${correctAnswer});
});
});
</script>
</div> </div>
`; `;
}, },
); );
resultHtml += if (currentQuestions.length === 0) {
currentQuestions.length === 0 resultHtml =
? '<div class="buttonwrapper"><b style="font-size: 2rem;">Nem található a keresésnek megfelelő feladat!</b></div>' '<div class="buttonwrapper"><b style="font-size: 2rem;">Nem található a keresésnek megfelelő feladat!</b></div>';
: `<script type="text/javascript"> }
startTimer = 1;
timer = 0;
</script>`;
return resultHtml; return resultHtml;
}; };
@ -200,7 +195,7 @@ const initializeMonthDropdown = (selectedYear) => {
// Convert to sorted array and create options // Convert to sorted array and create options
const uniqueMonths = Array.from(monthSet).sort(); const uniqueMonths = Array.from(monthSet).sort();
uniqueMonths.forEach((month) => { uniqueMonths.forEach((month) => {
monthOptions += `<option value="${month}" class="fdynamic">${getMonthLabel(month)}</option>`; monthOptions += `<option value="${month}">${getMonthLabel(month)}</option>`;
}); });
} else { } else {
// Extract year from selected value (e.g., "2024/" -> "2024") // Extract year from selected value (e.g., "2024/" -> "2024")
@ -220,44 +215,39 @@ const initializeMonthDropdown = (selectedYear) => {
// Special handling for known year patterns // Special handling for known year patterns
if (year === "2006") { if (year === "2006") {
// Preserve existing 2006 logic but add any new months found // Preserve existing 2006 logic but add any new months found
monthOptions += monthOptions += '<option value="1">Február-Március</option>';
'<option value="1" class="f2006">Február-Március</option>'; monthOptions += '<option value="2">Május-Június</option>';
monthOptions += '<option value="2" class="f2006">Május-Június</option>'; monthOptions += '<option value="3">Október-November</option>';
monthOptions +=
'<option value="3" class="f2006">Október-November</option>';
// Add any dynamic months not covered by the standard ones // Add any dynamic months not covered by the standard ones
uniqueMonths.forEach((month) => { uniqueMonths.forEach((month) => {
if (!["1", "2", "3"].includes(month)) { if (!["1", "2", "3"].includes(month)) {
monthOptions += `<option value="${month}" class="f2006">${getMonthLabel(month)}</option>`; monthOptions += `<option value="${month}">${getMonthLabel(month)}</option>`;
} }
}); });
} else if (year === "2016") { } else if (year === "2016") {
// Preserve existing 2016 logic but add any new months found // Preserve existing 2016 logic but add any new months found
monthOptions += '<option value="1" class="f">Május-Június</option>'; monthOptions += '<option value="1">Május-Június</option>';
monthOptions += '<option value="2" class="f">Október-November</option>'; monthOptions += '<option value="2">Október-November</option>';
monthOptions += monthOptions += '<option value="m1">1. Mintafeladatsor</option>';
'<option value="m1" class="f2016">1. Mintafeladatsor</option>'; monthOptions += '<option value="m2">2. Mintafeladatsor</option>';
monthOptions += monthOptions += '<option value="m3">3. Mintafeladatsor</option>';
'<option value="m2" class="f2016">2. Mintafeladatsor</option>';
monthOptions +=
'<option value="m3" class="f2016">3. Mintafeladatsor</option>';
// Add any dynamic months not covered // Add any dynamic months not covered
uniqueMonths.forEach((month) => { uniqueMonths.forEach((month) => {
if (!["1", "2", "m1", "m2", "m3"].includes(month)) { if (!["1", "2", "m1", "m2", "m3"].includes(month)) {
monthOptions += `<option value="${month}" class="f">${getMonthLabel(month)}</option>`; monthOptions += `<option value="${month}">${getMonthLabel(month)}</option>`;
} }
}); });
} else if (year === "2017") { } else if (year === "2017") {
// Preserve existing 2017 logic but add any new months found // Preserve existing 2017 logic but add any new months found
monthOptions += '<option value="1" class="f">Május-Június</option>'; monthOptions += '<option value="1">Május-Június</option>';
monthOptions += '<option value="2" class="f">Október-November</option>'; monthOptions += '<option value="2">Október-November</option>';
// Add any dynamic months // Add any dynamic months
uniqueMonths.forEach((month) => { uniqueMonths.forEach((month) => {
if (!["1", "2"].includes(month)) { if (!["1", "2"].includes(month)) {
monthOptions += `<option value="${month}" class="f">${getMonthLabel(month)}</option>`; monthOptions += `<option value="${month}">${getMonthLabel(month)}</option>`;
} }
}); });
} else { } else {
@ -266,16 +256,16 @@ const initializeMonthDropdown = (selectedYear) => {
const hasStandard2 = uniqueMonths.includes("2"); const hasStandard2 = uniqueMonths.includes("2");
if (hasStandard1) { if (hasStandard1) {
monthOptions += '<option value="1" class="f">Május-Június</option>'; monthOptions += '<option value="1">Május-Június</option>';
} }
if (hasStandard2) { if (hasStandard2) {
monthOptions += '<option value="2" class="f">Október-November</option>'; monthOptions += '<option value="2">Október-November</option>';
} }
// Add any non-standard months // Add any non-standard months
uniqueMonths.forEach((month) => { uniqueMonths.forEach((month) => {
if (!["1", "2"].includes(month)) { if (!["1", "2"].includes(month)) {
monthOptions += `<option value="${month}" class="f">${getMonthLabel(month)}</option>`; monthOptions += `<option value="${month}">${getMonthLabel(month)}</option>`;
} }
}); });
} }

2
frontend/js/plausible.js Normal file

File diff suppressed because one or more lines are too long

10
frontend/js/stats.js Normal file
View file

@ -0,0 +1,10 @@
import { init } from "./plausible.js";
init({
domain: "fizika.schmelczer.dev",
endpoint: "https://stats.schmelczer.dev/status",
autoCapturePageviews: true,
outboundLinks: true,
fileDownloads: true,
hashBasedRouting: true,
});

22
frontend/package-lock.json generated Normal file
View file

@ -0,0 +1,22 @@
{
"name": "fizika-frontend",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "fizika-frontend",
"version": "1.0.0",
"hasInstallScript": true,
"dependencies": {
"@plausible-analytics/tracker": "^0.4.5"
}
},
"node_modules/@plausible-analytics/tracker": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/@plausible-analytics/tracker/-/tracker-0.4.5.tgz",
"integrity": "sha512-6BfAGejXY+YA3Cw6LYT2Zpn4hTxDtPQAawFsYUsQCOg78wIS5C4deAGXTfJffa5VleMWITv5lpJ/EYuQBl1tPA==",
"license": "MIT"
}
}
}

13
frontend/package.json Normal file
View file

@ -0,0 +1,13 @@
{
"name": "fizika-frontend",
"version": "1.0.0",
"description": "Static frontend for the Fizika practice app",
"private": true,
"scripts": {
"vendor": "cp node_modules/@plausible-analytics/tracker/plausible.js js/plausible.js",
"postinstall": "npm run vendor"
},
"dependencies": {
"@plausible-analytics/tracker": "^0.4.5"
}
}