From 32655c9fa8f1520987270e303cb0aa77290f715e Mon Sep 17 00:00:00 2001 From: Ross Date: Sun, 5 Jul 2026 20:49:01 +0100 Subject: [PATCH] feat: add refresh buttons for exams and packets with loading state management --- css/main.css | 26 +++++ index.html | 80 ++++++++++++---- js/main.js | 264 +++++++++++++++++++++++++++++++++------------------ 3 files changed, 263 insertions(+), 107 deletions(-) diff --git a/css/main.css b/css/main.css index 43c609c..e699fe8 100644 --- a/css/main.css +++ b/css/main.css @@ -1321,4 +1321,30 @@ h2:has(+ #exam-list:empty), h2:has(+ #packet-list:empty){ to { box-shadow: 0 0 12px #F44336; } +} + +.refresh-button { + background-color: transparent; + border: 1px solid #555; + border-radius: 3px; + color: #bbb; + padding: 1px 6px; + font-size: 10px; + cursor: pointer; + margin-left: 10px; + vertical-align: middle; + transition: all 0.2s; +} + +.refresh-button:hover { + background-color: #333; + border-color: #888; + color: #fff; +} + +.global-refresh-container { + margin-bottom: 20px; + display: flex; + align-items: center; + gap: 15px; } \ No newline at end of file diff --git a/index.html b/index.html index 36d429c..2cba4dd 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,4 @@ - +
- -
-
-

Click buttons below to load an exam / packet

+
+ + Click buttons below to load an exam / packet + + +

Exams:

-

Packets:

+

+ Packets: + +

diff --git a/js/main.js b/js/main.js index 5346651..440ff1f 100644 --- a/js/main.js +++ b/js/main.js @@ -273,12 +273,29 @@ catch { * * if exam_query_url is defined in config.js this will be used in preference */ -async function retrievePacketList() { - let url = "packets/packets.json"; +function showLoadingState($btn, $targetContainer) { + if (!$btn || !$btn.length) return null; + let oldText = $btn.text(); + $btn.prop("disabled", true).text("..."); + if ($targetContainer && $targetContainer.length) { + $targetContainer.css({ + "opacity": "0.4", + "transition": "opacity 0.2s" + }); + } + return function restore(success = true) { + $btn.prop("disabled", false).text(oldText); + if ($targetContainer && $targetContainer.length) { + $targetContainer.css("opacity", "1"); + if (success) { + $targetContainer.fadeOut(100).fadeIn(100); + } + } + }; +} +async function retrievePacketList(target = "all", onComplete = null) { log.debug(`Load users from database...`) - //let users = await user_db.user.toArray(); - //log.debug(`available users "${users}"`) let cid = localStorage.getItem("cid.cid") let passcode = localStorage.getItem("cid.passcode") @@ -322,110 +339,147 @@ async function retrievePacketList() { } catch (e) { log.debug("Error parsing URL params for CID/passcode", e); } - } - try { - log.debug("Try to set exam query url") - if (URLS.exam_query_url != undefined) { - - if (global_cid == null) { - url = URLS.exam_query_url; - } else { - url = `${URLS.exam_query_url}/${global_cid}/${global_passcode}`; - } - } - log.debug(`url: ${url}`) - } catch (e) { - log.debug("Unable to set exam query url", e) - log.debug("this will default to packets/packets.json") - } - - if (url && (url.includes("exam/json") || url.endsWith("exam/json"))) { - const types = ["anatomy", "rapid", "short", "long"]; + // Clear UI elements depending on target + if (target === "all") { $("#packet-list").empty(); $("#exam-list").empty(); $("#cache-details summary").empty(); $("#cache-details ul").empty(); + } else if (target === "exams") { + $("#exam-list").empty(); + } else if (target === "packets") { + $("#packet-list").empty(); + } else if (["anatomy", "rapid", "short", "long"].includes(target)) { + $(`.exam-list.${target}`).find(".packet-button").remove(); + $(`.packet-list.${target}`).find(".packet-button").remove(); + } - types.forEach(type => { - let typeUrl = url; - if (url.includes("exam/json/unbased")) { - typeUrl = url.replace("exam/json/unbased", `exam/json/${type}/unbased`); - } else { - typeUrl = url.replace("exam/json", `exam/json/${type}`); - } + let promises = []; - $.ajax({ - dataType: "json", - cache: false, - url: typeUrl, - success: function(data) { - if (data.hasOwnProperty("exams")) { - setUserInfo(data); - - loadExamList(data, false); - } else { - loadPacketList(data); - } - }, - error: function(httpObj) { - console.debug(httpObj); - if (httpObj.status == 401 || httpObj.status == 404) { - localStorage.removeItem("cid.cid"); - localStorage.removeItem("cid.passcode"); - $.notify("Unable to login", "error"); - $.notify("This page will now reload", "error"); - setTimeout(() => { - window.location.reload(); - }, 3000); - $("#options-panel").show(); - $("#candidate-details").addClass("invalid-login"); - } - } - }); - }); - } else { - $.ajax({ + // 1. Fetch packets if target is "all" or "packets" + if (target === "all" || target === "packets") { + let packetsUrl = "packets/packets.json"; + let p = $.ajax({ dataType: "json", cache: false, - url: url, + url: packetsUrl, success: function(data) { - if (data.hasOwnProperty("exams")) { - loadExamList(data, true); - } else { + if (!data.hasOwnProperty("exams")) { loadPacketList(data); } }, - error: function(httpObj, textStatus) { - console.debug(httpObj); - if (httpObj.status == 401 || httpObj.status == 404) { - localStorage.removeItem("cid.cid"); - localStorage.removeItem("cid.passcode"); - $.notify("Unable to login", "error"); - $.notify("This page will now reload", "error"); - setTimeout(() => { - window.location.reload(); - }, 3000); - $("#options-panel").show(); - $("#candidate-details").addClass("invalid-login"); - } - }, - }) - .done(function() {}) - .fail(function() { - log.debug("Unable to load packets / exams, try loading '/packets' (legacy)") + error: function(jqXHR, textStatus, errorThrown) { + log.debug("Unable to load packets, trying legacy '/packets'"); $.getJSON("packets", function(data) { - if (data.hasOwnProperty("exams")) { - loadExamList(data, true); - } else { + if (!data.hasOwnProperty("exams")) { loadPacketList(data); } }).fail(function(jqXHR, textStatus, errorThrown) { console.warn("No packet list available"); }); + } + }); + promises.push(p); + } + + // 2. Fetch exams if target is "all" or "exams" or one of the types + if (target === "all" || target === "exams" || ["anatomy", "rapid", "short", "long"].includes(target)) { + let examUrl = null; + try { + if (URLS.exam_query_url != undefined) { + if (global_cid == null || String(global_cid).startsWith("u-")) { + examUrl = URLS.exam_query_url; + } else { + examUrl = `${URLS.exam_query_url}/${global_cid}/${global_passcode}`; + } + } + } catch (e) { + log.debug("Unable to set exam query url", e); + } + + if (examUrl) { + if (examUrl.includes("exam/json") || examUrl.endsWith("exam/json")) { + const types = ["anatomy", "rapid", "short", "long"]; + const typesToFetch = types.includes(target) ? [target] : types; + + typesToFetch.forEach(type => { + let typeUrl = examUrl; + if (examUrl.includes("exam/json/unbased")) { + typeUrl = examUrl.replace("exam/json/unbased", `exam/json/${type}/unbased`); + } else { + typeUrl = examUrl.replace("exam/json", `exam/json/${type}`); + } + + let p = $.ajax({ + dataType: "json", + cache: false, + url: typeUrl, + success: function(data) { + if (data.hasOwnProperty("exams")) { + setUserInfo(data); + loadExamList(data, false, target); + } + }, + error: function(httpObj) { + console.debug(httpObj); + if ((httpObj.status == 401 || httpObj.status == 404) && global_cid !== null && !String(global_cid).startsWith("u-")) { + localStorage.removeItem("cid.cid"); + localStorage.removeItem("cid.passcode"); + $.notify("Unable to login", "error"); + $.notify("This page will now reload", "error"); + setTimeout(() => { + window.location.reload(); + }, 3000); + $("#options-panel").show(); + $("#candidate-details").addClass("invalid-login"); + } + } + }); + promises.push(p); + }); + } else { + let p = $.ajax({ + dataType: "json", + cache: false, + url: examUrl, + success: function(data) { + if (data.hasOwnProperty("exams")) { + loadExamList(data, true, target); + } + }, + error: function(httpObj, textStatus) { + console.debug(httpObj); + if ((httpObj.status == 401 || httpObj.status == 404) && global_cid !== null && !String(global_cid).startsWith("u-")) { + localStorage.removeItem("cid.cid"); + localStorage.removeItem("cid.passcode"); + $.notify("Unable to login", "error"); + $.notify("This page will now reload", "error"); + setTimeout(() => { + window.location.reload(); + }, 3000); + $("#options-panel").show(); + $("#candidate-details").addClass("invalid-login"); + } + } + }); + promises.push(p); + } + } + } + + if (promises.length) { + Promise.all(promises) + .then(() => { + if (onComplete) onComplete(true); }) - .always(function() {}); + .catch((err) => { + console.warn("One or more requests failed", err); + if (onComplete) onComplete(false); + }); + } else { + if (onComplete) onComplete(true); } } @@ -435,7 +489,7 @@ async function retrievePacketList() { * * @param {JSON} data - json containing available exams */ -async function loadExamList(data, clearExistingList = true) { +async function loadExamList(data, clearExistingList = true, target = "all") { // If exam mode was enabled via URL param, hide packets by default if (typeof url_exam_mode !== "undefined" && url_exam_mode) { log.debug("URL exam mode active - hiding packets list"); @@ -491,8 +545,12 @@ async function loadExamList(data, clearExistingList = true) { // enable invalidation when the question is updated on the server let exam_generated_map = {}; if (clearExistingList) { - $("#packet-list").empty(); - $("#exam-list").empty(); + if (target === "all" || target === "packets") { + $("#packet-list").empty(); + } + if (target === "all" || target === "exams") { + $("#exam-list").empty(); + } $("#cache-details summary").empty(); $("#cache-details ul").empty(); } @@ -547,7 +605,11 @@ async function loadExamList(data, clearExistingList = true) { list = $(`.${target_list}.${exam.type}`); // or create and add it } else { - list = $(`
${exam.type}
`); + let refreshBtnHtml = ""; + if (target_list === "exam-list") { + refreshBtnHtml = ` `; + } + list = $(`
${exam.type}${refreshBtnHtml}
`); $(`#${target_list}`).append(list); } @@ -2904,6 +2966,26 @@ $("#btn-user-login").click(function(evt) { //$(".exam-wrapper").toggle(); }); +$("#btn-refresh-all").on("click", function(e) { + e.stopPropagation(); + let restore = showLoadingState($(this), $("#packets")); + retrievePacketList("all", restore); +}); +$("#btn-refresh-packets").on("click", function(e) { + e.stopPropagation(); + let restore = showLoadingState($(this), $("#packet-list")); + retrievePacketList("packets", restore); +}); + +// Delegate click handlers for the dynamic per-exam-type refresh buttons +$("#exam-list").on("click", ".type-refresh-button", function(e) { + e.stopPropagation(); + let type = $(this).data("type"); + let $categoryContainers = $(`.exam-list.${type}, .packet-list.${type}`); + let restore = showLoadingState($(this), $categoryContainers); + retrievePacketList(type, restore); +}); + $("#btn-login").click(function(evt) {